From 8d89fe80ff7abf6aaf301b2e024f92b63f0ec84a Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 30 Oct 2025 01:26:02 +0100 Subject: [PATCH] chore: Second cleanup wave - organize root directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Archive: 85 agent .txt files → docs/archive/agents/legacy_txt/ - Scripts: Move 110 shell scripts → scripts/ (keep deploy.sh in root) - Models: Move 18 .safetensors → ml/models/checkpoints/training_artifacts/ - Delete: 34 directories (~33GB freed) - target/, coverage_*, test artifacts - Build: Clean 14 build artifacts (.rlib, .o, .pid, binaries) - Tests: Move 14 .rs files → tests/standalone/ - SQL: Move 5 files → sql/ (keep init-db*.sql for Docker) - Wave 153: Archive to docs/archive/historical/wave153/ - Docs: Archive 9 markdown files to wave_d/reports/ and historical/ Total impact: ~34GB freed (both waves), root directory cleaned from 583 to ~40 essential files Directory count reduced from 65 to 31 (52% reduction) All historical data preserved in organized archive structure --- All targets: | 0 Error count: | 0 Production libs: | 0 Warning count: | 0 .../alternative/Cargo.toml.optimized | 0 .../server-extensions.cnf | 0 coverage_common/html/control.js | 99 - .../Work/foxhunt/common/src/database.rs.html | 1 - .../Work/foxhunt/common/src/error.rs.html | 1 - .../foxhunt/common/src/thresholds.rs.html | 1 - .../Work/foxhunt/common/src/trading.rs.html | 1 - .../Work/foxhunt/common/src/traits.rs.html | 1 - .../Work/foxhunt/common/src/types.rs.html | 1 - .../config/src/asset_classification.rs.html | 1 - .../config/src/compliance_config.rs.html | 1 - .../foxhunt/config/src/data_config.rs.html | 1 - .../foxhunt/config/src/data_providers.rs.html | 1 - .../Work/foxhunt/config/src/database.rs.html | 1 - .../Work/foxhunt/config/src/error.rs.html | 1 - .../Work/foxhunt/config/src/lib.rs.html | 1 - .../Work/foxhunt/config/src/manager.rs.html | 1 - .../Work/foxhunt/config/src/ml_config.rs.html | 1 - .../foxhunt/config/src/risk_config.rs.html | 1 - .../Work/foxhunt/config/src/runtime.rs.html | 1 - .../Work/foxhunt/config/src/schemas.rs.html | 1 - .../foxhunt/config/src/storage_config.rs.html | 1 - .../foxhunt/config/src/structures.rs.html | 1 - .../foxhunt/config/src/symbol_config.rs.html | 1 - .../Work/foxhunt/config/src/vault.rs.html | 1 - coverage_common/html/index.html | 1 - coverage_common/html/style.css | 194 - coverage_report_common/html/control.js | 99 - .../Work/foxhunt/common/src/database.rs.html | 1 - .../Work/foxhunt/common/src/error.rs.html | 1 - .../foxhunt/common/src/market_data.rs.html | 1 - .../foxhunt/common/src/ml_strategy.rs.html | 1 - .../foxhunt/common/src/thresholds.rs.html | 1 - .../Work/foxhunt/common/src/trading.rs.html | 1 - .../Work/foxhunt/common/src/traits.rs.html | 1 - .../Work/foxhunt/common/src/types.rs.html | 1 - .../config/src/asset_classification.rs.html | 1 - .../foxhunt/config/src/data_config.rs.html | 1 - .../foxhunt/config/src/data_providers.rs.html | 1 - .../Work/foxhunt/config/src/database.rs.html | 1 - .../Work/foxhunt/config/src/lib.rs.html | 1 - .../Work/foxhunt/config/src/manager.rs.html | 1 - .../Work/foxhunt/config/src/ml_config.rs.html | 1 - .../foxhunt/config/src/risk_config.rs.html | 1 - .../Work/foxhunt/config/src/runtime.rs.html | 1 - .../Work/foxhunt/config/src/schemas.rs.html | 1 - .../foxhunt/config/src/storage_config.rs.html | 1 - .../foxhunt/config/src/structures.rs.html | 1 - .../foxhunt/config/src/symbol_config.rs.html | 1 - .../Work/foxhunt/config/src/vault.rs.html | 1 - coverage_report_common/html/index.html | 1 - coverage_report_common/html/style.css | 194 - coverage_risk/html/control.js | 99 - .../Work/foxhunt/common/src/database.rs.html | 1 - .../Work/foxhunt/common/src/error.rs.html | 1 - .../Work/foxhunt/common/src/trading.rs.html | 1 - .../Work/foxhunt/common/src/traits.rs.html | 1 - .../Work/foxhunt/common/src/types.rs.html | 1 - .../config/src/asset_classification.rs.html | 1 - .../foxhunt/config/src/data_config.rs.html | 1 - .../foxhunt/config/src/data_providers.rs.html | 1 - .../Work/foxhunt/config/src/database.rs.html | 1 - .../Work/foxhunt/config/src/lib.rs.html | 1 - .../Work/foxhunt/config/src/manager.rs.html | 1 - .../Work/foxhunt/config/src/ml_config.rs.html | 1 - .../foxhunt/config/src/risk_config.rs.html | 1 - .../Work/foxhunt/config/src/runtime.rs.html | 1 - .../Work/foxhunt/config/src/schemas.rs.html | 1 - .../foxhunt/config/src/storage_config.rs.html | 1 - .../foxhunt/config/src/structures.rs.html | 1 - .../foxhunt/config/src/symbol_config.rs.html | 1 - .../Work/foxhunt/config/src/vault.rs.html | 1 - .../foxhunt/risk/src/circuit_breaker.rs.html | 1 - .../Work/foxhunt/risk/src/compliance.rs.html | 1 - .../foxhunt/risk/src/drawdown_monitor.rs.html | 1 - .../Work/foxhunt/risk/src/error.rs.html | 1 - .../foxhunt/risk/src/kelly_sizing.rs.html | 1 - .../Work/foxhunt/risk/src/lib.rs.html | 1 - .../Work/foxhunt/risk/src/operations.rs.html | 1 - .../foxhunt/risk/src/position_tracker.rs.html | 1 - .../Work/foxhunt/risk/src/risk_engine.rs.html | 1 - .../Work/foxhunt/risk/src/risk_types.rs.html | 1 - .../src/safety/emergency_response.rs.html | 1 - .../risk/src/safety/kill_switch.rs.html | 1 - .../Work/foxhunt/risk/src/safety/mod.rs.html | 1 - .../risk/src/safety/position_limiter.rs.html | 1 - .../src/safety/safety_coordinator.rs.html | 1 - .../risk/src/safety/trading_gate.rs.html | 1 - .../safety/unix_socket_kill_switch.rs.html | 1 - .../foxhunt/risk/src/stress_tester.rs.html | 1 - .../var_calculator/expected_shortfall.rs.html | 1 - .../historical_simulation.rs.html | 1 - .../src/var_calculator/monte_carlo.rs.html | 1 - .../src/var_calculator/parametric.rs.html | 1 - .../src/var_calculator/var_engine.rs.html | 1 - coverage_risk/html/index.html | 1 - coverage_risk/html/style.css | 194 - .../agents/AGENT_TXT_FILES_ANALYSIS.md | 316 + .../agents/legacy_txt/AGENT_10_1_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_10_3_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_10_6_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_10_7_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_152_SUMMARY.txt | 0 .../legacy_txt/AGENT_160_VISUAL_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_19_SUMMARY.txt | 0 .../legacy_txt/AGENT_223_VISUAL_SUMMARY.txt | 0 .../legacy_txt/AGENT_244_TEST_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_256_SUMMARY.txt | 0 .../AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt | 0 .../legacy_txt/AGENT_257_TEST_SUMMARY.txt | 0 .../legacy_txt/AGENT_258_VISUAL_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_395_COMPLETE.txt | 0 .../agents/legacy_txt/AGENT_43_SUMMARY.txt | 0 .../AGENT_5_ARCHITECTURE_DIAGRAM.txt | 0 .../agents/legacy_txt/AGENT_6_SUMMARY.txt | 0 .../AGENT_86_BENCHMARK_GAP_SUMMARY.txt | 0 .../legacy_txt/AGENT_86_FINAL_SUMMARY.txt | 0 .../legacy_txt/AGENT_916_VISUAL_SUMMARY.txt | 0 .../legacy_txt/AGENT_9_13_COMMIT_MESSAGE.txt | 0 .../legacy_txt/AGENT_9_13_VISUAL_SUMMARY.txt | 0 .../legacy_txt/AGENT_BLOCK02_SUMMARY.txt | 0 .../AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt | 0 .../AGENT_E6_BENCHMARK_RAW_OUTPUT.txt | 0 .../AGENT_E6_PERFORMANCE_VISUALIZATION.txt | 0 .../agents/legacy_txt/AGENT_F11_SUMMARY.txt | 0 .../legacy_txt/AGENT_F16_VISUAL_SUMMARY.txt | 0 .../AGENT_F23_EXECUTIVE_SUMMARY.txt | 0 .../legacy_txt/AGENT_G19_SUCCESS_SUMMARY.txt | 0 .../legacy_txt/AGENT_IMPL18_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_M13_MANIFEST.txt | 0 .../legacy_txt/AGENT_M13_QUICK_REFERENCE.txt | 0 .../AGENT_T22_DELIVERABLES_SUMMARY.txt | 0 .../agents/legacy_txt/AGENT_VAL28_SUMMARY.txt | 0 .../legacy_txt/AGENT_VAL30_QUICK_SUMMARY.txt | 0 .../AGENT_WIRE14_INTEGRATION_GAPS.txt | 0 docs/archive/agents/legacy_txt/README.md | 1 + .../agent_199_infrastructure_validation.txt | 0 .../agent_200_test_environment_setup.txt | 0 .../agent_219_async_audit_design.txt | 0 .../agent_228_implementation_report.txt | 0 .../agent_229v2_jwt_validation_report.txt | 0 .../agent_275_missing_fields_fixed.txt | 0 .../agent_278_trading_data_fixed.txt | 0 .../agent_279_format_strings_fixed.txt | 0 .../legacy_txt/agent_280_load_tests_fixed.txt | 0 .../agent_281_api_gateway_tests_fixed.txt | 0 .../agent_283_future_traits_fixed.txt | 0 .../legacy_txt/agent_284_criterion_fixed.txt | 0 .../agent_288_benchmark_deps_fixed.txt | 0 .../agent_289_database_tli_fixed.txt | 0 .../agent_291_tli_storage_fixed.txt | 0 .../agent_295_api_gateway_fixed.txt | 0 .../agent_297_backtesting_service_fixed.txt | 0 .../agent_304_remaining_crates_fixed.txt | 0 .../agent_306_api_gateway_unwrap_fixed.txt | 0 ...agent_307_trading_service_panics_fixed.txt | 0 .../agent_311_storage_safety_fixed.txt | 0 .../legacy_txt/agent_312_ml_safety_fixed.txt | 0 .../agent_313_trading_engine_safety_fixed.txt | 0 .../agent_314_backtesting_safety_fixed.txt | 0 .../agent_322_risk_precision_fixed.txt | 0 .../legacy_txt/agent_323_data_types_fixed.txt | 0 .../legacy_txt/agent_324_ml_types_fixed.txt | 0 .../agent_326_doc_markdown_fixes.txt | 0 .../agent_331_float_arithmetic_report.txt | 0 .../agent_334_float_arithmetic_ml.txt | 0 .../agent_335_as_conversions_fixed.txt | 0 .../agent_341_unsafe_documentation_report.txt | 0 .../agent_342_numeric_fallback_fixes.txt | 0 .../agent_349_backticks_1501_2000.txt | 0 .../agent_350_doc_backticks_part5.txt | 0 .../agent_367_field_visibility_report.txt | 0 .../agent_373_wave6_error_analysis.txt | 0 .../legacy_txt/agent_402_remaining_errors.txt | 0 .../agents/legacy_txt/agent_402_summary.txt | 0 .../agent_418_float_arithmetic_fixed.txt | 0 .../agent_422_as_conversions_report.txt | 0 .../legacy_txt/agent_437_map_err_fixes.txt | 0 .../agent_442_ml_indexing_final_report.txt | 0 .../agent_445_arithmetic_cleanup_part2.txt | 0 .../agent_451_unused_self_cleanup_final.txt | 0 .../agent_470_e0599_final_cleanup.txt | 0 .../agent_489_trading_engine_final.txt | 0 ...nt_comprehensive_finalization_analysis.txt | 0 .../CUDA_13_GPU_VALIDATION_CHECKLIST.md | 0 .../historical/DOCKER_CLEANUP_QUICK_REF.md | 0 .../LEGACY_256_CLEANUP_CHECKLIST.md | 0 .../EXECUTIVE_SUMMARY.md | 0 .../QUICK_REFERENCE.md | 0 .../wave153/wave153_bakeoff_kaggle}/README.md | 0 .../TECHNICAL_DETAILS.md | 0 .../analysis_report.json | 0 .../KRAKEN_ANALYSIS_SUMMARY.md | 0 .../analysis_report.json | 0 .../wave153_bakeoff_kraken}/btc_usd_raw.json | 0 .../download_kraken_data.py | 0 .../wave153_bakeoff_kraken}/eth_usd_raw.json | 0 .../reports/CLEANUP_REPORT_2025_10_30.md | 0 ...OCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md | 0 .../reports/DOCUMENTATION_ARCHIVAL_SUMMARY.md | 0 .../reports/POST_CLEANUP_VALIDATION_REPORT.md | 0 .../SCRIPTS_CLEANUP_REPORT_2025_10_30.md | 0 .../patches/RUNPOD_DEPLOY_PATCH.diff | 0 .../patches/batch_size_180_patch.diff | 0 dqn_tuning_archive_20251014.tar.gz | Bin 5996 -> 0 bytes echo | 2 - health_check.sh | 473 -- libregime_adx.rlib | Bin 45476 -> 0 bytes libtlob_transformer_test.rlib | Bin 5642 -> 0 bytes load-tests/IMMEDIATE_ACTIONS.md | 263 - load-tests/PERFORMANCE_SCORECARD.md | 85 - load-tests/README.md | 130 - load-tests/api_gateway_spike_load.json | 7 - load-tests/api_gateway_test.txt | 1 - load-tests/final_summary.txt | 5 - load-tests/ml_health_check.json | 1 - load-tests/ml_training_normal_load.json | 1 - load-tests/ml_training_normal_load.txt | 1 - load-tests/results_summary.txt | 6 - .../wave79/test1_normal_load.json | 217 - load_test_results/wave79/test_ml_health.txt | 87 - mamba2_training.pid | 1 - mamba2_training_200epoch.pid | 1 - .../best_epoch_0.safetensors | Bin .../best_epoch_0_optimizer.safetensors | Bin .../best_epoch_1.safetensors | Bin .../best_epoch_15.safetensors | Bin .../best_epoch_2.safetensors | Bin .../best_epoch_23.safetensors | Bin .../best_epoch_3.safetensors | Bin .../best_epoch_32.safetensors | Bin .../best_epoch_35.safetensors | Bin .../best_epoch_36.safetensors | Bin .../best_epoch_4.safetensors | Bin .../best_epoch_5.safetensors | Bin .../best_epoch_51.safetensors | Bin .../best_epoch_56.safetensors | Bin .../best_epoch_57.safetensors | Bin .../best_epoch_6.safetensors | Bin .../best_epoch_8.safetensors | Bin .../best_epoch_9.safetensors | Bin ml_strategy/Cargo.toml | 40 - ml_strategy/tests/shared_ml_strategy_test.rs | 428 -- .../compliance_validation_tests.txt | 7 - results/CHECKPOINT_BACKTEST_REPORT.md | 71 - results/authz_service_results.txt | 64 - results/backtest_results_20251014_133851.json | 1 - results/backtest_results_20251014_134158.json | 1 - results/backtest_results_20251014_135033.json | 30 - results/backtest_results_20251014_135038.json | 30 - results/backtest_results_20251014_135528.json | 30 - results/backtest_summary_20251014_143309.csv | 101 - results/backtest_summary_20251017_124647.csv | 101 - ...sive_backtest_results_20251014_143309.json | 1702 ----- ...sive_backtest_results_20251017_124647.json | 1702 ----- results/dqn_tuning_36trials_extracted.json | 354 - ...mble_backtest_results_20251014_151004.json | 87 - results/rate_limiter_results.txt | 144 - results/revocation_cache_results.txt | 241 - results/tuning_pilot_dqn.json | 65 - ...wave_comparison_ES.FUT_20251019_150543.csv | 10 - ...ave_comparison_ES.FUT_20251019_150543.json | 105 - run_comprehensive_tests.sh | 130 - runpod_debug/AGENT_05_MINIMAL_REPRODUCTION.md | 216 - runpod_debug/Cargo.toml | 31 - runpod_debug/Cargo_simple.toml | 17 - runpod_debug/DEPLOY_TESTS.md | 265 - runpod_debug/QUICK_START.md | 123 - runpod_debug/README.md | 157 - runpod_debug/SUMMARY.md | 226 - runpod_debug/test1_hello.rs | 8 - runpod_debug/test2_cuda_check.rs | 26 - runpod_debug/test3_candle_device.rs | 32 - runpod_debug/test4_parquet_read.rs | 35 - runpod_debug/test5_tft_minimal.rs | 26 - runpod_debug/upload_tests.sh | 33 - runpodctl | 0 .../DEPLOY_DQN_NOW.sh | 0 .../HYPEROPT_QUICK_MONITOR.sh | 0 .../LEVEL_1_ROLLBACK_TEST.sh | 0 .../LEVEL_2_ROLLBACK_TEST.sh | 0 .../LEVEL_3_ROLLBACK_TEST.sh | 0 .../MAMBA2_POD_MONITOR.sh | 0 .../QUICK_FIX_COMMANDS.sh | 0 .../analyze_clippy.sh | 0 .../apply_batch_size_fix.sh | 0 .../apply_ml_test_fixes.sh | 0 .../wave_scripts/WAVE112_QUICKSTART.sh | 0 .../WAVE113_AGENT34_VERIFICATION.sh | 0 .../WAVE113_CLIPPY_ACTION_PLAN.sh | 0 .../backtest_dqn_trials.sh | 0 .../backtest_dqn_trials_enhanced.sh | 0 .../benchmark_ensemble_db.sh | 0 .../benchmark_ensemble_db_quick.sh | 0 .../benchmark_mimalloc.sh | 0 .../benchmark_nextest.sh | 0 build.sh => scripts/build.sh | 0 bypass_cache.sh => scripts/bypass_cache.sh | 0 check-status.sh => scripts/check-status.sh | 0 check.sh => scripts/check.sh | 0 .../check_backtesting_health.sh | 0 .../check_hyperopt_status.sh | 0 .../concurrent_connection_test.sh | 0 .../cross_service_integration_test.sh | 0 .../database_validation.sh | 0 db_load_test.sh => scripts/db_load_test.sh | 0 .../db_load_test_pgbench.sh | 0 .../db_load_test_simple.sh | 0 .../db_load_test_v2.sh | 0 .../deploy_debug_image.sh | 0 .../deploy_fp32_production.sh | 0 .../deploy_mamba2_hyperopt.sh | 0 .../docker-init-migrations.sh | 0 .../e2e_integration_test.sh | 0 .../entrypoint-debug.sh | 0 .../entrypoint-generic.sh | 0 .../entrypoint-self-terminate.sh | 0 entrypoint.sh => scripts/entrypoint.sh | 0 .../entrypoint_debug.sh | 0 .../fix_api_gateway_mfa.sh | 0 .../fix_audit_compliance_part2.sh | 0 .../fix_mfa_compilation.sh | 0 fix_ml_tests.sh => scripts/fix_ml_tests.sh | 0 .../fix_oom_retry_compilation.sh | 0 .../fix_unsafe_blocks.sh | 0 .../fix_wave112_compilation.sh | 0 .../generate_dev_certs.sh | 0 .../grpc_integration_test.sh | 0 health-check.sh => scripts/health-check.sh | 0 scripts/health_check.sh | 531 +- .../launch_mamba2_training.sh | 0 .../mark_tests_ignored.sh | 0 measure_vram.sh => scripts/measure_vram.sh | 0 .../monitor_mamba2_hyperopt.sh | 0 .../optimize_batch_sizes.sh | 0 .../prefix_unused_vars.sh | 0 .../prepare-sqlx-offline.sh | 0 .../quick_health_check.sh | 0 scripts/run_comprehensive_tests.sh | 238 +- .../run_cross_validation.sh | 0 .../run_final_benchmarks.sh | 0 .../run_ghz_load_test.sh | 0 .../run_liquid_nn_tuning.sh | 0 .../run_load_tests.sh | 0 .../run_performance_benchmarks.sh | 0 .../run_ppo_comprehensive_tuning.sh | 0 .../run_smoke_tests.sh | 0 run_tests.sh => scripts/run_tests.sh | 0 .../run_tft_training.sh | 0 run_training.sh => scripts/run_training.sh | 0 .../runpod_validation_deploy.sh | 0 .../setup-database.sh | 0 setup.py => scripts/setup.py | 0 setup_lld.sh => scripts/setup_lld.sh | 0 .../simple_concurrent_test.sh | 0 .../staging_e2e_tests.sh | 0 start-tli.sh => scripts/start-tli.sh | 0 start.sh => scripts/start.sh | 0 .../start_all_services.sh | 0 .../start_backtesting.sh | 0 .../start_services.sh | 0 stop.sh => scripts/stop.sh | 0 stop_services.sh => scripts/stop_services.sh | 0 .../stub_ignored_tests.sh | 0 .../sustained_load_grpc_test.sh | 0 test_alerts.sh => scripts/test_alerts.sh | 0 .../test_circuit_breakers.sh | 0 .../test_crash_logging.sh | 0 .../test_dbn_loader_fix.sh | 0 .../test_dqn_checkpoints_quick.sh | 0 .../test_graceful_degradation.sh | 0 .../test_grpc_proxies.sh | 0 .../test_liquid_nn_readiness.sh | 0 .../test_market_data.sh | 0 .../test_ppo_checkpoint.sh | 0 .../upload_to_runpod_s3.sh | 0 .../validate-docker-config.sh | 0 .../validate_ab_testing_tdd.sh | 0 .../validate_agent_9_13.sh | 0 .../verify_dataset_coverage.sh | 0 .../verify_db_optimization.sh | 0 .../verify_dbn_fix.sh | 0 .../verify_dbn_loader_fix.sh | 0 .../verify_dockerfile_updates.sh | 0 .../verify_documentation_structure.sh | 0 .../verify_tft_checkpoint_fix.sh | 0 .../verify_tft_cuda_fix.sh | 0 .../verify_tft_cuda_setup.sh | 0 small_parquet_tool/Cargo.toml | 22 - small_parquet_tool/src/main.rs | 153 - smoke_test.pid | 1 - .../PAPER_TRADING_DIAGNOSTIC_QUERIES.sql | 0 .../test_pg_performance.sql | 0 .../test_stop_loss_debug.sql | 0 .../trading_workload.sql | 0 ...dbn_debug.5a8spbt8g603bypb5vds2bxfi.rcgu.o | Bin 3112 -> 0 bytes ...g_service_order_status_20251003_173606.txt | 0 ..._service_order_status_20251003_173658.json | 1 - ...g_service_order_status_20251003_173658.txt | 6048 ---------------- ...ing_service_orderbook_20251003_173658.json | 1 - ...ding_service_orderbook_20251003_173658.txt | 6037 ---------------- ...ing_service_portfolio_20251003_173658.json | 1 - ...ding_service_portfolio_20251003_173658.txt | 6048 ---------------- ...ing_service_positions_20251003_173658.json | 1 - ...ding_service_positions_20251003_173658.txt | 6051 ----------------- .../standalone/check_parquet_rows.rs | 0 .../standalone/inspect_schema.rs | 0 .../standalone/inspect_schema_simple.rs | 0 .../standalone/position_manager_fixes.rs | 0 .../standalone/redis_validation_test.rs | 0 .../standalone/test_dbn_debug.rs | 0 .../standalone/test_dbn_decoder.rs | 0 .../standalone/test_dqn_imports.rs | 0 .../standalone/test_hurst_div_zero.rs | 0 .../standalone/test_regime_db_integration.rs | 0 .../standalone/validate_14ns_claims.rs | 0 .../standalone/verify_normalization.rs | 0 .../standalone/verify_tft_lru_fix.rs | 0 .../watch_tuning_progress_updated.rs | 0 verify_normalization | Bin 3876608 -> 0 bytes 424 files changed, 859 insertions(+), 33479 deletions(-) delete mode 100644 All targets: delete mode 100644 Error count: delete mode 100644 Production libs: delete mode 100644 Warning count: rename Cargo.toml.optimized => config/alternative/Cargo.toml.optimized (100%) rename server-extensions.cnf => config/server-extensions.cnf (100%) delete mode 100644 coverage_common/html/control.js delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/compliance_config.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/error.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html delete mode 100644 coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html delete mode 100644 coverage_common/html/index.html delete mode 100644 coverage_common/html/style.css delete mode 100644 coverage_report_common/html/control.js delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/market_data.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/ml_strategy.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html delete mode 100644 coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html delete mode 100644 coverage_report_common/html/index.html delete mode 100644 coverage_report_common/html/style.css delete mode 100644 coverage_risk/html/control.js delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/circuit_breaker.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/compliance.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/drawdown_monitor.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/error.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/lib.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/operations.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/position_tracker.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_engine.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_types.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/emergency_response.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/kill_switch.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/mod.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/position_limiter.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/safety_coordinator.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/trading_gate.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/unix_socket_kill_switch.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/stress_tester.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/expected_shortfall.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/historical_simulation.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/monte_carlo.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/parametric.rs.html delete mode 100644 coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/var_engine.rs.html delete mode 100644 coverage_risk/html/index.html delete mode 100644 coverage_risk/html/style.css create mode 100644 docs/archive/agents/AGENT_TXT_FILES_ANALYSIS.md rename AGENT_10_1_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_10_1_SUMMARY.txt (100%) rename AGENT_10_3_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_10_3_SUMMARY.txt (100%) rename AGENT_10_6_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_10_6_SUMMARY.txt (100%) rename AGENT_10_7_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_10_7_SUMMARY.txt (100%) rename AGENT_152_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_152_SUMMARY.txt (100%) rename AGENT_160_VISUAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_160_VISUAL_SUMMARY.txt (100%) rename AGENT_19_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_19_SUMMARY.txt (100%) rename AGENT_223_VISUAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_223_VISUAL_SUMMARY.txt (100%) rename AGENT_244_TEST_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_244_TEST_SUMMARY.txt (100%) rename AGENT_256_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_256_SUMMARY.txt (100%) rename AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt (100%) rename AGENT_257_TEST_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_257_TEST_SUMMARY.txt (100%) rename AGENT_258_VISUAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_258_VISUAL_SUMMARY.txt (100%) rename AGENT_395_COMPLETE.txt => docs/archive/agents/legacy_txt/AGENT_395_COMPLETE.txt (100%) rename AGENT_43_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_43_SUMMARY.txt (100%) rename AGENT_5_ARCHITECTURE_DIAGRAM.txt => docs/archive/agents/legacy_txt/AGENT_5_ARCHITECTURE_DIAGRAM.txt (100%) rename AGENT_6_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_6_SUMMARY.txt (100%) rename AGENT_86_BENCHMARK_GAP_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_86_BENCHMARK_GAP_SUMMARY.txt (100%) rename AGENT_86_FINAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_86_FINAL_SUMMARY.txt (100%) rename AGENT_916_VISUAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_916_VISUAL_SUMMARY.txt (100%) rename AGENT_9_13_COMMIT_MESSAGE.txt => docs/archive/agents/legacy_txt/AGENT_9_13_COMMIT_MESSAGE.txt (100%) rename AGENT_9_13_VISUAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_9_13_VISUAL_SUMMARY.txt (100%) rename AGENT_BLOCK02_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_BLOCK02_SUMMARY.txt (100%) rename AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt => docs/archive/agents/legacy_txt/AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt (100%) rename AGENT_E6_BENCHMARK_RAW_OUTPUT.txt => docs/archive/agents/legacy_txt/AGENT_E6_BENCHMARK_RAW_OUTPUT.txt (100%) rename AGENT_E6_PERFORMANCE_VISUALIZATION.txt => docs/archive/agents/legacy_txt/AGENT_E6_PERFORMANCE_VISUALIZATION.txt (100%) rename AGENT_F11_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_F11_SUMMARY.txt (100%) rename AGENT_F16_VISUAL_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_F16_VISUAL_SUMMARY.txt (100%) rename AGENT_F23_EXECUTIVE_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_F23_EXECUTIVE_SUMMARY.txt (100%) rename AGENT_G19_SUCCESS_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_G19_SUCCESS_SUMMARY.txt (100%) rename AGENT_IMPL18_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_IMPL18_SUMMARY.txt (100%) rename AGENT_M13_MANIFEST.txt => docs/archive/agents/legacy_txt/AGENT_M13_MANIFEST.txt (100%) rename AGENT_M13_QUICK_REFERENCE.txt => docs/archive/agents/legacy_txt/AGENT_M13_QUICK_REFERENCE.txt (100%) rename AGENT_T22_DELIVERABLES_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_T22_DELIVERABLES_SUMMARY.txt (100%) rename AGENT_VAL28_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_VAL28_SUMMARY.txt (100%) rename AGENT_VAL30_QUICK_SUMMARY.txt => docs/archive/agents/legacy_txt/AGENT_VAL30_QUICK_SUMMARY.txt (100%) rename AGENT_WIRE14_INTEGRATION_GAPS.txt => docs/archive/agents/legacy_txt/AGENT_WIRE14_INTEGRATION_GAPS.txt (100%) create mode 100644 docs/archive/agents/legacy_txt/README.md rename agent_199_infrastructure_validation.txt => docs/archive/agents/legacy_txt/agent_199_infrastructure_validation.txt (100%) rename agent_200_test_environment_setup.txt => docs/archive/agents/legacy_txt/agent_200_test_environment_setup.txt (100%) rename agent_219_async_audit_design.txt => docs/archive/agents/legacy_txt/agent_219_async_audit_design.txt (100%) rename agent_228_implementation_report.txt => docs/archive/agents/legacy_txt/agent_228_implementation_report.txt (100%) rename agent_229v2_jwt_validation_report.txt => docs/archive/agents/legacy_txt/agent_229v2_jwt_validation_report.txt (100%) rename agent_275_missing_fields_fixed.txt => docs/archive/agents/legacy_txt/agent_275_missing_fields_fixed.txt (100%) rename agent_278_trading_data_fixed.txt => docs/archive/agents/legacy_txt/agent_278_trading_data_fixed.txt (100%) rename agent_279_format_strings_fixed.txt => docs/archive/agents/legacy_txt/agent_279_format_strings_fixed.txt (100%) rename agent_280_load_tests_fixed.txt => docs/archive/agents/legacy_txt/agent_280_load_tests_fixed.txt (100%) rename agent_281_api_gateway_tests_fixed.txt => docs/archive/agents/legacy_txt/agent_281_api_gateway_tests_fixed.txt (100%) rename agent_283_future_traits_fixed.txt => docs/archive/agents/legacy_txt/agent_283_future_traits_fixed.txt (100%) rename agent_284_criterion_fixed.txt => docs/archive/agents/legacy_txt/agent_284_criterion_fixed.txt (100%) rename agent_288_benchmark_deps_fixed.txt => docs/archive/agents/legacy_txt/agent_288_benchmark_deps_fixed.txt (100%) rename agent_289_database_tli_fixed.txt => docs/archive/agents/legacy_txt/agent_289_database_tli_fixed.txt (100%) rename agent_291_tli_storage_fixed.txt => docs/archive/agents/legacy_txt/agent_291_tli_storage_fixed.txt (100%) rename agent_295_api_gateway_fixed.txt => docs/archive/agents/legacy_txt/agent_295_api_gateway_fixed.txt (100%) rename agent_297_backtesting_service_fixed.txt => docs/archive/agents/legacy_txt/agent_297_backtesting_service_fixed.txt (100%) rename agent_304_remaining_crates_fixed.txt => docs/archive/agents/legacy_txt/agent_304_remaining_crates_fixed.txt (100%) rename agent_306_api_gateway_unwrap_fixed.txt => docs/archive/agents/legacy_txt/agent_306_api_gateway_unwrap_fixed.txt (100%) rename agent_307_trading_service_panics_fixed.txt => docs/archive/agents/legacy_txt/agent_307_trading_service_panics_fixed.txt (100%) rename agent_311_storage_safety_fixed.txt => docs/archive/agents/legacy_txt/agent_311_storage_safety_fixed.txt (100%) rename agent_312_ml_safety_fixed.txt => docs/archive/agents/legacy_txt/agent_312_ml_safety_fixed.txt (100%) rename agent_313_trading_engine_safety_fixed.txt => docs/archive/agents/legacy_txt/agent_313_trading_engine_safety_fixed.txt (100%) rename agent_314_backtesting_safety_fixed.txt => docs/archive/agents/legacy_txt/agent_314_backtesting_safety_fixed.txt (100%) rename agent_322_risk_precision_fixed.txt => docs/archive/agents/legacy_txt/agent_322_risk_precision_fixed.txt (100%) rename agent_323_data_types_fixed.txt => docs/archive/agents/legacy_txt/agent_323_data_types_fixed.txt (100%) rename agent_324_ml_types_fixed.txt => docs/archive/agents/legacy_txt/agent_324_ml_types_fixed.txt (100%) rename agent_326_doc_markdown_fixes.txt => docs/archive/agents/legacy_txt/agent_326_doc_markdown_fixes.txt (100%) rename agent_331_float_arithmetic_report.txt => docs/archive/agents/legacy_txt/agent_331_float_arithmetic_report.txt (100%) rename agent_334_float_arithmetic_ml.txt => docs/archive/agents/legacy_txt/agent_334_float_arithmetic_ml.txt (100%) rename agent_335_as_conversions_fixed.txt => docs/archive/agents/legacy_txt/agent_335_as_conversions_fixed.txt (100%) rename agent_341_unsafe_documentation_report.txt => docs/archive/agents/legacy_txt/agent_341_unsafe_documentation_report.txt (100%) rename agent_342_numeric_fallback_fixes.txt => docs/archive/agents/legacy_txt/agent_342_numeric_fallback_fixes.txt (100%) rename agent_349_backticks_1501_2000.txt => docs/archive/agents/legacy_txt/agent_349_backticks_1501_2000.txt (100%) rename agent_350_doc_backticks_part5.txt => docs/archive/agents/legacy_txt/agent_350_doc_backticks_part5.txt (100%) rename agent_367_field_visibility_report.txt => docs/archive/agents/legacy_txt/agent_367_field_visibility_report.txt (100%) rename agent_373_wave6_error_analysis.txt => docs/archive/agents/legacy_txt/agent_373_wave6_error_analysis.txt (100%) rename agent_402_remaining_errors.txt => docs/archive/agents/legacy_txt/agent_402_remaining_errors.txt (100%) rename agent_402_summary.txt => docs/archive/agents/legacy_txt/agent_402_summary.txt (100%) rename agent_418_float_arithmetic_fixed.txt => docs/archive/agents/legacy_txt/agent_418_float_arithmetic_fixed.txt (100%) rename agent_422_as_conversions_report.txt => docs/archive/agents/legacy_txt/agent_422_as_conversions_report.txt (100%) rename agent_437_map_err_fixes.txt => docs/archive/agents/legacy_txt/agent_437_map_err_fixes.txt (100%) rename agent_442_ml_indexing_final_report.txt => docs/archive/agents/legacy_txt/agent_442_ml_indexing_final_report.txt (100%) rename agent_445_arithmetic_cleanup_part2.txt => docs/archive/agents/legacy_txt/agent_445_arithmetic_cleanup_part2.txt (100%) rename agent_451_unused_self_cleanup_final.txt => docs/archive/agents/legacy_txt/agent_451_unused_self_cleanup_final.txt (100%) rename agent_470_e0599_final_cleanup.txt => docs/archive/agents/legacy_txt/agent_470_e0599_final_cleanup.txt (100%) rename agent_489_trading_engine_final.txt => docs/archive/agents/legacy_txt/agent_489_trading_engine_final.txt (100%) rename agent_comprehensive_finalization_analysis.txt => docs/archive/agents/legacy_txt/agent_comprehensive_finalization_analysis.txt (100%) rename CUDA_13_GPU_VALIDATION_CHECKLIST.md => docs/archive/historical/CUDA_13_GPU_VALIDATION_CHECKLIST.md (100%) rename DOCKER_CLEANUP_QUICK_REF.md => docs/archive/historical/DOCKER_CLEANUP_QUICK_REF.md (100%) rename LEGACY_256_CLEANUP_CHECKLIST.md => docs/archive/historical/LEGACY_256_CLEANUP_CHECKLIST.md (100%) rename {wave153_bakeoff_kaggle => docs/archive/historical/wave153/wave153_bakeoff_kaggle}/EXECUTIVE_SUMMARY.md (100%) rename {wave153_bakeoff_kaggle => docs/archive/historical/wave153/wave153_bakeoff_kaggle}/QUICK_REFERENCE.md (100%) rename {wave153_bakeoff_kaggle => docs/archive/historical/wave153/wave153_bakeoff_kaggle}/README.md (100%) rename {wave153_bakeoff_kaggle => docs/archive/historical/wave153/wave153_bakeoff_kaggle}/TECHNICAL_DETAILS.md (100%) rename {wave153_bakeoff_kaggle => docs/archive/historical/wave153/wave153_bakeoff_kaggle}/analysis_report.json (100%) rename {wave153_bakeoff_kraken => docs/archive/historical/wave153/wave153_bakeoff_kraken}/KRAKEN_ANALYSIS_SUMMARY.md (100%) rename {wave153_bakeoff_kraken => docs/archive/historical/wave153/wave153_bakeoff_kraken}/analysis_report.json (100%) rename {wave153_bakeoff_kraken => docs/archive/historical/wave153/wave153_bakeoff_kraken}/btc_usd_raw.json (100%) rename {wave153_bakeoff_kraken => docs/archive/historical/wave153/wave153_bakeoff_kraken}/download_kraken_data.py (100%) rename {wave153_bakeoff_kraken => docs/archive/historical/wave153/wave153_bakeoff_kraken}/eth_usd_raw.json (100%) rename CLEANUP_REPORT_2025_10_30.md => docs/archive/wave_d/reports/CLEANUP_REPORT_2025_10_30.md (100%) rename DOCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md => docs/archive/wave_d/reports/DOCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md (100%) rename DOCUMENTATION_ARCHIVAL_SUMMARY.md => docs/archive/wave_d/reports/DOCUMENTATION_ARCHIVAL_SUMMARY.md (100%) rename POST_CLEANUP_VALIDATION_REPORT.md => docs/archive/wave_d/reports/POST_CLEANUP_VALIDATION_REPORT.md (100%) rename SCRIPTS_CLEANUP_REPORT_2025_10_30.md => docs/archive/wave_d/reports/SCRIPTS_CLEANUP_REPORT_2025_10_30.md (100%) rename RUNPOD_DEPLOY_PATCH.diff => docs/patches/RUNPOD_DEPLOY_PATCH.diff (100%) rename batch_size_180_patch.diff => docs/patches/batch_size_180_patch.diff (100%) delete mode 100644 dqn_tuning_archive_20251014.tar.gz delete mode 100644 echo delete mode 100755 health_check.sh delete mode 100644 libregime_adx.rlib delete mode 100644 libtlob_transformer_test.rlib delete mode 100644 load-tests/IMMEDIATE_ACTIONS.md delete mode 100644 load-tests/PERFORMANCE_SCORECARD.md delete mode 100644 load-tests/README.md delete mode 100644 load-tests/api_gateway_spike_load.json delete mode 100644 load-tests/api_gateway_test.txt delete mode 100644 load-tests/final_summary.txt delete mode 100644 load-tests/ml_health_check.json delete mode 100644 load-tests/ml_training_normal_load.json delete mode 100644 load-tests/ml_training_normal_load.txt delete mode 100644 load-tests/results_summary.txt delete mode 100644 load_test_results/wave79/test1_normal_load.json delete mode 100644 load_test_results/wave79/test_ml_health.txt delete mode 100644 mamba2_training.pid delete mode 100644 mamba2_training_200epoch.pid rename best_epoch_0.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_0.safetensors (100%) rename best_epoch_0_optimizer.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_0_optimizer.safetensors (100%) rename best_epoch_1.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_1.safetensors (100%) rename best_epoch_15.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_15.safetensors (100%) rename best_epoch_2.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_2.safetensors (100%) rename best_epoch_23.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_23.safetensors (100%) rename best_epoch_3.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_3.safetensors (100%) rename best_epoch_32.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_32.safetensors (100%) rename best_epoch_35.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_35.safetensors (100%) rename best_epoch_36.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_36.safetensors (100%) rename best_epoch_4.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_4.safetensors (100%) rename best_epoch_5.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_5.safetensors (100%) rename best_epoch_51.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_51.safetensors (100%) rename best_epoch_56.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_56.safetensors (100%) rename best_epoch_57.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_57.safetensors (100%) rename best_epoch_6.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_6.safetensors (100%) rename best_epoch_8.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_8.safetensors (100%) rename best_epoch_9.safetensors => ml/models/checkpoints/training_artifacts/best_epoch_9.safetensors (100%) delete mode 100644 ml_strategy/Cargo.toml delete mode 100644 ml_strategy/tests/shared_ml_strategy_test.rs delete mode 100644 proptest-regressions/compliance_validation_tests.txt delete mode 100644 results/CHECKPOINT_BACKTEST_REPORT.md delete mode 100644 results/authz_service_results.txt delete mode 100644 results/backtest_results_20251014_133851.json delete mode 100644 results/backtest_results_20251014_134158.json delete mode 100644 results/backtest_results_20251014_135033.json delete mode 100644 results/backtest_results_20251014_135038.json delete mode 100644 results/backtest_results_20251014_135528.json delete mode 100644 results/backtest_summary_20251014_143309.csv delete mode 100644 results/backtest_summary_20251017_124647.csv delete mode 100644 results/comprehensive_backtest_results_20251014_143309.json delete mode 100644 results/comprehensive_backtest_results_20251017_124647.json delete mode 100644 results/dqn_tuning_36trials_extracted.json delete mode 100644 results/ensemble_backtest_results_20251014_151004.json delete mode 100644 results/rate_limiter_results.txt delete mode 100644 results/revocation_cache_results.txt delete mode 100644 results/tuning_pilot_dqn.json delete mode 100644 results/wave_comparison_ES.FUT_20251019_150543.csv delete mode 100644 results/wave_comparison_ES.FUT_20251019_150543.json delete mode 100755 run_comprehensive_tests.sh delete mode 100644 runpod_debug/AGENT_05_MINIMAL_REPRODUCTION.md delete mode 100644 runpod_debug/Cargo.toml delete mode 100644 runpod_debug/Cargo_simple.toml delete mode 100644 runpod_debug/DEPLOY_TESTS.md delete mode 100644 runpod_debug/QUICK_START.md delete mode 100644 runpod_debug/README.md delete mode 100644 runpod_debug/SUMMARY.md delete mode 100644 runpod_debug/test1_hello.rs delete mode 100644 runpod_debug/test2_cuda_check.rs delete mode 100644 runpod_debug/test3_candle_device.rs delete mode 100644 runpod_debug/test4_parquet_read.rs delete mode 100644 runpod_debug/test5_tft_minimal.rs delete mode 100755 runpod_debug/upload_tests.sh delete mode 100644 runpodctl rename DEPLOY_DQN_NOW.sh => scripts/DEPLOY_DQN_NOW.sh (100%) rename HYPEROPT_QUICK_MONITOR.sh => scripts/HYPEROPT_QUICK_MONITOR.sh (100%) rename LEVEL_1_ROLLBACK_TEST.sh => scripts/LEVEL_1_ROLLBACK_TEST.sh (100%) rename LEVEL_2_ROLLBACK_TEST.sh => scripts/LEVEL_2_ROLLBACK_TEST.sh (100%) rename LEVEL_3_ROLLBACK_TEST.sh => scripts/LEVEL_3_ROLLBACK_TEST.sh (100%) rename MAMBA2_POD_MONITOR.sh => scripts/MAMBA2_POD_MONITOR.sh (100%) rename QUICK_FIX_COMMANDS.sh => scripts/QUICK_FIX_COMMANDS.sh (100%) rename analyze_clippy.sh => scripts/analyze_clippy.sh (100%) rename apply_batch_size_fix.sh => scripts/apply_batch_size_fix.sh (100%) rename apply_ml_test_fixes.sh => scripts/apply_ml_test_fixes.sh (100%) rename WAVE112_QUICKSTART.sh => scripts/archive/wave_scripts/WAVE112_QUICKSTART.sh (100%) rename WAVE113_AGENT34_VERIFICATION.sh => scripts/archive/wave_scripts/WAVE113_AGENT34_VERIFICATION.sh (100%) rename WAVE113_CLIPPY_ACTION_PLAN.sh => scripts/archive/wave_scripts/WAVE113_CLIPPY_ACTION_PLAN.sh (100%) rename backtest_dqn_trials.sh => scripts/backtest_dqn_trials.sh (100%) rename backtest_dqn_trials_enhanced.sh => scripts/backtest_dqn_trials_enhanced.sh (100%) rename benchmark_ensemble_db.sh => scripts/benchmark_ensemble_db.sh (100%) rename benchmark_ensemble_db_quick.sh => scripts/benchmark_ensemble_db_quick.sh (100%) rename benchmark_mimalloc.sh => scripts/benchmark_mimalloc.sh (100%) rename benchmark_nextest.sh => scripts/benchmark_nextest.sh (100%) rename build.sh => scripts/build.sh (100%) rename bypass_cache.sh => scripts/bypass_cache.sh (100%) rename check-status.sh => scripts/check-status.sh (100%) rename check.sh => scripts/check.sh (100%) rename check_backtesting_health.sh => scripts/check_backtesting_health.sh (100%) rename check_hyperopt_status.sh => scripts/check_hyperopt_status.sh (100%) rename concurrent_connection_test.sh => scripts/concurrent_connection_test.sh (100%) rename cross_service_integration_test.sh => scripts/cross_service_integration_test.sh (100%) rename database_validation.sh => scripts/database_validation.sh (100%) rename db_load_test.sh => scripts/db_load_test.sh (100%) rename db_load_test_pgbench.sh => scripts/db_load_test_pgbench.sh (100%) rename db_load_test_simple.sh => scripts/db_load_test_simple.sh (100%) rename db_load_test_v2.sh => scripts/db_load_test_v2.sh (100%) rename deploy_debug_image.sh => scripts/deploy_debug_image.sh (100%) rename deploy_fp32_production.sh => scripts/deploy_fp32_production.sh (100%) rename deploy_mamba2_hyperopt.sh => scripts/deploy_mamba2_hyperopt.sh (100%) rename docker-init-migrations.sh => scripts/docker-init-migrations.sh (100%) rename e2e_integration_test.sh => scripts/e2e_integration_test.sh (100%) rename entrypoint-debug.sh => scripts/entrypoint-debug.sh (100%) rename entrypoint-generic.sh => scripts/entrypoint-generic.sh (100%) rename entrypoint-self-terminate.sh => scripts/entrypoint-self-terminate.sh (100%) rename entrypoint.sh => scripts/entrypoint.sh (100%) rename entrypoint_debug.sh => scripts/entrypoint_debug.sh (100%) rename fix_api_gateway_mfa.sh => scripts/fix_api_gateway_mfa.sh (100%) rename fix_audit_compliance_part2.sh => scripts/fix_audit_compliance_part2.sh (100%) rename fix_mfa_compilation.sh => scripts/fix_mfa_compilation.sh (100%) rename fix_ml_tests.sh => scripts/fix_ml_tests.sh (100%) rename fix_oom_retry_compilation.sh => scripts/fix_oom_retry_compilation.sh (100%) rename fix_unsafe_blocks.sh => scripts/fix_unsafe_blocks.sh (100%) rename fix_wave112_compilation.sh => scripts/fix_wave112_compilation.sh (100%) rename generate_dev_certs.sh => scripts/generate_dev_certs.sh (100%) rename grpc_integration_test.sh => scripts/grpc_integration_test.sh (100%) rename health-check.sh => scripts/health-check.sh (100%) rename launch_mamba2_training.sh => scripts/launch_mamba2_training.sh (100%) rename mark_tests_ignored.sh => scripts/mark_tests_ignored.sh (100%) rename measure_vram.sh => scripts/measure_vram.sh (100%) rename monitor_mamba2_hyperopt.sh => scripts/monitor_mamba2_hyperopt.sh (100%) rename optimize_batch_sizes.sh => scripts/optimize_batch_sizes.sh (100%) rename prefix_unused_vars.sh => scripts/prefix_unused_vars.sh (100%) rename prepare-sqlx-offline.sh => scripts/prepare-sqlx-offline.sh (100%) rename quick_health_check.sh => scripts/quick_health_check.sh (100%) rename run_cross_validation.sh => scripts/run_cross_validation.sh (100%) rename run_final_benchmarks.sh => scripts/run_final_benchmarks.sh (100%) rename run_ghz_load_test.sh => scripts/run_ghz_load_test.sh (100%) rename run_liquid_nn_tuning.sh => scripts/run_liquid_nn_tuning.sh (100%) rename run_load_tests.sh => scripts/run_load_tests.sh (100%) rename run_performance_benchmarks.sh => scripts/run_performance_benchmarks.sh (100%) rename run_ppo_comprehensive_tuning.sh => scripts/run_ppo_comprehensive_tuning.sh (100%) rename run_smoke_tests.sh => scripts/run_smoke_tests.sh (100%) rename run_tests.sh => scripts/run_tests.sh (100%) rename run_tft_training.sh => scripts/run_tft_training.sh (100%) rename run_training.sh => scripts/run_training.sh (100%) rename runpod_validation_deploy.sh => scripts/runpod_validation_deploy.sh (100%) rename setup-database.sh => scripts/setup-database.sh (100%) rename setup.py => scripts/setup.py (100%) rename setup_lld.sh => scripts/setup_lld.sh (100%) rename simple_concurrent_test.sh => scripts/simple_concurrent_test.sh (100%) rename staging_e2e_tests.sh => scripts/staging_e2e_tests.sh (100%) rename start-tli.sh => scripts/start-tli.sh (100%) rename start.sh => scripts/start.sh (100%) rename start_all_services.sh => scripts/start_all_services.sh (100%) rename start_backtesting.sh => scripts/start_backtesting.sh (100%) rename start_services.sh => scripts/start_services.sh (100%) rename stop.sh => scripts/stop.sh (100%) rename stop_services.sh => scripts/stop_services.sh (100%) rename stub_ignored_tests.sh => scripts/stub_ignored_tests.sh (100%) rename sustained_load_grpc_test.sh => scripts/sustained_load_grpc_test.sh (100%) rename test_alerts.sh => scripts/test_alerts.sh (100%) rename test_circuit_breakers.sh => scripts/test_circuit_breakers.sh (100%) rename test_crash_logging.sh => scripts/test_crash_logging.sh (100%) rename test_dbn_loader_fix.sh => scripts/test_dbn_loader_fix.sh (100%) rename test_dqn_checkpoints_quick.sh => scripts/test_dqn_checkpoints_quick.sh (100%) rename test_graceful_degradation.sh => scripts/test_graceful_degradation.sh (100%) rename test_grpc_proxies.sh => scripts/test_grpc_proxies.sh (100%) rename test_liquid_nn_readiness.sh => scripts/test_liquid_nn_readiness.sh (100%) rename test_market_data.sh => scripts/test_market_data.sh (100%) rename test_ppo_checkpoint.sh => scripts/test_ppo_checkpoint.sh (100%) rename upload_to_runpod_s3.sh => scripts/upload_to_runpod_s3.sh (100%) rename validate-docker-config.sh => scripts/validate-docker-config.sh (100%) rename validate_ab_testing_tdd.sh => scripts/validate_ab_testing_tdd.sh (100%) rename validate_agent_9_13.sh => scripts/validate_agent_9_13.sh (100%) rename verify_dataset_coverage.sh => scripts/verify_dataset_coverage.sh (100%) rename verify_db_optimization.sh => scripts/verify_db_optimization.sh (100%) rename verify_dbn_fix.sh => scripts/verify_dbn_fix.sh (100%) rename verify_dbn_loader_fix.sh => scripts/verify_dbn_loader_fix.sh (100%) rename verify_dockerfile_updates.sh => scripts/verify_dockerfile_updates.sh (100%) rename verify_documentation_structure.sh => scripts/verify_documentation_structure.sh (100%) rename verify_tft_checkpoint_fix.sh => scripts/verify_tft_checkpoint_fix.sh (100%) rename verify_tft_cuda_fix.sh => scripts/verify_tft_cuda_fix.sh (100%) rename verify_tft_cuda_setup.sh => scripts/verify_tft_cuda_setup.sh (100%) delete mode 100644 small_parquet_tool/Cargo.toml delete mode 100644 small_parquet_tool/src/main.rs delete mode 100644 smoke_test.pid rename PAPER_TRADING_DIAGNOSTIC_QUERIES.sql => sql/PAPER_TRADING_DIAGNOSTIC_QUERIES.sql (100%) rename test_pg_performance.sql => sql/test_pg_performance.sql (100%) rename test_stop_loss_debug.sql => sql/test_stop_loss_debug.sql (100%) rename trading_workload.sql => sql/trading_workload.sql (100%) delete mode 100644 test_dbn_debug.5a8spbt8g603bypb5vds2bxfi.rcgu.o delete mode 100644 test_results/grpc_load_tests/trading_service_order_status_20251003_173606.txt delete mode 100644 test_results/grpc_load_tests/trading_service_order_status_20251003_173658.json delete mode 100644 test_results/grpc_load_tests/trading_service_order_status_20251003_173658.txt delete mode 100644 test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.json delete mode 100644 test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.txt delete mode 100644 test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.json delete mode 100644 test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.txt delete mode 100644 test_results/grpc_load_tests/trading_service_positions_20251003_173658.json delete mode 100644 test_results/grpc_load_tests/trading_service_positions_20251003_173658.txt rename check_parquet_rows.rs => tests/standalone/check_parquet_rows.rs (100%) rename inspect_schema.rs => tests/standalone/inspect_schema.rs (100%) rename inspect_schema_simple.rs => tests/standalone/inspect_schema_simple.rs (100%) rename position_manager_fixes.rs => tests/standalone/position_manager_fixes.rs (100%) rename redis_validation_test.rs => tests/standalone/redis_validation_test.rs (100%) rename test_dbn_debug.rs => tests/standalone/test_dbn_debug.rs (100%) rename test_dbn_decoder.rs => tests/standalone/test_dbn_decoder.rs (100%) rename test_dqn_imports.rs => tests/standalone/test_dqn_imports.rs (100%) rename test_hurst_div_zero.rs => tests/standalone/test_hurst_div_zero.rs (100%) rename test_regime_db_integration.rs => tests/standalone/test_regime_db_integration.rs (100%) rename validate_14ns_claims.rs => tests/standalone/validate_14ns_claims.rs (100%) rename verify_normalization.rs => tests/standalone/verify_normalization.rs (100%) rename verify_tft_lru_fix.rs => tests/standalone/verify_tft_lru_fix.rs (100%) rename watch_tuning_progress_updated.rs => tests/standalone/watch_tuning_progress_updated.rs (100%) delete mode 100755 verify_normalization diff --git a/All targets: b/All targets: deleted file mode 100644 index e69de29bb..000000000 diff --git a/Error count: b/Error count: deleted file mode 100644 index e69de29bb..000000000 diff --git a/Production libs: b/Production libs: deleted file mode 100644 index e69de29bb..000000000 diff --git a/Warning count: b/Warning count: deleted file mode 100644 index e69de29bb..000000000 diff --git a/Cargo.toml.optimized b/config/alternative/Cargo.toml.optimized similarity index 100% rename from Cargo.toml.optimized rename to config/alternative/Cargo.toml.optimized diff --git a/server-extensions.cnf b/config/server-extensions.cnf similarity index 100% rename from server-extensions.cnf rename to config/server-extensions.cnf diff --git a/coverage_common/html/control.js b/coverage_common/html/control.js deleted file mode 100644 index 5897b005c..000000000 --- a/coverage_common/html/control.js +++ /dev/null @@ -1,99 +0,0 @@ - -function next_uncovered(selector, reverse, scroll_selector) { - function visit_element(element) { - element.classList.add("seen"); - element.classList.add("selected"); - - if (!scroll_selector) { - scroll_selector = "tr:has(.selected) td.line-number" - } - - const scroll_to = document.querySelector(scroll_selector); - if (scroll_to) { - scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"}); - } - } - - function select_one() { - if (!reverse) { - const previously_selected = document.querySelector(".selected"); - - if (previously_selected) { - previously_selected.classList.remove("selected"); - } - - return document.querySelector(selector + ":not(.seen)"); - } else { - const previously_selected = document.querySelector(".selected"); - - if (previously_selected) { - previously_selected.classList.remove("selected"); - previously_selected.classList.remove("seen"); - } - - const nodes = document.querySelectorAll(selector + ".seen"); - if (nodes) { - const last = nodes[nodes.length - 1]; // last - return last; - } else { - return undefined; - } - } - } - - function reset_all() { - if (!reverse) { - const all_seen = document.querySelectorAll(selector + ".seen"); - - if (all_seen) { - all_seen.forEach(e => e.classList.remove("seen")); - } - } else { - const all_seen = document.querySelectorAll(selector + ":not(.seen)"); - - if (all_seen) { - all_seen.forEach(e => e.classList.add("seen")); - } - } - - } - - const uncovered = select_one(); - - if (uncovered) { - visit_element(uncovered); - } else { - reset_all(); - - const uncovered = select_one(); - - if (uncovered) { - visit_element(uncovered); - } - } -} - -function next_line(reverse) { - next_uncovered("td.uncovered-line", reverse) -} - -function next_region(reverse) { - next_uncovered("span.red.region", reverse); -} - -function next_branch(reverse) { - next_uncovered("span.red.branch", reverse); -} - -document.addEventListener("keypress", function(event) { - const reverse = event.shiftKey; - if (event.code == "KeyL") { - next_line(reverse); - } - if (event.code == "KeyB") { - next_branch(reverse); - } - if (event.code == "KeyR") { - next_region(reverse); - } -}); diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html deleted file mode 100644 index c76b15d8f..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/database.rs
Line
Count
Source
1
//! Database connection utilities and configurations
2
//!
3
//! This module provides shared database connection management utilities
4
//! that can be used across all Foxhunt services.
5
6
use serde::{Deserialize, Serialize};
7
use sqlx::{Pool, Postgres};
8
use std::time::Duration;
9
use thiserror::Error;
10
11
// Import centralized database configuration
12
pub use config::database::DatabaseConfig;
13
use config::structures::BacktestingDatabaseConfig;
14
15
/// Database-specific errors
16
#[derive(Debug, Error)]
17
pub enum DatabaseError {
18
    /// Connection failed - wrapper around SQLx connection errors
19
    #[error("Connection failed: {0}")]
20
    Connection(#[from] sqlx::Error),
21
    /// Query exceeded maximum allowed execution time
22
    #[error("Query timeout: operation took {actual_ms}ms, max allowed {max_ms}ms")]
23
    QueryTimeout {
24
        /// Actual execution time in milliseconds
25
        actual_ms: u64,
26
        /// Maximum allowed execution time in milliseconds
27
        max_ms: u64,
28
    },
29
    /// Connection pool has no available connections
30
    #[error("Pool exhausted: no connections available")]
31
    PoolExhausted,
32
    /// Database configuration is invalid or missing required parameters
33
    #[error("Configuration error: {0}")]
34
    Configuration(String),
35
    /// Performance constraint violation detected
36
    #[error("Performance violation: {0}")]
37
    Performance(String),
38
}
39
40
/// Database connection configuration (local extended version)
41
#[derive(Debug, Clone, Deserialize, Serialize)]
42
pub struct LocalDatabaseConfig {
43
    /// Database connection URL
44
    pub url: String,
45
    /// Pool configuration
46
    pub pool: PoolConfig,
47
    /// Performance settings
48
    pub performance: PerformanceConfig,
49
}
50
51
/// Connection pool configuration
52
#[derive(Debug, Clone, Deserialize, Serialize)]
53
pub struct PoolConfig {
54
    /// Maximum number of connections in the pool
55
    pub max_connections: u32,
56
    /// Minimum number of connections to maintain
57
    pub min_connections: u32,
58
    /// Connection timeout in milliseconds
59
    pub connect_timeout_ms: u64,
60
    /// Connection acquire timeout in milliseconds
61
    pub acquire_timeout_ms: u64,
62
    /// Maximum connection lifetime in seconds
63
    pub max_lifetime_seconds: u64,
64
    /// Idle timeout in seconds
65
    pub idle_timeout_seconds: u64,
66
}
67
68
/// Performance configuration for HFT operations
69
#[derive(Debug, Clone, Deserialize, Serialize)]
70
pub struct PerformanceConfig {
71
    /// Query timeout in microseconds for HFT operations
72
    pub query_timeout_micros: u64,
73
    /// Enable connection prewarming
74
    pub enable_prewarming: bool,
75
    /// Enable statement preparation
76
    pub enable_prepared_statements: bool,
77
    /// Enable query logging for slow queries
78
    pub enable_slow_query_logging: bool,
79
    /// Slow query threshold in microseconds
80
    pub slow_query_threshold_micros: u64,
81
}
82
83
impl Default for LocalDatabaseConfig {
84
0
    fn default() -> Self {
85
0
        Self {
86
0
            url: "postgresql://foxhunt:password@localhost:5432/foxhunt".to_owned(),
87
0
            pool: PoolConfig::default(),
88
0
            performance: PerformanceConfig::default(),
89
0
        }
90
0
    }
91
}
92
93
impl Default for PoolConfig {
94
0
    fn default() -> Self {
95
0
        Self {
96
0
            max_connections: 50,
97
0
            min_connections: 10,
98
0
            connect_timeout_ms: 100,
99
0
            acquire_timeout_ms: 50,
100
0
            max_lifetime_seconds: 3600,
101
0
            idle_timeout_seconds: 300,
102
0
        }
103
0
    }
104
}
105
106
impl Default for PerformanceConfig {
107
0
    fn default() -> Self {
108
0
        Self {
109
0
            query_timeout_micros: 800, // <1ms for HFT operations
110
0
            enable_prewarming: true,
111
0
            enable_prepared_statements: true,
112
0
            enable_slow_query_logging: true,
113
0
            slow_query_threshold_micros: 1000, // Log queries >1ms
114
0
        }
115
0
    }
116
}
117
118
/// Convert from centralized config to common crate config with HFT optimizations
119
impl From<DatabaseConfig> for LocalDatabaseConfig {
120
0
    fn from(config: DatabaseConfig) -> Self {
121
0
        Self {
122
0
            url: config.url,
123
0
            pool: PoolConfig {
124
0
                max_connections: config.max_connections,
125
0
                min_connections: (config.max_connections / 5).max(2), // 20% of max, min 2
126
0
                connect_timeout_ms: config.connect_timeout.as_millis().min(100) as u64, // Convert to ms, cap at 100ms for HFT
127
0
                acquire_timeout_ms: 50,     // Fast acquire for HFT
128
0
                max_lifetime_seconds: 3600, // 1 hour default
129
0
                idle_timeout_seconds: 300,  // 5 minutes default
130
0
            },
131
0
            performance: PerformanceConfig {
132
0
                query_timeout_micros: config.query_timeout.as_micros().min(800) as u64, // Convert to microseconds, cap at 800μs for HFT
133
0
                enable_prewarming: true,
134
0
                enable_prepared_statements: true,
135
0
                enable_slow_query_logging: config.enable_query_logging,
136
0
                slow_query_threshold_micros: 1000, // 1ms threshold
137
0
            },
138
0
        }
139
0
    }
140
}
141
142
/// Convert from backtesting config to common crate config with backtesting optimizations
143
impl From<BacktestingDatabaseConfig> for LocalDatabaseConfig {
144
0
    fn from(config: BacktestingDatabaseConfig) -> Self {
145
0
        let max_conn = config.max_connections.unwrap_or(10);
146
0
        Self {
147
0
            url: config.database_url,
148
0
            pool: PoolConfig {
149
0
                max_connections: max_conn,
150
0
                min_connections: (max_conn / 4).max(2), // 25% of max, min 2
151
0
                connect_timeout_ms: config.acquire_timeout_ms.unwrap_or(1000), // Use acquire timeout as connection timeout
152
0
                acquire_timeout_ms: 100,    // Less strict for backtesting
153
0
                max_lifetime_seconds: 3600, // 1 hour default
154
0
                idle_timeout_seconds: 600,  // 10 minutes for backtesting
155
0
            },
156
0
            performance: PerformanceConfig {
157
0
                query_timeout_micros: 10000, // 10ms default for backtesting queries
158
0
                enable_prewarming: true,
159
0
                enable_prepared_statements: true,
160
0
                enable_slow_query_logging: config.enable_logging.unwrap_or(false),
161
0
                slow_query_threshold_micros: 5000, // 5ms threshold for backtesting
162
0
            },
163
0
        }
164
0
    }
165
}
166
167
/// Database connection pool wrapper
168
#[derive(Debug)]
169
pub struct DatabasePool {
170
    pool: Pool<Postgres>,
171
    config: LocalDatabaseConfig,
172
}
173
174
impl DatabasePool {
175
    /// Create a new database connection pool
176
0
    pub async fn new(config: LocalDatabaseConfig) -> Result<Self, DatabaseError> {
177
        use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
178
179
        // Parse connection options
180
0
        let mut connect_options: PgConnectOptions = config
181
0
            .url
182
0
            .parse()
183
0
            .map_err(|e| DatabaseError::Configuration(format!("Invalid URL: {}", e)))?;
184
185
        // Configure connection-level optimizations
186
0
        connect_options = connect_options
187
0
            .application_name("foxhunt-service")
188
0
            .statement_cache_capacity(1000);
189
190
        // Create connection pool with optimized settings
191
0
        let pool = PgPoolOptions::new()
192
0
            .max_connections(config.pool.max_connections)
193
0
            .min_connections(config.pool.min_connections)
194
0
            .acquire_timeout(Duration::from_millis(config.pool.acquire_timeout_ms))
195
0
            .max_lifetime(Duration::from_secs(config.pool.max_lifetime_seconds))
196
0
            .idle_timeout(Duration::from_secs(config.pool.idle_timeout_seconds))
197
0
            .test_before_acquire(true)
198
0
            .connect_with(connect_options)
199
0
            .await
200
0
            .map_err(DatabaseError::Connection)?;
201
202
        // Pre-warm connections if enabled
203
0
        if config.performance.enable_prewarming {
204
0
            for _ in 0..config.pool.min_connections {
205
0
                let _conn = pool.acquire().await.map_err(DatabaseError::Connection)?;
206
0
                sqlx::query("SELECT 1")
207
0
                    .fetch_one(&pool)
208
0
                    .await
209
0
                    .map_err(DatabaseError::Connection)?;
210
            }
211
0
        }
212
213
0
        Ok(Self { pool, config })
214
0
    }
215
216
    /// Get the underlying connection pool
217
0
    pub const fn pool(&self) -> &Pool<Postgres> {
218
0
        &self.pool
219
0
    }
220
221
    /// Get current configuration
222
0
    pub const fn config(&self) -> &LocalDatabaseConfig {
223
0
        &self.config
224
0
    }
225
226
    /// Health check for the database connection
227
0
    pub async fn health_check(&self) -> Result<(), DatabaseError> {
228
0
        let result = tokio::time::timeout(
229
0
            Duration::from_millis(100),
230
0
            sqlx::query("SELECT 1").fetch_one(&self.pool),
231
0
        )
232
0
        .await;
233
234
0
        match result {
235
0
            Ok(Ok(_)) => Ok(()),
236
0
            Ok(Err(e)) => Err(DatabaseError::Connection(e)),
237
0
            Err(_) => Err(DatabaseError::QueryTimeout {
238
0
                actual_ms: 100,
239
0
                max_ms: 100,
240
0
            }),
241
        }
242
0
    }
243
244
    /// Get connection pool statistics
245
0
    pub fn pool_stats(&self) -> PoolStats {
246
0
        PoolStats {
247
0
            size: self.pool.size(),
248
0
            idle: self.pool.num_idle() as u32,
249
0
            active: self.pool.size() - self.pool.num_idle() as u32,
250
0
            max_size: self.config.pool.max_connections,
251
0
        }
252
0
    }
253
}
254
255
/// Connection pool statistics
256
#[derive(Debug, Clone, Serialize, Deserialize)]
257
pub struct PoolStats {
258
    /// Current pool size
259
    pub size: u32,
260
    /// Number of idle connections
261
    pub idle: u32,
262
    /// Number of active connections
263
    pub active: u32,
264
    /// Maximum pool size
265
    pub max_size: u32,
266
}
267
268
impl PoolStats {
269
    /// Calculate pool utilization percentage
270
0
    pub fn utilization_percentage(&self) -> f64 {
271
0
        (self.active as f64 / self.max_size as f64) * 100.0
272
0
    }
273
274
    /// Check if pool is healthy (not over-utilized)
275
0
    pub fn is_healthy(&self) -> bool {
276
0
        self.utilization_percentage() < 80.0
277
0
    }
278
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html deleted file mode 100644 index e295925fa..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/error.rs
Line
Count
Source
1
//! Common error types and utilities
2
//!
3
//! This module provides shared error types and utilities used across
4
//! all Foxhunt services.
5
6
use serde::{Deserialize, Serialize};
7
use std::fmt;
8
use std::time::Duration;
9
use thiserror::Error;
10
11
/// Common error type for all Foxhunt services
12
#[derive(Debug, Error)]
13
pub enum CommonError {
14
    /// Database operation failed - wraps database-specific errors
15
    #[error("Database error: {0}")]
16
    Database(#[from] crate::database::DatabaseError),
17
    /// Configuration is invalid or missing required parameters
18
    #[error("Configuration error: {0}")]
19
    Configuration(String),
20
    /// Network communication error occurred
21
    #[error("Network error: {0}")]
22
    Network(String),
23
    /// Service-specific error with categorization for metrics
24
    #[error("Service error: {category} - {message}")]
25
    Service {
26
        /// Error category for classification
27
        category: ErrorCategory,
28
        /// Descriptive error message
29
        message: String,
30
    },
31
    /// Input validation failed
32
    #[error("Validation error: {0}")]
33
    Validation(String),
34
    /// Operation exceeded maximum allowed execution time
35
    #[error("Timeout error: operation took {actual_ms}ms, max allowed {max_ms}ms")]
36
    Timeout {
37
        /// Actual execution time in milliseconds
38
        actual_ms: u64,
39
        /// Maximum allowed execution time in milliseconds
40
        max_ms: u64,
41
    },
42
}
43
44
/// Error categories for classification and metrics
45
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46
pub enum ErrorCategory {
47
    /// Market data related errors
48
    MarketData,
49
    /// Trading and order management errors
50
    Trading,
51
    /// Network and communication errors
52
    Network,
53
    /// System and infrastructure errors
54
    System,
55
    /// Configuration errors
56
    Configuration,
57
    /// Validation errors
58
    Validation,
59
    /// Critical errors requiring immediate attention
60
    Critical,
61
    /// Connection errors (data providers)
62
    Connection,
63
    /// Authentication errors
64
    Authentication,
65
    /// Rate limiting errors
66
    RateLimit,
67
    /// Data parsing errors
68
    Parse,
69
    /// Subscription errors
70
    Subscription,
71
    /// Financial safety and calculation errors
72
    FinancialSafety,
73
    /// Risk management and circuit breakers
74
    RiskManagement,
75
    /// Database and persistence layer
76
    Database,
77
    /// Broker connectivity and execution
78
    Broker,
79
    /// Machine learning and AI errors
80
    MachineLearning,
81
    /// Security and authentication errors
82
    Security,
83
    /// Business logic errors
84
    BusinessLogic,
85
    /// Resource errors (not found, conflicts)
86
    Resource,
87
    /// Development and testing errors
88
    Development,
89
    /// Risk management errors
90
    Risk,
91
    /// Machine learning errors (alias for MachineLearning)
92
    ML,
93
    /// Unknown/other errors
94
    Other,
95
}
96
97
impl fmt::Display for ErrorCategory {
98
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99
0
        match self {
100
0
            Self::MarketData => write!(f, "MARKET_DATA"),
101
0
            Self::Trading => write!(f, "TRADING"),
102
0
            Self::Network => write!(f, "NETWORK"),
103
0
            Self::System => write!(f, "SYSTEM"),
104
0
            Self::Configuration => write!(f, "CONFIGURATION"),
105
0
            Self::Validation => write!(f, "VALIDATION"),
106
0
            Self::Critical => write!(f, "CRITICAL"),
107
0
            Self::Connection => write!(f, "CONNECTION"),
108
0
            Self::Authentication => write!(f, "AUTHENTICATION"),
109
0
            Self::RateLimit => write!(f, "RATE_LIMIT"),
110
0
            Self::Parse => write!(f, "PARSE"),
111
0
            Self::Subscription => write!(f, "SUBSCRIPTION"),
112
0
            Self::FinancialSafety => write!(f, "FINANCIAL_SAFETY"),
113
0
            Self::RiskManagement => write!(f, "RISK_MANAGEMENT"),
114
0
            Self::Database => write!(f, "DATABASE"),
115
0
            Self::Broker => write!(f, "BROKER"),
116
0
            Self::MachineLearning => write!(f, "MACHINE_LEARNING"),
117
0
            Self::Security => write!(f, "SECURITY"),
118
0
            Self::BusinessLogic => write!(f, "BUSINESS_LOGIC"),
119
0
            Self::Resource => write!(f, "RESOURCE"),
120
0
            Self::Development => write!(f, "DEVELOPMENT"),
121
0
            Self::Risk => write!(f, "RISK"),
122
0
            Self::ML => write!(f, "ML"),
123
0
            Self::Other => write!(f, "OTHER"),
124
        }
125
0
    }
126
}
127
128
/// Error severity levels for prioritization and alerting
129
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130
pub enum ErrorSeverity {
131
    /// Debug level - for development and troubleshooting
132
    Debug,
133
    /// Info level - informational messages
134
    Info,
135
    /// Warning level - potentially problematic situations
136
    Warn,
137
    /// Error level - error conditions that should be addressed
138
    Error,
139
    /// Critical level - serious error conditions requiring immediate attention
140
    Critical,
141
}
142
143
impl fmt::Display for ErrorSeverity {
144
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145
0
        match self {
146
0
            Self::Debug => write!(f, "DEBUG"),
147
0
            Self::Info => write!(f, "INFO"),
148
0
            Self::Warn => write!(f, "WARN"),
149
0
            Self::Error => write!(f, "ERROR"),
150
0
            Self::Critical => write!(f, "CRITICAL"),
151
        }
152
0
    }
153
}
154
155
/// Retry strategies for error recovery
156
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157
pub enum RetryStrategy {
158
    /// Do not retry - error is permanent
159
    NoRetry,
160
    /// Retry immediately without delay
161
    Immediate,
162
    /// Linear backoff with fixed intervals
163
    Linear {
164
        /// Base delay in milliseconds between retries
165
        base_delay_ms: u64,
166
    },
167
    /// Exponential backoff with jitter
168
    Exponential {
169
        /// Base delay in milliseconds for exponential backoff
170
        base_delay_ms: u64,
171
        /// Maximum delay cap in milliseconds
172
        max_delay_ms: u64,
173
    },
174
    /// Wait for circuit breaker to close
175
    CircuitBreaker,
176
}
177
178
impl RetryStrategy {
179
    /// Calculate delay for retry attempt
180
    #[must_use]
181
19
    pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
182
19
        match self {
183
3
            Self::NoRetry => None,
184
2
            Self::Immediate => Some(Duration::from_millis(0)),
185
4
            Self::Linear { base_delay_ms } => {
186
4
                Some(Duration::from_millis(base_delay_ms * u64::from(attempt)))
187
            },
188
            Self::Exponential {
189
8
                base_delay_ms,
190
8
                max_delay_ms,
191
            } => {
192
8
                let delay_ms = base_delay_ms * 2_u64.pow(attempt.min(10));
193
8
                let capped_delay = delay_ms.min(*max_delay_ms);
194
195
                // Add simple jitter (±10%)
196
8
                let jitter_ms = capped_delay / 10;
197
8
                let final_delay = capped_delay.saturating_sub(jitter_ms / 2);
198
199
8
                Some(Duration::from_millis(final_delay))
200
            },
201
2
            Self::CircuitBreaker => Some(Duration::from_secs(30)),
202
        }
203
19
    }
204
205
    /// Get maximum recommended retry attempts
206
    #[must_use]
207
0
    pub const fn max_attempts(&self) -> Option<u32> {
208
0
        match self {
209
0
            Self::NoRetry => Some(0),
210
0
            Self::Immediate => Some(3),
211
0
            Self::Linear { .. } => Some(5),
212
0
            Self::Exponential { .. } => Some(7),
213
0
            Self::CircuitBreaker => Some(1),
214
        }
215
0
    }
216
}
217
218
/// Convenience functions for creating common errors
219
impl CommonError {
220
    /// Create a configuration error
221
0
    pub fn config<S: Into<String>>(message: S) -> Self {
222
0
        Self::Configuration(message.into())
223
0
    }
224
225
    /// Create a network error
226
0
    pub fn network<S: Into<String>>(message: S) -> Self {
227
0
        Self::Network(message.into())
228
0
    }
229
230
    /// Create a service error with category
231
29
    pub fn service<S: Into<String>>(category: ErrorCategory, message: S) -> Self {
232
29
        Self::Service {
233
29
            category,
234
29
            message: message.into(),
235
29
        }
236
29
    }
237
238
    /// Create a validation error
239
0
    pub fn validation<S: Into<String>>(message: S) -> Self {
240
0
        Self::Validation(message.into())
241
0
    }
242
243
    /// Create a timeout error
244
0
    pub fn timeout(actual_ms: u64, max_ms: u64) -> Self {
245
0
        Self::Timeout { actual_ms, max_ms }
246
0
    }
247
248
    /// Create a machine learning specific service error
249
0
    pub fn ml<S: Into<String>, M: Into<String>>(model_name: S, message: M) -> Self {
250
0
        Self::Service {
251
0
            category: ErrorCategory::MachineLearning,
252
0
            message: format!("{}: {}", model_name.into(), message.into()),
253
0
        }
254
0
    }
255
256
    /// Create a serialization error
257
0
    pub fn serialization<S: Into<String>>(message: S) -> Self {
258
0
        Self::Service {
259
0
            category: ErrorCategory::Parse,
260
0
            message: format!("Serialization error: {}", message.into()),
261
0
        }
262
0
    }
263
264
    /// Create an internal error
265
0
    pub fn internal<S: Into<String>>(message: S) -> Self {
266
0
        Self::Service {
267
0
            category: ErrorCategory::System,
268
0
            message: format!("Internal error: {}", message.into()),
269
0
        }
270
0
    }
271
272
    /// Create a resource exhausted error
273
0
    pub fn resource_exhausted<S: Into<String>>(resource: S) -> Self {
274
0
        Self::Service {
275
0
            category: ErrorCategory::Resource,
276
0
            message: format!("Resource exhausted: {}", resource.into()),
277
0
        }
278
0
    }
279
280
    /// Get the error category for classification and metrics
281
0
    pub fn category(&self) -> ErrorCategory {
282
0
        match self {
283
0
            Self::Database(_) => ErrorCategory::Database,
284
0
            Self::Configuration(_) => ErrorCategory::Configuration,
285
0
            Self::Network(_) => ErrorCategory::Network,
286
0
            Self::Service { category, .. } => *category,
287
0
            Self::Validation(_) => ErrorCategory::Validation,
288
0
            Self::Timeout { .. } => ErrorCategory::System,
289
        }
290
0
    }
291
292
    /// Get error severity level
293
28
    pub fn severity(&self) -> ErrorSeverity {
294
28
        match self {
295
1
            Self::Database(_) => ErrorSeverity::Critical,
296
1
            Self::Configuration(_) => ErrorSeverity::Critical,
297
1
            Self::Network(_) => ErrorSeverity::Error,
298
23
            Self::Service { category, .. } => match category {
299
                ErrorCategory::Critical
300
                | ErrorCategory::FinancialSafety
301
3
                | ErrorCategory::Authentication => ErrorSeverity::Critical,
302
                ErrorCategory::Trading
303
                | ErrorCategory::RiskManagement
304
3
                | ErrorCategory::Database => ErrorSeverity::Error,
305
17
                _ => ErrorSeverity::Warn,
306
            },
307
1
            Self::Validation(_) => ErrorSeverity::Warn,
308
1
            Self::Timeout { .. } => ErrorSeverity::Error,
309
        }
310
28
    }
311
312
    /// Check if the error is retryable
313
11
    pub fn is_retryable(&self) -> bool {
314
11
        match self {
315
1
            Self::Database(_) => true,       // Database operations can be retried
316
1
            Self::Configuration(_) => false, // Configuration errors are permanent
317
1
            Self::Network(_) => true,        // Network errors are often transient
318
6
            Self::Service { category, .. } => !
matches!5
(
319
6
                category,
320
                ErrorCategory::Authentication
321
                    | ErrorCategory::Configuration
322
                    | ErrorCategory::Validation
323
            ),
324
1
            Self::Validation(_) => false, // Validation errors are permanent
325
1
            Self::Timeout { .. } => true, // Timeouts can be retried
326
        }
327
11
    }
328
329
    /// Get retry strategy for this error
330
11
    pub fn retry_strategy(&self) -> RetryStrategy {
331
11
        if !self.is_retryable() {
332
3
            return RetryStrategy::NoRetry;
333
8
        }
334
335
8
        match self {
336
1
            Self::Database(_) => RetryStrategy::Exponential {
337
1
                base_delay_ms: 1000,
338
1
                max_delay_ms: 10000,
339
1
            },
340
1
            Self::Network(_) => RetryStrategy::Linear { base_delay_ms: 500 },
341
5
            Self::Service { category, .. } => match category {
342
                ErrorCategory::Network | ErrorCategory::Connection => {
343
2
                    RetryStrategy::Linear { base_delay_ms: 500 }
344
                },
345
1
                ErrorCategory::RateLimit => RetryStrategy::Exponential {
346
1
                    base_delay_ms: 5000,
347
1
                    max_delay_ms: 60000,
348
1
                },
349
2
                _ => RetryStrategy::Immediate,
350
            },
351
1
            Self::Timeout { .. } => RetryStrategy::Linear {
352
1
                base_delay_ms: 1000,
353
1
            },
354
0
            _ => RetryStrategy::NoRetry,
355
        }
356
11
    }
357
}
358
359
/// Result type for common operations
360
pub type CommonResult<T> = Result<T, CommonError>;
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html deleted file mode 100644 index 61b9e27d4..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs
Line
Count
Source
1
//! Centralized threshold constants for the Foxhunt HFT system
2
//!
3
//! This module consolidates all hardcoded threshold values that were
4
//! previously scattered throughout the codebase. Constants here are
5
//! compile-time values for performance-critical operations.
6
//!
7
//! For runtime-configurable values, see the `config` crate's runtime module.
8
9
use std::time::Duration;
10
11
/// Risk management thresholds
12
pub mod risk {
13
    
14
15
    /// Breach severity warning threshold (percentage of limit)
16
    /// Used when position is at 80-90% of limit
17
    pub const BREACH_WARNING_PCT: u8 = 80;
18
19
    /// Breach severity soft threshold (percentage of limit)
20
    /// Used when position is at 90-100% of limit
21
    pub const BREACH_SOFT_PCT: u8 = 90;
22
23
    /// Breach severity hard threshold (percentage of limit)
24
    /// Used when position is at 100-120% of limit
25
    pub const BREACH_HARD_PCT: u8 = 100;
26
27
    /// Breach severity critical threshold (percentage of limit)
28
    /// Used when position exceeds 120% of limit
29
    pub const BREACH_CRITICAL_PCT: u8 = 120;
30
31
    /// Minimum capital adequacy ratio (Basel III standard)
32
    pub const MIN_CAPITAL_ADEQUACY_RATIO: f64 = 0.08;
33
34
    /// Minimum leverage ratio (Basel III standard)
35
    pub const MIN_LEVERAGE_RATIO: f64 = 0.03;
36
37
    /// Default VaR confidence level (95%)
38
    pub const DEFAULT_VAR_CONFIDENCE: f64 = 0.95;
39
40
    /// High VaR confidence level (99%)
41
    pub const HIGH_VAR_CONFIDENCE: f64 = 0.99;
42
43
    /// Maximum drawdown warning threshold (percentage)
44
    pub const MAX_DRAWDOWN_WARNING_PCT: u8 = 15;
45
46
    /// Maximum drawdown critical threshold (percentage)
47
    pub const MAX_DRAWDOWN_CRITICAL_PCT: u8 = 25;
48
}
49
50
/// VaR calculation constants
51
pub mod var {
52
    /// Z-score for 90% confidence level
53
    pub const Z_SCORE_P90: f64 = 1.282;
54
55
    /// Z-score for 95% confidence level
56
    pub const Z_SCORE_P95: f64 = 1.645;
57
58
    /// Z-score for 97.5% confidence level
59
    pub const Z_SCORE_P97_5: f64 = 1.96;
60
61
    /// Z-score for 99% confidence level
62
    pub const Z_SCORE_P99: f64 = 2.326;
63
64
    /// Z-score for 99.9% confidence level
65
    pub const Z_SCORE_P99_9: f64 = 3.09;
66
67
    /// Default lookback period for historical VaR (trading days)
68
    pub const DEFAULT_LOOKBACK_DAYS: usize = 252;
69
70
    /// Minimum data quality score for VaR calculation
71
    pub const MIN_DATA_QUALITY_SCORE: f64 = 0.6;
72
}
73
74
/// Performance and timing constants
75
pub mod performance {
76
    
77
78
    /// Maximum latency for HFT critical path operations (nanoseconds)
79
    pub const MAX_CRITICAL_PATH_LATENCY_NS: u64 = 14;
80
81
    /// Maximum acceptable latency for risk checks (microseconds)
82
    pub const MAX_RISK_CHECK_LATENCY_US: u64 = 50;
83
84
    /// Maximum latency for ML inference (microseconds)
85
    pub const MAX_ML_INFERENCE_LATENCY_US: u64 = 100;
86
87
    /// Default batch processing size
88
    pub const DEFAULT_BATCH_SIZE: usize = 100;
89
90
    /// Ring buffer size for lock-free operations
91
    pub const RING_BUFFER_SIZE: usize = 4096;
92
93
    /// Small batch size for SIMD operations
94
    pub const SIMD_BATCH_SIZE: usize = 8;
95
96
    /// Maximum small batch size
97
    pub const MAX_SMALL_BATCH_SIZE: usize = 10;
98
99
    /// Default worker thread count (adjusted based on CPU cores at runtime)
100
    pub const DEFAULT_WORKER_THREADS: usize = 4;
101
102
    /// Default queue capacity for async operations
103
    pub const DEFAULT_QUEUE_CAPACITY: usize = 10000;
104
}
105
106
/// Cache TTL defaults (can be overridden by runtime config)
107
pub mod cache {
108
    use super::Duration;
109
110
    /// Default TTL for position cache entries (1 minute)
111
    pub const POSITION_CACHE_TTL: Duration = Duration::from_secs(60);
112
113
    /// Default TTL for VaR calculation cache (1 hour)
114
    pub const VAR_CACHE_TTL: Duration = Duration::from_secs(3600);
115
116
    /// Default TTL for compliance check cache (24 hours)
117
    pub const COMPLIANCE_CACHE_TTL: Duration = Duration::from_secs(86400);
118
119
    /// Default TTL for market data cache (5 minutes)
120
    pub const MARKET_DATA_CACHE_TTL: Duration = Duration::from_secs(300);
121
122
    /// Default TTL for model predictions cache (1 minute)
123
    pub const MODEL_PREDICTION_CACHE_TTL: Duration = Duration::from_secs(60);
124
125
    /// Redis key TTL for position limits (5 minutes)
126
    pub const REDIS_POSITION_LIMIT_TTL_SECS: i32 = 300;
127
128
    /// Redis key TTL for compliance checks (24 hours)
129
    pub const REDIS_COMPLIANCE_TTL_SECS: i32 = 86400;
130
131
    /// Redis key TTL for VaR calculations (1 hour)
132
    pub const REDIS_VAR_TTL_SECS: i32 = 3600;
133
}
134
135
/// Database operation defaults
136
pub mod database {
137
    use super::Duration;
138
139
    /// Default query timeout for standard operations
140
    pub const QUERY_TIMEOUT: Duration = Duration::from_millis(1000);
141
142
    /// Default connection timeout
143
    pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100);
144
145
    /// Default pool acquire timeout
146
    pub const ACQUIRE_TIMEOUT: Duration = Duration::from_millis(50);
147
148
    /// Default connection lifetime (1 hour)
149
    pub const CONNECTION_LIFETIME: Duration = Duration::from_secs(3600);
150
151
    /// Default idle timeout (5 minutes)
152
    pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
153
154
    /// Default pool size
155
    pub const DEFAULT_POOL_SIZE: u32 = 20;
156
157
    /// Maximum pool size
158
    pub const MAX_POOL_SIZE: u32 = 100;
159
160
    /// Maximum query result limit
161
    pub const MAX_QUERY_LIMIT: i64 = 1000;
162
}
163
164
/// Network and gRPC defaults
165
pub mod network {
166
    use super::Duration;
167
168
    /// Default connect timeout for gRPC clients
169
    pub const GRPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
170
171
    /// Default request timeout for gRPC
172
    pub const GRPC_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
173
174
    /// Default keep-alive interval
175
    pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
176
177
    /// Keep-alive timeout
178
    pub const KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5);
179
180
    /// Maximum concurrent connections
181
    pub const MAX_CONCURRENT_CONNECTIONS: u32 = 100;
182
183
    /// HTTP/2 initial stream window size
184
    pub const INITIAL_STREAM_WINDOW_SIZE: u32 = 65535;
185
186
    /// HTTP/2 initial connection window size
187
    pub const INITIAL_CONNECTION_WINDOW_SIZE: u32 = 1048576;
188
}
189
190
/// Retry and recovery defaults
191
pub mod retry {
192
    use super::Duration;
193
194
    /// Initial delay for exponential backoff
195
    pub const INITIAL_RETRY_DELAY: Duration = Duration::from_millis(100);
196
197
    /// Maximum delay for exponential backoff
198
    pub const MAX_RETRY_DELAY: Duration = Duration::from_secs(30);
199
200
    /// Maximum retry attempts for critical operations
201
    pub const MAX_RETRY_ATTEMPTS: u32 = 3;
202
203
    /// Backoff multiplier for exponential backoff
204
    pub const BACKOFF_MULTIPLIER: f32 = 1.5;
205
206
    /// Maximum total duration for retry attempts
207
    pub const MAX_TOTAL_RETRY_DURATION: Duration = Duration::from_secs(60);
208
}
209
210
/// Health check and monitoring intervals
211
pub mod monitoring {
212
    use super::Duration;
213
214
    /// Default health check interval
215
    pub const HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(30);
216
217
    /// Default metrics collection interval
218
    pub const METRICS_COLLECTION_INTERVAL: Duration = Duration::from_secs(10);
219
220
    /// Default log flush interval
221
    pub const LOG_FLUSH_INTERVAL: Duration = Duration::from_secs(5);
222
223
    /// Circuit breaker check interval
224
    pub const CIRCUIT_BREAKER_CHECK_INTERVAL: Duration = Duration::from_millis(100);
225
226
    /// Kill switch session timeout (5 minutes)
227
    pub const KILL_SWITCH_SESSION_TIMEOUT: Duration = Duration::from_secs(300);
228
}
229
230
/// Event processing defaults
231
pub mod events {
232
    use super::Duration;
233
234
    /// Event batch timeout
235
    pub const BATCH_TIMEOUT: Duration = Duration::from_millis(100);
236
237
    /// Event batch size
238
    pub const BATCH_SIZE: usize = 100;
239
240
    /// Event retry delay
241
    pub const RETRY_DELAY: Duration = Duration::from_millis(50);
242
243
    /// Maximum event backlog before applying backpressure
244
    pub const MAX_EVENT_BACKLOG: usize = 10000;
245
246
    /// Maximum span buffer size for tracing
247
    pub const MAX_SPAN_BUFFER_SIZE: usize = 100_000;
248
249
    /// Span export batch size
250
    pub const SPAN_EXPORT_BATCH_SIZE: usize = 1000;
251
}
252
253
/// ML model constants
254
pub mod ml {
255
    use super::Duration;
256
257
    /// Maximum GPU batch size
258
    pub const MAX_GPU_BATCH_SIZE: usize = 8192;
259
260
    /// Maximum CPU batch size
261
    pub const MAX_CPU_BATCH_SIZE: usize = 1024;
262
263
    /// Default model cache cleanup interval (1 hour)
264
    pub const MODEL_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(3600);
265
266
    /// Default model health check interval (30 seconds)
267
    pub const MODEL_HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(30);
268
269
    /// Model deployment stage timeout (5 minutes)
270
    pub const DEPLOYMENT_STAGE_TIMEOUT: Duration = Duration::from_secs(300);
271
272
    /// Model deployment total timeout (30 minutes)
273
    pub const DEPLOYMENT_TOTAL_TIMEOUT: Duration = Duration::from_secs(1800);
274
275
    /// Model validation scan timeout (10 minutes)
276
    pub const VALIDATION_SCAN_TIMEOUT: Duration = Duration::from_secs(600);
277
278
    /// Canary deployment duration (5 minutes)
279
    pub const CANARY_DURATION: Duration = Duration::from_secs(300);
280
281
    /// Model rollback timeout (1 minute)
282
    pub const ROLLBACK_TIMEOUT: Duration = Duration::from_secs(60);
283
284
    /// Drift detection check interval (5 minutes)
285
    pub const DRIFT_CHECK_INTERVAL: Duration = Duration::from_secs(300);
286
287
    /// Drift detection warning threshold
288
    pub const DRIFT_WARNING_THRESHOLD: f64 = 0.05;
289
290
    /// Maximum recommendation age for Kelly sizing (1 minute)
291
    pub const MAX_KELLY_RECOMMENDATION_AGE: Duration = Duration::from_secs(60);
292
293
    /// Kelly sizing cache TTL (5 minutes)
294
    pub const KELLY_CACHE_TTL: Duration = Duration::from_secs(300);
295
}
296
297
/// Safety system defaults
298
pub mod safety {
299
    use super::Duration;
300
301
    /// Safety check timeout for production (5ms)
302
    pub const PRODUCTION_SAFETY_CHECK_TIMEOUT: Duration = Duration::from_millis(5);
303
304
    /// Safety check timeout for development (50ms)
305
    pub const DEVELOPMENT_SAFETY_CHECK_TIMEOUT: Duration = Duration::from_millis(50);
306
307
    /// Auto-recovery delay for production (30 minutes)
308
    pub const PRODUCTION_AUTO_RECOVERY_DELAY: Duration = Duration::from_secs(1800);
309
310
    /// Auto-recovery delay for development (1 minute)
311
    pub const DEVELOPMENT_AUTO_RECOVERY_DELAY: Duration = Duration::from_secs(60);
312
313
    /// Loss check interval for production (5 seconds)
314
    pub const PRODUCTION_LOSS_CHECK_INTERVAL: Duration = Duration::from_secs(5);
315
316
    /// Loss check interval for development (30 seconds)
317
    pub const DEVELOPMENT_LOSS_CHECK_INTERVAL: Duration = Duration::from_secs(30);
318
319
    /// Position check interval for production (2 seconds)
320
    pub const PRODUCTION_POSITION_CHECK_INTERVAL: Duration = Duration::from_secs(2);
321
322
    /// Position check interval for development (15 seconds)
323
    pub const DEVELOPMENT_POSITION_CHECK_INTERVAL: Duration = Duration::from_secs(15);
324
325
    /// Memory check interval
326
    pub const MEMORY_CHECK_INTERVAL: Duration = Duration::from_secs(1);
327
328
    /// Circuit breaker trip cooldown (30 seconds)
329
    pub const CIRCUIT_BREAKER_COOLDOWN: Duration = Duration::from_secs(30);
330
}
331
332
/// Time conversion constants
333
pub mod time {
334
    /// Nanoseconds per microsecond
335
    pub const NANOS_PER_MICRO: u64 = 1_000;
336
337
    /// Nanoseconds per millisecond
338
    pub const NANOS_PER_MILLI: u64 = 1_000_000;
339
340
    /// Nanoseconds per second
341
    pub const NANOS_PER_SECOND: u64 = 1_000_000_000;
342
343
    /// Microseconds per second
344
    pub const MICROS_PER_SECOND: u64 = 1_000_000;
345
346
    /// Milliseconds per second
347
    pub const MILLIS_PER_SECOND: u64 = 1_000;
348
349
    /// Seconds per minute
350
    pub const SECONDS_PER_MINUTE: u64 = 60;
351
352
    /// Seconds per hour
353
    pub const SECONDS_PER_HOUR: u64 = 3600;
354
355
    /// Seconds per day
356
    pub const SECONDS_PER_DAY: u64 = 86400;
357
358
    /// Trading days per year
359
    pub const TRADING_DAYS_PER_YEAR: usize = 252;
360
}
361
362
/// Financial constants
363
pub mod financial {
364
    /// Basis points per unit
365
    pub const BASIS_POINTS_PER_UNIT: u32 = 10_000;
366
367
    /// Cents per dollar
368
    pub const CENTS_PER_DOLLAR: u32 = 100;
369
370
    /// Default profit target in basis points (1%)
371
    pub const DEFAULT_PROFIT_TARGET_BPS: u32 = 100;
372
373
    /// Default stop loss in basis points (0.5%)
374
    pub const DEFAULT_STOP_LOSS_BPS: u32 = 50;
375
376
    /// Minimum return threshold in basis points
377
    pub const MIN_RETURN_THRESHOLD_BPS: i32 = 5;
378
379
    /// Price scaling factor (6 decimal places)
380
    pub const PRICE_SCALE: i64 = 1_000_000;
381
382
    /// Quantity scaling factor (6 decimal places)
383
    pub const QUANTITY_SCALE: i64 = 1_000_000;
384
385
    /// Money scaling factor (6 decimal places)
386
    pub const MONEY_SCALE: i64 = 1_000_000;
387
388
    /// Unified scaling factor for all financial operations
389
    pub const UNIFIED_SCALE_FACTOR: i64 = 1_000_000;
390
391
    /// ML precision factor (8 decimal places)
392
    pub const PRECISION_FACTOR: i64 = 100_000_000;
393
394
    /// VPIN precision factor (4 decimal places)
395
    pub const VPIN_PRECISION_FACTOR: i64 = 10_000;
396
}
397
398
/// Validation limits
399
pub mod limits {
400
    /// Maximum symbol length
401
    pub const MAX_SYMBOL_LENGTH: usize = 12;
402
403
    /// Maximum account ID length
404
    pub const MAX_ACCOUNT_ID_LENGTH: usize = 32;
405
406
    /// Maximum description length
407
    pub const MAX_DESCRIPTION_LENGTH: usize = 256;
408
409
    /// Maximum metadata key length
410
    pub const MAX_METADATA_KEY_LENGTH: usize = 64;
411
412
    /// Maximum metadata value length
413
    pub const MAX_METADATA_VALUE_LENGTH: usize = 512;
414
415
    /// Maximum metadata entries
416
    pub const MAX_METADATA_ENTRIES: usize = 100;
417
418
    /// Maximum price value
419
    pub const MAX_PRICE: f64 = 1_000_000.0;
420
421
    /// Minimum price value
422
    pub const MIN_PRICE: f64 = 0.000_001;
423
424
    /// Maximum quantity value
425
    pub const MAX_QUANTITY: f64 = 1_000_000_000.0;
426
427
    /// Minimum quantity value
428
    pub const MIN_QUANTITY: f64 = 0.000_001;
429
430
    /// Maximum leverage
431
    pub const MAX_LEVERAGE: f64 = 1000.0;
432
433
    /// Minimum leverage
434
    pub const MIN_LEVERAGE: f64 = 0.1;
435
436
    /// Maximum allocation size (1GB)
437
    pub const MAX_ALLOCATION_SIZE: usize = 1024 * 1024 * 1024;
438
439
    /// Maximum duration in milliseconds (24 hours)
440
    pub const MAX_DURATION_MILLIS: u64 = 24 * 60 * 60 * 1000;
441
}
442
443
/// Hardware alignment constants
444
pub mod hardware {
445
    /// CPU cache line size
446
    pub const CACHE_LINE_SIZE: usize = 64;
447
448
    /// SIMD alignment for AVX2
449
    pub const SIMD_ALIGNMENT: usize = 32;
450
451
    /// Page size (4KB)
452
    pub const PAGE_SIZE: usize = 4096;
453
}
454
455
#[cfg(test)]
456
mod tests {
457
    use super::*;
458
459
    #[test]
460
1
    fn test_breach_thresholds_ordered() {
461
1
        assert!(risk::BREACH_WARNING_PCT < risk::BREACH_SOFT_PCT);
462
1
        assert!(risk::BREACH_SOFT_PCT < risk::BREACH_HARD_PCT);
463
1
        assert!(risk::BREACH_HARD_PCT < risk::BREACH_CRITICAL_PCT);
464
1
    }
465
466
    #[test]
467
1
    fn test_var_z_scores_ordered() {
468
1
        assert!(var::Z_SCORE_P90 < var::Z_SCORE_P95);
469
1
        assert!(var::Z_SCORE_P95 < var::Z_SCORE_P97_5);
470
1
        assert!(var::Z_SCORE_P97_5 < var::Z_SCORE_P99);
471
1
        assert!(var::Z_SCORE_P99 < var::Z_SCORE_P99_9);
472
1
    }
473
474
    #[test]
475
1
    fn test_time_conversions() {
476
1
        assert_eq!(time::NANOS_PER_MICRO * 1000, time::NANOS_PER_MILLI);
477
1
        assert_eq!(time::NANOS_PER_MILLI * 1000, time::NANOS_PER_SECOND);
478
1
        assert_eq!(time::MICROS_PER_SECOND * 1000, time::NANOS_PER_SECOND);
479
1
    }
480
481
    #[test]
482
1
    fn test_financial_scales_consistent() {
483
1
        assert_eq!(financial::PRICE_SCALE, financial::UNIFIED_SCALE_FACTOR);
484
1
        assert_eq!(financial::QUANTITY_SCALE, financial::UNIFIED_SCALE_FACTOR);
485
1
        assert_eq!(financial::MONEY_SCALE, financial::UNIFIED_SCALE_FACTOR);
486
1
    }
487
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html deleted file mode 100644 index 2ab692ab7..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/trading.rs
Line
Count
Source
1
//! Trading-specific types and enums
2
//!
3
//! This module contains the canonical definitions for all trading-related
4
//! types used across the Foxhunt HFT system. This is the single source
5
//! of truth for all trading types.
6
7
use chrono::{DateTime, Utc};
8
use rust_decimal::Decimal;
9
use serde::{Deserialize, Serialize};
10
use std::fmt;
11
12
// ELIMINATED: Re-exports removed to force explicit imports
13
// REMOVED: TimeInForce duplicate - use canonical definition from common::types
14
15
// Currency moved to canonical source: common::types::Currency
16
17
/// Tick type for market data
18
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19
#[cfg_attr(feature = "database", derive(sqlx::Type))]
20
#[cfg_attr(
21
    feature = "database",
22
    sqlx(type_name = "tick_type", rename_all = "snake_case")
23
)]
24
pub enum TickType {
25
    /// Trade tick
26
    Trade,
27
    /// Bid price update
28
    Bid,
29
    /// Ask price update
30
    Ask,
31
    /// Quote update (bid and ask)
32
    Quote,
33
}
34
35
impl fmt::Display for TickType {
36
    /// Format the tick type for display
37
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38
0
        match self {
39
0
            Self::Trade => write!(f, "TRADE"),
40
0
            Self::Bid => write!(f, "BID"),
41
0
            Self::Ask => write!(f, "ASK"),
42
0
            Self::Quote => write!(f, "QUOTE"),
43
        }
44
0
    }
45
}
46
47
/// Order book action type
48
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49
pub enum BookAction {
50
    /// Update price level
51
    Update,
52
    /// Delete price level
53
    Delete,
54
    /// Clear entire book
55
    Clear,
56
}
57
58
impl fmt::Display for BookAction {
59
    /// Format the book action for display
60
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61
0
        match self {
62
0
            Self::Update => write!(f, "UPDATE"),
63
0
            Self::Delete => write!(f, "DELETE"),
64
0
            Self::Clear => write!(f, "CLEAR"),
65
        }
66
0
    }
67
}
68
69
/// Market regime classification
70
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71
pub enum MarketRegime {
72
    /// Normal market conditions
73
    Normal,
74
    /// Crisis/stress market conditions
75
    Crisis,
76
    /// Trending market (strong directional movement)
77
    Trending,
78
    /// Sideways/ranging market (low volatility)
79
    Sideways,
80
    /// Bull market (sustained upward trend)
81
    Bull,
82
    /// Bear market (sustained downward trend)
83
    Bear,
84
}
85
86
impl fmt::Display for MarketRegime {
87
    /// Format the market regime for display
88
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89
0
        match self {
90
0
            Self::Normal => write!(f, "NORMAL"),
91
0
            Self::Crisis => write!(f, "CRISIS"),
92
0
            Self::Trending => write!(f, "TRENDING"),
93
0
            Self::Sideways => write!(f, "SIDEWAYS"),
94
0
            Self::Bull => write!(f, "BULL"),
95
0
            Self::Bear => write!(f, "BEAR"),
96
        }
97
0
    }
98
}
99
100
/// Core Quantity type using fixed-point arithmetic for precise calculations
101
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
102
pub struct Quantity {
103
    /// Internal representation using 6 decimal places (scale factor of 1,000,000)
104
    value: u64,
105
}
106
107
impl Quantity {
108
    /// Scale factor for fixed-point arithmetic (6 decimal places)
109
    pub const SCALE: u64 = 1_000_000;
110
111
    /// Zero quantity
112
    pub const ZERO: Self = Self { value: 0 };
113
114
    /// Create a new quantity from a floating-point value
115
0
    pub fn new(value: f64) -> Result<Self, &'static str> {
116
0
        if value < 0.0 {
117
0
            return Err("Quantity cannot be negative");
118
0
        }
119
0
        if !value.is_finite() {
120
0
            return Err("Quantity must be finite");
121
0
        }
122
123
0
        let scaled = (value * Self::SCALE as f64).round() as u64;
124
0
        Ok(Self { value: scaled })
125
0
    }
126
127
    /// Create from raw internal value
128
0
    pub const fn from_raw(value: u64) -> Self {
129
0
        Self { value }
130
0
    }
131
132
    /// Get raw internal value
133
0
    pub const fn raw(&self) -> u64 {
134
0
        self.value
135
0
    }
136
137
    /// Convert to floating-point value
138
0
    pub fn to_f64(&self) -> f64 {
139
0
        self.value as f64 / Self::SCALE as f64
140
0
    }
141
142
    /// Convert to decimal
143
0
    pub fn to_decimal(&self) -> Decimal {
144
0
        Decimal::new(self.value as i64, 6)
145
0
    }
146
147
    /// Add two quantities
148
0
    pub fn add(&self, other: Self) -> Self {
149
0
        Self {
150
0
            value: self.value + other.value,
151
0
        }
152
0
    }
153
154
    /// Subtract two quantities
155
0
    pub fn subtract(&self, other: Self) -> Self {
156
0
        Self {
157
0
            value: self.value.saturating_sub(other.value),
158
0
        }
159
0
    }
160
}
161
162
impl fmt::Display for Quantity {
163
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164
0
        write!(f, "{:.6}", self.to_f64())
165
0
    }
166
}
167
168
impl std::ops::Add for Quantity {
169
    type Output = Self;
170
171
0
    fn add(self, other: Self) -> Self::Output {
172
0
        Self {
173
0
            value: self.value + other.value,
174
0
        }
175
0
    }
176
}
177
178
impl std::ops::Sub for Quantity {
179
    type Output = Self;
180
181
0
    fn sub(self, other: Self) -> Self::Output {
182
0
        Self {
183
0
            value: self.value.saturating_sub(other.value),
184
0
        }
185
0
    }
186
}
187
188
/// Order event for tracking order lifecycle
189
#[derive(Debug, Clone, Serialize, Deserialize)]
190
pub struct OrderEvent {
191
    /// Unique order identifier
192
    pub order_id: String,
193
    /// Trading symbol
194
    pub symbol: String,
195
    /// Order type (Market, Limit, etc.)
196
    pub order_type: OrderType,
197
    /// Order side (Buy/Sell)
198
    pub side: OrderSide,
199
    /// Order quantity
200
    pub quantity: Quantity,
201
    /// Order price (None for market orders)
202
    pub price: Option<Decimal>,
203
    /// Event timestamp
204
    pub timestamp: DateTime<Utc>,
205
    /// Strategy identifier
206
    pub strategy_id: String,
207
    /// Type of order event
208
    pub event_type: OrderEventType,
209
    /// Previous quantity for modifications
210
    pub previous_quantity: Option<Quantity>,
211
    /// Previous price for modifications
212
    pub previous_price: Option<Decimal>,
213
    /// Reason for cancellation or modification
214
    pub reason: Option<String>,
215
}
216
217
/// Types of order events
218
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219
pub enum OrderEventType {
220
    /// Order was placed
221
    Placed,
222
    /// Order was modified
223
    Modified,
224
    /// Order was cancelled
225
    Cancelled,
226
    /// Order was rejected
227
    Rejected,
228
    /// Order expired
229
    Expired,
230
}
231
232
impl fmt::Display for OrderEventType {
233
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234
0
        match self {
235
0
            Self::Placed => write!(f, "PLACED"),
236
0
            Self::Modified => write!(f, "MODIFIED"),
237
0
            Self::Cancelled => write!(f, "CANCELLED"),
238
0
            Self::Rejected => write!(f, "REJECTED"),
239
0
            Self::Expired => write!(f, "EXPIRED"),
240
        }
241
0
    }
242
}
243
244
/// Order type enumeration
245
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
246
pub enum OrderType {
247
    /// Market order - execute immediately at best available price
248
    Market,
249
    /// Limit order - execute only at specified price or better
250
    Limit,
251
    /// Stop order - becomes market order when stop price is reached
252
    Stop,
253
    /// Stop-limit order - becomes limit order when stop price is reached
254
    StopLimit,
255
}
256
257
impl fmt::Display for OrderType {
258
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259
0
        match self {
260
0
            Self::Market => write!(f, "MARKET"),
261
0
            Self::Limit => write!(f, "LIMIT"),
262
0
            Self::Stop => write!(f, "STOP"),
263
0
            Self::StopLimit => write!(f, "STOP_LIMIT"),
264
        }
265
0
    }
266
}
267
268
/// Order side enumeration
269
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
270
pub enum OrderSide {
271
    /// Buy order
272
    Buy,
273
    /// Sell order
274
    Sell,
275
}
276
277
impl fmt::Display for OrderSide {
278
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279
0
        match self {
280
0
            Self::Buy => write!(f, "BUY"),
281
0
            Self::Sell => write!(f, "SELL"),
282
        }
283
0
    }
284
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html deleted file mode 100644 index 7ae308797..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/traits.rs
Line
Count
Source
1
//! Common traits used across services
2
//!
3
//! This module provides shared traits that define common interfaces
4
//! for services in the Foxhunt HFT trading system.
5
6
use crate::error::CommonResult;
7
use crate::types::{ServiceStatus, Timestamp};
8
use async_trait::async_trait;
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
12
/// Trait for configurable components
13
#[async_trait]
14
pub trait Configurable {
15
    /// Configuration type for this component
16
    type Config: Clone + Send + Sync;
17
18
    /// Apply configuration changes
19
    async fn configure(&mut self, config: Self::Config) -> CommonResult<()>;
20
21
    /// Get current configuration
22
    fn get_config(&self) -> &Self::Config;
23
24
    /// Validate configuration before applying
25
    fn validate_config(config: &Self::Config) -> CommonResult<()>;
26
}
27
28
/// Trait for health check capabilities
29
#[async_trait]
30
pub trait HealthCheck {
31
    /// Perform a health check
32
    async fn health_check(&self) -> CommonResult<HealthStatus>;
33
34
    /// Get detailed health information
35
    async fn detailed_health(&self) -> CommonResult<DetailedHealth>;
36
}
37
38
/// Health status for components
39
#[derive(Debug, Clone, Serialize, Deserialize)]
40
pub struct HealthStatus {
41
    /// Overall health status
42
    pub status: ServiceStatus,
43
    /// Timestamp of the health check
44
    pub timestamp: Timestamp,
45
    /// Optional message
46
    pub message: Option<String>,
47
}
48
49
/// Detailed health information
50
#[derive(Debug, Clone, Serialize, Deserialize)]
51
pub struct DetailedHealth {
52
    /// Basic health status
53
    pub status: HealthStatus,
54
    /// Component-specific metrics
55
    pub metrics: HashMap<String, f64>,
56
    /// Sub-component health statuses
57
    pub components: HashMap<String, HealthStatus>,
58
}
59
60
/// Trait for metrics collection
61
pub trait Metrics {
62
    /// Metrics type for this component
63
    type Metrics: Clone + Send + Sync + Serialize;
64
65
    /// Get current metrics
66
    fn get_metrics(&self) -> Self::Metrics;
67
68
    /// Reset metrics counters
69
    fn reset_metrics(&mut self);
70
}
71
72
/// Trait for service lifecycle management
73
#[async_trait]
74
pub trait Service: Send + Sync {
75
    /// Start the service
76
    async fn start(&mut self) -> CommonResult<()>;
77
78
    /// Stop the service gracefully
79
    async fn stop(&mut self) -> CommonResult<()>;
80
81
    /// Get current service status
82
    fn status(&self) -> ServiceStatus;
83
84
    /// Get service name
85
    fn name(&self) -> &str;
86
87
    /// Get service version
88
    fn version(&self) -> &str;
89
}
90
91
/// Trait for components that can be reloaded
92
#[async_trait]
93
pub trait Reloadable {
94
    /// Reload the component (hot reload)
95
    async fn reload(&mut self) -> CommonResult<()>;
96
97
    /// Check if reload is supported
98
0
    fn supports_reload(&self) -> bool {
99
0
        true
100
0
    }
101
}
102
103
/// Trait for components with graceful shutdown
104
#[async_trait]
105
pub trait GracefulShutdown {
106
    /// Initiate graceful shutdown
107
    async fn shutdown(&mut self) -> CommonResult<()>;
108
109
    /// Force shutdown (emergency stop)
110
    async fn force_shutdown(&mut self) -> CommonResult<()>;
111
112
    /// Get shutdown timeout duration in seconds
113
0
    fn shutdown_timeout_seconds(&self) -> u64 {
114
0
        30 // Default 30 seconds
115
0
    }
116
}
117
118
/// Trait for components that support circuit breaking
119
pub trait CircuitBreaker {
120
    /// Check if circuit is open
121
    fn is_circuit_open(&self) -> bool;
122
123
    /// Get failure count
124
    fn failure_count(&self) -> u64;
125
126
    /// Reset circuit breaker
127
    fn reset_circuit(&mut self);
128
}
129
130
/// Trait for rate-limited operations
131
pub trait RateLimited {
132
    /// Check if operation is allowed under rate limits
133
    fn is_allowed(&self) -> bool;
134
135
    /// Get current rate limit status
136
    fn rate_limit_status(&self) -> RateLimitStatus;
137
}
138
139
/// Rate limit status information
140
#[derive(Debug, Clone, Serialize, Deserialize)]
141
pub struct RateLimitStatus {
142
    /// Current request count in the window
143
    pub current_count: u64,
144
    /// Maximum requests allowed in the window
145
    pub max_requests: u64,
146
    /// Time window in seconds
147
    pub window_seconds: u64,
148
    /// Seconds until window resets
149
    pub reset_in_seconds: u64,
150
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html deleted file mode 100644 index 3d2b535ec..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/types.rs
Line
Count
Source
1
//! Common data types used across services
2
//!
3
//! This module provides shared data types that are used throughout
4
//! the Foxhunt HFT trading system. This includes both infrastructure types
5
//! and core trading types migrated from foxhunt-common-types.
6
7
use crate::error::ErrorCategory;
8
use chrono::{DateTime, Utc};
9
// ELIMINATED: Re-exports removed to force explicit imports
10
// NO RE-EXPORTS: Import rust_decimal::Decimal directly in each crate that needs it
11
use rust_decimal::Decimal; // Internal use only - other crates must import directly
12
use serde::{Deserialize, Serialize};
13
use serde_json::Value;
14
use std::collections::HashMap;
15
use std::sync::{Arc, Mutex, RwLock};
16
17
use crate::error::{CommonError, ErrorCategory as CommonErrorCategory};
18
use num_traits::FromPrimitive;
19
use std::convert::TryFrom;
20
use std::fmt;
21
use std::iter::Sum;
22
use std::num::ParseIntError;
23
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
24
use std::str::FromStr;
25
use uuid::Uuid;
26
27
// =============================================================================
28
// Type Aliases for Complex Types
29
// =============================================================================
30
31
/// Common error type for async operations
32
pub type AsyncResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
33
34
/// Thread-safe hash map for shared state
35
pub type SharedHashMap<K, V> = Arc<RwLock<HashMap<K, V>>>;
36
37
/// Thread-safe hash map with Mutex for shared state
38
pub type MutexHashMap<K, V> = Arc<Mutex<HashMap<K, V>>>;
39
40
/// Thread-safe container for any value
41
pub type SharedValue<T> = Arc<RwLock<T>>;
42
43
/// Thread-safe container with Mutex for any value
44
pub type MutexValue<T> = Arc<Mutex<T>>;
45
46
// Trading-specific type aliases
47
/// Map of positions by symbol
48
pub type PositionMap<T> = SharedHashMap<String, T>;
49
50
/// Map of orders by order ID
51
pub type OrderMap<T> = SharedHashMap<String, T>;
52
53
/// Map of accounts by account ID
54
pub type AccountMap<T> = SharedHashMap<String, T>;
55
56
/// Map of instruments by instrument ID
57
pub type InstrumentMap<T> = SharedHashMap<String, T>;
58
59
/// Map of market data by symbol
60
pub type MarketDataMap<T> = SharedHashMap<String, T>;
61
62
/// Cache entry with timestamp
63
pub type CacheEntry<T> = (T, DateTime<Utc>);
64
65
/// Cache map with timestamped entries
66
pub type CacheMap<K, V> = SharedHashMap<K, CacheEntry<V>>;
67
68
/// Risk factor loadings by instrument
69
pub type RiskFactorMap = SharedHashMap<String, HashMap<String, Decimal>>;
70
71
/// Performance metrics history
72
pub type PerformanceHistory<T> = SharedHashMap<String, std::collections::VecDeque<T>>;
73
74
/// Model registry for ML models
75
pub type ModelRegistry<T> = SharedHashMap<String, T>;
76
77
/// Generic configuration cache
78
pub type ConfigCache<K, V> = SharedHashMap<K, V>;
79
80
// =============================================================================
81
// Event Types - Moved from trading_engine to enforce pure client architecture
82
// =============================================================================
83
84
/// Order events for the complete order lifecycle
85
#[derive(Debug, Clone, Serialize, Deserialize)]
86
pub struct OrderEvent {
87
    /// Unique identifier for the order
88
    pub order_id: OrderId,
89
    /// Trading symbol for the order
90
    pub symbol: Symbol,
91
    /// Type of order (market, limit, stop, etc.)
92
    pub order_type: OrderType,
93
    /// Order side (buy or sell)
94
    pub side: OrderSide,
95
    /// Order quantity
96
    pub quantity: Quantity,
97
    /// Order price (None for market orders)
98
    pub price: Option<Price>,
99
    /// Timestamp when the event occurred
100
    pub timestamp: DateTime<Utc>,
101
    /// Strategy or client identifier
102
    pub strategy_id: String,
103
    /// Order event type (placed, modified, cancelled)
104
    pub event_type: OrderEventType,
105
    /// Previous quantity for modifications
106
    pub previous_quantity: Option<Quantity>,
107
    /// Previous price for modifications
108
    pub previous_price: Option<Price>,
109
    /// Reason for cancellation or modification
110
    pub reason: Option<String>,
111
}
112
113
/// Types of order events
114
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115
pub enum OrderEventType {
116
    /// Order was placed
117
    Placed,
118
    /// Order was modified
119
    Modified,
120
    /// Order was cancelled
121
    Cancelled,
122
    /// Order was rejected
123
    Rejected,
124
}
125
126
// =============================================================================
127
// Core Data Types
128
// =============================================================================
129
130
/// Unique identifier for services
131
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
132
pub struct ServiceId(pub String);
133
134
impl ServiceId {
135
    /// Create a new service ID
136
2
    pub fn new<S: Into<String>>(id: S) -> Self {
137
2
        Self(id.into())
138
2
    }
139
140
    /// Get the inner string value
141
    /// Get the execution ID as a string slice
142
    /// Get execution ID as string slice
143
2
    pub fn as_str(&self) -> &str {
144
2
        &self.0
145
2
    }
146
}
147
148
impl fmt::Display for ServiceId {
149
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150
1
        write!(f, "{}", self.0)
151
1
    }
152
}
153
154
impl From<&str> for ServiceId {
155
0
    fn from(s: &str) -> Self {
156
0
        Self(s.to_owned())
157
0
    }
158
}
159
160
impl From<String> for ServiceId {
161
1
    fn from(s: String) -> Self {
162
1
        Self(s)
163
1
    }
164
}
165
166
/// Service status enumeration
167
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168
pub enum ServiceStatus {
169
    /// Service is starting up
170
    Starting,
171
    /// Service is running normally
172
    Running,
173
    /// Service is degraded but functional
174
    Degraded,
175
    /// Service is stopping
176
    Stopping,
177
    /// Service is stopped
178
    Stopped,
179
    /// Service has encountered an error
180
    Error,
181
    /// Service is in maintenance mode
182
    Maintenance,
183
}
184
185
impl fmt::Display for ServiceStatus {
186
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187
0
        match self {
188
0
            Self::Starting => write!(f, "STARTING"),
189
0
            Self::Running => write!(f, "RUNNING"),
190
0
            Self::Degraded => write!(f, "DEGRADED"),
191
0
            Self::Stopping => write!(f, "STOPPING"),
192
0
            Self::Stopped => write!(f, "STOPPED"),
193
0
            Self::Error => write!(f, "ERROR"),
194
0
            Self::Maintenance => write!(f, "MAINTENANCE"),
195
        }
196
0
    }
197
}
198
199
impl ServiceStatus {
200
    /// Check if the service is healthy
201
4
    pub fn is_healthy(&self) -> bool {
202
4
        
matches!2
(self, Self::Running | Self::Starting)
203
4
    }
204
205
    /// Check if the service is available for requests
206
4
    pub fn is_available(&self) -> bool {
207
4
        
matches!2
(self, Self::Running | Self::Degraded)
208
4
    }
209
}
210
211
/// Configuration version for tracking changes
212
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213
pub struct ConfigVersion {
214
    /// Version number
215
    pub version: u64,
216
    /// Timestamp when version was created
217
    pub timestamp: DateTime<Utc>,
218
    /// Optional description of changes
219
    pub description: Option<String>,
220
}
221
222
impl ConfigVersion {
223
    /// Create a new config version
224
1
    pub fn new(version: u64) -> Self {
225
1
        Self {
226
1
            version,
227
1
            timestamp: Utc::now(),
228
1
            description: None,
229
1
        }
230
1
    }
231
232
    /// Create a new config version with description
233
1
    pub fn with_description<S: Into<String>>(version: u64, description: S) -> Self {
234
1
        Self {
235
1
            version,
236
1
            timestamp: Utc::now(),
237
1
            description: Some(description.into()),
238
1
        }
239
1
    }
240
}
241
242
// TECHNICAL DEBT ELIMINATED - Use DateTime<Utc> directly instead of Timestamp alias
243
244
/// Timestamp type alias for consistency across the system
245
pub type Timestamp = DateTime<Utc>;
246
247
/// Request ID for tracing and correlation
248
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
249
pub struct RequestId(pub Uuid);
250
251
impl Default for RequestId {
252
    /// Create a default request ID with a new UUID
253
0
    fn default() -> Self {
254
0
        Self::new()
255
0
    }
256
}
257
258
impl RequestId {
259
    /// Generate a new random request ID
260
2
    pub fn new() -> Self {
261
2
        Self(Uuid::new_v4())
262
2
    }
263
264
    /// Create from UUID
265
0
    pub fn from_uuid(uuid: Uuid) -> Self {
266
0
        Self(uuid)
267
0
    }
268
269
    /// Get the inner UUID
270
0
    pub fn as_uuid(&self) -> Uuid {
271
0
        self.0
272
0
    }
273
}
274
275
// Default implementation is now in the derive macro above
276
277
// =============================================================================
278
// MARKET DATA EVENT TYPES (Consolidated from data and trading_engine crates)
279
// =============================================================================
280
281
/// Market data event types - CANONICAL DEFINITION
282
#[derive(Debug, Clone, Serialize, Deserialize)]
283
pub enum MarketDataEvent {
284
    /// Quote update (bid/ask)
285
    Quote(QuoteEvent),
286
    /// Trade execution
287
    Trade(TradeEvent),
288
    /// Aggregate trade data
289
    Aggregate(Aggregate),
290
    /// Bar/candle data
291
    Bar(BarEvent),
292
    /// Level 2 market data update
293
    Level2(Level2Update),
294
    /// Market status update
295
    Status(MarketStatus),
296
    /// Connection status updates
297
    ConnectionStatus(ConnectionEvent),
298
    /// Error events with details
299
    Error(ErrorEvent),
300
    /// Order book update
301
    OrderBook(OrderBookEvent),
302
    /// Level 2 order book snapshot
303
    OrderBookL2Snapshot(OrderBookSnapshot),
304
    /// Level 2 order book incremental update
305
    OrderBookL2Update(OrderBookUpdate),
306
}
307
308
/// Quote event structure - CANONICAL DEFINITION
309
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
310
pub struct QuoteEvent {
311
    /// Symbol
312
    pub symbol: String,
313
    /// Bid price
314
    pub bid: Option<Decimal>,
315
    /// Ask price
316
    pub ask: Option<Decimal>,
317
    /// Bid size
318
    pub bid_size: Option<Decimal>,
319
    /// Ask size
320
    pub ask_size: Option<Decimal>,
321
    /// Exchange
322
    pub exchange: Option<String>,
323
    /// Bid exchange
324
    pub bid_exchange: Option<String>,
325
    /// Ask exchange
326
    pub ask_exchange: Option<String>,
327
    /// Quote conditions
328
    pub conditions: Vec<String>,
329
    /// Timestamp
330
    pub timestamp: DateTime<Utc>,
331
    /// Sequence number
332
    pub sequence: u64,
333
}
334
335
impl QuoteEvent {
336
    /// Create a new quote event
337
    #[must_use]
338
6
    pub fn new(symbol: String, timestamp: DateTime<Utc>) -> Self {
339
6
        Self {
340
6
            symbol,
341
6
            bid: None,
342
6
            ask: None,
343
6
            bid_size: None,
344
6
            ask_size: None,
345
6
            exchange: None,
346
6
            bid_exchange: None,
347
6
            ask_exchange: None,
348
6
            conditions: Vec::new(),
349
6
            timestamp,
350
6
            sequence: 0,
351
6
        }
352
6
    }
353
354
    /// Set bid price and size
355
4
    pub fn with_bid(mut self, price: Decimal, size: Decimal) -> Self {
356
4
        self.bid = Some(price);
357
4
        self.bid_size = Some(size);
358
4
        self
359
4
    }
360
361
    /// Set ask price and size
362
4
    pub fn with_ask(mut self, price: Decimal, size: Decimal) -> Self {
363
4
        self.ask = Some(price);
364
4
        self.ask_size = Some(size);
365
4
        self
366
4
    }
367
368
    /// Set exchange
369
1
    pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
370
1
        self.exchange = Some(exchange.into());
371
1
        self
372
1
    }
373
374
    /// Set bid exchange
375
0
    pub fn with_bid_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
376
0
        self.bid_exchange = Some(exchange.into());
377
0
        self
378
0
    }
379
380
    /// Set ask exchange
381
0
    pub fn with_ask_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
382
0
        self.ask_exchange = Some(exchange.into());
383
0
        self
384
0
    }
385
386
    /// Add quote condition
387
0
    pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
388
0
        self.conditions.push(condition.into());
389
0
        self
390
0
    }
391
392
    /// Set sequence number
393
1
    pub fn with_sequence(mut self, sequence: u64) -> Self {
394
1
        self.sequence = sequence;
395
1
        self
396
1
    }
397
398
    /// Get mid price
399
1
    pub fn mid_price(&self) -> Option<Decimal> {
400
1
        match (self.bid, self.ask) {
401
1
            (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::from(2)),
402
0
            _ => None,
403
        }
404
1
    }
405
406
    /// Get spread
407
1
    pub fn spread(&self) -> Option<Decimal> {
408
1
        match (self.bid, self.ask) {
409
1
            (Some(bid), Some(ask)) => Some(ask - bid),
410
0
            _ => None,
411
        }
412
1
    }
413
}
414
415
/// Trade event structure - CANONICAL DEFINITION
416
#[derive(Debug, Clone, Serialize, Deserialize)]
417
pub struct TradeEvent {
418
    /// Symbol
419
    pub symbol: String,
420
    /// Trade price
421
    pub price: Decimal,
422
    /// Trade size
423
    pub size: Decimal,
424
    /// Trade ID
425
    pub trade_id: Option<String>,
426
    /// Exchange
427
    pub exchange: Option<String>,
428
    /// Trade conditions
429
    pub conditions: Vec<String>,
430
    /// Timestamp
431
    pub timestamp: DateTime<Utc>,
432
    /// Sequence number
433
    pub sequence: u64,
434
}
435
436
impl TradeEvent {
437
    /// Create a new trade event
438
    #[must_use]
439
4
    pub fn new(symbol: String, price: Decimal, size: Decimal, timestamp: DateTime<Utc>) -> Self {
440
4
        Self {
441
4
            symbol,
442
4
            price,
443
4
            size,
444
4
            trade_id: None,
445
4
            exchange: None,
446
4
            conditions: Vec::new(),
447
4
            timestamp,
448
4
            sequence: 0,
449
4
        }
450
4
    }
451
452
    /// Set trade ID
453
0
    pub fn with_trade_id<S: Into<String>>(mut self, trade_id: S) -> Self {
454
0
        self.trade_id = Some(trade_id.into());
455
0
        self
456
0
    }
457
458
    /// Set exchange
459
0
    pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
460
0
        self.exchange = Some(exchange.into());
461
0
        self
462
0
    }
463
464
    /// Add trade condition
465
0
    pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
466
0
        self.conditions.push(condition.into());
467
0
        self
468
0
    }
469
470
    /// Set sequence number
471
0
    pub fn with_sequence(mut self, sequence: u64) -> Self {
472
0
        self.sequence = sequence;
473
0
        self
474
0
    }
475
476
    /// Get notional value
477
1
    pub fn notional_value(&self) -> Decimal {
478
1
        self.price * self.size
479
1
    }
480
}
481
482
/// Aggregate trade data
483
#[derive(Debug, Clone, Serialize, Deserialize)]
484
pub struct Aggregate {
485
    /// Symbol
486
    pub symbol: String,
487
    /// Open price
488
    pub open: Decimal,
489
    /// High price
490
    pub high: Decimal,
491
    /// Low price
492
    pub low: Decimal,
493
    /// Close price
494
    pub close: Decimal,
495
    /// Volume
496
    pub volume: Decimal,
497
    /// Volume weighted average price
498
    pub vwap: Option<Decimal>,
499
    /// Start timestamp
500
    pub start_timestamp: DateTime<Utc>,
501
    /// End timestamp
502
    pub end_timestamp: DateTime<Utc>,
503
}
504
505
/// Bar/candle event structure
506
#[derive(Debug, Clone, Serialize, Deserialize)]
507
pub struct BarEvent {
508
    /// Symbol
509
    pub symbol: String,
510
    /// Open price
511
    pub open: Decimal,
512
    /// High price
513
    pub high: Decimal,
514
    /// Low price
515
    pub low: Decimal,
516
    /// Close price
517
    pub close: Decimal,
518
    /// Volume
519
    pub volume: Decimal,
520
    /// Volume weighted average price
521
    pub vwap: Option<Decimal>,
522
    /// Start timestamp
523
    pub start_timestamp: DateTime<Utc>,
524
    /// End timestamp
525
    pub end_timestamp: DateTime<Utc>,
526
    /// Timeframe (e.g., "1m", "5m", "1h")
527
    pub timeframe: String,
528
}
529
530
/// Level 2 market data update
531
#[derive(Debug, Clone, Serialize, Deserialize)]
532
pub struct Level2Update {
533
    /// Symbol
534
    pub symbol: String,
535
    /// Bid levels
536
    pub bids: Vec<PriceLevel>,
537
    /// Ask levels
538
    pub asks: Vec<PriceLevel>,
539
    /// Timestamp
540
    pub timestamp: DateTime<Utc>,
541
}
542
543
/// Price level for order book
544
#[derive(Debug, Clone, Serialize, Deserialize)]
545
pub struct PriceLevel {
546
    /// Price
547
    pub price: Decimal,
548
    /// Size at this price level
549
    pub size: Decimal,
550
}
551
552
/// Order book snapshot from providers
553
#[derive(Debug, Clone, Serialize, Deserialize)]
554
pub struct OrderBookSnapshot {
555
    /// Symbol
556
    pub symbol: String,
557
    /// Bid levels (price, size) sorted by price descending
558
    pub bids: Vec<PriceLevel>,
559
    /// Ask levels (price, size) sorted by price ascending
560
    pub asks: Vec<PriceLevel>,
561
    /// Exchange
562
    pub exchange: String,
563
    /// Timestamp of snapshot
564
    pub timestamp: DateTime<Utc>,
565
    /// Sequence number
566
    pub sequence: u64,
567
}
568
569
/// Incremental order book update from providers
570
#[derive(Debug, Clone, Serialize, Deserialize)]
571
pub struct OrderBookUpdate {
572
    /// Symbol
573
    pub symbol: String,
574
    /// Changes to bid levels
575
    pub bid_changes: Vec<PriceLevelChange>,
576
    /// Changes to ask levels
577
    pub ask_changes: Vec<PriceLevelChange>,
578
    /// Exchange
579
    pub exchange: String,
580
    /// Timestamp of update
581
    pub timestamp: DateTime<Utc>,
582
    /// Sequence number
583
    pub sequence: u64,
584
}
585
586
/// Change to a price level
587
#[derive(Debug, Clone, Serialize, Deserialize)]
588
pub struct PriceLevelChange {
589
    /// Price level being modified
590
    pub price: Decimal,
591
    /// New size (0 = remove level)
592
    pub size: Decimal,
593
    /// Type of change
594
    pub change_type: PriceLevelChangeType,
595
    /// Side (bid or ask)
596
    pub side: OrderBookSide,
597
}
598
599
/// Type of price level change
600
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
601
pub enum PriceLevelChangeType {
602
    /// Add new price level
603
    Add,
604
    /// Update existing price level
605
    Update,
606
    /// Remove price level
607
    Delete,
608
}
609
610
/// Order book side
611
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
612
pub enum OrderBookSide {
613
    /// Bid side
614
    Bid,
615
    /// Ask side
616
    Ask,
617
}
618
619
/// Market status information
620
#[derive(Debug, Clone, Serialize, Deserialize)]
621
pub struct MarketStatus {
622
    /// Market
623
    pub market: String,
624
    /// Status (open, closed, early_hours, etc.)
625
    pub status: String,
626
    /// Timestamp
627
    pub timestamp: DateTime<Utc>,
628
}
629
630
/// Connection event for status updates
631
#[derive(Debug, Clone, Serialize, Deserialize)]
632
pub struct ConnectionEvent {
633
    /// Provider name
634
    pub provider: String,
635
    /// Connection status
636
    pub status: ConnectionStatus,
637
    /// Optional message
638
    pub message: Option<String>,
639
    /// Timestamp
640
    pub timestamp: DateTime<Utc>,
641
}
642
643
/// Connection status enumeration
644
/// Connection status for data providers and brokers
645
#[derive(Debug, Clone, Serialize, Deserialize)]
646
#[cfg_attr(feature = "database", derive(sqlx::Type))]
647
#[cfg_attr(
648
    feature = "database",
649
    sqlx(type_name = "connection_status", rename_all = "snake_case")
650
)]
651
pub enum ConnectionStatus {
652
    /// Successfully connected and operational
653
    Connected,
654
    /// Disconnected from the service
655
    Disconnected,
656
    /// Currently attempting to reconnect
657
    Reconnecting,
658
}
659
660
/// Error event structure
661
#[derive(Debug, Clone, Serialize, Deserialize)]
662
pub struct ErrorEvent {
663
    /// Provider name
664
    pub provider: String,
665
    /// Error message
666
    pub message: String,
667
    /// Error category
668
    pub category: ErrorCategory,
669
    /// Timestamp
670
    pub timestamp: DateTime<Utc>,
671
}
672
673
// ErrorCategory is imported from crate::error as CommonErrorCategory
674
675
/// Order book event
676
#[derive(Debug, Clone, Serialize, Deserialize)]
677
pub struct OrderBookEvent {
678
    /// Symbol
679
    pub symbol: String,
680
    /// Timestamp
681
    pub timestamp: DateTime<Utc>,
682
    /// Bid levels
683
    pub bids: Vec<(Price, Quantity)>,
684
    /// Ask levels
685
    pub asks: Vec<(Price, Quantity)>,
686
}
687
688
/// Data types for subscription
689
#[derive(Debug, Clone, Serialize, Deserialize)]
690
pub enum DataType {
691
    /// Real-time quotes
692
    Quotes,
693
    /// Real-time trades
694
    Trades,
695
    /// Aggregate/minute bars
696
    Aggregates,
697
    /// Level 2 order book
698
    Level2,
699
    /// Market status
700
    Status,
701
    /// Historical bars/aggregates
702
    Bars,
703
    /// Order book data
704
    OrderBook,
705
    /// Volume data
706
    Volume,
707
}
708
709
/// Market data subscription request
710
#[derive(Debug, Clone, Serialize, Deserialize)]
711
pub struct Subscription {
712
    /// Symbols to subscribe to
713
    pub symbols: Vec<String>,
714
    /// Data types to subscribe to
715
    pub data_types: Vec<DataType>,
716
    /// Exchange filter (optional)
717
    pub exchanges: Vec<String>,
718
}
719
720
impl MarketDataEvent {
721
    /// Get the symbol for any market data event
722
1
    pub fn symbol(&self) -> &str {
723
1
        match self {
724
1
            MarketDataEvent::Quote(q) => &q.symbol,
725
0
            MarketDataEvent::Trade(t) => &t.symbol,
726
0
            MarketDataEvent::Aggregate(a) => &a.symbol,
727
0
            MarketDataEvent::Bar(b) => &b.symbol,
728
0
            MarketDataEvent::Level2(l) => &l.symbol,
729
0
            MarketDataEvent::Status(s) => &s.market,
730
0
            MarketDataEvent::ConnectionStatus(_) => "",
731
0
            MarketDataEvent::Error(_) => "",
732
0
            MarketDataEvent::OrderBook(o) => &o.symbol,
733
0
            MarketDataEvent::OrderBookL2Snapshot(s) => &s.symbol,
734
0
            MarketDataEvent::OrderBookL2Update(u) => &u.symbol,
735
        }
736
1
    }
737
738
    /// Get the timestamp for any market data event
739
1
    pub fn timestamp(&self) -> Option<DateTime<Utc>> {
740
1
        match self {
741
0
            MarketDataEvent::Quote(q) => Some(q.timestamp),
742
1
            MarketDataEvent::Trade(t) => Some(t.timestamp),
743
0
            MarketDataEvent::Aggregate(a) => Some(a.end_timestamp),
744
0
            MarketDataEvent::Bar(b) => Some(b.end_timestamp),
745
0
            MarketDataEvent::Level2(l) => Some(l.timestamp),
746
0
            MarketDataEvent::Status(s) => Some(s.timestamp),
747
0
            MarketDataEvent::ConnectionStatus(c) => Some(c.timestamp),
748
0
            MarketDataEvent::Error(e) => Some(e.timestamp),
749
0
            MarketDataEvent::OrderBook(o) => Some(o.timestamp),
750
0
            MarketDataEvent::OrderBookL2Snapshot(s) => Some(s.timestamp),
751
0
            MarketDataEvent::OrderBookL2Update(u) => Some(u.timestamp),
752
        }
753
1
    }
754
}
755
impl fmt::Display for RequestId {
756
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
757
0
        write!(f, "{}", self.0)
758
0
    }
759
}
760
761
/// Connection information for services
762
#[derive(Debug, Clone, Serialize, Deserialize)]
763
pub struct ConnectionInfo {
764
    /// Host address
765
    pub host: String,
766
    /// Port number
767
    pub port: u16,
768
    /// Whether TLS is enabled
769
    pub tls: bool,
770
    /// Connection timeout in milliseconds
771
    pub timeout_ms: u64,
772
}
773
774
impl ConnectionInfo {
775
    /// Create new connection info
776
1
    pub fn new<S: Into<String>>(host: S, port: u16) -> Self {
777
1
        Self {
778
1
            host: host.into(),
779
1
            port,
780
1
            tls: false,
781
1
            timeout_ms: 5000,
782
1
        }
783
1
    }
784
785
    /// Enable TLS
786
1
    pub fn with_tls(mut self) -> Self {
787
1
        self.tls = true;
788
1
        self
789
1
    }
790
791
    /// Set timeout
792
0
    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
793
0
        self.timeout_ms = timeout_ms;
794
0
        self
795
0
    }
796
797
    /// Get connection URL
798
2
    pub fn url(&self) -> String {
799
2
        let scheme = if self.tls { 
"https"1
} else {
"http"1
};
800
2
        format!("{}://{}:{}", scheme, self.host, self.port)
801
2
    }
802
}
803
804
/// Resource limits for services
805
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
806
pub struct ResourceLimits {
807
    /// Maximum memory usage in bytes
808
    pub max_memory_bytes: Option<u64>,
809
    /// Maximum CPU usage as percentage (0-100)
810
    pub max_cpu_percent: Option<f64>,
811
    /// Maximum number of open file descriptors
812
    pub max_file_descriptors: Option<u32>,
813
    /// Maximum number of network connections
814
    pub max_connections: Option<u32>,
815
}
816
817
// =============================================================================
818
// TRADING TYPES (Migrated from foxhunt-common-types)
819
// =============================================================================
820
821
/// Common error types for trading operations
822
///
823
/// This error type implements Send + Sync for use in async contexts
824
#[derive(thiserror::Error, Debug)]
825
pub enum CommonTypeError {
826
    /// Invalid price value
827
    #[error("Invalid price: {value} - {reason}")]
828
    InvalidPrice {
829
        /// The invalid price value as string
830
        value: String,
831
        /// Reason why the price is invalid
832
        reason: String,
833
    },
834
835
    /// Invalid quantity value
836
    #[error("Invalid quantity: {value} - {reason}")]
837
    InvalidQuantity {
838
        /// The invalid quantity value as string
839
        value: String,
840
        /// Reason why the quantity is invalid
841
        reason: String,
842
    },
843
844
    /// Invalid identifier
845
    #[error("Invalid {field}: {reason}")]
846
    InvalidIdentifier {
847
        /// The field name that contains the invalid identifier
848
        field: String,
849
        /// Reason why the identifier is invalid
850
        reason: String,
851
    },
852
853
    /// Validation error
854
    #[error("Validation error for {field}: {reason}")]
855
    ValidationError {
856
        /// The field name that failed validation
857
        field: String,
858
        /// Reason why the validation failed
859
        reason: String,
860
    },
861
862
    /// Conversion error
863
    #[error("Conversion error: {message}")]
864
    ConversionError {
865
        /// Detailed error message describing the conversion failure
866
        message: String,
867
    },
868
869
    /// I/O error
870
    #[error("I/O error: {0}")]
871
    IoError(#[from] std::io::Error),
872
873
    /// JSON serialization/deserialization error
874
    #[error("JSON error: {0}")]
875
    JsonError(#[from] serde_json::Error),
876
877
    /// Float parsing error
878
    #[error("Float parsing error: {0}")]
879
    ParseFloatError(#[from] std::num::ParseFloatError),
880
881
    /// Integer parsing error
882
    #[error("Integer parsing error: {0}")]
883
    ParseIntError(#[from] std::num::ParseIntError),
884
}
885
886
// Manual trait implementations for CommonTypeError
887
// (Cannot derive Clone, PartialEq, Eq, Serialize due to std::io::Error and serde_json::Error)
888
889
impl Clone for CommonTypeError {
890
    /// Clone the error, converting IO and JSON errors to conversion errors
891
2
    fn clone(&self) -> Self {
892
2
        match self {
893
1
            Self::InvalidPrice { value, reason } => Self::InvalidPrice {
894
1
                value: value.clone(),
895
1
                reason: reason.clone(),
896
1
            },
897
0
            Self::InvalidQuantity { value, reason } => Self::InvalidQuantity {
898
0
                value: value.clone(),
899
0
                reason: reason.clone(),
900
0
            },
901
0
            Self::InvalidIdentifier { field, reason } => Self::InvalidIdentifier {
902
0
                field: field.clone(),
903
0
                reason: reason.clone(),
904
0
            },
905
0
            Self::ValidationError { field, reason } => Self::ValidationError {
906
0
                field: field.clone(),
907
0
                reason: reason.clone(),
908
0
            },
909
0
            Self::ConversionError { message } => Self::ConversionError {
910
0
                message: message.clone(),
911
0
            },
912
            // Cannot clone std::io::Error or serde_json::Error, so create new instances
913
1
            Self::IoError(e) => Self::ConversionError {
914
1
                message: format!("I/O error: {}", e),
915
1
            },
916
0
            Self::JsonError(e) => Self::ConversionError {
917
0
                message: format!("JSON error: {}", e),
918
0
            },
919
0
            Self::ParseFloatError(e) => Self::ParseFloatError(e.clone()),
920
0
            Self::ParseIntError(e) => Self::ParseIntError(e.clone()),
921
        }
922
2
    }
923
}
924
impl PartialEq for CommonTypeError {
925
    /// Compare two errors for equality
926
3
    fn eq(&self, other: &Self) -> bool {
927
3
        match (self, other) {
928
            (
929
                Self::InvalidPrice {
930
3
                    value: v1,
931
3
                    reason: r1,
932
                },
933
                Self::InvalidPrice {
934
3
                    value: v2,
935
3
                    reason: r2,
936
                },
937
3
            ) => v1 == v2 && 
r1 == r22
,
938
            (
939
                Self::InvalidQuantity {
940
0
                    value: v1,
941
0
                    reason: r1,
942
                },
943
                Self::InvalidQuantity {
944
0
                    value: v2,
945
0
                    reason: r2,
946
                },
947
0
            ) => v1 == v2 && r1 == r2,
948
            (
949
                Self::InvalidIdentifier {
950
0
                    field: f1,
951
0
                    reason: r1,
952
                },
953
                Self::InvalidIdentifier {
954
0
                    field: f2,
955
0
                    reason: r2,
956
                },
957
0
            ) => f1 == f2 && r1 == r2,
958
            (
959
                Self::ValidationError {
960
0
                    field: f1,
961
0
                    reason: r1,
962
                },
963
                Self::ValidationError {
964
0
                    field: f2,
965
0
                    reason: r2,
966
                },
967
0
            ) => f1 == f2 && r1 == r2,
968
0
            (Self::ConversionError { message: m1 }, Self::ConversionError { message: m2 }) => {
969
0
                m1 == m2
970
            },
971
0
            (Self::ParseFloatError(e1), Self::ParseFloatError(e2)) => e1 == e2,
972
0
            (Self::ParseIntError(e1), Self::ParseIntError(e2)) => e1 == e2,
973
            // std::io::Error and serde_json::Error don't implement PartialEq, so they're never equal
974
0
            (Self::IoError(_), Self::IoError(_)) => false,
975
0
            (Self::JsonError(_), Self::JsonError(_)) => false,
976
0
            _ => false,
977
        }
978
3
    }
979
}
980
981
impl Eq for CommonTypeError {}
982
983
// Note: Display is automatically implemented by thiserror::Error derive
984
// based on the #[error("...")] attributes on each variant
985
impl Serialize for CommonTypeError {
986
1
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
987
1
    where
988
1
        S: serde::Serializer,
989
    {
990
        use serde::ser::SerializeStruct;
991
1
        match self {
992
0
            Self::InvalidPrice { value, reason } => {
993
0
                let mut state = serializer.serialize_struct("InvalidPrice", 2)?;
994
0
                state.serialize_field("value", value)?;
995
0
                state.serialize_field("reason", reason)?;
996
0
                state.end()
997
            },
998
0
            Self::InvalidQuantity { value, reason } => {
999
0
                let mut state = serializer.serialize_struct("InvalidQuantity", 2)?;
1000
0
                state.serialize_field("value", value)?;
1001
0
                state.serialize_field("reason", reason)?;
1002
0
                state.end()
1003
            },
1004
0
            Self::InvalidIdentifier { field, reason } => {
1005
0
                let mut state = serializer.serialize_struct("InvalidIdentifier", 2)?;
1006
0
                state.serialize_field("field", field)?;
1007
0
                state.serialize_field("reason", reason)?;
1008
0
                state.end()
1009
            },
1010
1
            Self::ValidationError { field, reason } => {
1011
1
                let mut state = serializer.serialize_struct("ValidationError", 2)
?0
;
1012
1
                state.serialize_field("field", field)
?0
;
1013
1
                state.serialize_field("reason", reason)
?0
;
1014
1
                state.end()
1015
            },
1016
0
            Self::ConversionError { message } => {
1017
0
                let mut state = serializer.serialize_struct("ConversionError", 1)?;
1018
0
                state.serialize_field("message", message)?;
1019
0
                state.end()
1020
            },
1021
0
            Self::IoError(e) => {
1022
0
                let mut state = serializer.serialize_struct("IoError", 1)?;
1023
0
                state.serialize_field("message", &format!("I/O error: {}", e))?;
1024
0
                state.end()
1025
            },
1026
0
            Self::JsonError(e) => {
1027
0
                let mut state = serializer.serialize_struct("JsonError", 1)?;
1028
0
                state.serialize_field("message", &format!("JSON error: {}", e))?;
1029
0
                state.end()
1030
            },
1031
0
            Self::ParseFloatError(e) => {
1032
0
                let mut state = serializer.serialize_struct("ParseFloatError", 1)?;
1033
0
                state.serialize_field("message", &format!("Float parsing error: {}", e))?;
1034
0
                state.end()
1035
            },
1036
0
            Self::ParseIntError(e) => {
1037
0
                let mut state = serializer.serialize_struct("ParseIntError", 1)?;
1038
0
                state.serialize_field("message", &format!("Integer parsing error: {}", e))?;
1039
0
                state.end()
1040
            },
1041
        }
1042
1
    }
1043
}
1044
1045
impl<'de> Deserialize<'de> for CommonTypeError {
1046
1
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1047
1
    where
1048
1
        D: serde::Deserializer<'de>,
1049
    {
1050
        // For deserialization, we'll convert everything to ConversionError since
1051
        // we can't reconstruct std::io::Error or serde_json::Error from serialized form
1052
        use serde::de::{MapAccess, Visitor};
1053
        use std::fmt;
1054
1055
        struct CommonTypeErrorVisitor;
1056
1057
        impl<'de> Visitor<'de> for CommonTypeErrorVisitor {
1058
            type Value = CommonTypeError;
1059
1060
0
            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1061
0
                formatter.write_str("a CommonTypeError")
1062
0
            }
1063
1064
1
            fn visit_map<V>(self, mut map: V) -> Result<CommonTypeError, V::Error>
1065
1
            where
1066
1
                V: MapAccess<'de>,
1067
            {
1068
                // For simplicity, deserialize everything as ConversionError
1069
1
                let mut message = String::new();
1070
3
                while let Some(
key2
) = map.next_key::<String>()
?0
{
1071
2
                    let value: serde_json::Value = map.next_value()
?0
;
1072
2
                    if key == "message" {
1073
0
                        if let Some(msg) = value.as_str() {
1074
0
                            message = msg.to_string();
1075
0
                        }
1076
2
                    } else {
1077
2
                        message = format!("Deserialized error: {}: {}", key, value);
1078
2
                    }
1079
                }
1080
1
                if message.is_empty() {
1081
0
                    message = "Unknown deserialized error".to_string();
1082
1
                }
1083
1
                Ok(CommonTypeError::ConversionError { message })
1084
1
            }
1085
        }
1086
1087
1
        deserializer.deserialize_struct(
1088
            "CommonTypeError",
1089
1
            &["value", "reason", "field", "message"],
1090
1
            CommonTypeErrorVisitor,
1091
        )
1092
1
    }
1093
}
1094
1095
// =============================================================================
1096
// ORDER TYPES (Moved from trading_engine)
1097
// =============================================================================
1098
1099
/// Order type specifying execution behavior - CANONICAL DEFINITION
1100
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1101
#[non_exhaustive]
1102
pub enum OrderType {
1103
    /// Market order - executes immediately at current market price
1104
    Market,
1105
    /// Limit order - executes only at specified price or better
1106
    Limit,
1107
    /// Stop order - becomes market order when stop price is reached
1108
    Stop,
1109
    /// Stop-limit order - becomes limit order when stop price is reached
1110
    StopLimit,
1111
    /// Iceberg order - large order split into smaller visible portions
1112
    Iceberg,
1113
    /// Trailing stop order - stop price adjusts with favorable price movement
1114
    TrailingStop,
1115
    /// Hidden order - not displayed in order book
1116
    Hidden,
1117
}
1118
1119
impl fmt::Display for OrderType {
1120
11
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1121
11
        match self {
1122
2
            Self::Market => write!(f, "MARKET"),
1123
2
            Self::Limit => write!(f, "LIMIT"),
1124
2
            Self::Stop => write!(f, "STOP"),
1125
2
            Self::StopLimit => write!(f, "STOP_LIMIT"),
1126
1
            Self::Iceberg => write!(f, "ICEBERG"),
1127
1
            Self::TrailingStop => write!(f, "TRAILING_STOP"),
1128
1
            Self::Hidden => write!(f, "HIDDEN"),
1129
        }
1130
11
    }
1131
}
1132
1133
impl Default for OrderType {
1134
    /// Returns the default order type (Market)
1135
2
    fn default() -> Self {
1136
2
        Self::Market
1137
2
    }
1138
}
1139
1140
impl TryFrom<i32> for OrderType {
1141
    type Error = String;
1142
1143
9
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1144
9
        match value {
1145
2
            0 => Ok(OrderType::Market),
1146
2
            1 => Ok(OrderType::Limit),
1147
2
            2 => Ok(OrderType::Stop),
1148
1
            3 => Ok(OrderType::StopLimit),
1149
0
            4 => Ok(OrderType::Iceberg),
1150
0
            5 => Ok(OrderType::TrailingStop),
1151
0
            6 => Ok(OrderType::Hidden),
1152
2
            _ => Err(format!("Invalid OrderType: {}", value)),
1153
        }
1154
9
    }
1155
}
1156
1157
/// Supported broker types - CANONICAL DEFINITION
1158
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1159
pub enum BrokerType {
1160
    /// Interactive Brokers TWS/API
1161
    InteractiveBrokers,
1162
    /// IC Markets FIX API
1163
    ICMarkets,
1164
    /// Paper trading simulation
1165
    PaperTrading,
1166
    /// Demo/Test broker
1167
    Demo,
1168
}
1169
1170
impl Default for BrokerType {
1171
    /// Returns the default broker type (InteractiveBrokers)
1172
1
    fn default() -> Self {
1173
1
        Self::InteractiveBrokers
1174
1
    }
1175
}
1176
1177
/// Order status throughout its lifecycle - CANONICAL DEFINITION
1178
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1179
#[non_exhaustive]
1180
pub enum OrderStatus {
1181
    /// Order has been created but not yet submitted to broker
1182
    Created,
1183
    /// Order has been submitted to broker for execution
1184
    Submitted,
1185
    /// Order has been partially executed with remaining quantity
1186
    PartiallyFilled,
1187
    /// Order has been completely executed
1188
    Filled,
1189
    /// Order was rejected by broker or exchange
1190
    Rejected,
1191
    /// Order was cancelled by user or system
1192
    Cancelled,
1193
    /// New order accepted by broker
1194
    New,
1195
    /// Order expired due to time restrictions
1196
    Expired,
1197
    /// Order is pending broker acceptance
1198
    Pending,
1199
    /// Order is actively working in the market
1200
    Working,
1201
    /// Order status is unknown or not yet determined
1202
    Unknown,
1203
    /// Order is temporarily suspended
1204
    Suspended,
1205
    /// Order cancellation is pending
1206
    PendingCancel,
1207
    /// Order modification is pending
1208
    PendingReplace,
1209
}
1210
impl fmt::Display for OrderStatus {
1211
9
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1212
9
        match self {
1213
2
            Self::Created => write!(f, "CREATED"),
1214
1
            Self::Submitted => write!(f, "SUBMITTED"),
1215
1
            Self::PartiallyFilled => write!(f, "PARTIALLY_FILLED"),
1216
2
            Self::Filled => write!(f, "FILLED"),
1217
1
            Self::Rejected => write!(f, "REJECTED"),
1218
2
            Self::Cancelled => write!(f, "CANCELLED"),
1219
0
            Self::New => write!(f, "NEW"),
1220
0
            Self::Expired => write!(f, "EXPIRED"),
1221
0
            Self::Pending => write!(f, "PENDING"),
1222
0
            Self::Working => write!(f, "WORKING"),
1223
0
            Self::Unknown => write!(f, "UNKNOWN"),
1224
0
            Self::Suspended => write!(f, "SUSPENDED"),
1225
0
            Self::PendingCancel => write!(f, "PENDING_CANCEL"),
1226
0
            Self::PendingReplace => write!(f, "PENDING_REPLACE"),
1227
        }
1228
9
    }
1229
}
1230
1231
impl Default for OrderStatus {
1232
    /// Returns the default order status (Created)
1233
0
    fn default() -> Self {
1234
0
        Self::Created
1235
0
    }
1236
}
1237
1238
impl TryFrom<i32> for OrderStatus {
1239
    type Error = String;
1240
1241
8
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1242
8
        match value {
1243
2
            0 => Ok(OrderStatus::Created),
1244
0
            1 => Ok(OrderStatus::Submitted),
1245
0
            2 => Ok(OrderStatus::PartiallyFilled),
1246
2
            3 => Ok(OrderStatus::Filled),
1247
0
            4 => Ok(OrderStatus::Rejected),
1248
2
            5 => Ok(OrderStatus::Cancelled),
1249
0
            6 => Ok(OrderStatus::New),
1250
0
            7 => Ok(OrderStatus::Expired),
1251
0
            8 => Ok(OrderStatus::Pending),
1252
0
            9 => Ok(OrderStatus::Working),
1253
0
            10 => Ok(OrderStatus::Unknown),
1254
0
            11 => Ok(OrderStatus::Suspended),
1255
0
            12 => Ok(OrderStatus::PendingCancel),
1256
0
            13 => Ok(OrderStatus::PendingReplace),
1257
2
            _ => Err(format!("Invalid OrderStatus: {}", value)),
1258
        }
1259
8
    }
1260
}
1261
1262
/// Order side - whether the order is a buy or sell - CANONICAL DEFINITION
1263
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1264
pub enum OrderSide {
1265
    /// Buy order - purchasing securities
1266
    Buy,
1267
    /// Sell order - selling securities
1268
    Sell,
1269
}
1270
1271
impl fmt::Display for OrderSide {
1272
4
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1273
4
        match self {
1274
2
            Self::Buy => write!(f, "BUY"),
1275
2
            Self::Sell => write!(f, "SELL"),
1276
        }
1277
4
    }
1278
}
1279
1280
impl Default for OrderSide {
1281
    /// Returns the default order side (Buy)
1282
1
    fn default() -> Self {
1283
1
        Self::Buy
1284
1
    }
1285
}
1286
1287
impl TryFrom<i32> for OrderSide {
1288
    type Error = String;
1289
1290
6
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1291
6
        match value {
1292
2
            0 => Ok(OrderSide::Buy),
1293
2
            1 => Ok(OrderSide::Sell),
1294
2
            _ => Err(format!("Invalid OrderSide: {}", value)),
1295
        }
1296
6
    }
1297
}
1298
1299
// REMOVED: Side alias - use OrderSide directly
1300
1301
/// Currency enumeration - CANONICAL DEFINITION
1302
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1303
#[cfg_attr(feature = "database", derive(sqlx::Type))]
1304
pub enum Currency {
1305
    /// US Dollar
1306
    USD,
1307
    /// Euro
1308
    EUR,
1309
    /// British Pound Sterling
1310
    GBP,
1311
    /// Japanese Yen
1312
    JPY,
1313
    /// Swiss Franc
1314
    CHF,
1315
    /// Canadian Dollar
1316
    CAD,
1317
    /// Australian Dollar
1318
    AUD,
1319
    /// New Zealand Dollar
1320
    NZD,
1321
    /// Bitcoin
1322
    BTC,
1323
    /// Ethereum
1324
    ETH,
1325
}
1326
1327
impl fmt::Display for Currency {
1328
11
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1329
11
        match self {
1330
4
            Self::USD => write!(f, "USD"),
1331
2
            Self::EUR => write!(f, "EUR"),
1332
1
            Self::GBP => write!(f, "GBP"),
1333
1
            Self::JPY => write!(f, "JPY"),
1334
0
            Self::CHF => write!(f, "CHF"),
1335
0
            Self::CAD => write!(f, "CAD"),
1336
0
            Self::AUD => write!(f, "AUD"),
1337
0
            Self::NZD => write!(f, "NZD"),
1338
2
            Self::BTC => write!(f, "BTC"),
1339
1
            Self::ETH => write!(f, "ETH"),
1340
        }
1341
11
    }
1342
}
1343
1344
impl Default for Currency {
1345
    /// Returns the default currency (USD)
1346
2
    fn default() -> Self {
1347
2
        Self::USD
1348
2
    }
1349
}
1350
1351
/// Time in force enumeration - CANONICAL DEFINITION
1352
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1353
pub enum TimeInForce {
1354
    /// Order is valid for the current trading day only
1355
    Day,
1356
    /// Order remains active until explicitly cancelled
1357
    GoodTillCancel,
1358
    /// Order must be executed immediately or cancelled
1359
    ImmediateOrCancel,
1360
    /// Order must be executed completely or cancelled
1361
    FillOrKill,
1362
}
1363
1364
impl fmt::Display for TimeInForce {
1365
8
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1366
8
        match self {
1367
2
            Self::Day => write!(f, "DAY"),
1368
2
            Self::GoodTillCancel => write!(f, "GTC"),
1369
2
            Self::ImmediateOrCancel => write!(f, "IOC"),
1370
2
            Self::FillOrKill => write!(f, "FOK"),
1371
        }
1372
8
    }
1373
}
1374
1375
impl Default for TimeInForce {
1376
    /// Returns the default time in force (Day)
1377
15
    fn default() -> Self {
1378
15
        Self::Day
1379
15
    }
1380
}
1381
1382
// =============================================================================
1383
// CORE ID TYPES (MIGRATED FROM TRADING_ENGINE)
1384
// =============================================================================
1385
1386
// Duplicate TradeId removed - using definition from line 1008
1387
1388
/// Event identifier for tracking system events
1389
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1390
pub struct EventId(String);
1391
1392
impl EventId {
1393
    /// Create a new random event ID
1394
1
    pub fn new() -> Self {
1395
        use uuid::Uuid;
1396
1
        Self(Uuid::new_v4().to_string())
1397
1
    }
1398
1399
    /// Create an event ID from a string, generating new if empty
1400
1
    pub fn from_string<S: Into<String>>(id: S) -> Self {
1401
1
        let id = id.into();
1402
1
        if id.is_empty() {
1403
1
            Self::new() // Generate new ID if empty
1404
        } else {
1405
0
            Self(id)
1406
        }
1407
1
    }
1408
1409
    /// Get the string value of the event ID
1410
1
    pub fn value(&self) -> &str {
1411
1
        &self.0
1412
1
    }
1413
}
1414
1415
impl fmt::Display for EventId {
1416
    /// Format the event ID for display
1417
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1418
0
        write!(f, "{}", self.0)
1419
0
    }
1420
}
1421
1422
impl From<String> for EventId {
1423
    /// Create an EventId from a String
1424
0
    fn from(s: String) -> Self {
1425
0
        Self(s)
1426
0
    }
1427
}
1428
1429
impl Default for EventId {
1430
    /// Create a default EventId with a new UUID
1431
0
    fn default() -> Self {
1432
0
        Self::new()
1433
0
    }
1434
}
1435
1436
/// Fill identifier with validation
1437
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1438
pub struct FillId(String);
1439
1440
impl FillId {
1441
    /// Create a new fill ID with validation
1442
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1443
0
        let id = id.into();
1444
0
        if id.is_empty() {
1445
0
            return Err(CommonTypeError::ValidationError {
1446
0
                field: "fill_id".to_owned(),
1447
0
                reason: "Fill ID cannot be empty".to_owned(),
1448
0
            });
1449
0
        }
1450
0
        Ok(Self(id))
1451
0
    }
1452
1453
    /// Get the fill ID as a string slice
1454
0
    pub fn as_str(&self) -> &str {
1455
0
        &self.0
1456
0
    }
1457
    /// Convert the fill ID into an owned string
1458
    /// Convert the execution ID into an owned string
1459
    /// Convert execution ID into owned string
1460
0
    pub fn into_string(self) -> String {
1461
0
        self.0
1462
0
    }
1463
}
1464
1465
impl fmt::Display for FillId {
1466
    /// Format the fill ID for display
1467
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1468
0
        write!(f, "{}", self.0)
1469
0
    }
1470
}
1471
1472
/// Aggregate identifier with validation
1473
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1474
pub struct AggregateId(String);
1475
1476
impl AggregateId {
1477
    /// Create a new aggregate ID with validation
1478
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1479
0
        let id = id.into();
1480
0
        if id.is_empty() {
1481
0
            return Err(CommonTypeError::ValidationError {
1482
0
                field: "aggregate_id".to_owned(),
1483
0
                reason: "Aggregate ID cannot be empty".to_owned(),
1484
0
            });
1485
0
        }
1486
0
        Ok(Self(id))
1487
0
    }
1488
1489
    /// Get the aggregate ID as a string slice
1490
0
    pub fn as_str(&self) -> &str {
1491
0
        &self.0
1492
0
    }
1493
    /// Convert the aggregate ID into an owned string
1494
0
    pub fn into_string(self) -> String {
1495
0
        self.0
1496
0
    }
1497
}
1498
1499
impl fmt::Display for AggregateId {
1500
    /// Format the aggregate ID for display
1501
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1502
0
        write!(f, "{}", self.0)
1503
0
    }
1504
}
1505
1506
/// Asset identifier with validation
1507
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1508
pub struct AssetId(String);
1509
1510
impl AssetId {
1511
    /// Create a new asset ID with validation
1512
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1513
0
        let id = id.into();
1514
0
        if id.is_empty() {
1515
0
            return Err(CommonTypeError::ValidationError {
1516
0
                field: "asset_id".to_owned(),
1517
0
                reason: "Asset ID cannot be empty".to_owned(),
1518
0
            });
1519
0
        }
1520
0
        Ok(Self(id))
1521
0
    }
1522
1523
    /// Get the asset ID as a string slice
1524
0
    pub fn as_str(&self) -> &str {
1525
0
        &self.0
1526
0
    }
1527
    /// Convert the asset ID into an owned string
1528
0
    pub fn into_string(self) -> String {
1529
0
        self.0
1530
0
    }
1531
}
1532
1533
impl fmt::Display for AssetId {
1534
    /// Format the asset ID for display
1535
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1536
0
        write!(f, "{}", self.0)
1537
0
    }
1538
}
1539
1540
/// Client identifier with validation
1541
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1542
pub struct ClientId(String);
1543
1544
impl ClientId {
1545
    /// Create a new client ID with validation
1546
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1547
0
        let id = id.into();
1548
0
        if id.is_empty() {
1549
0
            return Err(CommonTypeError::ValidationError {
1550
0
                field: "client_id".to_owned(),
1551
0
                reason: "Client ID cannot be empty".to_owned(),
1552
0
            });
1553
0
        }
1554
0
        Ok(Self(id))
1555
0
    }
1556
1557
    /// Get the client ID as a string slice
1558
0
    pub fn as_str(&self) -> &str {
1559
0
        &self.0
1560
0
    }
1561
    /// Convert the client ID into an owned string
1562
0
    pub fn into_string(self) -> String {
1563
0
        self.0
1564
0
    }
1565
}
1566
1567
impl fmt::Display for ClientId {
1568
    /// Format the client ID for display
1569
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1570
0
        write!(f, "{}", self.0)
1571
0
    }
1572
}
1573
1574
// =============================================================================
1575
// CORE TRADING TYPES - MIGRATED FROM TRADING_ENGINE
1576
// =============================================================================
1577
1578
/// Canonical Order struct - UNIFIED DEFINITION based on Agent 1's comprehensive analysis
1579
/// This represents the single source of truth for Order across all services
1580
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1581
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
1582
pub struct Order {
1583
    // Core Identity
1584
    /// Unique order identifier
1585
    pub id: OrderId,
1586
    /// Client-provided order identifier
1587
    pub client_order_id: Option<String>,
1588
    /// Broker-assigned order identifier
1589
    pub broker_order_id: Option<String>,
1590
    /// Account identifier for the order
1591
    pub account_id: Option<String>,
1592
1593
    // Trading Details
1594
    /// Trading symbol for the order
1595
    pub symbol: Symbol,
1596
    /// Order side (buy or sell)
1597
    pub side: OrderSide,
1598
    /// Type of order (market, limit, etc.)
1599
    pub order_type: OrderType,
1600
    /// Current status of the order
1601
    pub status: OrderStatus,
1602
    /// Time in force policy
1603
    pub time_in_force: TimeInForce,
1604
1605
    // Quantities & Pricing
1606
    /// Total order quantity
1607
    pub quantity: Quantity,
1608
    /// Limit price for the order
1609
    pub price: Option<Price>,
1610
    /// Stop price for stop orders
1611
    pub stop_price: Option<Price>,
1612
    /// Quantity that has been filled
1613
    pub filled_quantity: Quantity,
1614
    /// Remaining quantity to be filled
1615
    pub remaining_quantity: Quantity,
1616
    /// Average execution price
1617
    pub average_price: Option<Price>,
1618
    /// Alias for average_price for database compatibility
1619
    pub avg_fill_price: Option<Price>,
1620
1621
    // Strategy Fields (from Agent 1)
1622
    /// Parent order ID for iceberg/algo orders
1623
    pub parent_id: Option<String>,
1624
    /// Execution algorithm name
1625
    pub execution_algorithm: Option<String>,
1626
    /// Execution algorithm parameters stored as JSON
1627
    pub execution_params: Value,
1628
1629
    // Risk Management (from Agent 1)
1630
    /// Stop loss price for risk management
1631
    pub stop_loss: Option<Price>,
1632
    /// Take profit price for profit taking
1633
    pub take_profit: Option<Price>,
1634
1635
    // Timestamps
1636
    /// Order creation timestamp
1637
    pub created_at: HftTimestamp,
1638
    /// Last update timestamp
1639
    pub updated_at: Option<HftTimestamp>,
1640
    /// Order expiration timestamp
1641
    pub expires_at: Option<HftTimestamp>,
1642
1643
    // Extensibility
1644
    /// Additional order metadata stored as JSON
1645
    pub metadata: Value,
1646
}
1647
1648
impl Order {
1649
    /// Create a new order with canonical fields
1650
14
    pub fn new(
1651
14
        symbol: Symbol,
1652
14
        side: OrderSide,
1653
14
        quantity: Quantity,
1654
14
        price: Option<Price>,
1655
14
        order_type: OrderType,
1656
14
    ) -> Self {
1657
14
        let now = HftTimestamp::now_or_zero();
1658
14
        Self {
1659
14
            // Core Identity
1660
14
            id: OrderId::new(),
1661
14
            client_order_id: None,
1662
14
            broker_order_id: None,
1663
14
            account_id: None,
1664
14
1665
14
            // Trading Details
1666
14
            symbol,
1667
14
            side,
1668
14
            order_type,
1669
14
            status: OrderStatus::Created,
1670
14
            time_in_force: TimeInForce::default(),
1671
14
1672
14
            // Quantities & Pricing
1673
14
            quantity,
1674
14
            price,
1675
14
            stop_price: None,
1676
14
            filled_quantity: Quantity::ZERO,
1677
14
            remaining_quantity: quantity,
1678
14
            average_price: None,
1679
14
            avg_fill_price: None, // Database compatibility alias
1680
14
1681
14
            // Strategy Fields
1682
14
            parent_id: None,
1683
14
            execution_algorithm: None,
1684
14
            execution_params: serde_json::json!({}),
1685
14
1686
14
            // Risk Management
1687
14
            stop_loss: None,
1688
14
            take_profit: None,
1689
14
1690
14
            // Timestamps
1691
14
            created_at: now,
1692
14
            updated_at: None,
1693
14
            expires_at: None,
1694
14
1695
14
            // Extensibility
1696
14
            metadata: serde_json::json!({}),
1697
14
        }
1698
14
    }
1699
1700
    /// Check if the order is fully filled
1701
12
    pub fn is_filled(&self) -> bool {
1702
12
        self.filled_quantity == self.quantity
1703
12
    }
1704
1705
    /// Check if the order is partially filled
1706
3
    pub fn is_partially_filled(&self) -> bool {
1707
3
        self.filled_quantity > Quantity::ZERO && 
self.filled_quantity < self.quantity2
1708
3
    }
1709
1710
    /// Calculate fill percentage
1711
5
    pub fn fill_percentage(&self) -> f64 {
1712
5
        if self.quantity.is_zero() {
1713
1
            0.0
1714
        } else {
1715
4
            (self.filled_quantity.to_f64() / self.quantity.to_f64()) * 100.0
1716
        }
1717
5
    }
1718
1719
    /// Set client order ID for tracking
1720
1
    pub fn with_client_order_id(mut self, client_order_id: String) -> Self {
1721
1
        self.client_order_id = Some(client_order_id);
1722
1
        self
1723
1
    }
1724
1725
    /// Set account ID
1726
1
    pub fn with_account_id(mut self, account_id: String) -> Self {
1727
1
        self.account_id = Some(account_id);
1728
1
        self
1729
1
    }
1730
1731
    /// Set time in force
1732
1
    pub fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self {
1733
1
        self.time_in_force = time_in_force;
1734
1
        self
1735
1
    }
1736
1737
    /// Set stop price
1738
0
    pub fn with_stop_price(mut self, stop_price: Price) -> Self {
1739
0
        self.stop_price = Some(stop_price);
1740
0
        self
1741
0
    }
1742
1743
    /// Set execution algorithm
1744
0
    pub fn with_execution_algorithm(mut self, algorithm: String) -> Self {
1745
0
        self.execution_algorithm = Some(algorithm);
1746
0
        self
1747
0
    }
1748
1749
    /// Add execution parameter
1750
0
    pub fn with_execution_param(mut self, key: String, value: f64) -> Self {
1751
0
        if let Some(obj) = self.execution_params.as_object_mut() {
1752
0
            obj.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
1753
0
        } else {
1754
0
            let mut map = serde_json::Map::new();
1755
0
            map.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
1756
0
            self.execution_params = Value::Object(map);
1757
0
        }
1758
0
        self
1759
0
    }
1760
1761
    /// Set stop loss
1762
0
    pub fn with_stop_loss(mut self, stop_loss: Price) -> Self {
1763
0
        self.stop_loss = Some(stop_loss);
1764
0
        self
1765
0
    }
1766
1767
    /// Set take profit
1768
0
    pub fn with_take_profit(mut self, take_profit: Price) -> Self {
1769
0
        self.take_profit = Some(take_profit);
1770
0
        self
1771
0
    }
1772
1773
    /// Add metadata
1774
0
    pub fn with_metadata(mut self, key: String, value: String) -> Self {
1775
0
        if let Some(obj) = self.metadata.as_object_mut() {
1776
0
            obj.insert(key, Value::String(value));
1777
0
        } else {
1778
0
            let mut map = serde_json::Map::new();
1779
0
            map.insert(key, Value::String(value));
1780
0
            self.metadata = Value::Object(map);
1781
0
        }
1782
0
        self
1783
0
    }
1784
1785
    /// Update order status and timestamp
1786
10
    pub fn update_status(&mut self, status: OrderStatus) {
1787
10
        self.status = status;
1788
10
        self.updated_at = Some(HftTimestamp::now_or_zero());
1789
10
    }
1790
1791
    /// Fill order with given quantity and price
1792
11
    pub fn fill(
1793
11
        &mut self,
1794
11
        fill_quantity: Quantity,
1795
11
        fill_price: Price,
1796
11
    ) -> Result<(), CommonTypeError> {
1797
11
        if self.filled_quantity + fill_quantity > self.quantity {
1798
1
            return Err(CommonTypeError::ValidationError {
1799
1
                field: "fill_quantity".to_string(),
1800
1
                reason: "Fill quantity exceeds remaining quantity".to_string(),
1801
1
            });
1802
10
        }
1803
1804
        // Update filled quantity
1805
10
        let previous_filled = self.filled_quantity;
1806
10
        self.filled_quantity = self.filled_quantity + fill_quantity;
1807
10
        self.remaining_quantity = self.quantity - self.filled_quantity;
1808
1809
        // Update average price
1810
10
        if let Some(
avg_price5
) = self.average_price {
1811
5
            let total_value = avg_price.to_f64() * previous_filled.to_f64()
1812
5
                + fill_price.to_f64() * fill_quantity.to_f64();
1813
5
            let new_avg = Some(
1814
5
                Price::from_f64(total_value / self.filled_quantity.to_f64()).unwrap_or(fill_price),
1815
5
            );
1816
5
            self.average_price = new_avg;
1817
5
            self.avg_fill_price = new_avg; // Keep in sync
1818
5
        } else {
1819
5
            self.average_price = Some(fill_price);
1820
5
            self.avg_fill_price = Some(fill_price); // Keep in sync
1821
5
        }
1822
1823
        // Update status
1824
10
        if self.is_filled() {
1825
4
            self.update_status(OrderStatus::Filled);
1826
6
        } else {
1827
6
            self.update_status(OrderStatus::PartiallyFilled);
1828
6
        }
1829
1830
10
        Ok(())
1831
11
    }
1832
1833
    /// Create a limit order - convenience constructor
1834
12
    pub fn limit(symbol: Symbol, side: OrderSide, quantity: Quantity, price: Price) -> Self {
1835
12
        Self::new(symbol, side, quantity, Some(price), OrderType::Limit)
1836
12
    }
1837
1838
    /// Create a market order - convenience constructor
1839
1
    pub fn market(symbol: Symbol, side: OrderSide, quantity: Quantity) -> Self {
1840
1
        Self::new(symbol, side, quantity, None, OrderType::Market)
1841
1
    }
1842
1843
    /// Get symbol hash for performance-critical operations
1844
1
    pub fn symbol_hash(&self) -> i64 {
1845
        use std::collections::hash_map::DefaultHasher;
1846
        use std::hash::{Hash, Hasher};
1847
1848
1
        let mut hasher = DefaultHasher::new();
1849
1
        self.symbol.as_str().hash(&mut hasher);
1850
1
        hasher.finish() as i64
1851
1
    }
1852
1853
    /// Get order timestamp
1854
0
    pub fn timestamp(&self) -> HftTimestamp {
1855
0
        self.created_at
1856
0
    }
1857
}
1858
1859
impl Default for Order {
1860
0
    fn default() -> Self {
1861
0
        Self {
1862
0
            id: OrderId::new(),
1863
0
            client_order_id: None,
1864
0
            broker_order_id: None,
1865
0
            account_id: None,
1866
0
1867
0
            symbol: Symbol::from("DEFAULT"),
1868
0
            side: OrderSide::Buy,
1869
0
            order_type: OrderType::Market,
1870
0
            status: OrderStatus::Created,
1871
0
            time_in_force: TimeInForce::Day,
1872
0
1873
0
            quantity: Quantity::ONE,
1874
0
            price: None,
1875
0
            stop_price: None,
1876
0
            filled_quantity: Quantity::ZERO,
1877
0
            remaining_quantity: Quantity::ONE,
1878
0
            average_price: None,
1879
0
            avg_fill_price: None,
1880
0
1881
0
            parent_id: None,
1882
0
            execution_algorithm: None,
1883
0
            execution_params: serde_json::json!({}),
1884
0
1885
0
            stop_loss: None,
1886
0
            take_profit: None,
1887
0
1888
0
            created_at: HftTimestamp::now().unwrap_or(HftTimestamp { nanos: 0 }),
1889
0
            updated_at: None,
1890
0
            expires_at: None,
1891
0
1892
0
            metadata: serde_json::json!({}),
1893
0
        }
1894
0
    }
1895
}
1896
1897
/// Represents a trading position - CANONICAL DEFINITION
1898
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1899
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
1900
pub struct Position {
1901
    /// Unique position identifier
1902
    pub id: Uuid,
1903
1904
    /// Trading symbol
1905
    pub symbol: String,
1906
1907
    /// Position quantity (positive for long, negative for short)
1908
    pub quantity: Decimal,
1909
1910
    /// Average entry price
1911
    pub avg_price: Decimal,
1912
1913
    /// Average cost per share
1914
    pub avg_cost: Decimal,
1915
1916
    /// Cost basis for tax calculations
1917
    pub basis: Decimal,
1918
1919
    /// Average entry price
1920
    pub average_price: Decimal,
1921
1922
    /// Market value of position
1923
    pub market_value: Decimal,
1924
1925
    /// Unrealized P&L
1926
    pub unrealized_pnl: Decimal,
1927
1928
    /// Realized P&L
1929
    pub realized_pnl: Decimal,
1930
1931
    /// Position creation timestamp
1932
    pub created_at: DateTime<Utc>,
1933
1934
    /// Last update timestamp
1935
    pub updated_at: DateTime<Utc>,
1936
1937
    /// Last updated timestamp
1938
    pub last_updated: DateTime<Utc>,
1939
1940
    /// Current market price (for P&L calculation)
1941
    pub current_price: Option<Decimal>,
1942
1943
    /// Position size in base currency
1944
    pub notional_value: Decimal,
1945
1946
    /// Margin requirement
1947
    pub margin_requirement: Decimal,
1948
}
1949
1950
impl Position {
1951
    /// Create a new position
1952
7
    pub fn new(symbol: String, quantity: Decimal, avg_price: Decimal) -> Self {
1953
7
        let now = Utc::now();
1954
7
        let notional_value = quantity.abs() * avg_price;
1955
1956
7
        Self {
1957
7
            id: Uuid::new_v4(),
1958
7
            symbol,
1959
7
            quantity,
1960
7
            avg_price,
1961
7
            avg_cost: avg_price, // Keep avg_cost synchronized with avg_price
1962
7
            basis: quantity * avg_price, // Cost basis calculation
1963
7
            average_price: avg_price, // Same as avg_price for compatibility
1964
7
            market_value: notional_value, // Initialize market value to notional value
1965
7
            unrealized_pnl: Decimal::ZERO,
1966
7
            realized_pnl: Decimal::ZERO,
1967
7
            created_at: now,
1968
7
            updated_at: now,
1969
7
            last_updated: now, // Same as updated_at for compatibility
1970
7
            current_price: None,
1971
7
            notional_value,
1972
7
            margin_requirement: notional_value
1973
7
                * Decimal::from_str_exact("0.02").unwrap_or(Decimal::ZERO), // 2% margin
1974
7
        }
1975
7
    }
1976
1977
    /// Check if position is long
1978
2
    pub fn is_long(&self) -> bool {
1979
2
        self.quantity > Decimal::ZERO
1980
2
    }
1981
1982
    /// Check if position is short
1983
2
    pub fn is_short(&self) -> bool {
1984
2
        self.quantity < Decimal::ZERO
1985
2
    }
1986
1987
    /// Calculate unrealized P&L based on current price
1988
3
    pub fn calculate_unrealized_pnl(&mut self, current_price: Decimal) {
1989
3
        self.current_price = Some(current_price);
1990
3
        self.market_value = self.quantity.abs() * current_price;
1991
        // For both long and short: quantity * (current_price - avg_price)
1992
3
        self.unrealized_pnl = self.quantity * (current_price - self.avg_price);
1993
3
        let now = Utc::now();
1994
3
        self.updated_at = now;
1995
3
        self.last_updated = now; // Keep alias synchronized
1996
3
    }
1997
1998
    /// Get total P&L (realized + unrealized)
1999
1
    pub fn total_pnl(&self) -> Decimal {
2000
1
        self.realized_pnl + self.unrealized_pnl
2001
1
    }
2002
2003
    /// Calculate return on investment percentage
2004
2
    pub fn roi_percentage(&self) -> Decimal {
2005
2
        if self.notional_value.is_zero() {
2006
1
            Decimal::ZERO
2007
        } else {
2008
1
            self.total_pnl() / self.notional_value * Decimal::from(100)
2009
        }
2010
2
    }
2011
}
2012
2013
/// Represents a trade execution - CANONICAL DEFINITION
2014
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2015
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
2016
pub struct Execution {
2017
    /// Unique execution identifier
2018
    pub id: Uuid,
2019
2020
    /// Related order ID
2021
    pub order_id: Uuid,
2022
2023
    /// Trading symbol
2024
    pub symbol: String,
2025
2026
    /// Executed quantity
2027
    pub quantity: Decimal,
2028
2029
    /// Execution price
2030
    pub price: Decimal,
2031
2032
    /// Execution side
2033
    pub side: OrderSide,
2034
2035
    /// Trading fees
2036
    pub fees: Decimal,
2037
2038
    /// Fee currency
2039
    pub fee_currency: String,
2040
2041
    /// Execution timestamp
2042
    pub executed_at: DateTime<Utc>,
2043
2044
    /// Execution timestamp
2045
    pub timestamp: DateTime<Utc>,
2046
2047
    /// Symbol hash for performance
2048
    pub symbol_hash: i64,
2049
2050
    /// Broker execution ID
2051
    pub broker_execution_id: Option<String>,
2052
2053
    /// Counterparty information
2054
    pub counterparty: Option<String>,
2055
2056
    /// Trade venue
2057
    pub venue: Option<String>,
2058
2059
    /// Gross trade value
2060
    pub gross_value: Decimal,
2061
2062
    /// Net trade value (after fees)
2063
    pub net_value: Decimal,
2064
}
2065
2066
impl Execution {
2067
    /// Create a new execution
2068
5
    pub fn new(
2069
5
        order_id: Uuid,
2070
5
        symbol: String,
2071
5
        quantity: Decimal,
2072
5
        price: Decimal,
2073
5
        side: OrderSide,
2074
5
        fees: Decimal,
2075
5
    ) -> Self {
2076
5
        let gross_value = quantity * price;
2077
5
        let net_value = if side == OrderSide::Buy {
2078
4
            gross_value + fees
2079
        } else {
2080
1
            gross_value - fees
2081
        };
2082
5
        let now = Utc::now();
2083
5
        let symbol_hash = Self::hash_symbol(&symbol);
2084
2085
5
        Self {
2086
5
            id: Uuid::new_v4(),
2087
5
            order_id,
2088
5
            symbol,
2089
5
            quantity,
2090
5
            price,
2091
5
            side,
2092
5
            fees,
2093
5
            fee_currency: "USD".to_string(), // Default to USD
2094
5
            executed_at: now,
2095
5
            timestamp: now, // Same as executed_at for compatibility
2096
5
            symbol_hash,
2097
5
            broker_execution_id: None,
2098
5
            counterparty: None,
2099
5
            venue: None,
2100
5
            gross_value,
2101
5
            net_value,
2102
5
        }
2103
5
    }
2104
2105
    /// Calculate effective price including fees
2106
2
    pub fn effective_price(&self) -> Decimal {
2107
2
        if self.quantity.is_zero() {
2108
1
            self.price
2109
        } else {
2110
1
            self.net_value / self.quantity
2111
        }
2112
2
    }
2113
2114
    /// Hash symbol for performance
2115
5
    fn hash_symbol(symbol: &str) -> i64 {
2116
        use std::collections::hash_map::DefaultHasher;
2117
        use std::hash::{Hash, Hasher};
2118
2119
5
        let mut hasher = DefaultHasher::new();
2120
5
        symbol.hash(&mut hasher);
2121
5
        hasher.finish() as i64
2122
5
    }
2123
}
2124
2125
/// Core Price type using fixed-point arithmetic for precision
2126
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2127
pub struct Price {
2128
    value: u64,
2129
}
2130
2131
impl Price {
2132
    /// Zero price constant
2133
    pub const ZERO: Self = Self { value: 0 };
2134
    /// One unit price constant (1.0)
2135
    pub const ONE: Self = Self { value: 100_000_000 };
2136
    /// One cent constant (0.01)
2137
    pub const CENT: Self = Self { value: 1_000_000 };
2138
    /// Maximum price value
2139
    pub const MAX: Self = Self { value: u64::MAX };
2140
2141
    /// Create a Price from a floating-point value
2142
70
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
2143
70
        if value < 0.0 || 
!value.is_finite()66
{
2144
8
            return Err(CommonTypeError::InvalidPrice {
2145
8
                value: value.to_string(),
2146
8
                reason: "Price validation failed".to_owned(),
2147
8
            });
2148
62
        }
2149
62
        Ok(Self {
2150
62
            value: (value * 100_000_000.0).round() as u64,
2151
62
        })
2152
70
    }
2153
2154
    /// Convert to floating-point representation
2155
    #[must_use]
2156
    /// Convert the quantity to a floating point value
2157
51
    pub fn to_f64(&self) -> f64 {
2158
51
        self.value as f64 / 100_000_000.0
2159
51
    }
2160
2161
    /// Get floating-point representation (alias for to_f64)
2162
    /// Convert quantity to f64 representation
2163
    /// Convert quantity to f64 representation
2164
    /// Convert quantity to f64 representation
2165
    #[must_use]
2166
0
    pub fn as_f64(&self) -> f64 {
2167
0
        self.to_f64()
2168
0
    }
2169
2170
    /// Create a zero price
2171
    /// Create a zero quantity
2172
    /// Create zero quantity
2173
    #[must_use]
2174
0
    pub const fn zero() -> Self {
2175
0
        Self::ZERO
2176
0
    }
2177
2178
    /// Convert to Decimal type for precise calculations
2179
1
    pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
2180
1
        Decimal::from_f64(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidPrice {
2181
0
            value: "0.0".to_owned(),
2182
0
            reason: "Price to Decimal conversion failed".to_owned(),
2183
0
        })
2184
1
    }
2185
2186
    /// Create a Price from a Decimal value
2187
    #[must_use]
2188
1
    pub fn from_decimal(decimal: Decimal) -> Self {
2189
1
        Self::from(decimal)
2190
1
    }
2191
2192
    /// Create a new Price (alias for from_f64)
2193
    /// Create a new quantity from a floating point value
2194
    /// Create new quantity from f64 value
2195
0
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
2196
0
        Self::from_f64(value)
2197
0
    }
2198
2199
    /// Get the raw internal value representation
2200
    /// Get the raw internal value
2201
    /// Get the raw internal value representation
2202
    #[must_use]
2203
1
    pub const fn raw_value(&self) -> u64 {
2204
1
        self.value
2205
1
    }
2206
2207
    /// Get the price as a u64 value (same as raw_value)
2208
    /// Convert to u64 representation
2209
    /// Convert quantity to u64 representation
2210
    #[must_use]
2211
0
    pub const fn as_u64(&self) -> u64 {
2212
0
        self.value
2213
0
    }
2214
2215
    /// Create a Price from a raw u64 value
2216
    /// Create a quantity from raw internal value
2217
    /// Create quantity from raw u64 value
2218
    #[must_use]
2219
0
    pub const fn from_raw(value: u64) -> Self {
2220
0
        Self { value }
2221
0
    }
2222
2223
    /// Convert price to cents (divides by 1M for 8 decimal places)
2224
    #[must_use]
2225
2
    pub const fn to_cents(&self) -> u64 {
2226
2
        self.value / 1_000_000
2227
2
    }
2228
2229
    /// Create a Price from cents value
2230
    #[must_use]
2231
2
    pub const fn from_cents(cents: u64) -> Self {
2232
2
        Self {
2233
2
            value: cents * 1_000_000,
2234
2
        }
2235
2
    }
2236
2237
    /// Check if the price is zero
2238
    /// Check if the quantity is zero
2239
    /// Check if quantity is zero
2240
    #[must_use]
2241
2
    pub const fn is_zero(&self) -> bool {
2242
2
        self.value == 0
2243
2
    }
2244
2245
    /// Check if the price is non-zero (has some value)
2246
    /// Check if the quantity is non-zero (has some value)
2247
    /// Check if quantity has a non-zero value
2248
    #[must_use]
2249
0
    pub const fn is_some(&self) -> bool {
2250
0
        !self.is_zero()
2251
0
    }
2252
2253
    /// Check if the price is zero (has no value)
2254
    /// Check if the quantity is zero (has no value)
2255
    /// Check if quantity is zero (none)
2256
    #[must_use]
2257
0
    pub const fn is_none(&self) -> bool {
2258
0
        self.is_zero()
2259
0
    }
2260
2261
    /// Get a reference to this price
2262
    /// Get a reference to self
2263
    /// Get a reference to self
2264
    #[must_use]
2265
0
    pub const fn as_ref(&self) -> &Self {
2266
0
        self
2267
0
    }
2268
2269
    /// Get the absolute value of the price (prices are always positive)
2270
    /// Get the absolute value (quantities are always positive)
2271
    /// Get absolute value (always positive for Quantity)
2272
    #[must_use]
2273
0
    pub const fn abs(&self) -> Self {
2274
0
        *self
2275
0
    }
2276
2277
    /// Multiply this price by another price
2278
1
    pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
2279
1
        *self * other
2280
1
    }
2281
2282
    /// Subtract another price from this price
2283
    /// Subtract another quantity from this quantity
2284
    /// Subtract another quantity from this quantity
2285
    /// Subtract another quantity from this quantity
2286
    /// Subtract another quantity from this quantity
2287
    #[must_use]
2288
0
    pub fn subtract(&self, other: Self) -> Self {
2289
0
        *self - other
2290
0
    }
2291
2292
    /// Divide this price by a floating point divisor
2293
0
    pub fn divide(&self, divisor: f64) -> Result<Self, CommonTypeError> {
2294
0
        *self / divisor
2295
0
    }
2296
}
2297
2298
impl fmt::Display for Price {
2299
    /// Format the price for display with 8 decimal places
2300
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2301
2
        write!(f, "{:.8}", self.to_f64())
2302
2
    }
2303
}
2304
2305
impl Default for Price {
2306
    /// Returns the default price (zero)
2307
0
    fn default() -> Self {
2308
0
        Self::ZERO
2309
0
    }
2310
}
2311
2312
impl FromStr for Price {
2313
    type Err = CommonTypeError;
2314
2315
5
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2316
5
        let 
parsed_value3
= s
2317
5
            .parse::<f64>()
2318
5
            .map_err(|_| CommonTypeError::InvalidPrice {
2319
2
                value: s.to_owned(),
2320
2
                reason: format!("Cannot parse '{}' as price", s),
2321
2
            })?;
2322
3
        Self::from_f64(parsed_value)
2323
5
    }
2324
}
2325
2326
impl Add for Price {
2327
    type Output = Self;
2328
4
    fn add(self, rhs: Self) -> Self::Output {
2329
4
        Self {
2330
4
            value: self.value.saturating_add(rhs.value),
2331
4
        }
2332
4
    }
2333
}
2334
2335
impl Sub for Price {
2336
    type Output = Self;
2337
3
    fn sub(self, rhs: Self) -> Self::Output {
2338
3
        Self {
2339
3
            value: self.value.saturating_sub(rhs.value),
2340
3
        }
2341
3
    }
2342
}
2343
2344
impl Mul<f64> for Price {
2345
    type Output = Result<Self, CommonTypeError>;
2346
2
    fn mul(self, rhs: f64) -> Self::Output {
2347
2
        Self::from_f64(self.to_f64() * rhs)
2348
2
    }
2349
}
2350
2351
impl Div<f64> for Price {
2352
    type Output = Result<Self, CommonTypeError>;
2353
4
    fn div(self, rhs: f64) -> Self::Output {
2354
4
        if rhs == 0.0 {
2355
2
            return Err(CommonTypeError::ConversionError {
2356
2
                message: "Cannot divide price by zero".to_owned(),
2357
2
            });
2358
2
        }
2359
2
        Self::from_f64(self.to_f64() / rhs)
2360
4
    }
2361
}
2362
2363
impl From<Decimal> for Price {
2364
1
    fn from(decimal: Decimal) -> Self {
2365
1
        let f64_val: f64 = TryInto::<f64>::try_into(decimal).unwrap_or_else(|_| 
{0
2366
0
            tracing::warn!("Failed to convert Decimal to f64, using 0.0 as fallback");
2367
0
            0.0_f64
2368
0
        });
2369
1
        Self::from_f64(f64_val).unwrap_or_else(|_| 
{0
2370
0
            tracing::warn!(
2371
0
                "Failed to create Price from f64 value {}, using ZERO",
2372
                f64_val
2373
            );
2374
0
            Self::ZERO
2375
0
        })
2376
1
    }
2377
}
2378
2379
impl From<Price> for Decimal {
2380
0
    fn from(price: Price) -> Self {
2381
0
        price.to_decimal().unwrap_or(Decimal::ZERO)
2382
0
    }
2383
}
2384
2385
// TryFrom<Quantity> for Decimal removed due to conflicting blanket implementation
2386
// Use qty.to_decimal() directly instead
2387
impl From<Quantity> for Decimal {
2388
0
    fn from(qty: Quantity) -> Self {
2389
0
        qty.to_decimal().unwrap_or(Decimal::ZERO)
2390
0
    }
2391
}
2392
2393
// TryFrom<Quantity> for Decimal removed due to conflict with From implementation
2394
// Use the From implementation instead which handles errors by returning ZERO
2395
2396
impl Mul<Self> for Price {
2397
    type Output = Result<Self, CommonTypeError>;
2398
1
    fn mul(self, rhs: Self) -> Self::Output {
2399
1
        Self::from_f64(self.to_f64() * rhs.to_f64())
2400
1
    }
2401
}
2402
2403
impl TryFrom<String> for Price {
2404
    type Error = CommonTypeError;
2405
0
    fn try_from(s: String) -> Result<Self, Self::Error> {
2406
0
        Self::from_str(&s)
2407
0
    }
2408
}
2409
2410
impl TryFrom<&str> for Price {
2411
    type Error = CommonTypeError;
2412
0
    fn try_from(s: &str) -> Result<Self, Self::Error> {
2413
0
        Self::from_str(s)
2414
0
    }
2415
}
2416
2417
impl PartialEq<f64> for Price {
2418
4
    fn eq(&self, other: &f64) -> bool {
2419
4
        (self.to_f64() - other).abs() < f64::EPSILON
2420
4
    }
2421
}
2422
2423
impl PartialEq<Price> for f64 {
2424
1
    fn eq(&self, other: &Price) -> bool {
2425
1
        (self - other.to_f64()).abs() < f64::EPSILON
2426
1
    }
2427
}
2428
2429
impl AddAssign for Price {
2430
1
    fn add_assign(&mut self, rhs: Self) {
2431
1
        self.value = self.value.saturating_add(rhs.value);
2432
1
    }
2433
}
2434
2435
impl SubAssign for Price {
2436
0
    fn sub_assign(&mut self, rhs: Self) {
2437
0
        self.value = self.value.saturating_sub(rhs.value);
2438
0
    }
2439
}
2440
2441
impl MulAssign<f64> for Price {
2442
0
    fn mul_assign(&mut self, rhs: f64) {
2443
0
        if let Ok(result) = self.mul(rhs) {
2444
0
            *self = result;
2445
0
        }
2446
        // If multiplication fails, self remains unchanged
2447
0
    }
2448
}
2449
2450
impl DivAssign<f64> for Price {
2451
0
    fn div_assign(&mut self, rhs: f64) {
2452
0
        if let Ok(result) = self.div(rhs) {
2453
0
            *self = result;
2454
0
        }
2455
        // If division fails, self remains unchanged
2456
0
    }
2457
}
2458
2459
impl PartialOrd<f64> for Price {
2460
2
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
2461
2
        self.to_f64().partial_cmp(other)
2462
2
    }
2463
}
2464
2465
impl PartialOrd<Price> for f64 {
2466
0
    fn partial_cmp(&self, other: &Price) -> Option<std::cmp::Ordering> {
2467
0
        self.partial_cmp(&other.to_f64())
2468
0
    }
2469
}
2470
2471
/// Core Quantity type using fixed-point arithmetic
2472
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2473
pub struct Quantity {
2474
    value: u64,
2475
}
2476
2477
impl Quantity {
2478
    /// Zero quantity constant
2479
    pub const ZERO: Self = Self { value: 0 };
2480
    /// One unit quantity constant
2481
    pub const ONE: Self = Self { value: 100_000_000 };
2482
    /// Maximum possible quantity
2483
    pub const MAX: Self = Self { value: u64::MAX };
2484
2485
    /// Create a Quantity from a floating point value
2486
61
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
2487
61
        if value < 0.0 || 
!value.is_finite()59
{
2488
4
            return Err(CommonTypeError::InvalidQuantity {
2489
4
                value: value.to_string(),
2490
4
                reason: "Quantity validation failed".to_owned(),
2491
4
            });
2492
57
        }
2493
57
        Ok(Self {
2494
57
            value: (value * 100_000_000.0).round() as u64,
2495
57
        })
2496
61
    }
2497
2498
    /// Convert quantity to floating point representation
2499
    #[must_use]
2500
46
    pub fn to_f64(&self) -> f64 {
2501
46
        self.value as f64 / 100_000_000.0
2502
46
    }
2503
2504
    /// Convert the quantity to a Decimal value
2505
0
    pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
2506
0
        Decimal::from_f64(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidQuantity {
2507
0
            value: "0.0".to_owned(),
2508
0
            reason: "Quantity to Decimal conversion failed".to_owned(),
2509
0
        })
2510
0
    }
2511
2512
    /// Get the internal value representation
2513
    #[must_use]
2514
0
    pub const fn value(&self) -> u64 {
2515
0
        self.value
2516
0
    }
2517
2518
    /// Get the raw internal value representation
2519
    #[must_use]
2520
1
    pub const fn raw_value(&self) -> u64 {
2521
1
        self.value
2522
1
    }
2523
2524
    /// Convert quantity to u64 representation
2525
    #[must_use]
2526
0
    pub const fn as_u64(&self) -> u64 {
2527
0
        self.value
2528
0
    }
2529
2530
    /// Create quantity from raw u64 value
2531
    #[must_use]
2532
0
    pub const fn from_raw(value: u64) -> Self {
2533
0
        Self { value }
2534
0
    }
2535
2536
    /// Create new quantity from f64 value
2537
1
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
2538
1
        Self::from_f64(value)
2539
1
    }
2540
2541
    /// Create zero quantity
2542
    #[must_use]
2543
0
    pub const fn zero() -> Self {
2544
0
        Self::ZERO
2545
0
    }
2546
2547
    /// Create a quantity from an i64 value
2548
0
    pub fn from_i64(value: i64) -> Result<Self, CommonTypeError> {
2549
0
        Self::from_f64(value as f64)
2550
0
    }
2551
2552
    /// Create a quantity from a u64 value
2553
0
    pub fn from_u64(value: u64) -> Result<Self, CommonTypeError> {
2554
0
        Self::from_f64(value as f64)
2555
0
    }
2556
2557
    /// Create a quantity from a Decimal value
2558
0
    pub fn from_decimal(decimal: Decimal) -> Result<Self, CommonTypeError> {
2559
        use std::convert::TryFrom;
2560
0
        Self::try_from(decimal).map_err(|_| CommonTypeError::InvalidQuantity {
2561
0
            value: decimal.to_string(),
2562
0
            reason: "Failed to convert Decimal to Quantity".to_owned(),
2563
0
        })
2564
0
    }
2565
2566
    /// Check if quantity is zero
2567
    #[must_use]
2568
10
    pub const fn is_zero(&self) -> bool {
2569
10
        self.value == 0
2570
10
    }
2571
2572
    /// Check if quantity has a non-zero value
2573
    #[must_use]
2574
0
    pub const fn is_some(&self) -> bool {
2575
0
        !self.is_zero()
2576
0
    }
2577
2578
    /// Check if quantity is zero (none)
2579
    #[must_use]
2580
0
    pub const fn is_none(&self) -> bool {
2581
0
        self.is_zero()
2582
0
    }
2583
2584
    /// Get a reference to self
2585
    #[must_use]
2586
0
    pub const fn as_ref(&self) -> &Self {
2587
0
        self
2588
0
    }
2589
2590
    /// Get absolute value (always positive for Quantity)
2591
    #[must_use]
2592
0
    pub const fn abs(&self) -> Self {
2593
0
        *self
2594
0
    }
2595
2596
    /// Get the sign of the quantity (1.0 for positive, 0.0 for zero)
2597
    #[must_use]
2598
0
    pub const fn signum(&self) -> f64 {
2599
0
        if self.value > 0 {
2600
0
            1.0
2601
        } else {
2602
0
            0.0
2603
        }
2604
0
    }
2605
2606
    /// Check if quantity is positive
2607
    #[must_use]
2608
5
    pub const fn is_positive(&self) -> bool {
2609
5
        self.value > 0
2610
5
    }
2611
2612
    /// Check if quantity is negative (always false for Quantity)
2613
    #[must_use]
2614
2
    pub const fn is_negative(&self) -> bool {
2615
2
        false
2616
2
    }
2617
2618
    /// Convert quantity to f64 representation
2619
    #[must_use]
2620
0
    pub fn as_f64(&self) -> f64 {
2621
0
        self.to_f64()
2622
0
    }
2623
2624
    /// Create quantity from number of shares
2625
    #[must_use]
2626
2
    pub const fn from_shares(shares: u64) -> Self {
2627
2
        Self {
2628
2
            value: shares * 100_000_000,
2629
2
        }
2630
2
    }
2631
2632
    /// Convert quantity to number of shares
2633
    #[must_use]
2634
2
    pub const fn to_shares(&self) -> u64 {
2635
2
        self.value / 100_000_000
2636
2
    }
2637
2638
    /// Multiply this quantity by another quantity
2639
0
    pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
2640
0
        Self::from_f64(self.to_f64() * other.to_f64())
2641
0
    }
2642
2643
    /// Subtract another quantity from this quantity
2644
    #[must_use]
2645
0
    pub fn subtract(&self, other: Self) -> Self {
2646
0
        *self - other
2647
0
    }
2648
}
2649
2650
impl Default for Quantity {
2651
0
    fn default() -> Self {
2652
0
        Self::ZERO
2653
0
    }
2654
}
2655
2656
impl FromStr for Quantity {
2657
    type Err = CommonTypeError;
2658
2659
1
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2660
1
        let parsed_value = s
2661
1
            .parse::<f64>()
2662
1
            .map_err(|_| CommonTypeError::InvalidQuantity {
2663
0
                value: s.to_owned(),
2664
0
                reason: format!("Cannot parse '{}' as quantity", s),
2665
0
            })?;
2666
1
        Self::from_f64(parsed_value)
2667
1
    }
2668
}
2669
2670
impl fmt::Display for Quantity {
2671
    /// Format the quantity for display with 8 decimal places
2672
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2673
0
        write!(f, "{:.8}", self.to_f64())
2674
0
    }
2675
}
2676
2677
impl TryFrom<i32> for Quantity {
2678
    type Error = CommonTypeError;
2679
1
    fn try_from(value: i32) -> Result<Self, Self::Error> {
2680
1
        Self::new(f64::from(value))
2681
1
    }
2682
}
2683
2684
impl TryFrom<u64> for Quantity {
2685
    type Error = CommonTypeError;
2686
0
    fn try_from(value: u64) -> Result<Self, Self::Error> {
2687
0
        Self::new(value as f64)
2688
0
    }
2689
}
2690
2691
impl TryFrom<f64> for Quantity {
2692
    type Error = CommonTypeError;
2693
0
    fn try_from(value: f64) -> Result<Self, Self::Error> {
2694
0
        Self::new(value)
2695
0
    }
2696
}
2697
2698
impl TryFrom<Decimal> for Quantity {
2699
    type Error = CommonTypeError;
2700
1
    fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
2701
1
        let f64_val: f64 =
2702
1
            TryInto::<f64>::try_into(decimal).map_err(|_| CommonTypeError::ConversionError {
2703
0
                message: "Failed to convert Decimal to f64".to_owned(),
2704
0
            })?;
2705
1
        Self::from_f64(f64_val)
2706
1
    }
2707
}
2708
2709
impl TryFrom<String> for Quantity {
2710
    type Error = CommonTypeError;
2711
0
    fn try_from(s: String) -> Result<Self, Self::Error> {
2712
0
        Self::from_str(&s)
2713
0
    }
2714
}
2715
2716
impl TryFrom<&str> for Quantity {
2717
    type Error = CommonTypeError;
2718
1
    fn try_from(s: &str) -> Result<Self, Self::Error> {
2719
1
        Self::from_str(s)
2720
1
    }
2721
}
2722
2723
impl PartialEq<f64> for Quantity {
2724
0
    fn eq(&self, other: &f64) -> bool {
2725
0
        (self.to_f64() - other).abs() < f64::EPSILON
2726
0
    }
2727
}
2728
2729
impl PartialEq<Quantity> for f64 {
2730
0
    fn eq(&self, other: &Quantity) -> bool {
2731
0
        (self - other.to_f64()).abs() < f64::EPSILON
2732
0
    }
2733
}
2734
2735
impl PartialOrd<f64> for Quantity {
2736
0
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
2737
0
        self.to_f64().partial_cmp(other)
2738
0
    }
2739
}
2740
2741
impl PartialOrd<Quantity> for f64 {
2742
0
    fn partial_cmp(&self, other: &Quantity) -> Option<std::cmp::Ordering> {
2743
0
        self.partial_cmp(&other.to_f64())
2744
0
    }
2745
}
2746
2747
impl Add for Quantity {
2748
    type Output = Self;
2749
29
    fn add(self, rhs: Self) -> Self::Output {
2750
29
        Self {
2751
29
            value: self.value.saturating_add(rhs.value),
2752
29
        }
2753
29
    }
2754
}
2755
2756
impl Sub for Quantity {
2757
    type Output = Self;
2758
14
    fn sub(self, rhs: Self) -> Self::Output {
2759
14
        Self {
2760
14
            value: self.value.saturating_sub(rhs.value),
2761
14
        }
2762
14
    }
2763
}
2764
2765
impl Mul<f64> for Quantity {
2766
    type Output = Result<Self, CommonTypeError>;
2767
1
    fn mul(self, rhs: f64) -> Self::Output {
2768
1
        Self::from_f64(self.to_f64() * rhs)
2769
1
    }
2770
}
2771
2772
impl Div<f64> for Quantity {
2773
    type Output = Result<Self, CommonTypeError>;
2774
2
    fn div(self, rhs: f64) -> Self::Output {
2775
2
        if rhs == 0.0 {
2776
1
            return Err(CommonTypeError::ConversionError {
2777
1
                message: "Cannot divide quantity by zero".to_owned(),
2778
1
            });
2779
1
        }
2780
1
        Self::from_f64(self.to_f64() / rhs)
2781
2
    }
2782
}
2783
2784
impl Sum for Quantity {
2785
2
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
2786
6
        
iter2
.
fold2
(Self::ZERO, |acc, x| acc + x)
2787
2
    }
2788
}
2789
2790
impl<'quantity> Sum<&'quantity Self> for Quantity {
2791
0
    fn sum<I: Iterator<Item = &'quantity Self>>(iter: I) -> Self {
2792
0
        iter.fold(Self::ZERO, |acc, x| acc + *x)
2793
0
    }
2794
}
2795
2796
// =============================================================================
2797
// SQLX IMPLEMENTATIONS FOR FINANCIAL TYPES
2798
// =============================================================================
2799
2800
#[cfg(feature = "database")]
2801
mod sqlx_impls {
2802
    use super::{HftTimestamp, MarketRegime, OrderSide, OrderStatus, OrderType, Price, Quantity};
2803
    use rust_decimal::Decimal as RustDecimal;
2804
    use sqlx::{
2805
        decode::Decode,
2806
        encode::{Encode, IsNull},
2807
        error::BoxDynError,
2808
        postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres},
2809
        Type,
2810
    };
2811
2812
    // SQLx implementations for Price
2813
    impl Type<Postgres> for Price {
2814
0
        fn type_info() -> PgTypeInfo {
2815
0
            PgTypeInfo::with_name("NUMERIC")
2816
0
        }
2817
    }
2818
2819
    impl<'q> Encode<'q, Postgres> for Price {
2820
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2821
            // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
2822
0
            let decimal_value = RustDecimal::new(self.raw_value() as i64, 8);
2823
0
            decimal_value.encode_by_ref(buf)
2824
0
        }
2825
    }
2826
2827
    impl<'r> Decode<'r, Postgres> for Price {
2828
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2829
            // Decode from NUMERIC to rust_decimal::Decimal
2830
0
            let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
2831
2832
            // Validate scale matches our fixed-point precision (8 decimal places)
2833
0
            if decimal_value.scale() != 8 {
2834
0
                return Err(format!(
2835
0
                    "Invalid scale for Price: expected 8, got {}",
2836
0
                    decimal_value.scale()
2837
0
                )
2838
0
                .into());
2839
0
            }
2840
2841
            // Extract mantissa and convert to our u64 representation
2842
0
            let mantissa = decimal_value.mantissa();
2843
0
            let inner_val = u64::try_from(mantissa)
2844
0
                .map_err(|_| "Failed to convert negative or overflowing NUMERIC to Price")?;
2845
2846
0
            Ok(Price::from_raw(inner_val))
2847
0
        }
2848
    }
2849
    // SQLx implementations for Quantity
2850
    impl Type<Postgres> for Quantity {
2851
0
        fn type_info() -> PgTypeInfo {
2852
0
            PgTypeInfo::with_name("NUMERIC")
2853
0
        }
2854
    }
2855
2856
    impl<'q> Encode<'q, Postgres> for Quantity {
2857
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2858
            // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
2859
0
            let decimal_value = RustDecimal::new(self.raw_value() as i64, 8);
2860
0
            decimal_value.encode_by_ref(buf)
2861
0
        }
2862
    }
2863
2864
    impl<'r> Decode<'r, Postgres> for Quantity {
2865
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2866
            // Decode from NUMERIC to rust_decimal::Decimal
2867
0
            let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
2868
2869
            // Validate scale matches our fixed-point precision (8 decimal places)
2870
0
            if decimal_value.scale() != 8 {
2871
0
                return Err(format!(
2872
0
                    "Invalid scale for Quantity: expected 8, got {}",
2873
0
                    decimal_value.scale()
2874
0
                )
2875
0
                .into());
2876
0
            }
2877
2878
            // Extract mantissa and convert to our u64 representation
2879
0
            let mantissa = decimal_value.mantissa();
2880
0
            let inner_val = u64::try_from(mantissa)
2881
0
                .map_err(|_| "Failed to convert negative or overflowing NUMERIC to Quantity")?;
2882
2883
0
            Ok(Quantity::from_raw(inner_val))
2884
0
        }
2885
    }
2886
2887
    // SQLx implementations for TimeInForce
2888
    impl Type<Postgres> for super::TimeInForce {
2889
0
        fn type_info() -> PgTypeInfo {
2890
0
            PgTypeInfo::with_name("TEXT")
2891
0
        }
2892
    }
2893
2894
    impl<'q> Encode<'q, Postgres> for super::TimeInForce {
2895
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2896
            // Use the Display trait to convert enum to string representation
2897
0
            <&str as Encode<Postgres>>::encode(self.to_string().as_str(), buf)
2898
0
        }
2899
    }
2900
2901
    impl<'r> Decode<'r, Postgres> for super::TimeInForce {
2902
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2903
            // Decode from TEXT to string, then parse to enum
2904
0
            let s = <&str as Decode<Postgres>>::decode(value)?;
2905
0
            match s {
2906
0
                "DAY" => Ok(super::TimeInForce::Day),
2907
0
                "GTC" => Ok(super::TimeInForce::GoodTillCancel),
2908
0
                "IOC" => Ok(super::TimeInForce::ImmediateOrCancel),
2909
0
                "FOK" => Ok(super::TimeInForce::FillOrKill),
2910
0
                _ => Err(format!("Invalid TimeInForce value: {}", s).into()),
2911
            }
2912
0
        }
2913
    }
2914
2915
    // SQLx implementations for OrderStatus
2916
    impl Type<Postgres> for OrderStatus {
2917
0
        fn type_info() -> PgTypeInfo {
2918
0
            PgTypeInfo::with_name("TEXT")
2919
0
        }
2920
    }
2921
2922
    impl<'q> Encode<'q, Postgres> for OrderStatus {
2923
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2924
0
            let value = match self {
2925
0
                OrderStatus::Created => "CREATED",
2926
0
                OrderStatus::Submitted => "SUBMITTED",
2927
0
                OrderStatus::PartiallyFilled => "PARTIALLY_FILLED",
2928
0
                OrderStatus::Filled => "FILLED",
2929
0
                OrderStatus::Rejected => "REJECTED",
2930
0
                OrderStatus::Cancelled => "CANCELLED",
2931
0
                OrderStatus::New => "NEW",
2932
0
                OrderStatus::Expired => "EXPIRED",
2933
0
                OrderStatus::Pending => "PENDING",
2934
0
                OrderStatus::Working => "WORKING",
2935
0
                OrderStatus::Unknown => "UNKNOWN",
2936
0
                OrderStatus::Suspended => "SUSPENDED",
2937
0
                OrderStatus::PendingCancel => "PENDING_CANCEL",
2938
0
                OrderStatus::PendingReplace => "PENDING_REPLACE",
2939
            };
2940
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
2941
0
        }
2942
    }
2943
2944
    impl<'r> Decode<'r, Postgres> for OrderStatus {
2945
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2946
0
            let s = <String as Decode<Postgres>>::decode(value)?;
2947
0
            match s.as_str() {
2948
0
                "CREATED" => Ok(OrderStatus::Created),
2949
0
                "SUBMITTED" => Ok(OrderStatus::Submitted),
2950
0
                "PARTIALLY_FILLED" => Ok(OrderStatus::PartiallyFilled),
2951
0
                "FILLED" => Ok(OrderStatus::Filled),
2952
0
                "REJECTED" => Ok(OrderStatus::Rejected),
2953
0
                "CANCELLED" => Ok(OrderStatus::Cancelled),
2954
0
                "NEW" => Ok(OrderStatus::New),
2955
0
                "EXPIRED" => Ok(OrderStatus::Expired),
2956
0
                "PENDING" => Ok(OrderStatus::Pending),
2957
0
                "WORKING" => Ok(OrderStatus::Working),
2958
0
                "UNKNOWN" => Ok(OrderStatus::Unknown),
2959
0
                "SUSPENDED" => Ok(OrderStatus::Suspended),
2960
0
                "PENDING_CANCEL" => Ok(OrderStatus::PendingCancel),
2961
0
                "PENDING_REPLACE" => Ok(OrderStatus::PendingReplace),
2962
0
                _ => Err(format!("Invalid OrderStatus value: {}", s).into()),
2963
            }
2964
0
        }
2965
    }
2966
2967
    // SQLx implementations for OrderSide
2968
    impl Type<Postgres> for OrderSide {
2969
0
        fn type_info() -> PgTypeInfo {
2970
0
            PgTypeInfo::with_name("TEXT")
2971
0
        }
2972
    }
2973
2974
    impl<'q> Encode<'q, Postgres> for OrderSide {
2975
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2976
0
            let value = match self {
2977
0
                OrderSide::Buy => "BUY",
2978
0
                OrderSide::Sell => "SELL",
2979
            };
2980
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
2981
0
        }
2982
    }
2983
2984
    impl<'r> Decode<'r, Postgres> for OrderSide {
2985
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2986
0
            let s = <String as Decode<Postgres>>::decode(value)?;
2987
0
            match s.as_str() {
2988
0
                "BUY" => Ok(OrderSide::Buy),
2989
0
                "SELL" => Ok(OrderSide::Sell),
2990
0
                _ => Err(format!("Invalid OrderSide value: {}", s).into()),
2991
            }
2992
0
        }
2993
    }
2994
2995
    // SQLx implementations for OrderType
2996
    impl Type<Postgres> for OrderType {
2997
0
        fn type_info() -> PgTypeInfo {
2998
0
            PgTypeInfo::with_name("TEXT")
2999
0
        }
3000
    }
3001
3002
    impl<'q> Encode<'q, Postgres> for OrderType {
3003
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3004
0
            let value = match self {
3005
0
                OrderType::Market => "MARKET",
3006
0
                OrderType::Limit => "LIMIT",
3007
0
                OrderType::Stop => "STOP",
3008
0
                OrderType::StopLimit => "STOP_LIMIT",
3009
0
                OrderType::Iceberg => "ICEBERG",
3010
0
                OrderType::TrailingStop => "TRAILING_STOP",
3011
0
                OrderType::Hidden => "HIDDEN",
3012
            };
3013
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3014
0
        }
3015
    }
3016
3017
    impl<'r> Decode<'r, Postgres> for OrderType {
3018
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3019
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3020
0
            match s.as_str() {
3021
0
                "MARKET" => Ok(OrderType::Market),
3022
0
                "LIMIT" => Ok(OrderType::Limit),
3023
0
                "STOP" => Ok(OrderType::Stop),
3024
0
                "STOP_LIMIT" => Ok(OrderType::StopLimit),
3025
0
                "ICEBERG" => Ok(OrderType::Iceberg),
3026
0
                "TRAILING_STOP" => Ok(OrderType::TrailingStop),
3027
0
                "HIDDEN" => Ok(OrderType::Hidden),
3028
0
                _ => Err(format!("Invalid OrderType value: {}", s).into()),
3029
            }
3030
0
        }
3031
    }
3032
3033
    // SQLx implementations for MarketRegime
3034
    impl Type<Postgres> for MarketRegime {
3035
0
        fn type_info() -> PgTypeInfo {
3036
0
            PgTypeInfo::with_name("TEXT")
3037
0
        }
3038
    }
3039
3040
    impl<'q> Encode<'q, Postgres> for MarketRegime {
3041
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3042
0
            let value = match self {
3043
0
                MarketRegime::Normal => "NORMAL",
3044
0
                MarketRegime::Crisis => "CRISIS",
3045
0
                MarketRegime::Trending => "TRENDING",
3046
0
                MarketRegime::Sideways => "SIDEWAYS",
3047
0
                MarketRegime::Bull => "BULL",
3048
0
                MarketRegime::Bear => "BEAR",
3049
0
                MarketRegime::HighVolatility => "HIGH_VOLATILITY",
3050
0
                MarketRegime::LowVolatility => "LOW_VOLATILITY",
3051
0
                MarketRegime::Volatile => "VOLATILE",
3052
0
                MarketRegime::Calm => "CALM",
3053
0
                MarketRegime::Unknown => "UNKNOWN",
3054
0
                MarketRegime::Recovery => "RECOVERY",
3055
0
                MarketRegime::Bubble => "BUBBLE",
3056
0
                MarketRegime::Correction => "CORRECTION",
3057
0
                MarketRegime::Custom(id) => {
3058
0
                    return <String as Encode<Postgres>>::encode_by_ref(
3059
0
                        &format!("CUSTOM_{}", id),
3060
0
                        buf,
3061
                    )
3062
                },
3063
            };
3064
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3065
0
        }
3066
    }
3067
3068
    impl<'r> Decode<'r, Postgres> for MarketRegime {
3069
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3070
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3071
0
            match s.as_str() {
3072
0
                "NORMAL" => Ok(MarketRegime::Normal),
3073
0
                "CRISIS" => Ok(MarketRegime::Crisis),
3074
0
                "TRENDING" => Ok(MarketRegime::Trending),
3075
0
                "SIDEWAYS" => Ok(MarketRegime::Sideways),
3076
0
                "BULL" => Ok(MarketRegime::Bull),
3077
0
                "BEAR" => Ok(MarketRegime::Bear),
3078
0
                "HIGH_VOLATILITY" => Ok(MarketRegime::HighVolatility),
3079
0
                "LOW_VOLATILITY" => Ok(MarketRegime::LowVolatility),
3080
0
                "VOLATILE" => Ok(MarketRegime::Volatile),
3081
0
                "CALM" => Ok(MarketRegime::Calm),
3082
0
                "UNKNOWN" => Ok(MarketRegime::Unknown),
3083
0
                "RECOVERY" => Ok(MarketRegime::Recovery),
3084
0
                "BUBBLE" => Ok(MarketRegime::Bubble),
3085
0
                "CORRECTION" => Ok(MarketRegime::Correction),
3086
                _ => {
3087
                    // Handle Custom(id) format
3088
0
                    if let Some(id_str) = s.strip_prefix("CUSTOM_") {
3089
0
                        if let Ok(id) = id_str.parse::<usize>() {
3090
0
                            Ok(MarketRegime::Custom(id))
3091
                        } else {
3092
0
                            Err(format!("Invalid MarketRegime Custom ID: {}", id_str).into())
3093
                        }
3094
                    } else {
3095
0
                        Err(format!("Invalid MarketRegime value: {}", s).into())
3096
                    }
3097
                },
3098
            }
3099
0
        }
3100
    }
3101
3102
    // SQLx implementations for HftTimestamp
3103
    // Maps to PostgreSQL BIGINT (stores nanoseconds since Unix epoch)
3104
    // Note: Limited to i64::MAX nanoseconds (year 2262) due to PostgreSQL BIGINT constraints
3105
    impl<'q> Encode<'q, Postgres> for HftTimestamp {
3106
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3107
            // Cast u64 to i64 for PostgreSQL BIGINT compatibility
3108
0
            <i64 as Encode<Postgres>>::encode(self.nanos() as i64, buf)
3109
0
        }
3110
    }
3111
3112
    impl<'r> Decode<'r, Postgres> for HftTimestamp {
3113
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3114
0
            let val = <i64 as Decode<Postgres>>::decode(value)?;
3115
            // Cast i64 back to u64 for internal representation
3116
0
            Ok(HftTimestamp::from_nanos(val as u64))
3117
0
        }
3118
    }
3119
3120
    impl Type<Postgres> for HftTimestamp {
3121
0
        fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
3122
0
            <i64 as Type<Postgres>>::type_info()
3123
0
        }
3124
3125
0
        fn compatible(ty: &<Postgres as sqlx::Database>::TypeInfo) -> bool {
3126
0
            <i64 as Type<Postgres>>::compatible(ty)
3127
0
        }
3128
    }
3129
3130
    // SQLx implementations for OrderId (uses BIGINT for u64)
3131
    impl Type<Postgres> for super::OrderId {
3132
0
        fn type_info() -> PgTypeInfo {
3133
0
            PgTypeInfo::with_name("BIGINT")
3134
0
        }
3135
    }
3136
3137
    impl<'q> Encode<'q, Postgres> for super::OrderId {
3138
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3139
0
            <i64 as Encode<Postgres>>::encode_by_ref(&(self.value() as i64), buf)
3140
0
        }
3141
    }
3142
3143
    impl<'r> Decode<'r, Postgres> for super::OrderId {
3144
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3145
0
            let id = <i64 as Decode<Postgres>>::decode(value)?;
3146
0
            Ok(super::OrderId::from_u64(id as u64))
3147
0
        }
3148
    }
3149
}
3150
3151
/// Volume type - alias for Quantity with the same fixed-point arithmetic
3152
/// SQLx traits are automatically inherited from Quantity
3153
pub type Volume = Quantity;
3154
3155
// ORDER TYPES ALREADY DEFINED ABOVE - No need to re-export from trading_engine
3156
// =============================================================================
3157
// CORE ID TYPES (MOVED FROM TRADING_ENGINE)
3158
// =============================================================================
3159
3160
/// Order identifier with ultra-fast atomic generation
3161
/// Replaces slow UUID generation (1ms+) with atomic increment (~5ns)
3162
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3163
pub struct OrderId(u64);
3164
3165
impl Default for OrderId {
3166
0
    fn default() -> Self {
3167
0
        Self::new()
3168
0
    }
3169
}
3170
3171
impl OrderId {
3172
    /// Generate next `OrderId` using atomic counter - <50ns performance
3173
1.01k
    pub fn new() -> Self {
3174
        use std::sync::atomic::{AtomicU64, Ordering};
3175
        static COUNTER: AtomicU64 = AtomicU64::new(1);
3176
1.01k
        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
3177
1.01k
    }
3178
3179
    /// Create `OrderId` from u64 value
3180
    #[must_use]
3181
2
    pub const fn from_u64(value: u64) -> Self {
3182
2
        Self(value)
3183
2
    }
3184
3185
    /// Get u64 value
3186
    #[must_use]
3187
8
    pub const fn value(&self) -> u64 {
3188
8
        self.0
3189
8
    }
3190
3191
    /// Get u64 value for performance-critical code (alias for value)
3192
    #[must_use]
3193
1
    pub const fn as_u64(&self) -> u64 {
3194
1
        self.0
3195
1
    }
3196
3197
    /// Get as string for compatibility
3198
    #[must_use]
3199
0
    pub fn as_str(&self) -> String {
3200
0
        self.0.to_string()
3201
0
    }
3202
}
3203
3204
impl fmt::Display for OrderId {
3205
    /// Format the order ID for display
3206
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3207
1
        write!(f, "{}", self.0)
3208
1
    }
3209
}
3210
3211
impl From<u64> for OrderId {
3212
    /// Create an OrderId from a u64 value
3213
0
    fn from(value: u64) -> Self {
3214
0
        Self(value)
3215
0
    }
3216
}
3217
3218
impl From<OrderId> for u64 {
3219
    /// Convert an OrderId to u64
3220
0
    fn from(order_id: OrderId) -> Self {
3221
0
        order_id.0
3222
0
    }
3223
}
3224
3225
impl FromStr for OrderId {
3226
    type Err = ParseIntError;
3227
3228
2
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3229
2
        s.parse::<u64>().map(OrderId)
3230
2
    }
3231
}
3232
3233
impl From<String> for OrderId {
3234
    /// Create an OrderId from a String, generating new ID if parsing fails
3235
2
    fn from(s: String) -> Self {
3236
2
        s.parse().unwrap_or_else(|_| 
Self::new1
())
3237
2
    }
3238
}
3239
3240
impl From<&str> for OrderId {
3241
    /// Create an OrderId from a &str, generating new ID if parsing fails
3242
0
    fn from(s: &str) -> Self {
3243
0
        s.parse().unwrap_or_else(|_| Self::new())
3244
0
    }
3245
}
3246
3247
/// Execution identifier with validation
3248
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3249
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3250
pub struct ExecutionId(String);
3251
3252
impl ExecutionId {
3253
    /// Create a new execution ID with validation
3254
3
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3255
3
        let id = id.into();
3256
3
        if id.trim().is_empty() {
3257
2
            return Err(CommonTypeError::ValidationError {
3258
2
                field: "execution_id".to_owned(),
3259
2
                reason: "Execution ID cannot be empty".to_owned(),
3260
2
            });
3261
1
        }
3262
1
        Ok(Self(id))
3263
3
    }
3264
3265
    /// Generate a new random execution ID
3266
1
    pub fn generate() -> Self {
3267
1
        Self(uuid::Uuid::new_v4().to_string())
3268
1
    }
3269
3270
    /// Get execution ID as string slice
3271
3
    pub fn as_str(&self) -> &str {
3272
3
        &self.0
3273
3
    }
3274
3275
    /// Convert execution ID into owned string
3276
0
    pub fn into_string(self) -> String {
3277
0
        self.0
3278
0
    }
3279
}
3280
3281
impl fmt::Display for ExecutionId {
3282
    /// Format the execution ID for display
3283
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3284
0
        write!(f, "{}", self.0)
3285
0
    }
3286
}
3287
3288
impl FromStr for ExecutionId {
3289
    type Err = CommonTypeError;
3290
3291
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3292
0
        Self::new(s)
3293
0
    }
3294
}
3295
3296
/// Trade identifier with validation
3297
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3298
pub struct TradeId(String);
3299
3300
impl TradeId {
3301
    /// Create a new trade ID with validation
3302
2
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3303
2
        let id = id.into();
3304
2
        if id.is_empty() {
3305
1
            return Err(CommonTypeError::ValidationError {
3306
1
                field: "trade_id".to_owned(),
3307
1
                reason: "Trade ID cannot be empty".to_owned(),
3308
1
            });
3309
1
        }
3310
1
        Ok(Self(id))
3311
2
    }
3312
3313
    /// Get the trade ID as a string slice
3314
1
    pub fn as_str(&self) -> &str {
3315
1
        &self.0
3316
1
    }
3317
    /// Convert the trade ID into an owned string
3318
0
    pub fn into_string(self) -> String {
3319
0
        self.0
3320
0
    }
3321
}
3322
3323
impl fmt::Display for TradeId {
3324
    /// Format the trade ID for display
3325
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3326
0
        write!(f, "{}", self.0)
3327
0
    }
3328
}
3329
3330
/// Trading symbol with validation
3331
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3332
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3333
pub struct Symbol {
3334
    value: String,
3335
}
3336
3337
impl Symbol {
3338
    /// Create a new symbol from a string
3339
    #[must_use]
3340
20
    pub const fn new(s: String) -> Self {
3341
20
        Self { value: s }
3342
20
    }
3343
3344
    /// Create a new Symbol with validation
3345
6
    pub fn new_validated(s: String) -> Result<Self, CommonTypeError> {
3346
6
        if s.trim().is_empty() {
3347
4
            return Err(CommonTypeError::ValidationError {
3348
4
                field: "symbol".to_string(),
3349
4
                reason: "Symbol cannot be empty".to_string(),
3350
4
            });
3351
2
        }
3352
2
        Ok(Self { value: s })
3353
6
    }
3354
3355
    /// Create a Symbol from &str with validation
3356
0
    pub fn from_str_validated(s: &str) -> Result<Self, CommonTypeError> {
3357
0
        Self::new_validated(s.to_owned())
3358
0
    }
3359
3360
    /// Get the symbol as a string slice
3361
    #[must_use]
3362
7
    pub fn as_str(&self) -> &str {
3363
7
        &self.value
3364
7
    }
3365
    /// Get the symbol value as a string slice
3366
    #[must_use]
3367
0
    pub fn value(&self) -> &str {
3368
0
        &self.value
3369
0
    }
3370
    /// Get the symbol as bytes
3371
    #[must_use]
3372
0
    pub fn as_bytes(&self) -> &[u8] {
3373
0
        self.value.as_bytes()
3374
0
    }
3375
    /// Check if the symbol is empty
3376
    #[must_use]
3377
2
    pub fn is_empty(&self) -> bool {
3378
2
        self.value.is_empty()
3379
2
    }
3380
    /// Convert the symbol to uppercase
3381
    #[must_use]
3382
2
    pub fn to_uppercase(&self) -> String {
3383
2
        self.value.to_uppercase()
3384
2
    }
3385
    /// Replace occurrences in the symbol
3386
    #[must_use]
3387
2
    pub fn replace(&self, from: &str, to: &str) -> String {
3388
2
        self.value.replace(from, to)
3389
2
    }
3390
3391
    /// Helper for risk management - creates a 'NONE' symbol
3392
    #[must_use]
3393
1
    pub fn none() -> Self {
3394
1
        "NONE".parse().unwrap()
3395
1
    }
3396
3397
    /// Check if the symbol contains a pattern
3398
    #[must_use]
3399
4
    pub fn contains(&self, pattern: &str) -> bool {
3400
4
        self.value.contains(pattern)
3401
4
    }
3402
}
3403
3404
impl FromStr for Symbol {
3405
    type Err = std::convert::Infallible;
3406
3407
6
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3408
6
        Ok(Self {
3409
6
            value: s.to_owned(),
3410
6
        })
3411
6
    }
3412
}
3413
3414
// Additional implementation to support conversion from &Symbol to &str
3415
impl AsRef<str> for Symbol {
3416
    /// Convert symbol to string reference
3417
0
    fn as_ref(&self) -> &str {
3418
0
        &self.value
3419
0
    }
3420
}
3421
3422
impl fmt::Display for Symbol {
3423
    /// Format the symbol for display
3424
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3425
0
        write!(f, "{}", self.value)
3426
0
    }
3427
}
3428
3429
impl From<String> for Symbol {
3430
    /// Create a Symbol from a String
3431
0
    fn from(s: String) -> Self {
3432
0
        Self::new(s)
3433
0
    }
3434
}
3435
impl From<&str> for Symbol {
3436
    /// Create a Symbol from a &str
3437
19
    fn from(s: &str) -> Self {
3438
19
        Self::new(s.to_owned())
3439
19
    }
3440
}
3441
3442
// TryFrom implementations removed due to conflicting blanket implementations
3443
// Use Symbol::new_validated() or Symbol::from_validated() directly instead
3444
3445
impl Default for Symbol {
3446
    /// Returns the default symbol (empty string)
3447
0
    fn default() -> Self {
3448
0
        Self::new(String::new())
3449
0
    }
3450
}
3451
3452
impl PartialEq<str> for Symbol {
3453
0
    fn eq(&self, other: &str) -> bool {
3454
0
        self.value == other
3455
0
    }
3456
}
3457
3458
impl PartialEq<&str> for Symbol {
3459
2
    fn eq(&self, other: &&str) -> bool {
3460
2
        self.value == *other
3461
2
    }
3462
}
3463
3464
impl PartialEq<String> for Symbol {
3465
1
    fn eq(&self, other: &String) -> bool {
3466
1
        &self.value == other
3467
1
    }
3468
}
3469
3470
impl PartialEq<Symbol> for &str {
3471
2
    fn eq(&self, other: &Symbol) -> bool {
3472
2
        *self == other.value
3473
2
    }
3474
}
3475
3476
impl PartialEq<Symbol> for String {
3477
1
    fn eq(&self, other: &Symbol) -> bool {
3478
1
        self == &other.value
3479
1
    }
3480
}
3481
3482
// TimeInForce moved to canonical source: common::types::TimeInForce
3483
3484
// Currency moved to canonical source: common::types::Currency
3485
3486
// Price moved to canonical source: common::types::Price
3487
3488
// Quantity moved to canonical source: common::types::Quantity
3489
// Volume moved to canonical source: common::types::Quantity (as Volume alias)
3490
3491
/// Money amount with currency
3492
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3493
pub struct Money {
3494
    /// The monetary amount
3495
    pub amount: Decimal,
3496
    /// The currency of the amount
3497
    pub currency: Currency,
3498
}
3499
3500
impl Money {
3501
    /// Create new money amount
3502
3
    pub const fn new(amount: Decimal, currency: Currency) -> Self {
3503
3
        Self { amount, currency }
3504
3
    }
3505
}
3506
3507
impl fmt::Display for Money {
3508
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3509
2
        write!(f, "{} {}", self.amount, self.currency)
3510
2
    }
3511
}
3512
3513
// OrderId moved to canonical source: common::types::OrderId
3514
3515
// TradeId moved to canonical source: common::types::TradeId
3516
3517
// Symbol moved to canonical source: common::types::Symbol
3518
3519
/// Type-safe account identifier
3520
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3521
pub struct AccountId(String);
3522
3523
impl AccountId {
3524
    /// Create a new account ID with validation
3525
3
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3526
3
        let id = id.into();
3527
3
        if id.trim().is_empty() {
3528
2
            return Err(CommonTypeError::InvalidIdentifier {
3529
2
                field: "account_id".to_string(),
3530
2
                reason: "Account ID cannot be empty".to_string(),
3531
2
            });
3532
1
        }
3533
1
        Ok(Self(id))
3534
3
    }
3535
3536
    /// Get the ID as a string slice
3537
0
    pub fn as_str(&self) -> &str {
3538
0
        &self.0
3539
0
    }
3540
3541
    /// Convert to owned String
3542
0
    pub fn into_string(self) -> String {
3543
0
        self.0
3544
0
    }
3545
}
3546
3547
impl fmt::Display for AccountId {
3548
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3549
0
        write!(f, "{}", self.0)
3550
0
    }
3551
}
3552
3553
/// High-precision timestamp for HFT applications - CANONICAL DEFINITION
3554
/// Robust implementation with error handling for financial safety
3555
#[derive(
3556
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
3557
)]
3558
pub struct HftTimestamp {
3559
    nanos: u64,
3560
}
3561
3562
impl HftTimestamp {
3563
    /// Get current timestamp with error handling for financial safety
3564
26
    pub fn now() -> Result<Self, CommonError> {
3565
        use std::time::{SystemTime, UNIX_EPOCH};
3566
26
        let nanos = SystemTime::now()
3567
26
            .duration_since(UNIX_EPOCH)
3568
26
            .map_err(|e| CommonError::Service {
3569
0
                category: CommonErrorCategory::System,
3570
0
                message: format!("System time before UNIX epoch: {e}"),
3571
0
            })?
3572
26
            .as_nanos() as u64;
3573
26
        Ok(Self { nanos })
3574
26
    }
3575
3576
    /// Get current timestamp with error handling for financial safety (CommonTypeError version)
3577
1
    pub fn now_common() -> Result<Self, CommonTypeError> {
3578
        use std::time::{SystemTime, UNIX_EPOCH};
3579
1
        let nanos = SystemTime::now()
3580
1
            .duration_since(UNIX_EPOCH)
3581
1
            .map_err(|e| CommonTypeError::ConversionError {
3582
0
                message: format!("System time before UNIX epoch: {e}"),
3583
0
            })?
3584
1
            .as_nanos() as u64;
3585
1
        Ok(Self { nanos })
3586
1
    }
3587
3588
    /// Get current timestamp or zero if system time is invalid
3589
    #[must_use]
3590
25
    pub fn now_or_zero() -> Self {
3591
25
        Self::now().unwrap_or(Self { nanos: 0 })
3592
25
    }
3593
3594
    /// Get nanoseconds since epoch
3595
    #[must_use]
3596
4
    pub const fn nanos(self) -> u64 {
3597
4
        self.nanos
3598
4
    }
3599
3600
    /// Create from nanoseconds since epoch
3601
    #[must_use]
3602
2
    pub const fn from_nanos(nanos: u64) -> Self {
3603
2
        Self { nanos }
3604
2
    }
3605
3606
    /// Create from signed nanoseconds (cast to unsigned)
3607
    #[must_use]
3608
0
    pub const fn from_nanos_i64(nanos: i64) -> Self {
3609
0
        Self {
3610
0
            nanos: nanos as u64,
3611
0
        }
3612
0
    }
3613
3614
    /// Get nanoseconds since epoch
3615
0
    pub const fn as_nanos(&self) -> u64 {
3616
0
        self.nanos
3617
0
    }
3618
3619
    /// Convert to DateTime<Utc>
3620
1
    pub fn to_datetime(&self) -> DateTime<Utc> {
3621
1
        let secs = self.nanos / 1_000_000_000;
3622
1
        let nsecs = (self.nanos % 1_000_000_000) as u32;
3623
1
        DateTime::from_timestamp(secs as i64, nsecs).unwrap_or_default()
3624
1
    }
3625
}
3626
3627
impl fmt::Display for HftTimestamp {
3628
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3629
0
        write!(f, "{}", self.to_datetime())
3630
0
    }
3631
}
3632
3633
/// Generic timestamp for general use cases
3634
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3635
pub struct GenericTimestamp {
3636
    nanos: u64,
3637
}
3638
3639
impl GenericTimestamp {
3640
    /// Create from nanoseconds since epoch
3641
    #[must_use]
3642
0
    pub const fn from_nanos(nanos: u64) -> Self {
3643
0
        Self { nanos }
3644
0
    }
3645
3646
    /// Get nanoseconds since epoch
3647
    #[must_use]
3648
0
    pub const fn nanos(&self) -> u64 {
3649
0
        self.nanos
3650
0
    }
3651
}
3652
3653
// =============================================================================
3654
// MARKET TYPES (MIGRATED FROM TRADING_ENGINE)
3655
// =============================================================================
3656
3657
/// Market regime enumeration for position sizing scaling and risk management
3658
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3659
pub enum MarketRegime {
3660
    /// Normal market conditions
3661
    Normal,
3662
    /// Crisis/stress market conditions
3663
    Crisis,
3664
    /// Trending market (strong directional movement)
3665
    Trending,
3666
    /// Sideways/ranging market (low volatility)
3667
    Sideways,
3668
    /// Bull market (sustained upward trend)
3669
    Bull,
3670
    /// Bear market (sustained downward trend)
3671
    Bear,
3672
    /// High volatility market conditions
3673
    HighVolatility,
3674
    /// Low volatility market conditions
3675
    LowVolatility,
3676
    /// Volatile market conditions (alias for `HighVolatility`)
3677
    Volatile,
3678
    /// Calm market conditions (alias for `LowVolatility`)
3679
    Calm,
3680
    /// Unknown/unclassified regime
3681
    Unknown,
3682
    /// Recovery regime - transitioning from crisis
3683
    Recovery,
3684
    /// Bubble regime - unsustainable upward movement
3685
    Bubble,
3686
    /// Correction regime - temporary downward adjustment
3687
    Correction,
3688
    /// Custom regime with numeric identifier
3689
    Custom(usize),
3690
}
3691
3692
impl Default for MarketRegime {
3693
0
    fn default() -> Self {
3694
0
        Self::Normal
3695
0
    }
3696
}
3697
3698
impl fmt::Display for MarketRegime {
3699
6
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3700
6
        match self {
3701
1
            Self::Normal => write!(f, "Normal"),
3702
1
            Self::Crisis => write!(f, "Crisis"),
3703
0
            Self::Trending => write!(f, "Trending"),
3704
0
            Self::Sideways => write!(f, "Sideways"),
3705
1
            Self::Bull => write!(f, "Bull"),
3706
1
            Self::Bear => write!(f, "Bear"),
3707
1
            Self::HighVolatility => write!(f, "HighVolatility"),
3708
0
            Self::LowVolatility => write!(f, "LowVolatility"),
3709
0
            Self::Volatile => write!(f, "Volatile"),
3710
0
            Self::Calm => write!(f, "Calm"),
3711
0
            Self::Unknown => write!(f, "Unknown"),
3712
0
            Self::Recovery => write!(f, "Recovery"),
3713
0
            Self::Bubble => write!(f, "Bubble"),
3714
0
            Self::Correction => write!(f, "Correction"),
3715
1
            Self::Custom(id) => write!(f, "Custom({id})"),
3716
        }
3717
6
    }
3718
}
3719
3720
/// Tick type enumeration for market data
3721
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3722
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3723
#[cfg_attr(
3724
    feature = "database",
3725
    sqlx(type_name = "tick_type", rename_all = "snake_case")
3726
)]
3727
pub enum TickType {
3728
    /// Trade execution tick
3729
    Trade,
3730
    /// Bid price update tick
3731
    Bid,
3732
    /// Ask price update tick
3733
    Ask,
3734
    /// Quote (bid/ask) update tick
3735
    Quote,
3736
}
3737
3738
/// Exchange enumeration for trading venues
3739
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3740
pub enum Exchange {
3741
    /// New York Stock Exchange
3742
    NYSE,
3743
    /// NASDAQ
3744
    NASDAQ,
3745
    /// Chicago Mercantile Exchange
3746
    CME,
3747
    /// Intercontinental Exchange
3748
    ICE,
3749
    /// London Stock Exchange
3750
    LSE,
3751
    /// Tokyo Stock Exchange
3752
    TSE,
3753
    /// Hong Kong Stock Exchange
3754
    HKEX,
3755
    /// Shanghai Stock Exchange
3756
    SSE,
3757
    /// Shenzhen Stock Exchange
3758
    SZSE,
3759
    /// Euronext
3760
    EURONEXT,
3761
    /// Deutsche Börse
3762
    XETRA,
3763
    /// Chicago Board of Trade
3764
    CBOT,
3765
    /// Chicago Board Options Exchange
3766
    CBOE,
3767
    /// BATS Global Markets
3768
    BATS,
3769
    /// IEX Exchange
3770
    IEX,
3771
    /// Interactive Brokers
3772
    IBKR,
3773
    /// IC Markets
3774
    ICMARKETS,
3775
    /// Forex.com
3776
    FOREX,
3777
    /// Binance
3778
    BINANCE,
3779
    /// Coinbase
3780
    COINBASE,
3781
    /// Kraken
3782
    KRAKEN,
3783
    /// Unknown or unrecognized exchange
3784
    UNKNOWN,
3785
}
3786
3787
impl Default for Exchange {
3788
0
    fn default() -> Self {
3789
0
        Self::UNKNOWN
3790
0
    }
3791
}
3792
3793
impl fmt::Display for Exchange {
3794
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3795
0
        match self {
3796
0
            Self::NYSE => write!(f, "NYSE"),
3797
0
            Self::NASDAQ => write!(f, "NASDAQ"),
3798
0
            Self::CME => write!(f, "CME"),
3799
0
            Self::ICE => write!(f, "ICE"),
3800
0
            Self::LSE => write!(f, "LSE"),
3801
0
            Self::TSE => write!(f, "TSE"),
3802
0
            Self::HKEX => write!(f, "HKEX"),
3803
0
            Self::SSE => write!(f, "SSE"),
3804
0
            Self::SZSE => write!(f, "SZSE"),
3805
0
            Self::EURONEXT => write!(f, "EURONEXT"),
3806
0
            Self::XETRA => write!(f, "XETRA"),
3807
0
            Self::CBOT => write!(f, "CBOT"),
3808
0
            Self::CBOE => write!(f, "CBOE"),
3809
0
            Self::BATS => write!(f, "BATS"),
3810
0
            Self::IEX => write!(f, "IEX"),
3811
0
            Self::IBKR => write!(f, "IBKR"),
3812
0
            Self::ICMARKETS => write!(f, "ICMARKETS"),
3813
0
            Self::FOREX => write!(f, "FOREX"),
3814
0
            Self::BINANCE => write!(f, "BINANCE"),
3815
0
            Self::COINBASE => write!(f, "COINBASE"),
3816
0
            Self::KRAKEN => write!(f, "KRAKEN"),
3817
0
            Self::UNKNOWN => write!(f, "UNKNOWN"),
3818
        }
3819
0
    }
3820
}
3821
3822
impl FromStr for Exchange {
3823
    type Err = CommonTypeError;
3824
3825
4
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3826
4
        match s.to_uppercase().as_str() {
3827
4
            "NYSE" => 
Ok(Self::NYSE)1
,
3828
3
            "NASDAQ" => 
Ok(Self::NASDAQ)2
,
3829
1
            "CME" => 
Ok(Self::CME)0
,
3830
1
            "ICE" => 
Ok(Self::ICE)0
,
3831
1
            "LSE" => 
Ok(Self::LSE)0
,
3832
1
            "TSE" => 
Ok(Self::TSE)0
,
3833
1
            "HKEX" => 
Ok(Self::HKEX)0
,
3834
1
            "SSE" => 
Ok(Self::SSE)0
,
3835
1
            "SZSE" => 
Ok(Self::SZSE)0
,
3836
1
            "EURONEXT" => 
Ok(Self::EURONEXT)0
,
3837
1
            "XETRA" => 
Ok(Self::XETRA)0
,
3838
1
            "CBOT" => 
Ok(Self::CBOT)0
,
3839
1
            "CBOE" => 
Ok(Self::CBOE)0
,
3840
1
            "BATS" => 
Ok(Self::BATS)0
,
3841
1
            "IEX" => 
Ok(Self::IEX)0
,
3842
1
            "IBKR" => 
Ok(Self::IBKR)0
,
3843
1
            "ICMARKETS" => 
Ok(Self::ICMARKETS)0
,
3844
1
            "FOREX" => 
Ok(Self::FOREX)0
,
3845
1
            "BINANCE" => 
Ok(Self::BINANCE)0
,
3846
1
            "COINBASE" => 
Ok(Self::COINBASE)0
,
3847
1
            "KRAKEN" => 
Ok(Self::KRAKEN)0
,
3848
1
            "UNKNOWN" => 
Ok(Self::UNKNOWN)0
,
3849
1
            _ => Ok(Self::UNKNOWN), // Default to UNKNOWN for unrecognized exchanges
3850
        }
3851
4
    }
3852
}
3853
3854
/// Market tick data structure - CANONICAL SINGLE SOURCE OF TRUTH
3855
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3856
pub struct MarketTick {
3857
    /// Trading symbol
3858
    pub symbol: Symbol,
3859
    /// Tick price
3860
    pub price: Price,
3861
    /// Tick size/quantity
3862
    pub size: Quantity,
3863
    /// Tick timestamp
3864
    pub timestamp: HftTimestamp,
3865
    /// Type of tick (trade, bid, ask, quote)
3866
    pub tick_type: TickType,
3867
    /// Exchange where the tick occurred
3868
    pub exchange: Exchange,
3869
    /// Sequence number for ordering
3870
    pub sequence_number: u64,
3871
}
3872
3873
impl MarketTick {
3874
    /// Create a new market tick with current timestamp
3875
0
    pub fn new(
3876
0
        symbol: Symbol,
3877
0
        price: Price,
3878
0
        size: Quantity,
3879
0
        tick_type: TickType,
3880
0
        exchange: Exchange,
3881
0
        sequence_number: u64,
3882
0
    ) -> Result<Self, CommonError> {
3883
        Ok(Self {
3884
0
            symbol,
3885
0
            price,
3886
0
            size,
3887
0
            timestamp: HftTimestamp::now()?,
3888
0
            tick_type,
3889
0
            exchange,
3890
0
            sequence_number,
3891
        })
3892
0
    }
3893
3894
    /// Create a new market tick with specified timestamp (for backtesting)
3895
    #[must_use]
3896
0
    pub const fn with_timestamp(
3897
0
        symbol: Symbol,
3898
0
        price: Price,
3899
0
        size: Quantity,
3900
0
        timestamp: HftTimestamp,
3901
0
        tick_type: TickType,
3902
0
        exchange: Exchange,
3903
0
        sequence_number: u64,
3904
0
    ) -> Self {
3905
0
        Self {
3906
0
            symbol,
3907
0
            price,
3908
0
            size,
3909
0
            timestamp,
3910
0
            tick_type,
3911
0
            exchange,
3912
0
            sequence_number,
3913
0
        }
3914
0
    }
3915
}
3916
3917
/// Trading signal for algorithmic trading
3918
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3919
pub struct TradingSignal {
3920
    /// Signal ID
3921
    pub signal_id: Uuid,
3922
    /// Symbol this signal applies to
3923
    pub symbol: Symbol,
3924
    /// Signal strength (-1.0 to 1.0)
3925
    pub strength: f64,
3926
    /// Signal direction
3927
    pub direction: OrderSide,
3928
    /// Confidence level (0.0 to 1.0)
3929
    pub confidence: f64,
3930
    /// Signal generation timestamp
3931
    pub timestamp: HftTimestamp,
3932
    /// Signal source/strategy
3933
    pub source: String,
3934
    /// Additional metadata
3935
    pub metadata: std::collections::HashMap<String, String>,
3936
}
3937
3938
impl TradingSignal {
3939
    /// Create a new trading signal
3940
3
    pub fn new(
3941
3
        symbol: Symbol,
3942
3
        strength: f64,
3943
3
        direction: OrderSide,
3944
3
        confidence: f64,
3945
3
        source: String,
3946
3
    ) -> Result<Self, CommonTypeError> {
3947
3
        if !(0.0..=1.0).contains(&confidence) {
3948
1
            return Err(CommonTypeError::ValidationError {
3949
1
                field: "confidence".to_owned(),
3950
1
                reason: "Confidence must be between 0.0 and 1.0".to_owned(),
3951
1
            });
3952
2
        }
3953
2
        if !(-1.0..=1.0).contains(&strength) {
3954
1
            return Err(CommonTypeError::ValidationError {
3955
1
                field: "strength".to_owned(),
3956
1
                reason: "Strength must be between -1.0 and 1.0".to_owned(),
3957
1
            });
3958
1
        }
3959
3960
        Ok(Self {
3961
1
            signal_id: Uuid::new_v4(),
3962
1
            symbol,
3963
1
            strength,
3964
1
            direction,
3965
1
            confidence,
3966
1
            timestamp: HftTimestamp::now_common()
?0
,
3967
1
            source,
3968
1
            metadata: std::collections::HashMap::new(),
3969
        })
3970
3
    }
3971
3972
    /// Add metadata to the signal
3973
    #[must_use]
3974
0
    pub fn with_metadata(mut self, key: String, value: String) -> Self {
3975
0
        self.metadata.insert(key, value);
3976
0
        self
3977
0
    }
3978
}
3979
3980
// =============================================================================
3981
// HIGH-PERFORMANCE TYPES FOR COPY/CLONE OPTIMIZATION
3982
// =============================================================================
3983
3984
/// Lightweight Order reference for high-performance contexts requiring Copy trait
3985
///
3986
/// This struct contains only the essential order data needed for performance-critical
3987
/// operations like `SmallBatchRing` processing, while maintaining Copy semantics.
3988
/// For full order details, use the complete Order struct.
3989
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3990
pub struct OrderRef {
3991
    /// Order ID (u64 for performance)
3992
    pub id: u64,
3993
    /// Symbol hash for fast lookups
3994
    pub symbol_hash: i64,
3995
    /// Order side (Buy/Sell)
3996
    pub side: OrderSide,
3997
    /// Order type
3998
    pub order_type: OrderType,
3999
    /// Quantity (fixed-point u64)
4000
    pub quantity: u64,
4001
    /// Price (fixed-point u64, 0 for market orders)
4002
    pub price: u64,
4003
    /// Timestamp (nanoseconds since epoch)
4004
    pub timestamp: u64,
4005
}
4006
4007
impl OrderRef {
4008
    /// Create `OrderRef` from a full Order struct
4009
    #[must_use]
4010
1
    pub fn from_order(order: &Order) -> Self {
4011
        Self {
4012
1
            id: order.id.value(),
4013
1
            symbol_hash: order.symbol_hash(),
4014
1
            side: order.side,
4015
1
            order_type: order.order_type,
4016
1
            quantity: order.quantity.raw_value(),
4017
1
            price: order.price.map_or(0, |p| p.raw_value()),
4018
1
            timestamp: order.created_at.nanos(),
4019
        }
4020
1
    }
4021
4022
    /// Create a limit order reference
4023
    #[must_use]
4024
0
    pub fn limit(symbol_hash: i64, side: OrderSide, quantity: u64, price: u64) -> Self {
4025
0
        Self {
4026
0
            id: OrderId::new().value(),
4027
0
            symbol_hash,
4028
0
            side,
4029
0
            order_type: OrderType::Limit,
4030
0
            quantity,
4031
0
            price,
4032
0
            timestamp: HftTimestamp::now_or_zero().nanos(),
4033
0
        }
4034
0
    }
4035
4036
    /// Create a market order reference  
4037
    #[must_use]
4038
0
    pub fn market(symbol_hash: i64, side: OrderSide, quantity: u64) -> Self {
4039
0
        Self {
4040
0
            id: OrderId::new().value(),
4041
0
            symbol_hash,
4042
0
            side,
4043
0
            order_type: OrderType::Market,
4044
0
            quantity,
4045
0
            price: 0,
4046
0
            timestamp: HftTimestamp::now_or_zero().nanos(),
4047
0
        }
4048
0
    }
4049
4050
    /// Get quantity as Quantity type
4051
    #[must_use]
4052
0
    pub const fn get_quantity(&self) -> Quantity {
4053
0
        Quantity::from_raw(self.quantity)
4054
0
    }
4055
4056
    /// Get price as Price type (None for market orders)
4057
    #[must_use]
4058
0
    pub const fn get_price(&self) -> Option<Price> {
4059
0
        if self.price == 0 {
4060
0
            None
4061
        } else {
4062
0
            Some(Price::from_raw(self.price))
4063
        }
4064
0
    }
4065
4066
    /// Check if this is a buy order
4067
    #[must_use]
4068
0
    pub fn is_buy(&self) -> bool {
4069
0
        self.side == OrderSide::Buy
4070
0
    }
4071
4072
    /// Check if this is a sell order
4073
    #[must_use]
4074
0
    pub fn is_sell(&self) -> bool {
4075
0
        self.side == OrderSide::Sell
4076
0
    }
4077
4078
    /// Check if this is a market order
4079
    #[must_use]
4080
0
    pub fn is_market_order(&self) -> bool {
4081
0
        self.order_type == OrderType::Market || self.price == 0
4082
0
    }
4083
4084
    /// Check if this is a limit order
4085
    #[must_use]
4086
0
    pub fn is_limit_order(&self) -> bool {
4087
0
        self.order_type == OrderType::Limit && self.price > 0
4088
0
    }
4089
}
4090
4091
impl Default for OrderRef {
4092
0
    fn default() -> Self {
4093
0
        Self {
4094
0
            id: 0,
4095
0
            symbol_hash: 0,
4096
0
            side: OrderSide::Buy,
4097
0
            order_type: OrderType::Market,
4098
0
            quantity: 0,
4099
0
            price: 0,
4100
0
            timestamp: 0,
4101
0
        }
4102
0
    }
4103
}
4104
4105
// =============================================================================
4106
// COMPREHENSIVE TESTS
4107
// =============================================================================
4108
4109
#[cfg(test)]
4110
mod tests {
4111
    use super::*;
4112
    use std::str::FromStr;
4113
4114
    // =============================================================================
4115
    // Price Tests
4116
    // =============================================================================
4117
4118
    #[test]
4119
1
    fn test_price_from_f64_valid() {
4120
1
        let price = Price::from_f64(100.50).unwrap();
4121
1
        assert_eq!(price.to_f64(), 100.50);
4122
1
    }
4123
4124
    #[test]
4125
1
    fn test_price_from_f64_negative() {
4126
1
        let result = Price::from_f64(-10.0);
4127
1
        assert!(result.is_err());
4128
1
    }
4129
4130
    #[test]
4131
1
    fn test_price_from_f64_nan() {
4132
1
        let result = Price::from_f64(f64::NAN);
4133
1
        assert!(result.is_err());
4134
1
    }
4135
4136
    #[test]
4137
1
    fn test_price_from_f64_infinity() {
4138
1
        let result = Price::from_f64(f64::INFINITY);
4139
1
        assert!(result.is_err());
4140
1
    }
4141
4142
    #[test]
4143
1
    fn test_price_constants() {
4144
1
        assert_eq!(Price::ZERO.to_f64(), 0.0);
4145
1
        assert_eq!(Price::ONE.to_f64(), 1.0);
4146
1
        assert_eq!(Price::CENT.to_f64(), 0.01);
4147
1
    }
4148
4149
    #[test]
4150
1
    fn test_price_addition() {
4151
1
        let p1 = Price::from_f64(10.0).unwrap();
4152
1
        let p2 = Price::from_f64(5.5).unwrap();
4153
1
        let result = p1 + p2;
4154
1
        assert!((result.to_f64() - 15.5).abs() < 0.00001);
4155
1
    }
4156
4157
    #[test]
4158
1
    fn test_price_subtraction() {
4159
1
        let p1 = Price::from_f64(10.0).unwrap();
4160
1
        let p2 = Price::from_f64(5.5).unwrap();
4161
1
        let result = p1 - p2;
4162
1
        assert!((result.to_f64() - 4.5).abs() < 0.00001);
4163
1
    }
4164
4165
    #[test]
4166
1
    fn test_price_multiplication() {
4167
1
        let price = Price::from_f64(10.0).unwrap();
4168
1
        let result = (price * 2.5).unwrap();
4169
1
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4170
1
    }
4171
4172
    #[test]
4173
1
    fn test_price_division() {
4174
1
        let price = Price::from_f64(10.0).unwrap();
4175
1
        let result = (price / 2.0).unwrap();
4176
1
        assert!((result.to_f64() - 5.0).abs() < 0.00001);
4177
1
    }
4178
4179
    #[test]
4180
1
    fn test_price_division_by_zero() {
4181
1
        let price = Price::from_f64(10.0).unwrap();
4182
1
        let result = price / 0.0;
4183
1
        assert!(result.is_err());
4184
1
    }
4185
4186
    #[test]
4187
1
    fn test_price_from_cents() {
4188
1
        let price = Price::from_cents(150);
4189
1
        assert!((price.to_f64() - 1.50).abs() < 0.00001);
4190
1
    }
4191
4192
    #[test]
4193
1
    fn test_price_to_cents() {
4194
1
        let price = Price::from_f64(1.50).unwrap();
4195
1
        assert_eq!(price.to_cents(), 150);
4196
1
    }
4197
4198
    #[test]
4199
1
    fn test_price_is_zero() {
4200
1
        assert!(Price::ZERO.is_zero());
4201
1
        assert!(!Price::from_f64(1.0).unwrap().is_zero());
4202
1
    }
4203
4204
    #[test]
4205
1
    fn test_price_from_str() {
4206
1
        let price = Price::from_str("123.45").unwrap();
4207
1
        assert!((price.to_f64() - 123.45).abs() < 0.00001);
4208
1
    }
4209
4210
    #[test]
4211
1
    fn test_price_from_str_invalid() {
4212
1
        let result = Price::from_str("invalid");
4213
1
        assert!(result.is_err());
4214
1
    }
4215
4216
    #[test]
4217
1
    fn test_price_display() {
4218
1
        let price = Price::from_f64(123.456789).unwrap();
4219
1
        let display = format!("{}", price);
4220
1
        assert!(display.starts_with("123.45678"));
4221
1
    }
4222
4223
    #[test]
4224
1
    fn test_price_partial_eq_f64() {
4225
1
        let price = Price::from_f64(10.0).unwrap();
4226
1
        assert_eq!(price, 10.0);
4227
1
        assert_eq!(10.0, price);
4228
1
    }
4229
4230
    #[test]
4231
1
    fn test_price_multiply_price() {
4232
1
        let p1 = Price::from_f64(10.0).unwrap();
4233
1
        let p2 = Price::from_f64(2.5).unwrap();
4234
1
        let result = p1.multiply(p2).unwrap();
4235
1
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4236
1
    }
4237
4238
    // =============================================================================
4239
    // Quantity Tests
4240
    // =============================================================================
4241
4242
    #[test]
4243
1
    fn test_quantity_from_f64_valid() {
4244
1
        let qty = Quantity::from_f64(100.5).unwrap();
4245
1
        assert_eq!(qty.to_f64(), 100.5);
4246
1
    }
4247
4248
    #[test]
4249
1
    fn test_quantity_from_f64_negative() {
4250
1
        let result = Quantity::from_f64(-10.0);
4251
1
        assert!(result.is_err());
4252
1
    }
4253
4254
    #[test]
4255
1
    fn test_quantity_from_f64_nan() {
4256
1
        let result = Quantity::from_f64(f64::NAN);
4257
1
        assert!(result.is_err());
4258
1
    }
4259
4260
    #[test]
4261
1
    fn test_quantity_constants() {
4262
1
        assert_eq!(Quantity::ZERO.to_f64(), 0.0);
4263
1
        assert_eq!(Quantity::ONE.to_f64(), 1.0);
4264
1
    }
4265
4266
    #[test]
4267
1
    fn test_quantity_addition() {
4268
1
        let q1 = Quantity::from_f64(10.0).unwrap();
4269
1
        let q2 = Quantity::from_f64(5.5).unwrap();
4270
1
        let result = q1 + q2;
4271
1
        assert!((result.to_f64() - 15.5).abs() < 0.00001);
4272
1
    }
4273
4274
    #[test]
4275
1
    fn test_quantity_subtraction() {
4276
1
        let q1 = Quantity::from_f64(10.0).unwrap();
4277
1
        let q2 = Quantity::from_f64(5.5).unwrap();
4278
1
        let result = q1 - q2;
4279
1
        assert!((result.to_f64() - 4.5).abs() < 0.00001);
4280
1
    }
4281
4282
    #[test]
4283
1
    fn test_quantity_multiplication() {
4284
1
        let qty = Quantity::from_f64(10.0).unwrap();
4285
1
        let result = (qty * 2.5).unwrap();
4286
1
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4287
1
    }
4288
4289
    #[test]
4290
1
    fn test_quantity_division() {
4291
1
        let qty = Quantity::from_f64(10.0).unwrap();
4292
1
        let result = (qty / 2.0).unwrap();
4293
1
        assert!((result.to_f64() - 5.0).abs() < 0.00001);
4294
1
    }
4295
4296
    #[test]
4297
1
    fn test_quantity_division_by_zero() {
4298
1
        let qty = Quantity::from_f64(10.0).unwrap();
4299
1
        let result = qty / 0.0;
4300
1
        assert!(result.is_err());
4301
1
    }
4302
4303
    #[test]
4304
1
    fn test_quantity_is_zero() {
4305
1
        assert!(Quantity::ZERO.is_zero());
4306
1
        assert!(!Quantity::from_f64(1.0).unwrap().is_zero());
4307
1
    }
4308
4309
    #[test]
4310
1
    fn test_quantity_is_positive() {
4311
1
        assert!(Quantity::from_f64(1.0).unwrap().is_positive());
4312
1
        assert!(!Quantity::ZERO.is_positive());
4313
1
    }
4314
4315
    #[test]
4316
1
    fn test_quantity_is_negative() {
4317
        // Quantity is always non-negative
4318
1
        assert!(!Quantity::from_f64(1.0).unwrap().is_negative());
4319
1
        assert!(!Quantity::ZERO.is_negative());
4320
1
    }
4321
4322
    #[test]
4323
1
    fn test_quantity_from_shares() {
4324
1
        let qty = Quantity::from_shares(100);
4325
1
        assert_eq!(qty.to_shares(), 100);
4326
1
    }
4327
4328
    #[test]
4329
1
    fn test_quantity_sum() {
4330
1
        let quantities = vec![
4331
1
            Quantity::from_f64(1.0).unwrap(),
4332
1
            Quantity::from_f64(2.0).unwrap(),
4333
1
            Quantity::from_f64(3.0).unwrap(),
4334
        ];
4335
1
        let sum: Quantity = quantities.into_iter().sum();
4336
1
        assert!((sum.to_f64() - 6.0).abs() < 0.00001);
4337
1
    }
4338
4339
    #[test]
4340
1
    fn test_quantity_try_from_i32() {
4341
1
        let qty = Quantity::try_from(100i32).unwrap();
4342
1
        assert_eq!(qty.to_f64(), 100.0);
4343
1
    }
4344
4345
    #[test]
4346
1
    fn test_quantity_try_from_string() {
4347
1
        let qty = Quantity::try_from("123.45").unwrap();
4348
1
        assert!((qty.to_f64() - 123.45).abs() < 0.00001);
4349
1
    }
4350
4351
    // =============================================================================
4352
    // Money Tests
4353
    // =============================================================================
4354
4355
    #[test]
4356
1
    fn test_money_new() {
4357
1
        let amount = Decimal::from_f64(100.50).unwrap();
4358
1
        let money = Money::new(amount, Currency::USD);
4359
1
        assert_eq!(money.currency, Currency::USD);
4360
1
        assert_eq!(money.amount, amount);
4361
1
    }
4362
4363
    #[test]
4364
1
    fn test_money_display() {
4365
1
        let amount = Decimal::from_f64(100.50).unwrap();
4366
1
        let money = Money::new(amount, Currency::USD);
4367
1
        let display = format!("{}", money);
4368
1
        assert!(display.contains("100.5"));
4369
1
        assert!(display.contains("USD"));
4370
1
    }
4371
4372
    // =============================================================================
4373
    // Symbol Tests
4374
    // =============================================================================
4375
4376
    #[test]
4377
1
    fn test_symbol_new() {
4378
1
        let symbol = Symbol::new("AAPL".to_string());
4379
1
        assert_eq!(symbol.as_str(), "AAPL");
4380
1
    }
4381
4382
    #[test]
4383
1
    fn test_symbol_new_validated_valid() {
4384
1
        let symbol = Symbol::new_validated("AAPL".to_string()).unwrap();
4385
1
        assert_eq!(symbol.as_str(), "AAPL");
4386
1
    }
4387
4388
    #[test]
4389
1
    fn test_symbol_new_validated_empty() {
4390
1
        let result = Symbol::new_validated("".to_string());
4391
1
        assert!(result.is_err());
4392
1
    }
4393
4394
    #[test]
4395
1
    fn test_symbol_new_validated_whitespace() {
4396
1
        let result = Symbol::new_validated("   ".to_string());
4397
1
        assert!(result.is_err());
4398
1
    }
4399
4400
    #[test]
4401
1
    fn test_symbol_from_str() {
4402
1
        let symbol = Symbol::from_str("AAPL").unwrap();
4403
1
        assert_eq!(symbol.as_str(), "AAPL");
4404
1
    }
4405
4406
    #[test]
4407
1
    fn test_symbol_to_uppercase() {
4408
1
        let symbol = Symbol::from_str("aapl").unwrap();
4409
1
        assert_eq!(symbol.to_uppercase(), "AAPL");
4410
1
    }
4411
4412
    #[test]
4413
1
    fn test_symbol_replace() {
4414
1
        let symbol = Symbol::from_str("AAPL.US").unwrap();
4415
1
        assert_eq!(symbol.replace(".US", ""), "AAPL");
4416
1
    }
4417
4418
    #[test]
4419
1
    fn test_symbol_contains() {
4420
1
        let symbol = Symbol::from_str("AAPL.US").unwrap();
4421
1
        assert!(symbol.contains("AAPL"));
4422
1
        assert!(!symbol.contains("MSFT"));
4423
1
    }
4424
4425
    #[test]
4426
1
    fn test_symbol_partial_eq_str() {
4427
1
        let symbol = Symbol::from_str("AAPL").unwrap();
4428
1
        assert_eq!("AAPL", symbol);
4429
1
        assert_eq!(symbol.as_str(), "AAPL");
4430
1
    }
4431
4432
    #[test]
4433
1
    fn test_symbol_none() {
4434
1
        let symbol = Symbol::none();
4435
1
        assert_eq!(symbol.as_str(), "NONE");
4436
1
    }
4437
4438
    // =============================================================================
4439
    // TimeInForce Tests
4440
    // =============================================================================
4441
4442
    #[test]
4443
1
    fn test_time_in_force_display() {
4444
1
        assert_eq!(format!("{}", TimeInForce::Day), "DAY");
4445
1
        assert_eq!(format!("{}", TimeInForce::GoodTillCancel), "GTC");
4446
1
        assert_eq!(format!("{}", TimeInForce::ImmediateOrCancel), "IOC");
4447
1
        assert_eq!(format!("{}", TimeInForce::FillOrKill), "FOK");
4448
1
    }
4449
4450
    #[test]
4451
1
    fn test_time_in_force_default() {
4452
1
        assert_eq!(TimeInForce::default(), TimeInForce::Day);
4453
1
    }
4454
4455
    // =============================================================================
4456
    // OrderType Tests
4457
    // =============================================================================
4458
4459
    #[test]
4460
1
    fn test_order_type_display() {
4461
1
        assert_eq!(format!("{}", OrderType::Market), "MARKET");
4462
1
        assert_eq!(format!("{}", OrderType::Limit), "LIMIT");
4463
1
        assert_eq!(format!("{}", OrderType::Stop), "STOP");
4464
1
        assert_eq!(format!("{}", OrderType::StopLimit), "STOP_LIMIT");
4465
1
    }
4466
4467
    #[test]
4468
1
    fn test_order_type_try_from_i32_valid() {
4469
1
        assert_eq!(OrderType::try_from(0).unwrap(), OrderType::Market);
4470
1
        assert_eq!(OrderType::try_from(1).unwrap(), OrderType::Limit);
4471
1
        assert_eq!(OrderType::try_from(2).unwrap(), OrderType::Stop);
4472
1
    }
4473
4474
    #[test]
4475
1
    fn test_order_type_try_from_i32_invalid() {
4476
1
        let result = OrderType::try_from(99);
4477
1
        assert!(result.is_err());
4478
1
    }
4479
4480
    #[test]
4481
1
    fn test_order_type_default() {
4482
1
        assert_eq!(OrderType::default(), OrderType::Market);
4483
1
    }
4484
4485
    // =============================================================================
4486
    // OrderStatus Tests
4487
    // =============================================================================
4488
4489
    #[test]
4490
1
    fn test_order_status_display() {
4491
1
        assert_eq!(format!("{}", OrderStatus::Created), "CREATED");
4492
1
        assert_eq!(format!("{}", OrderStatus::Filled), "FILLED");
4493
1
        assert_eq!(format!("{}", OrderStatus::Cancelled), "CANCELLED");
4494
1
    }
4495
4496
    #[test]
4497
1
    fn test_order_status_try_from_i32_valid() {
4498
1
        assert_eq!(OrderStatus::try_from(0).unwrap(), OrderStatus::Created);
4499
1
        assert_eq!(OrderStatus::try_from(3).unwrap(), OrderStatus::Filled);
4500
1
        assert_eq!(OrderStatus::try_from(5).unwrap(), OrderStatus::Cancelled);
4501
1
    }
4502
4503
    #[test]
4504
1
    fn test_order_status_try_from_i32_invalid() {
4505
1
        let result = OrderStatus::try_from(99);
4506
1
        assert!(result.is_err());
4507
1
    }
4508
4509
    // =============================================================================
4510
    // OrderSide Tests
4511
    // =============================================================================
4512
4513
    #[test]
4514
1
    fn test_order_side_display() {
4515
1
        assert_eq!(format!("{}", OrderSide::Buy), "BUY");
4516
1
        assert_eq!(format!("{}", OrderSide::Sell), "SELL");
4517
1
    }
4518
4519
    #[test]
4520
1
    fn test_order_side_try_from_i32_valid() {
4521
1
        assert_eq!(OrderSide::try_from(0).unwrap(), OrderSide::Buy);
4522
1
        assert_eq!(OrderSide::try_from(1).unwrap(), OrderSide::Sell);
4523
1
    }
4524
4525
    #[test]
4526
1
    fn test_order_side_try_from_i32_invalid() {
4527
1
        let result = OrderSide::try_from(99);
4528
1
        assert!(result.is_err());
4529
1
    }
4530
4531
    #[test]
4532
1
    fn test_order_side_default() {
4533
1
        assert_eq!(OrderSide::default(), OrderSide::Buy);
4534
1
    }
4535
4536
    // =============================================================================
4537
    // Currency Tests
4538
    // =============================================================================
4539
4540
    #[test]
4541
1
    fn test_currency_display() {
4542
1
        assert_eq!(format!("{}", Currency::USD), "USD");
4543
1
        assert_eq!(format!("{}", Currency::EUR), "EUR");
4544
1
        assert_eq!(format!("{}", Currency::BTC), "BTC");
4545
1
    }
4546
4547
    #[test]
4548
1
    fn test_currency_default() {
4549
1
        assert_eq!(Currency::default(), Currency::USD);
4550
1
    }
4551
4552
    // =============================================================================
4553
    // Error Type Tests
4554
    // =============================================================================
4555
4556
    #[test]
4557
1
    fn test_common_type_error_invalid_price() {
4558
1
        let error = CommonTypeError::InvalidPrice {
4559
1
            value: "abc".to_string(),
4560
1
            reason: "not a number".to_string(),
4561
1
        };
4562
1
        let display = format!("{}", error);
4563
1
        assert!(display.contains("abc"));
4564
1
    }
4565
4566
    #[test]
4567
1
    fn test_common_type_error_invalid_quantity() {
4568
1
        let error = CommonTypeError::InvalidQuantity {
4569
1
            value: "xyz".to_string(),
4570
1
            reason: "not a number".to_string(),
4571
1
        };
4572
1
        let display = format!("{}", error);
4573
1
        assert!(display.contains("xyz"));
4574
1
    }
4575
4576
    #[test]
4577
1
    fn test_common_type_error_validation() {
4578
1
        let error = CommonTypeError::ValidationError {
4579
1
            field: "symbol".to_string(),
4580
1
            reason: "cannot be empty".to_string(),
4581
1
        };
4582
1
        let display = format!("{}", error);
4583
1
        assert!(display.contains("symbol"));
4584
1
    }
4585
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html deleted file mode 100644 index 569699379..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs
Line
Count
Source
1
//! Comprehensive Asset Classification Configuration System
2
//!
3
//! This module provides production-ready asset classification capabilities with:
4
//! - Sophisticated asset class hierarchies
5
//! - Dynamic trading parameter configuration
6
//! - Pattern-based symbol matching with regex support
7
//! - Database-backed configuration with hot-reload
8
//! - Volatility profiling and risk management integration
9
10
use chrono::{DateTime, Datelike, NaiveTime, Utc};
11
use log;
12
use regex::Regex;
13
use rust_decimal::{prelude::FromPrimitive, Decimal};
14
use serde::{Deserialize, Serialize};
15
use std::collections::HashMap;
16
use uuid::Uuid;
17
18
/// Comprehensive asset classification enum with detailed sub-categories
19
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20
pub enum AssetClass {
21
    /// Equity instruments with sector-specific characteristics
22
    Equity {
23
        sector: EquitySector,
24
        market_cap: MarketCapTier,
25
        region: GeographicRegion,
26
    },
27
    /// Futures contracts with underlying asset classification
28
    Future {
29
        underlying: FutureType,
30
        expiry_type: ExpiryType,
31
        exchange: String,
32
    },
33
    /// Foreign exchange pairs with specific characteristics
34
    Forex {
35
        base: String,
36
        quote: String,
37
        pair_type: ForexPairType,
38
    },
39
    /// Cryptocurrency assets with network and type classification
40
    Crypto {
41
        network: String,
42
        crypto_type: CryptoType,
43
        market_cap_rank: Option<u32>,
44
    },
45
    /// Commodity instruments with category classification
46
    Commodity {
47
        category: CommodityType,
48
        storage_type: StorageType,
49
    },
50
    /// Fixed income securities
51
    FixedIncome {
52
        instrument_type: FixedIncomeType,
53
        credit_rating: CreditRating,
54
        maturity: MaturityBucket,
55
    },
56
    /// Derivatives and structured products
57
    Derivative {
58
        underlying_class: Box<AssetClass>,
59
        derivative_type: DerivativeType,
60
    },
61
    /// Unknown or unclassified assets (conservative defaults)
62
    Unknown,
63
}
64
65
/// Equity sector classifications aligned with industry standards
66
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
67
pub enum EquitySector {
68
    Technology,
69
    Healthcare,
70
    Financial,
71
    ConsumerDiscretionary,
72
    ConsumerStaples,
73
    Industrial,
74
    Energy,
75
    Materials,
76
    Utilities,
77
    RealEstate,
78
    CommunicationServices,
79
}
80
81
/// Market capitalization tiers for equity classification
82
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
83
pub enum MarketCapTier {
84
    LargeCap, // > $10B
85
    MidCap,   // $2B - $10B
86
    SmallCap, // $300M - $2B
87
    MicroCap, // < $300M
88
}
89
90
/// Geographic regions for asset classification
91
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
92
pub enum GeographicRegion {
93
    NorthAmerica,
94
    Europe,
95
    Asia,
96
    EmergingMarkets,
97
    Global,
98
}
99
100
/// Future contract underlying asset types
101
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
102
pub enum FutureType {
103
    Equity,
104
    Currency,
105
    Commodity,
106
    Interest,
107
    Volatility,
108
}
109
110
/// Futures expiry categorization
111
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
112
pub enum ExpiryType {
113
    Weekly,
114
    Monthly,
115
    Quarterly,
116
    Annual,
117
}
118
119
/// Forex pair type classification
120
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
121
pub enum ForexPairType {
122
    Major,   // EUR/USD, GBP/USD, USD/JPY, etc.
123
    Minor,   // Cross-currency pairs without USD
124
    Exotic,  // Emerging market currencies
125
    JPYPair, // Special handling for JPY pairs
126
}
127
128
/// Cryptocurrency type classification
129
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
130
pub enum CryptoType {
131
    Bitcoin,
132
    Ethereum,
133
    Stablecoin,
134
    AltcoinMajor, // Top 20 market cap
135
    AltcoinMinor, // Beyond top 20
136
    DeFi,
137
    GameFi,
138
    Meme,
139
}
140
141
/// Commodity categories
142
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
143
pub enum CommodityType {
144
    PreciousMetals,
145
    Energy,
146
    Agricultural,
147
    IndustrialMetals,
148
    Livestock,
149
}
150
151
/// Storage characteristics for commodities
152
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
153
pub enum StorageType {
154
    Physical,
155
    Financial,
156
}
157
158
/// Fixed income instrument types
159
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
160
pub enum FixedIncomeType {
161
    Government,
162
    Corporate,
163
    Municipal,
164
    InflationProtected,
165
}
166
167
/// Credit rating classifications
168
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
169
pub enum CreditRating {
170
    AAA,
171
    AA,
172
    A,
173
    BBB,
174
    BB,
175
    B,
176
    CCC,
177
    Unrated,
178
}
179
180
/// Maturity buckets for fixed income
181
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
182
pub enum MaturityBucket {
183
    ShortTerm,  // < 2 years
184
    MediumTerm, // 2-10 years
185
    LongTerm,   // > 10 years
186
}
187
188
/// Derivative instrument types
189
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
190
pub enum DerivativeType {
191
    Option,
192
    Swap,
193
    Forward,
194
    Structured,
195
}
196
197
/// Comprehensive volatility profile with regime-aware parameters
198
#[derive(Debug, Clone, Serialize, Deserialize)]
199
pub struct VolatilityProfile {
200
    /// Base annual volatility (standard market conditions)
201
    pub base_annual_volatility: f64,
202
    /// Stress volatility multiplier for high-stress periods
203
    pub stress_volatility_multiplier: f64,
204
    /// Intraday volatility pattern (hourly multipliers)
205
    pub intraday_pattern: Vec<f64>,
206
    /// Volatility clustering parameter (GARCH-like)
207
    pub volatility_persistence: f64,
208
    /// Jump risk probability and magnitude
209
    pub jump_risk: JumpRiskProfile,
210
}
211
212
/// Jump risk characteristics
213
#[derive(Debug, Clone, Serialize, Deserialize)]
214
pub struct JumpRiskProfile {
215
    /// Probability of large price jumps per day
216
    pub jump_probability: f64,
217
    /// Average magnitude of jumps (as fraction of price)
218
    pub jump_magnitude: f64,
219
    /// Maximum expected jump size
220
    pub max_jump_size: f64,
221
}
222
223
/// Dynamic trading parameters that adapt to market conditions
224
#[derive(Debug, Clone, Serialize, Deserialize)]
225
pub struct TradingParameters {
226
    /// Position sizing constraints
227
    pub position_limits: PositionLimits,
228
    /// Risk management thresholds
229
    pub risk_thresholds: RiskThresholds,
230
    /// Execution parameters
231
    pub execution_config: ExecutionConfig,
232
    /// Market making parameters (if applicable)
233
    pub market_making: Option<MarketMakingConfig>,
234
}
235
236
/// Position sizing and exposure limits
237
#[derive(Debug, Clone, Serialize, Deserialize)]
238
pub struct PositionLimits {
239
    /// Maximum position size as fraction of portfolio NAV
240
    pub max_position_fraction: f64,
241
    /// Maximum leverage allowed for this asset
242
    pub max_leverage: f64,
243
    /// Concentration limit (max % of total positions in this asset class)
244
    pub concentration_limit: f64,
245
    /// Minimum position size (to avoid micro-positions)
246
    pub min_position_size: Decimal,
247
}
248
249
/// Risk management thresholds and limits
250
#[derive(Debug, Clone, Serialize, Deserialize)]
251
pub struct RiskThresholds {
252
    /// VaR limit as fraction of portfolio
253
    pub var_limit: f64,
254
    /// Daily loss limit
255
    pub daily_loss_limit: f64,
256
    /// Stop-loss threshold
257
    pub stop_loss_threshold: f64,
258
    /// Volatility circuit breaker threshold
259
    pub volatility_circuit_breaker: f64,
260
    /// Maximum drawdown before position reduction
261
    pub max_drawdown_threshold: f64,
262
}
263
264
/// Execution configuration parameters
265
#[derive(Debug, Clone, Serialize, Deserialize)]
266
pub struct ExecutionConfig {
267
    /// Preferred order types for this asset
268
    pub preferred_order_types: Vec<OrderType>,
269
    /// Tick size for price increments
270
    pub tick_size: Decimal,
271
    /// Minimum order size
272
    pub min_order_size: Decimal,
273
    /// Maximum order size before breaking up
274
    pub max_order_size: Decimal,
275
    /// Execution time constraints
276
    pub time_in_force_default: TimeInForce,
277
    /// Slippage tolerance
278
    pub slippage_tolerance: f64,
279
}
280
281
/// Market making specific configuration
282
#[derive(Debug, Clone, Serialize, Deserialize)]
283
pub struct MarketMakingConfig {
284
    /// Bid-ask spread targets
285
    pub target_spread: f64,
286
    /// Inventory limits
287
    pub max_inventory: Decimal,
288
    /// Quote size
289
    pub quote_size: Decimal,
290
    /// Refresh frequency
291
    pub refresh_frequency: std::time::Duration,
292
}
293
294
/// Order type enumeration
295
#[derive(Debug, Clone, Serialize, Deserialize)]
296
pub enum OrderType {
297
    Market,
298
    Limit,
299
    Stop,
300
    StopLimit,
301
    Hidden,
302
    Iceberg,
303
}
304
305
/// Time in force options
306
#[derive(Debug, Clone, Serialize, Deserialize)]
307
pub enum TimeInForce {
308
    Day,
309
    GoodTillCancel,
310
    ImmediateOrCancel,
311
    FillOrKill,
312
    GTD, // Good Till Date
313
}
314
315
/// Symbol pattern matching configuration with compiled regex
316
#[derive(Debug, Clone, Serialize, Deserialize)]
317
pub struct AssetConfig {
318
    /// UUID for database storage
319
    pub id: Uuid,
320
    /// Human-readable name for this configuration
321
    pub name: String,
322
    /// Regex pattern for symbol matching
323
    pub symbol_pattern: String,
324
    /// Compiled regex (not serialized, rebuilt on load)
325
    #[serde(skip)]
326
    pub compiled_pattern: Option<Regex>,
327
    /// Asset class classification
328
    pub asset_class: AssetClass,
329
    /// Volatility profile
330
    pub volatility_profile: VolatilityProfile,
331
    /// Trading parameters
332
    pub trading_parameters: TradingParameters,
333
    /// Priority for pattern matching (higher = checked first)
334
    pub priority: u32,
335
    /// Whether this configuration is active
336
    pub is_active: bool,
337
    /// Creation timestamp
338
    pub created_at: DateTime<Utc>,
339
    /// Last update timestamp
340
    pub updated_at: DateTime<Utc>,
341
    /// Trading hours (if applicable)
342
    pub trading_hours: Option<TradingHours>,
343
    /// Settlement details
344
    pub settlement_config: SettlementConfig,
345
}
346
347
/// Trading hours configuration
348
#[derive(Debug, Clone, Serialize, Deserialize)]
349
pub struct TradingHours {
350
    /// Regular trading session start
351
    pub market_open: NaiveTime,
352
    /// Regular trading session end
353
    pub market_close: NaiveTime,
354
    /// Pre-market session (if available)
355
    pub pre_market: Option<(NaiveTime, NaiveTime)>,
356
    /// After-hours session (if available)
357
    pub after_hours: Option<(NaiveTime, NaiveTime)>,
358
    /// Timezone for these hours
359
    pub timezone: String,
360
    /// Days of week when trading is active (0=Sunday, 6=Saturday)
361
    pub trading_days: Vec<u8>,
362
}
363
364
/// Settlement configuration
365
#[derive(Debug, Clone, Serialize, Deserialize)]
366
pub struct SettlementConfig {
367
    /// Settlement period (T+n days)
368
    pub settlement_days: u32,
369
    /// Settlement currency
370
    pub settlement_currency: String,
371
    /// Whether physical delivery is possible
372
    pub physical_settlement: bool,
373
}
374
375
/// Asset classification manager with caching and hot-reload capabilities
376
pub struct AssetClassificationManager {
377
    /// Asset configurations indexed by priority
378
    configs: Vec<AssetConfig>,
379
    /// Explicit symbol mappings for fast lookup
380
    symbol_cache: HashMap<String, AssetClass>,
381
    /// Last configuration reload timestamp
382
    last_reload: DateTime<Utc>,
383
    /// Configuration reload interval
384
    reload_interval: std::time::Duration,
385
}
386
387
impl AssetClassificationManager {
388
    /// Create a new asset classification manager
389
17
    pub fn new() -> Self {
390
17
        Self {
391
17
            configs: Vec::new(),
392
17
            symbol_cache: HashMap::new(),
393
17
            last_reload: Utc::now(),
394
17
            reload_interval: std::time::Duration::from_secs(300), // 5 minutes
395
17
        }
396
17
    }
397
398
    /// Load configurations from database
399
14
    pub async fn load_configurations(
400
14
        &mut self,
401
14
        configs: Vec<AssetConfig>,
402
14
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
403
14
        self.configs = configs;
404
        // Sort by priority (highest first)
405
23
        
self.configs14
.
sort_by14
(|a, b| b.priority.cmp(&a.priority));
406
407
        // Compile regex patterns
408
51
        for 
config37
in &mut self.configs {
409
37
            match Regex::new(&config.symbol_pattern) {
410
36
                Ok(regex) => config.compiled_pattern = Some(regex),
411
1
                Err(e) => {
412
1
                    log::warn!(
413
0
                        "Failed to compile regex pattern '{}': {}",
414
                        config.symbol_pattern,
415
                        e
416
                    );
417
1
                    config.is_active = false;
418
                }
419
            }
420
        }
421
422
14
        self.last_reload = Utc::now();
423
14
        log::info!(
424
0
            "Loaded {} asset classification configurations",
425
0
            self.configs.len()
426
        );
427
14
        Ok(())
428
14
    }
429
430
    /// Classify a symbol using the configured rules
431
15
    pub fn classify_symbol(&self, symbol: &str) -> AssetClass {
432
15
        let symbol_upper = symbol.to_uppercase();
433
434
        // Check cache first
435
15
        if let Some(
asset_class0
) = self.symbol_cache.get(&symbol_upper) {
436
0
            return asset_class.clone();
437
15
        }
438
439
        // Check pattern rules in priority order
440
31
        for 
config28
in &self.configs {
441
28
            if !config.is_active {
442
0
                continue;
443
28
            }
444
445
28
            if let Some(ref regex) = config.compiled_pattern {
446
28
                if regex.is_match(&symbol_upper) {
447
12
                    return config.asset_class.clone();
448
16
                }
449
0
            }
450
        }
451
452
3
        AssetClass::Unknown
453
15
    }
454
455
    /// Get complete asset configuration for a symbol
456
27
    pub fn get_asset_config(&self, symbol: &str) -> Option<&AssetConfig> {
457
27
        let symbol_upper = symbol.to_uppercase();
458
459
43
        for 
config41
in &self.configs {
460
41
            if !config.is_active {
461
0
                continue;
462
41
            }
463
464
41
            if let Some(ref regex) = config.compiled_pattern {
465
41
                if regex.is_match(&symbol_upper) {
466
25
                    return Some(config);
467
16
                }
468
0
            }
469
        }
470
471
2
        None
472
27
    }
473
474
    /// Get volatility profile for a symbol
475
11
    pub fn get_volatility_profile(&self, symbol: &str) -> Option<&VolatilityProfile> {
476
11
        self.get_asset_config(symbol)
477
11
            .map(|config| &config.volatility_profile)
478
11
    }
479
480
    /// Get trading parameters for a symbol
481
9
    pub fn get_trading_parameters(&self, symbol: &str) -> Option<&TradingParameters> {
482
9
        self.get_asset_config(symbol)
483
9
            .map(|config| &config.trading_parameters)
484
9
    }
485
486
    /// Get daily volatility estimate for a symbol
487
9
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
488
9
        if let Some(
profile8
) = self.get_volatility_profile(symbol) {
489
8
            profile.base_annual_volatility / 252.0_f64.sqrt()
490
        } else {
491
1
            0.5 / 252.0_f64.sqrt() // Default high volatility
492
        }
493
9
    }
494
495
    /// Get position sizing recommendation
496
5
    pub fn get_position_size_recommendation(
497
5
        &self,
498
5
        symbol: &str,
499
5
        portfolio_nav: Decimal,
500
5
    ) -> Option<Decimal> {
501
5
        if let Some(config) = self.get_asset_config(symbol) {
502
5
            let max_fraction = config
503
5
                .trading_parameters
504
5
                .position_limits
505
5
                .max_position_fraction;
506
5
            if let Some(decimal_fraction) = Decimal::from_f64(max_fraction) {
507
5
                Some(portfolio_nav * decimal_fraction)
508
            } else {
509
0
                Some(Decimal::ZERO)
510
            }
511
        } else {
512
0
            None
513
        }
514
5
    }
515
516
    /// Check if symbol is within trading hours
517
2
    pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime<Utc>) -> bool {
518
2
        if let Some(config) = self.get_asset_config(symbol) {
519
2
            if let Some(
ref trading_hours1
) = config.trading_hours {
520
                // Simplified check - in production would need proper timezone handling
521
1
                let weekday = timestamp.weekday().num_days_from_sunday() as u8;
522
1
                trading_hours.trading_days.contains(&weekday)
523
            } else {
524
1
                true // No trading hours restriction
525
            }
526
        } else {
527
0
            true // Default to always active for unknown symbols
528
        }
529
2
    }
530
531
    /// Add explicit symbol mapping to cache
532
0
    pub fn cache_symbol_mapping(&mut self, symbol: String, asset_class: AssetClass) {
533
0
        self.symbol_cache.insert(symbol.to_uppercase(), asset_class);
534
0
    }
535
536
    /// Clear symbol cache
537
0
    pub fn clear_cache(&mut self) {
538
0
        self.symbol_cache.clear();
539
0
    }
540
541
    /// Check if configuration needs reload
542
0
    pub fn needs_reload(&self) -> bool {
543
0
        Utc::now().signed_duration_since(self.last_reload)
544
0
            > chrono::Duration::from_std(self.reload_interval).unwrap_or_default()
545
0
    }
546
547
    /// Get all active configurations
548
2
    pub fn get_active_configurations(&self) -> Vec<&AssetConfig> {
549
2
        self.configs
550
2
            .iter()
551
2
            .filter(|config| config.is_active)
552
2
            .collect()
553
2
    }
554
555
    /// Get configurations by asset class
556
0
    pub fn get_configurations_by_class(&self, asset_class: &AssetClass) -> Vec<&AssetConfig> {
557
0
        self.configs
558
0
            .iter()
559
0
            .filter(|config| config.is_active && &config.asset_class == asset_class)
560
0
            .collect()
561
0
    }
562
}
563
564
impl Default for AssetClassificationManager {
565
0
    fn default() -> Self {
566
0
        Self::new()
567
0
    }
568
}
569
570
/// Create default asset configurations for common instruments
571
11
pub fn create_default_configurations() -> Vec<AssetConfig> {
572
11
    let mut configs = Vec::new();
573
11
    let now = Utc::now();
574
575
    // Blue chip US equities
576
11
    configs.push(AssetConfig {
577
11
        id: Uuid::new_v4(),
578
11
        name: "Blue Chip US Equities".to_string(),
579
11
        symbol_pattern: "^(AAPL|MSFT|GOOGL|AMZN|META|TSLA|NVDA|JPM|JNJ|V|PG|UNH|HD|BAC|DIS|MA|NFLX|CRM|ADBE|PYPL|INTC|CMCSA|PFE|T|VZ|MRK|WMT|KO|NKE|CVX|XOM)$".to_string(),
580
11
        compiled_pattern: None,
581
11
        asset_class: AssetClass::Equity {
582
11
            sector: EquitySector::Technology,
583
11
            market_cap: MarketCapTier::LargeCap,
584
11
            region: GeographicRegion::NorthAmerica,
585
11
        },
586
11
        volatility_profile: VolatilityProfile {
587
11
            base_annual_volatility: 0.25,
588
11
            stress_volatility_multiplier: 2.0,
589
11
            intraday_pattern: vec![1.0; 24], // Flat pattern for simplicity
590
11
            volatility_persistence: 0.85,
591
11
            jump_risk: JumpRiskProfile {
592
11
                jump_probability: 0.02,
593
11
                jump_magnitude: 0.05,
594
11
                max_jump_size: 0.15,
595
11
            },
596
11
        },
597
11
        trading_parameters: TradingParameters {
598
11
            position_limits: PositionLimits {
599
11
                max_position_fraction: 0.20,
600
11
                max_leverage: 2.0,
601
11
                concentration_limit: 0.30,
602
11
                min_position_size: Decimal::from(100),
603
11
            },
604
11
            risk_thresholds: RiskThresholds {
605
11
                var_limit: 0.05,
606
11
                daily_loss_limit: 0.03,
607
11
                stop_loss_threshold: 0.10,
608
11
                volatility_circuit_breaker: 0.05,
609
11
                max_drawdown_threshold: 0.15,
610
11
            },
611
11
            execution_config: ExecutionConfig {
612
11
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
613
11
                tick_size: "0.01".parse().unwrap(),
614
11
                min_order_size: Decimal::from(1),
615
11
                max_order_size: Decimal::from(10000),
616
11
                time_in_force_default: TimeInForce::Day,
617
11
                slippage_tolerance: 0.001,
618
11
            },
619
11
            market_making: None,
620
11
        },
621
11
        priority: 100,
622
11
        is_active: true,
623
11
        created_at: now,
624
11
        updated_at: now,
625
11
        trading_hours: Some(TradingHours {
626
11
            market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
627
11
            market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
628
11
            pre_market: Some((NaiveTime::from_hms_opt(4, 0, 0).unwrap(), NaiveTime::from_hms_opt(9, 30, 0).unwrap())),
629
11
            after_hours: Some((NaiveTime::from_hms_opt(16, 0, 0).unwrap(), NaiveTime::from_hms_opt(20, 0, 0).unwrap())),
630
11
            timezone: "America/New_York".to_string(),
631
11
            trading_days: vec![1, 2, 3, 4, 5], // Monday-Friday
632
11
        }),
633
11
        settlement_config: SettlementConfig {
634
11
            settlement_days: 2,
635
11
            settlement_currency: "USD".to_string(),
636
11
            physical_settlement: false,
637
11
        },
638
11
    });
639
640
    // Major cryptocurrency pairs
641
11
    configs.push(AssetConfig {
642
11
        id: Uuid::new_v4(),
643
11
        name: "Major Cryptocurrencies".to_string(),
644
11
        symbol_pattern: "^(BTC|ETH|BTCUSD|ETHUSD|BTCUSDT|ETHUSDT).*$".to_string(),
645
11
        compiled_pattern: None,
646
11
        asset_class: AssetClass::Crypto {
647
11
            network: "Bitcoin".to_string(),
648
11
            crypto_type: CryptoType::Bitcoin,
649
11
            market_cap_rank: Some(1),
650
11
        },
651
11
        volatility_profile: VolatilityProfile {
652
11
            base_annual_volatility: 0.80,
653
11
            stress_volatility_multiplier: 3.0,
654
11
            intraday_pattern: vec![1.0; 24],
655
11
            volatility_persistence: 0.90,
656
11
            jump_risk: JumpRiskProfile {
657
11
                jump_probability: 0.05,
658
11
                jump_magnitude: 0.10,
659
11
                max_jump_size: 0.30,
660
11
            },
661
11
        },
662
11
        trading_parameters: TradingParameters {
663
11
            position_limits: PositionLimits {
664
11
                max_position_fraction: 0.10,
665
11
                max_leverage: 1.5,
666
11
                concentration_limit: 0.15,
667
11
                min_position_size: "0.001".parse().unwrap(),
668
11
            },
669
11
            risk_thresholds: RiskThresholds {
670
11
                var_limit: 0.10,
671
11
                daily_loss_limit: 0.05,
672
11
                stop_loss_threshold: 0.15,
673
11
                volatility_circuit_breaker: 0.15,
674
11
                max_drawdown_threshold: 0.25,
675
11
            },
676
11
            execution_config: ExecutionConfig {
677
11
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
678
11
                tick_size: "0.01".parse().unwrap(),
679
11
                min_order_size: "0.001".parse().unwrap(),
680
11
                max_order_size: Decimal::from(100),
681
11
                time_in_force_default: TimeInForce::GoodTillCancel,
682
11
                slippage_tolerance: 0.005,
683
11
            },
684
11
            market_making: None,
685
11
        },
686
11
        priority: 90,
687
11
        is_active: true,
688
11
        created_at: now,
689
11
        updated_at: now,
690
11
        trading_hours: None, // 24/7 trading
691
11
        settlement_config: SettlementConfig {
692
11
            settlement_days: 0,
693
11
            settlement_currency: "USD".to_string(),
694
11
            physical_settlement: true,
695
11
        },
696
11
    });
697
698
    // Major forex pairs
699
11
    configs.push(AssetConfig {
700
11
        id: Uuid::new_v4(),
701
11
        name: "Major Forex Pairs".to_string(),
702
11
        symbol_pattern: "^(EUR|GBP|USD|JPY|AUD|CAD|CHF|NZD)(USD|EUR|GBP|JPY)$".to_string(),
703
11
        compiled_pattern: None,
704
11
        asset_class: AssetClass::Forex {
705
11
            base: "EUR".to_string(),
706
11
            quote: "USD".to_string(),
707
11
            pair_type: ForexPairType::Major,
708
11
        },
709
11
        volatility_profile: VolatilityProfile {
710
11
            base_annual_volatility: 0.12,
711
11
            stress_volatility_multiplier: 2.5,
712
11
            intraday_pattern: vec![1.0; 24],
713
11
            volatility_persistence: 0.80,
714
11
            jump_risk: JumpRiskProfile {
715
11
                jump_probability: 0.01,
716
11
                jump_magnitude: 0.02,
717
11
                max_jump_size: 0.08,
718
11
            },
719
11
        },
720
11
        trading_parameters: TradingParameters {
721
11
            position_limits: PositionLimits {
722
11
                max_position_fraction: 0.30,
723
11
                max_leverage: 10.0,
724
11
                concentration_limit: 0.40,
725
11
                min_position_size: Decimal::from(1000),
726
11
            },
727
11
            risk_thresholds: RiskThresholds {
728
11
                var_limit: 0.03,
729
11
                daily_loss_limit: 0.02,
730
11
                stop_loss_threshold: 0.05,
731
11
                volatility_circuit_breaker: 0.03,
732
11
                max_drawdown_threshold: 0.10,
733
11
            },
734
11
            execution_config: ExecutionConfig {
735
11
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
736
11
                tick_size: "0.00001".parse().unwrap(),
737
11
                min_order_size: Decimal::from(1000),
738
11
                max_order_size: Decimal::from(10000000),
739
11
                time_in_force_default: TimeInForce::GoodTillCancel,
740
11
                slippage_tolerance: 0.0002,
741
11
            },
742
11
            market_making: Some(MarketMakingConfig {
743
11
                target_spread: 0.0001,
744
11
                max_inventory: Decimal::from(100000),
745
11
                quote_size: Decimal::from(10000),
746
11
                refresh_frequency: std::time::Duration::from_millis(100),
747
11
            }),
748
11
        },
749
11
        priority: 80,
750
11
        is_active: true,
751
11
        created_at: now,
752
11
        updated_at: now,
753
11
        trading_hours: None, // 24/5 trading
754
11
        settlement_config: SettlementConfig {
755
11
            settlement_days: 2,
756
11
            settlement_currency: "USD".to_string(),
757
11
            physical_settlement: false,
758
11
        },
759
11
    });
760
761
11
    configs
762
11
}
763
764
#[cfg(test)]
765
mod tests {
766
    use super::*;
767
768
    #[tokio::test]
769
1
    async fn test_symbol_classification() {
770
1
        let mut manager = AssetClassificationManager::new();
771
1
        let configs = create_default_configurations();
772
1
        manager.load_configurations(configs).await.unwrap();
773
774
        // Test blue chip classification
775
1
        match manager.classify_symbol("AAPL") {
776
1
            AssetClass::Equity {
777
1
                sector: EquitySector::Technology,
778
1
                ..
779
1
            } => (),
780
1
            _ => 
panic!0
(
"AAPL should be classified as Technology equity"0
),
781
1
        }
782
1
783
1
        // Test crypto classification
784
1
        match manager.classify_symbol("BTCUSD") {
785
1
            AssetClass::Crypto {
786
1
                crypto_type: CryptoType::Bitcoin,
787
1
                ..
788
1
            } => (),
789
1
            _ => 
panic!0
(
"BTCUSD should be classified as Bitcoin crypto"0
),
790
1
        }
791
1
792
1
        // Test unknown symbol
793
1
        assert_eq!(manager.classify_symbol("UNKNOWN"), AssetClass::Unknown);
794
1
    }
795
796
    #[tokio::test]
797
1
    async fn test_volatility_profile() {
798
1
        let mut manager = AssetClassificationManager::new();
799
1
        let configs = create_default_configurations();
800
1
        manager.load_configurations(configs).await.unwrap();
801
802
1
        let profile = manager.get_volatility_profile("AAPL").unwrap();
803
1
        assert_eq!(profile.base_annual_volatility, 0.25);
804
805
1
        let daily_vol = manager.get_daily_volatility("AAPL");
806
1
        assert!((daily_vol - (0.25 / 252.0_f64.sqrt())).abs() < 1e-10);
807
1
    }
808
809
    #[tokio::test]
810
1
    async fn test_trading_parameters() {
811
1
        let mut manager = AssetClassificationManager::new();
812
1
        let configs = create_default_configurations();
813
1
        manager.load_configurations(configs).await.unwrap();
814
815
1
        let params = manager.get_trading_parameters("AAPL").unwrap();
816
1
        assert_eq!(params.position_limits.max_position_fraction, 0.20);
817
1
        assert_eq!(params.position_limits.max_leverage, 2.0);
818
1
    }
819
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/compliance_config.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/compliance_config.rs.html deleted file mode 100644 index 27468da88..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/compliance_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/compliance_config.rs
Line
Count
Source
1
//! Compliance rule configuration and hot-reload support
2
//!
3
//! Provides database-backed compliance rule loading with PostgreSQL NOTIFY/LISTEN
4
//! for hot-reload capabilities. Integrates with the ComplianceValidator in the
5
//! risk crate to enable dynamic rule configuration without service restarts.
6
7
use serde::{Deserialize, Serialize};
8
9
#[cfg(feature = "postgres")]
10
use crate::error::ConfigResult;
11
#[cfg(feature = "postgres")]
12
use std::collections::HashMap;
13
#[cfg(feature = "postgres")]
14
use std::sync::Arc;
15
#[cfg(feature = "postgres")]
16
use std::time::Duration;
17
#[cfg(feature = "postgres")]
18
use tokio::sync::RwLock;
19
#[cfg(feature = "postgres")]
20
use tracing::{error, info};
21
22
#[cfg(feature = "postgres")]
23
use sqlx::postgres::{PgListener, PgPool};
24
25
/// Compliance rule loader with PostgreSQL integration and hot-reload support
26
///
27
/// Loads compliance rules from the PostgreSQL database and automatically
28
/// reloads them when changes are detected via PostgreSQL NOTIFY/LISTEN.
29
#[cfg(feature = "postgres")]
30
pub struct PostgresComplianceRuleLoader {
31
    /// Database connection pool
32
    pool: PgPool,
33
    /// PostgreSQL listener for rule change notifications
34
    listener: Arc<RwLock<Option<PgListener>>>,
35
    /// Cached compliance rules by rule_id
36
    rules_cache: Arc<RwLock<HashMap<String, ComplianceRuleConfig>>>,
37
    /// Cache timeout duration
38
    cache_timeout: Duration,
39
}
40
41
/// Compliance rule configuration structure
42
///
43
/// Represents a compliance rule loaded from the database.
44
/// This structure is designed to be compatible with both the database
45
/// schema and the ComplianceRule type in the risk crate.
46
#[derive(Debug, Clone, Serialize, Deserialize)]
47
#[cfg_attr(feature = "postgres", derive(sqlx::FromRow))]
48
pub struct ComplianceRuleConfig {
49
    /// Unique rule identifier
50
    pub rule_id: String,
51
    /// Human-readable rule name
52
    pub name: String,
53
    /// Detailed description
54
    pub description: String,
55
    /// Rule type (POSITION_LIMIT, MARKET_ABUSE, etc.)
56
    pub rule_type: String,
57
    /// Whether the rule is active
58
    pub active: bool,
59
    /// Rule version for audit trail
60
    pub version: i32,
61
    /// Severity level (Info, Low, Medium, High, Critical)
62
    pub severity: String,
63
    /// Priority for evaluation (0-100)
64
    pub priority: i32,
65
    /// Flexible rule parameters as JSON
66
    #[cfg_attr(feature = "postgres", sqlx(json))]
67
    pub parameters: serde_json::Value,
68
    /// Regulatory framework
69
    pub regulatory_framework: Option<String>,
70
    /// Regulatory reference
71
    pub regulatory_reference: Option<String>,
72
}
73
74
#[cfg(feature = "postgres")]
75
impl PostgresComplianceRuleLoader {
76
    /// Creates a new compliance rule loader with PostgreSQL integration
77
    ///
78
    /// # Arguments
79
    ///
80
    /// * `database_url` - PostgreSQL connection URL
81
    ///
82
    /// # Returns
83
    ///
84
    /// Result containing the initialized loader or an error
85
    pub async fn new(database_url: &str) -> ConfigResult<Self> {
86
        let pool = PgPool::connect(database_url).await?;
87
88
        Ok(Self {
89
            pool,
90
            listener: Arc::new(RwLock::new(None)),
91
            rules_cache: Arc::new(RwLock::new(HashMap::new())),
92
            cache_timeout: Duration::from_secs(300), // 5 minutes
93
        })
94
    }
95
96
    /// Creates a loader with an existing connection pool
97
    ///
98
    /// # Arguments
99
    ///
100
    /// * `pool` - Existing PostgreSQL connection pool
101
    ///
102
    /// # Returns
103
    ///
104
    /// Configured compliance rule loader
105
    pub fn with_pool(pool: PgPool) -> Self {
106
        Self {
107
            pool,
108
            listener: Arc::new(RwLock::new(None)),
109
            rules_cache: Arc::new(RwLock::new(HashMap::new())),
110
            cache_timeout: Duration::from_secs(300),
111
        }
112
    }
113
114
    /// Starts listening for rule change notifications
115
    ///
116
    /// Initiates PostgreSQL NOTIFY/LISTEN for hot-reload capabilities.
117
    /// When a rule is changed in the database, the cache will be automatically
118
    /// invalidated and reloaded.
119
    ///
120
    /// # Returns
121
    ///
122
    /// Result indicating success or error
123
    pub async fn start_listener(&self) -> ConfigResult<()> {
124
        let mut listener = PgListener::connect_with(&self.pool)
125
            .await?;
126
127
        listener
128
            .listen("compliance_rules_changed")
129
            .await?;
130
131
        *self.listener.write().await = Some(listener);
132
133
        info!("PostgreSQL NOTIFY/LISTEN started for compliance rule hot-reload");
134
135
        // Spawn background task to handle notifications
136
        let listener_clone = Arc::clone(&self.listener);
137
        let cache_clone = Arc::clone(&self.rules_cache);
138
        let pool_clone = self.pool.clone();
139
140
        tokio::spawn(async move {
141
            loop {
142
                let mut listener_guard = listener_clone.write().await;
143
                if let Some(listener) = listener_guard.as_mut() {
144
                    match listener.try_recv().await {
145
                        Ok(Some(notification)) => {
146
                            info!(
147
                                "Compliance rule change notification received: {}",
148
                                notification.payload()
149
                            );
150
151
                            // Parse notification payload to get rule_id
152
                            if let Ok(payload) = serde_json::from_str::<serde_json::Value>(
153
                                notification.payload(),
154
                            ) {
155
                                if let Some(rule_id) = payload.get("rule_id").and_then(|v| v.as_str()) {
156
                                    // Invalidate cache for this rule
157
                                    cache_clone.write().await.remove(rule_id);
158
                                    info!("Invalidated cache for compliance rule: {}", rule_id);
159
160
                                    // Optionally reload the rule immediately
161
                                    if let Err(e) = Self::reload_rule_static(&pool_clone, &cache_clone, rule_id).await {
162
                                        error!("Failed to reload compliance rule {}: {}", rule_id, e);
163
                                    }
164
                                }
165
                            }
166
                        }
167
                        Ok(None) => {
168
                            // No notification available, continue
169
                            tokio::time::sleep(Duration::from_millis(100)).await;
170
                        }
171
                        Err(e) => {
172
                            error!("Error receiving compliance rule notification: {}", e);
173
                            tokio::time::sleep(Duration::from_secs(1)).await;
174
                        }
175
                    }
176
                }
177
                drop(listener_guard);
178
                tokio::time::sleep(Duration::from_millis(100)).await;
179
            }
180
        });
181
182
        Ok(())
183
    }
184
185
    /// Static helper for reloading a single rule (used in background task)
186
    async fn reload_rule_static(
187
        pool: &PgPool,
188
        cache: &Arc<RwLock<HashMap<String, ComplianceRuleConfig>>>,
189
        rule_id: &str,
190
    ) -> ConfigResult<()> {
191
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
192
                           severity, priority, parameters, regulatory_framework, regulatory_reference
193
                    FROM compliance_rules
194
                    WHERE rule_id = $1 AND active = true";
195
196
        let row = sqlx::query_as::<_, ComplianceRuleConfig>(query)
197
            .bind(rule_id)
198
            .fetch_optional(pool)
199
            .await?;
200
201
        if let Some(rule) = row {
202
            cache.write().await.insert(rule_id.to_string(), rule);
203
            info!("Reloaded compliance rule: {}", rule_id);
204
        }
205
206
        Ok(())
207
    }
208
209
    /// Loads all active compliance rules from the database
210
    ///
211
    /// # Returns
212
    ///
213
    /// Vector of active compliance rules
214
    pub async fn load_all_active_rules(&self) -> ConfigResult<Vec<ComplianceRuleConfig>> {
215
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
216
                           severity, priority, parameters, regulatory_framework, regulatory_reference
217
                    FROM compliance_rules
218
                    WHERE active = true
219
                      AND effective_date <= NOW()
220
                      AND (expiry_date IS NULL OR expiry_date > NOW())
221
                    ORDER BY priority DESC, created_at ASC";
222
223
        let rules = sqlx::query_as::<_, ComplianceRuleConfig>(query)
224
            .fetch_all(&self.pool)
225
            .await?;
226
227
        // Update cache
228
        let mut cache = self.rules_cache.write().await;
229
        for rule in &rules {
230
            cache.insert(rule.rule_id.clone(), rule.clone());
231
        }
232
233
        info!("Loaded {} active compliance rules", rules.len());
234
235
        Ok(rules)
236
    }
237
238
    /// Loads rules filtered by type
239
    ///
240
    /// # Arguments
241
    ///
242
    /// * `rule_type` - Rule type to filter by (e.g., "POSITION_LIMIT")
243
    ///
244
    /// # Returns
245
    ///
246
    /// Vector of rules matching the specified type
247
    pub async fn load_rules_by_type(&self, rule_type: &str) -> ConfigResult<Vec<ComplianceRuleConfig>> {
248
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
249
                           severity, priority, parameters, regulatory_framework, regulatory_reference
250
                    FROM compliance_rules
251
                    WHERE active = true
252
                      AND rule_type::text = $1
253
                      AND effective_date <= NOW()
254
                      AND (expiry_date IS NULL OR expiry_date > NOW())
255
                    ORDER BY priority DESC";
256
257
        let rules = sqlx::query_as::<_, ComplianceRuleConfig>(query)
258
            .bind(rule_type)
259
            .fetch_all(&self.pool)
260
            .await?;
261
262
        Ok(rules)
263
    }
264
265
    /// Gets a specific rule by ID (with caching)
266
    ///
267
    /// # Arguments
268
    ///
269
    /// * `rule_id` - Unique rule identifier
270
    ///
271
    /// # Returns
272
    ///
273
    /// Optional compliance rule configuration
274
    pub async fn get_rule(&self, rule_id: &str) -> ConfigResult<Option<ComplianceRuleConfig>> {
275
        // Check cache first
276
        {
277
            let cache = self.rules_cache.read().await;
278
            if let Some(rule) = cache.get(rule_id) {
279
                return Ok(Some(rule.clone()));
280
            }
281
        }
282
283
        // Load from database if not in cache
284
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
285
                           severity, priority, parameters, regulatory_framework, regulatory_reference
286
                    FROM compliance_rules
287
                    WHERE rule_id = $1 AND active = true";
288
289
        let rule = sqlx::query_as::<_, ComplianceRuleConfig>(query)
290
            .bind(rule_id)
291
            .fetch_optional(&self.pool)
292
            .await?;
293
294
        // Update cache if found
295
        if let Some(ref rule_data) = rule {
296
            self.rules_cache.write().await.insert(rule_id.to_string(), rule_data.clone());
297
        }
298
299
        Ok(rule)
300
    }
301
302
    /// Records a compliance rule execution for audit trail
303
    ///
304
    /// # Arguments
305
    ///
306
    /// * `rule_id` - Rule that was executed
307
    /// * `result` - Execution result (PASS, WARN, FAIL, ERROR)
308
    /// * `violation_detected` - Whether a violation was detected
309
    /// * `order_id` - Optional order ID
310
    /// * `instrument_id` - Optional instrument ID
311
    ///
312
    /// # Returns
313
    ///
314
    /// Result indicating success or error
315
    pub async fn record_execution(
316
        &self,
317
        rule_id: &str,
318
        result: &str,
319
        violation_detected: bool,
320
        order_id: Option<&str>,
321
        instrument_id: Option<&str>,
322
    ) -> ConfigResult<()> {
323
        let query = "SELECT record_compliance_rule_execution($1, $2, $3, $4, $5, NULL, NULL, NULL)";
324
325
        sqlx::query(query)
326
            .bind(rule_id)
327
            .bind(result)
328
            .bind(violation_detected)
329
            .bind(order_id)
330
            .bind(instrument_id)
331
            .execute(&self.pool)
332
            .await?;
333
334
        Ok(())
335
    }
336
337
    /// Clears the rule cache (forces reload on next access)
338
    pub async fn clear_cache(&self) {
339
        self.rules_cache.write().await.clear();
340
        info!("Compliance rule cache cleared");
341
    }
342
343
    /// Gets the current cache size
344
    pub async fn cache_size(&self) -> usize {
345
        self.rules_cache.read().await.len()
346
    }
347
}
348
349
#[cfg(test)]
350
mod tests {
351
    use super::*;
352
353
    #[test]
354
1
    fn test_compliance_rule_config_structure() {
355
1
        let rule = ComplianceRuleConfig {
356
1
            rule_id: "test_rule".to_string(),
357
1
            name: "Test Rule".to_string(),
358
1
            description: "Test description".to_string(),
359
1
            rule_type: "POSITION_LIMIT".to_string(),
360
1
            active: true,
361
1
            version: 1,
362
1
            severity: "High".to_string(),
363
1
            priority: 80,
364
1
            parameters: serde_json::json!({"max_position": 1000000}),
365
1
            regulatory_framework: Some("Basel III".to_string()),
366
1
            regulatory_reference: Some("Article 123".to_string()),
367
1
        };
368
369
1
        assert_eq!(rule.rule_id, "test_rule");
370
1
        assert!(rule.active);
371
1
        assert_eq!(rule.priority, 80);
372
1
    }
373
374
    #[test]
375
1
    fn test_compliance_rule_config_serialization() {
376
1
        let rule = ComplianceRuleConfig {
377
1
            rule_id: "test_rule".to_string(),
378
1
            name: "Test Rule".to_string(),
379
1
            description: "Test description".to_string(),
380
1
            rule_type: "MARKET_ABUSE".to_string(),
381
1
            active: true,
382
1
            version: 1,
383
1
            severity: "Critical".to_string(),
384
1
            priority: 95,
385
1
            parameters: serde_json::json!({"threshold": 1000000}),
386
1
            regulatory_framework: None,
387
1
            regulatory_reference: None,
388
1
        };
389
390
1
        let json = serde_json::to_string(&rule).expect("Failed to serialize");
391
1
        assert!(json.contains("test_rule"));
392
1
        assert!(json.contains("MARKET_ABUSE"));
393
394
1
        let deserialized: ComplianceRuleConfig =
395
1
            serde_json::from_str(&json).expect("Failed to deserialize");
396
1
        assert_eq!(deserialized.rule_id, rule.rule_id);
397
1
        assert_eq!(deserialized.severity, rule.severity);
398
1
    }
399
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html deleted file mode 100644 index 0049e9ec6..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/data_config.rs
Line
Count
Source
1
//! Data configuration
2
3
use num_cpus;
4
use serde::{Deserialize, Serialize};
5
6
#[derive(Debug, Clone, Serialize, Deserialize)]
7
pub struct DataConfig {
8
    pub provider: String,
9
    pub symbols: Vec<String>,
10
    pub batch_size: usize,
11
    pub buffer_size: usize,
12
}
13
14
#[derive(Debug, Clone, Serialize, Deserialize)]
15
pub struct DataMicrostructureConfig {
16
    pub enable_bid_ask_spread: bool,
17
    pub enable_order_flow: bool,
18
    pub tick_size: f64,
19
    pub lot_size: f64,
20
    pub bid_ask_spread: bool,
21
    pub volume_imbalance: bool,
22
    pub price_impact: bool,
23
    pub kyle_lambda: bool,
24
    pub amihud_ratio: bool,
25
}
26
27
impl Default for DataMicrostructureConfig {
28
0
    fn default() -> Self {
29
0
        Self {
30
0
            enable_bid_ask_spread: true,
31
0
            enable_order_flow: true,
32
0
            tick_size: 0.01,
33
0
            lot_size: 100.0,
34
0
            bid_ask_spread: true,
35
0
            volume_imbalance: true,
36
0
            price_impact: false,
37
0
            kyle_lambda: false,
38
0
            amihud_ratio: false,
39
0
        }
40
0
    }
41
}
42
43
#[derive(Debug, Clone, Serialize, Deserialize)]
44
pub struct DataTLOBConfig {
45
    pub depth_levels: usize,
46
    pub enable_imbalance: bool,
47
    pub enable_pressure: bool,
48
    pub window_size: usize,
49
}
50
51
#[derive(Debug, Clone, Serialize, Deserialize)]
52
pub struct DataTechnicalIndicatorsConfig {
53
    pub enable_moving_averages: bool,
54
    pub enable_momentum: bool,
55
    pub enable_volatility: bool,
56
    pub window_sizes: Vec<usize>,
57
    pub ma_periods: Vec<usize>,
58
    pub rsi_periods: Vec<usize>,
59
    pub bollinger_periods: Vec<usize>,
60
    pub macd: DataMACDConfig,
61
}
62
63
impl Default for DataTechnicalIndicatorsConfig {
64
0
    fn default() -> Self {
65
0
        Self {
66
0
            enable_moving_averages: true,
67
0
            enable_momentum: true,
68
0
            enable_volatility: true,
69
0
            window_sizes: vec![10, 20, 50],
70
0
            ma_periods: vec![10, 20, 50, 200],
71
0
            rsi_periods: vec![14],
72
0
            bollinger_periods: vec![20],
73
0
            macd: DataMACDConfig::default(),
74
0
        }
75
0
    }
76
}
77
78
#[derive(Debug, Clone, Serialize, Deserialize)]
79
pub struct TrainingBenzingaConfig {
80
    pub api_key: String,
81
    pub api_key_env: String,
82
    pub symbols: Vec<String>,
83
    pub data_types: Vec<String>,
84
    pub timeout: u64,
85
    pub rate_limit: usize,
86
    pub batch_size: usize,
87
    pub enable_caching: bool,
88
}
89
90
impl Default for TrainingBenzingaConfig {
91
0
    fn default() -> Self {
92
0
        Self {
93
0
            api_key: String::new(),
94
0
            api_key_env: "BENZINGA_API_KEY".to_string(),
95
0
            symbols: vec!["SPY".to_string(), "AAPL".to_string()],
96
0
            data_types: vec![
97
0
                "news".to_string(),
98
0
                "sentiment".to_string(),
99
0
                "ratings".to_string(),
100
0
                "options".to_string(),
101
0
            ],
102
0
            timeout: 30,
103
0
            rate_limit: 60,
104
0
            batch_size: 1000,
105
0
            enable_caching: true,
106
0
        }
107
0
    }
108
}
109
110
#[derive(Debug, Clone, Serialize, Deserialize)]
111
pub enum DataCompressionAlgorithm {
112
    GZIP,
113
    ZSTD,
114
    LZ4,
115
    Snappy,
116
    None,
117
}
118
119
#[derive(Debug, Clone, Serialize, Deserialize)]
120
pub struct DataCompressionConfig {
121
    pub algorithm: DataCompressionAlgorithm,
122
    pub enabled: bool,
123
    pub level: Option<i32>,
124
}
125
126
impl Default for DataCompressionConfig {
127
0
    fn default() -> Self {
128
0
        Self {
129
0
            algorithm: DataCompressionAlgorithm::ZSTD,
130
0
            enabled: true,
131
0
            level: Some(3),
132
0
        }
133
0
    }
134
}
135
#[derive(Debug, Clone, Serialize, Deserialize)]
136
pub struct DataVersioningConfig {
137
    pub enabled: bool,
138
    pub version_format: String,
139
    pub keep_versions: usize,
140
}
141
142
impl Default for DataVersioningConfig {
143
0
    fn default() -> Self {
144
0
        Self {
145
0
            enabled: false,
146
0
            version_format: "v%Y%m%d_%H%M%S".to_string(),
147
0
            keep_versions: 5,
148
0
        }
149
0
    }
150
}
151
152
#[derive(Debug, Clone, Serialize, Deserialize)]
153
pub struct DataRetentionConfig {
154
    pub auto_cleanup: bool,
155
    pub retention_days: u32,
156
}
157
158
impl Default for DataRetentionConfig {
159
0
    fn default() -> Self {
160
0
        Self {
161
0
            auto_cleanup: false,
162
0
            retention_days: 30,
163
0
        }
164
0
    }
165
}
166
167
#[derive(Debug, Clone, Serialize, Deserialize)]
168
pub enum DataStorageFormat {
169
    Parquet,
170
    Arrow,
171
    Json,
172
    Csv,
173
    CSV,
174
    HDF5,
175
}
176
177
#[derive(Debug, Clone, Serialize, Deserialize)]
178
pub struct DataStorageConfig {
179
    pub format: DataStorageFormat,
180
    pub compression: DataCompressionConfig,
181
    pub path: String,
182
    pub base_directory: std::path::PathBuf,
183
    pub partition_by: Vec<String>,
184
    pub versioning: DataVersioningConfig,
185
    pub retention: DataRetentionConfig,
186
}
187
188
impl Default for DataStorageConfig {
189
0
    fn default() -> Self {
190
0
        Self {
191
0
            format: DataStorageFormat::Parquet,
192
0
            compression: DataCompressionConfig::default(),
193
0
            path: "./data".to_string(),
194
0
            base_directory: std::path::PathBuf::from("./data"),
195
0
            partition_by: vec!["symbol".to_string(), "date".to_string()],
196
0
            versioning: DataVersioningConfig::default(),
197
0
            retention: DataRetentionConfig::default(),
198
0
        }
199
0
    }
200
}
201
202
#[derive(Debug, Clone, Serialize, Deserialize)]
203
pub struct DataRegimeDetectionConfig {
204
    pub enable_hmm: bool,
205
    pub enable_clustering: bool,
206
    pub window_size: usize,
207
    pub n_states: usize,
208
    pub volatility_regime: bool,
209
    pub trend_regime: bool,
210
    pub volume_regime: bool,
211
    pub correlation_regime: bool,
212
    pub lookback_period: usize,
213
}
214
215
impl Default for DataRegimeDetectionConfig {
216
0
    fn default() -> Self {
217
0
        Self {
218
0
            enable_hmm: false,
219
0
            enable_clustering: false,
220
0
            window_size: 100,
221
0
            n_states: 3,
222
0
            volatility_regime: true,
223
0
            trend_regime: true,
224
0
            volume_regime: false,
225
0
            correlation_regime: false,
226
0
            lookback_period: 252,
227
0
        }
228
0
    }
229
}
230
231
#[derive(Debug, Clone, Serialize, Deserialize)]
232
pub struct DataProcessingConfig {
233
    pub worker_threads: usize,
234
    pub batch_size: usize,
235
    pub buffer_size: usize,
236
    pub timeout: u64,
237
    pub parallel_processing: bool,
238
}
239
240
impl Default for DataProcessingConfig {
241
0
    fn default() -> Self {
242
0
        Self {
243
0
            worker_threads: num_cpus::get(),
244
0
            batch_size: 1000,
245
0
            buffer_size: 10000,
246
0
            timeout: 300,
247
0
            parallel_processing: true,
248
0
        }
249
0
    }
250
}
251
252
#[derive(Debug, Clone, Serialize, Deserialize)]
253
pub struct DataTrainingConfig {
254
    pub batch_size: usize,
255
    pub sequence_length: usize,
256
    pub validation_split: f64,
257
    pub test_split: f64,
258
    pub sources: DataSourcesConfig,
259
    pub features: TrainingFeatureEngineeringConfig,
260
    pub validation: DataValidationConfig,
261
    pub storage: DataStorageConfig,
262
    pub processing: DataProcessingConfig,
263
    pub rate_limit: usize,
264
}
265
266
impl Default for DataTrainingConfig {
267
0
    fn default() -> Self {
268
0
        Self {
269
0
            batch_size: 32,
270
0
            sequence_length: 100,
271
0
            validation_split: 0.2,
272
0
            test_split: 0.1,
273
0
            sources: DataSourcesConfig::default(),
274
0
            features: TrainingFeatureEngineeringConfig::default(),
275
0
            validation: DataValidationConfig::default(),
276
0
            storage: DataStorageConfig::default(),
277
0
            processing: DataProcessingConfig::default(),
278
0
            rate_limit: 100,
279
0
        }
280
0
    }
281
}
282
283
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284
pub struct DataSourcesConfig {
285
    pub databento: Option<DatabentoConfig>,
286
    pub benzinga: Option<TrainingBenzingaConfig>,
287
    #[serde(default)]
288
    pub enable_realtime: bool,
289
    pub interactive_brokers: Option<InteractiveBrokersConfig>,
290
    pub icmarkets: Option<ICMarketsConfig>,
291
    pub historical: Option<HistoricalDataConfig>,
292
}
293
294
#[derive(Debug, Clone, Serialize, Deserialize)]
295
pub struct InteractiveBrokersConfig {
296
    pub host: String,
297
    pub port: u16,
298
    pub client_id: i32,
299
    pub timeout_seconds: u64,
300
}
301
302
#[derive(Debug, Clone, Serialize, Deserialize)]
303
pub struct ICMarketsConfig {
304
    pub api_key: String,
305
    pub environment: String,
306
}
307
308
#[derive(Debug, Clone, Serialize, Deserialize)]
309
pub struct HistoricalDataConfig {
310
    pub enabled: bool,
311
    pub batch_size: usize,
312
    pub parallel_downloads: usize,
313
}
314
315
#[derive(Debug, Clone, Serialize, Deserialize)]
316
pub struct DatabentoConfig {
317
    pub api_key: String,
318
    pub dataset: String,
319
    pub symbols: Vec<String>,
320
    pub schema: String,
321
    pub stype_in: String,
322
}
323
324
#[derive(Debug, Clone, Serialize, Deserialize)]
325
pub struct DataValidationConfig {
326
    #[serde(default)]
327
    pub enable_price_validation: bool,
328
    #[serde(default)]
329
    pub enable_volume_validation: bool,
330
    #[serde(default)]
331
    pub price_threshold: f64,
332
    #[serde(default)]
333
    pub volume_threshold: f64,
334
    #[serde(default)]
335
    pub outlier_method: OutlierDetectionMethod,
336
    #[serde(default)]
337
    pub max_price_change: f64,
338
    #[serde(default)]
339
    pub max_volume_change: f64,
340
    #[serde(default)]
341
    pub max_timestamp_drift: i64,
342
    #[serde(default)]
343
    pub price_validation: bool,
344
    #[serde(default)]
345
    pub volume_validation: bool,
346
    #[serde(default)]
347
    pub timestamp_validation: bool,
348
    #[serde(default)]
349
    pub outlier_detection: bool,
350
    #[serde(default)]
351
    pub missing_data_handling: MissingDataHandling,
352
}
353
354
impl Default for DataValidationConfig {
355
0
    fn default() -> Self {
356
0
        Self {
357
0
            enable_price_validation: true,
358
0
            enable_volume_validation: true,
359
0
            price_threshold: 0.1,
360
0
            volume_threshold: 0.2,
361
0
            outlier_method: OutlierDetectionMethod::ZScore,
362
0
            max_price_change: 0.05,
363
0
            max_volume_change: 2.0,
364
0
            max_timestamp_drift: 1000,
365
0
            price_validation: true,
366
0
            volume_validation: true,
367
0
            timestamp_validation: true,
368
0
            outlier_detection: true,
369
0
            missing_data_handling: MissingDataHandling::Skip,
370
0
        }
371
0
    }
372
}
373
374
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
375
pub enum MissingDataHandling {
376
    #[default]
377
    Skip,
378
    Drop,
379
    Interpolate,
380
    ForwardFill,
381
    BackwardFill,
382
    FillForward,
383
    FillBackward,
384
    Mean,
385
    Median,
386
    Error,
387
}
388
389
#[derive(Debug, Clone, Serialize, Deserialize)]
390
pub struct TrainingFeatureEngineeringConfig {
391
    pub enable_normalization: bool,
392
    pub enable_scaling: bool,
393
    pub enable_log_returns: bool,
394
    pub lookback_window: usize,
395
    pub regime_detection: DataRegimeDetectionConfig,
396
    pub technical_indicators: DataTechnicalIndicatorsConfig,
397
    pub microstructure: DataMicrostructureConfig,
398
}
399
400
impl Default for TrainingFeatureEngineeringConfig {
401
0
    fn default() -> Self {
402
0
        Self {
403
0
            enable_normalization: true,
404
0
            enable_scaling: true,
405
0
            enable_log_returns: true,
406
0
            lookback_window: 100,
407
0
            regime_detection: DataRegimeDetectionConfig::default(),
408
0
            technical_indicators: DataTechnicalIndicatorsConfig::default(),
409
0
            microstructure: DataMicrostructureConfig::default(),
410
0
        }
411
0
    }
412
}
413
414
#[derive(Debug, Clone, Serialize, Deserialize)]
415
pub struct DataTemporalConfig {
416
    pub enable_time_features: bool,
417
    pub enable_seasonal: bool,
418
    pub timezone: String,
419
    pub business_hours_only: bool,
420
    pub market_session: bool,
421
    pub holiday_effects: bool,
422
    pub expiration_effects: bool,
423
}
424
425
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426
pub enum OutlierDetectionMethod {
427
    #[default]
428
    ZScore,
429
    IQR,
430
    Isolation,
431
    IsolationForest,
432
    LocalOutlierFactor,
433
    None,
434
}
435
436
#[derive(Debug, Clone, Serialize, Deserialize)]
437
pub struct DataModuleConfig {
438
    pub data_path: String,
439
    pub batch_size: usize,
440
    pub num_workers: usize,
441
    pub cache_size: usize,
442
    pub settings: DataModuleSettings,
443
    pub interactive_brokers: Option<InteractiveBrokersConfig>,
444
}
445
446
#[derive(Debug, Clone, Serialize, Deserialize)]
447
pub struct DataModuleSettings {
448
    pub enable_preprocessing: bool,
449
    pub enable_validation: bool,
450
    pub max_memory_usage: usize,
451
    pub market_data_buffer_size: usize,
452
    pub order_event_buffer_size: usize,
453
}
454
455
#[derive(Debug, Clone, Serialize, Deserialize)]
456
pub struct DataMACDConfig {
457
    pub fast_period: usize,
458
    pub slow_period: usize,
459
    pub signal_period: usize,
460
    pub enabled: bool,
461
}
462
463
impl Default for DataMACDConfig {
464
0
    fn default() -> Self {
465
0
        Self {
466
0
            fast_period: 12,
467
0
            slow_period: 26,
468
0
            signal_period: 9,
469
0
            enabled: true,
470
0
        }
471
0
    }
472
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html deleted file mode 100644 index f00f74997..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs
Line
Count
Source
1
//! Data provider endpoint configuration
2
//!
3
//! Centralizes all hardcoded API endpoints for data providers, enabling
4
//! environment-specific configurations and easy switching between dev/staging/prod.
5
6
use serde::{Deserialize, Serialize};
7
8
/// Environment specification for data providers
9
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10
pub enum DataProviderEnvironment {
11
    /// Development environment with potentially mocked or sandbox endpoints
12
    Development,
13
    /// Staging environment for pre-production testing
14
    Staging,
15
    /// Production environment with live data
16
    Production,
17
}
18
19
impl DataProviderEnvironment {
20
    /// Detect environment from FOXHUNT_ENV environment variable
21
4
    pub fn from_env() -> Self {
22
4
        match std::env::var("FOXHUNT_ENV")
23
4
            .unwrap_or_else(|_| 
"development"1
.
to_string1
())
24
4
            .to_lowercase()
25
4
            .as_str()
26
        {
27
4
            "prod" | "production" => 
Self::Production1
,
28
3
            "staging" | 
"stage"2
=>
Self::Staging1
,
29
2
            _ => Self::Development,
30
        }
31
4
    }
32
}
33
34
/// Databento endpoint configuration
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct DatabentoEndpoints {
37
    /// WebSocket URL for real-time data streaming
38
    pub websocket_url: String,
39
    /// HTTP base URL for historical data queries
40
    pub historical_base_url: String,
41
}
42
43
impl DatabentoEndpoints {
44
    /// Create configuration from environment variables with fallback to defaults
45
3
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
46
3
        let (ws_default, http_default) = match environment {
47
3
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
48
3
                "wss://gateway.databento.com/v0/subscribe",
49
3
                "https://hist.databento.com",
50
3
            ),
51
0
            DataProviderEnvironment::Staging => (
52
0
                "wss://staging-gateway.databento.com/v0/subscribe",
53
0
                "https://staging-hist.databento.com",
54
0
            ),
55
        };
56
57
        Self {
58
3
            websocket_url: std::env::var("DATABENTO_WS_URL")
59
3
                .unwrap_or_else(|_| 
ws_default2
.
to_string2
()),
60
3
            historical_base_url: std::env::var("DATABENTO_HTTP_URL")
61
3
                .unwrap_or_else(|_| http_default.to_string()),
62
        }
63
3
    }
64
}
65
66
impl Default for DatabentoEndpoints {
67
0
    fn default() -> Self {
68
0
        Self::from_env(DataProviderEnvironment::from_env())
69
0
    }
70
}
71
72
/// Benzinga endpoint configuration
73
#[derive(Debug, Clone, Serialize, Deserialize)]
74
pub struct BenzingaEndpoints {
75
    /// WebSocket URL for real-time news and sentiment streaming
76
    pub websocket_url: String,
77
    /// HTTP base URL for API queries
78
    pub api_base_url: String,
79
}
80
81
impl BenzingaEndpoints {
82
    /// Create configuration from environment variables with fallback to defaults
83
2
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
84
2
        let (ws_default, api_default) = match environment {
85
2
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
86
2
                "wss://api.benzinga.com/api/v1/stream",
87
2
                "https://api.benzinga.com/api/v2",
88
2
            ),
89
0
            DataProviderEnvironment::Staging => (
90
0
                "wss://staging-api.benzinga.com/api/v1/stream",
91
0
                "https://staging-api.benzinga.com/api/v2",
92
0
            ),
93
        };
94
95
        Self {
96
2
            websocket_url: std::env::var("BENZINGA_WS_URL")
97
2
                .unwrap_or_else(|_| ws_default.to_string()),
98
2
            api_base_url: std::env::var("BENZINGA_API_URL")
99
2
                .unwrap_or_else(|_| api_default.to_string()),
100
        }
101
2
    }
102
}
103
104
impl Default for BenzingaEndpoints {
105
0
    fn default() -> Self {
106
0
        Self::from_env(DataProviderEnvironment::from_env())
107
0
    }
108
}
109
110
/// Alpaca endpoint configuration
111
#[derive(Debug, Clone, Serialize, Deserialize)]
112
pub struct AlpacaEndpoints {
113
    /// Base URL for trading operations (paper or live)
114
    pub trading_base_url: String,
115
    /// Base URL for market data queries
116
    pub data_base_url: String,
117
}
118
119
impl AlpacaEndpoints {
120
    /// Create configuration from environment variables with fallback to defaults
121
3
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
122
3
        let (trading_default, data_default) = match environment {
123
1
            DataProviderEnvironment::Development => (
124
1
                "https://paper-api.alpaca.markets",
125
1
                "https://data.alpaca.markets",
126
1
            ),
127
0
            DataProviderEnvironment::Staging => (
128
0
                "https://paper-api.alpaca.markets",
129
0
                "https://data.alpaca.markets",
130
0
            ),
131
2
            DataProviderEnvironment::Production => (
132
2
                "https://api.alpaca.markets",
133
2
                "https://data.alpaca.markets",
134
2
            ),
135
        };
136
137
        Self {
138
3
            trading_base_url: std::env::var("ALPACA_TRADING_URL")
139
3
                .unwrap_or_else(|_| trading_default.to_string()),
140
3
            data_base_url: std::env::var("ALPACA_DATA_URL")
141
3
                .unwrap_or_else(|_| data_default.to_string()),
142
        }
143
3
    }
144
}
145
146
impl Default for AlpacaEndpoints {
147
0
    fn default() -> Self {
148
0
        Self::from_env(DataProviderEnvironment::from_env())
149
0
    }
150
}
151
152
/// Interactive Brokers Gateway configuration
153
#[derive(Debug, Clone, Serialize, Deserialize)]
154
pub struct IBGatewayConfig {
155
    /// Gateway host (typically localhost for local TWS/Gateway)
156
    pub host: String,
157
    /// Gateway port (7497 for paper trading, 7496 for live, 4001 for IB Gateway)
158
    pub port: u16,
159
}
160
161
impl IBGatewayConfig {
162
    /// Create configuration from environment variables with fallback to defaults
163
3
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
164
3
        let (host_default, port_default) = match environment {
165
1
            DataProviderEnvironment::Development => ("127.0.0.1", 7497), // Paper trading
166
0
            DataProviderEnvironment::Staging => ("127.0.0.1", 7497),     // Paper trading
167
2
            DataProviderEnvironment::Production => ("127.0.0.1", 7496),  // Live trading
168
        };
169
170
        Self {
171
3
            host: std::env::var("IB_GATEWAY_HOST")
172
3
                .unwrap_or_else(|_| host_default.to_string()),
173
3
            port: std::env::var("IB_GATEWAY_PORT")
174
3
                .ok()
175
3
                .and_then(|s| 
s.parse()0
.
ok0
())
176
3
                .unwrap_or(port_default),
177
        }
178
3
    }
179
}
180
181
impl Default for IBGatewayConfig {
182
0
    fn default() -> Self {
183
0
        Self::from_env(DataProviderEnvironment::from_env())
184
0
    }
185
}
186
187
/// Master configuration for all data provider endpoints
188
#[derive(Debug, Clone, Serialize, Deserialize)]
189
pub struct DataProviderConfig {
190
    /// Current environment
191
    pub environment: DataProviderEnvironment,
192
    /// Databento endpoints
193
    pub databento: DatabentoEndpoints,
194
    /// Benzinga endpoints
195
    pub benzinga: BenzingaEndpoints,
196
    /// Alpaca endpoints
197
    pub alpaca: AlpacaEndpoints,
198
    /// Interactive Brokers Gateway configuration
199
    pub ib_gateway: IBGatewayConfig,
200
}
201
202
impl DataProviderConfig {
203
    /// Create configuration from environment
204
0
    pub fn from_env() -> Self {
205
0
        let environment = DataProviderEnvironment::from_env();
206
0
        Self {
207
0
            databento: DatabentoEndpoints::from_env(environment),
208
0
            benzinga: BenzingaEndpoints::from_env(environment),
209
0
            alpaca: AlpacaEndpoints::from_env(environment),
210
0
            ib_gateway: IBGatewayConfig::from_env(environment),
211
0
            environment,
212
0
        }
213
0
    }
214
215
    /// Create configuration for specific environment
216
1
    pub fn for_environment(environment: DataProviderEnvironment) -> Self {
217
1
        Self {
218
1
            databento: DatabentoEndpoints::from_env(environment),
219
1
            benzinga: BenzingaEndpoints::from_env(environment),
220
1
            alpaca: AlpacaEndpoints::from_env(environment),
221
1
            ib_gateway: IBGatewayConfig::from_env(environment),
222
1
            environment,
223
1
        }
224
1
    }
225
}
226
227
impl Default for DataProviderConfig {
228
0
    fn default() -> Self {
229
0
        Self::from_env()
230
0
    }
231
}
232
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236
237
    #[test]
238
1
    fn test_environment_detection() {
239
1
        std::env::set_var("FOXHUNT_ENV", "production");
240
1
        assert_eq!(
241
1
            DataProviderEnvironment::from_env(),
242
            DataProviderEnvironment::Production
243
        );
244
245
1
        std::env::set_var("FOXHUNT_ENV", "staging");
246
1
        assert_eq!(
247
1
            DataProviderEnvironment::from_env(),
248
            DataProviderEnvironment::Staging
249
        );
250
251
1
        std::env::set_var("FOXHUNT_ENV", "development");
252
1
        assert_eq!(
253
1
            DataProviderEnvironment::from_env(),
254
            DataProviderEnvironment::Development
255
        );
256
257
1
        std::env::remove_var("FOXHUNT_ENV");
258
1
        assert_eq!(
259
1
            DataProviderEnvironment::from_env(),
260
            DataProviderEnvironment::Development
261
        );
262
1
    }
263
264
    #[test]
265
1
    fn test_databento_defaults() {
266
1
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
267
1
        assert_eq!(
268
            config.websocket_url,
269
            "wss://gateway.databento.com/v0/subscribe"
270
        );
271
1
        assert_eq!(config.historical_base_url, "https://hist.databento.com");
272
1
    }
273
274
    #[test]
275
1
    fn test_benzinga_defaults() {
276
1
        let config = BenzingaEndpoints::from_env(DataProviderEnvironment::Production);
277
1
        assert_eq!(
278
            config.websocket_url,
279
            "wss://api.benzinga.com/api/v1/stream"
280
        );
281
1
        assert_eq!(config.api_base_url, "https://api.benzinga.com/api/v2");
282
1
    }
283
284
    #[test]
285
1
    fn test_alpaca_defaults() {
286
1
        let dev_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Development);
287
1
        assert_eq!(
288
            dev_config.trading_base_url,
289
            "https://paper-api.alpaca.markets"
290
        );
291
292
1
        let prod_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Production);
293
1
        assert_eq!(prod_config.trading_base_url, "https://api.alpaca.markets");
294
1
    }
295
296
    #[test]
297
1
    fn test_ib_gateway_defaults() {
298
1
        let dev_config = IBGatewayConfig::from_env(DataProviderEnvironment::Development);
299
1
        assert_eq!(dev_config.host, "127.0.0.1");
300
1
        assert_eq!(dev_config.port, 7497);
301
302
1
        let prod_config = IBGatewayConfig::from_env(DataProviderEnvironment::Production);
303
1
        assert_eq!(prod_config.port, 7496);
304
1
    }
305
306
    #[test]
307
1
    fn test_environment_variable_override() {
308
1
        std::env::set_var("DATABENTO_WS_URL", "wss://custom.databento.com");
309
1
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
310
1
        assert_eq!(config.websocket_url, "wss://custom.databento.com");
311
1
        std::env::remove_var("DATABENTO_WS_URL");
312
1
    }
313
314
    #[test]
315
1
    fn test_master_config() {
316
1
        let config = DataProviderConfig::for_environment(DataProviderEnvironment::Production);
317
1
        assert_eq!(config.environment, DataProviderEnvironment::Production);
318
1
        assert!(!config.databento.websocket_url.is_empty());
319
1
        assert!(!config.benzinga.websocket_url.is_empty());
320
1
        assert!(!config.alpaca.trading_base_url.is_empty());
321
1
        assert!(!config.ib_gateway.host.is_empty());
322
1
    }
323
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html deleted file mode 100644 index 38806d3c8..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/database.rs
Line
Count
Source
1
//! Database configuration for PostgreSQL connections and connection pooling.
2
//!
3
//! This module provides comprehensive database configuration structures for managing
4
//! PostgreSQL connections, connection pools, and transaction settings in the Foxhunt
5
//! HFT trading system. It supports connection pooling, timeout management, and
6
//! transaction isolation levels optimized for high-frequency trading workloads.
7
8
use serde::{Deserialize, Serialize};
9
use std::time::Duration;
10
11
#[cfg(feature = "postgres")]
12
use sqlx::Row;
13
14
/// Main database configuration structure for PostgreSQL connections.
15
///
16
/// Provides comprehensive database connection settings including connection pooling,
17
/// timeouts, logging, and transaction management. Optimized for high-frequency
18
/// trading workloads with appropriate defaults for low-latency operations.
19
#[derive(Debug, Clone, Serialize, Deserialize)]
20
pub struct DatabaseConfig {
21
    /// PostgreSQL connection URL (e.g., "postgresql://user:pass@host:port/database")
22
    pub url: String,
23
    /// Maximum number of connections in the pool
24
    pub max_connections: u32,
25
    /// Minimum number of connections to maintain in the pool
26
    pub min_connections: u32,
27
    /// Timeout for establishing new database connections
28
    pub connect_timeout: std::time::Duration,
29
    /// Timeout for individual query execution
30
    pub query_timeout: std::time::Duration,
31
    /// Enable detailed query logging for debugging
32
    pub enable_query_logging: bool,
33
    /// Application name to identify connections in PostgreSQL logs
34
    pub application_name: Option<String>,
35
    /// Connection pool configuration settings
36
    pub pool: PoolConfig,
37
    /// Transaction management configuration
38
    pub transaction: TransactionConfig,
39
}
40
41
impl Default for DatabaseConfig {
42
0
    fn default() -> Self {
43
0
        Self::new()
44
0
    }
45
}
46
47
impl DatabaseConfig {
48
    /// Creates a new DatabaseConfig with sensible defaults for development.
49
    ///
50
    /// Returns a configuration suitable for local development with a PostgreSQL
51
    /// database running on localhost. Production deployments should override
52
    /// these settings through environment variables or configuration files.
53
14
    pub fn new() -> Self {
54
14
        Self {
55
14
            url: "postgresql://localhost/foxhunt".to_string(),
56
14
            max_connections: 10,
57
14
            min_connections: 1,
58
14
            connect_timeout: Duration::from_secs(30),
59
14
            query_timeout: Duration::from_secs(60),
60
14
            enable_query_logging: false,
61
14
            application_name: Some("foxhunt".to_string()),
62
14
            pool: PoolConfig::default(),
63
14
            transaction: TransactionConfig::default(),
64
14
        }
65
14
    }
66
67
    /// Validates the database configuration for correctness.
68
    ///
69
    /// Performs basic validation checks on the configuration parameters to ensure
70
    /// they are valid before attempting to establish database connections.
71
    ///
72
    /// # Errors
73
    ///
74
    /// Returns an error string if the configuration is invalid, such as:
75
    /// - Empty database URL
76
    /// - Invalid connection parameters
77
5
    pub fn validate(&self) -> Result<(), String> {
78
5
        if self.url.is_empty() {
79
3
            return Err("Database URL cannot be empty".to_string());
80
2
        }
81
2
        Ok(())
82
5
    }
83
}
84
85
/// Database connection pool configuration.
86
///
87
/// Manages the behavior of the connection pool including connection lifecycle,
88
/// timeouts, and health checking. Optimized for high-frequency trading workloads
89
/// where connection availability and low latency are critical.
90
#[derive(Debug, Clone, Serialize, Deserialize)]
91
pub struct PoolConfig {
92
    /// Minimum number of connections to maintain in the pool
93
    pub min_connections: u32,
94
    /// Maximum number of connections allowed in the pool
95
    pub max_connections: u32,
96
    /// Timeout in seconds for acquiring a connection from the pool
97
    pub acquire_timeout_secs: u64,
98
    /// Maximum lifetime in seconds for a connection before it's recycled
99
    pub max_lifetime_secs: u64,
100
    /// Timeout in seconds before idle connections are closed
101
    pub idle_timeout_secs: u64,
102
    /// Whether to test connections before returning them from the pool
103
    pub test_before_acquire: bool,
104
    /// Database URL for pool connections
105
    pub database_url: String,
106
    /// Enable periodic health checks for pool connections
107
    pub health_check_enabled: bool,
108
    /// Interval in seconds between health checks
109
    pub health_check_interval_secs: u64,
110
}
111
112
impl Default for PoolConfig {
113
24
    fn default() -> Self {
114
24
        Self {
115
24
            min_connections: 1,
116
24
            max_connections: 10,
117
24
            acquire_timeout_secs: 30,
118
24
            max_lifetime_secs: 1800,
119
24
            idle_timeout_secs: 600,
120
24
            test_before_acquire: true,
121
24
            database_url: "postgresql://localhost/foxhunt".to_string(),
122
24
            health_check_enabled: true,
123
24
            health_check_interval_secs: 60,
124
24
        }
125
24
    }
126
}
127
128
/// Database transaction configuration and retry policies.
129
///
130
/// Configures transaction behavior including isolation levels, timeouts,
131
/// and retry mechanisms. Critical for maintaining data consistency in
132
/// high-frequency trading operations while handling transient failures.
133
#[derive(Debug, Clone, Serialize, Deserialize)]
134
pub struct TransactionConfig {
135
    /// PostgreSQL transaction isolation level (e.g., "READ_COMMITTED", "SERIALIZABLE")
136
    pub isolation_level: String,
137
    /// Default timeout duration for transactions
138
    pub timeout: Duration,
139
    /// Default timeout in seconds for transactions
140
    pub default_timeout_secs: u64,
141
    /// Enable automatic retry on transaction failures
142
    pub enable_retry: bool,
143
    /// Maximum number of retry attempts for failed transactions
144
    pub max_retries: u32,
145
    /// Delay in milliseconds between retry attempts
146
    pub retry_delay_ms: u64,
147
    /// Maximum number of nested savepoints allowed
148
    pub max_savepoints: u32,
149
}
150
151
impl Default for TransactionConfig {
152
27
    fn default() -> Self {
153
27
        Self {
154
27
            isolation_level: "READ_COMMITTED".to_string(),
155
27
            timeout: Duration::from_secs(30),
156
27
            default_timeout_secs: 30,
157
27
            enable_retry: true,
158
27
            max_retries: 3,
159
27
            retry_delay_ms: 100,
160
27
            max_savepoints: 10,
161
27
        }
162
27
    }
163
}
164
165
/// Database loader for symbol configurations with PostgreSQL integration.
166
///
167
/// Provides high-performance loading and caching of symbol configurations
168
/// from the PostgreSQL database. Supports real-time updates through PostgreSQL
169
/// NOTIFY/LISTEN for configuration hot-reload capabilities.
170
#[cfg(feature = "postgres")]
171
pub struct PostgresSymbolConfigLoader {
172
    /// Database connection pool
173
    pool: sqlx::PgPool,
174
    /// Configuration cache timeout
175
    cache_timeout: Duration,
176
    /// PostgreSQL listener for configuration changes
177
    listener: Option<sqlx::postgres::PgListener>,
178
}
179
180
#[cfg(feature = "postgres")]
181
impl PostgresSymbolConfigLoader {
182
    /// Creates a new PostgreSQL symbol configuration loader.
183
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
184
        let pool = sqlx::PgPool::connect(database_url).await?;
185
186
        Ok(Self {
187
            pool,
188
            cache_timeout: Duration::from_secs(300), // 5 minutes
189
            listener: None,
190
        })
191
    }
192
193
    /// Creates a new loader with an existing connection pool.
194
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
195
        Self {
196
            pool,
197
            cache_timeout: Duration::from_secs(300),
198
            listener: None,
199
        }
200
    }
201
202
    /// Loads a symbol configuration by symbol name.
203
    pub async fn load_symbol_config(
204
        &self,
205
        symbol: &str,
206
    ) -> Result<Option<crate::symbol_config::SymbolConfig>, sqlx::Error> {
207
        // Simplified implementation using basic sqlx::query instead of macros
208
        let query = "
209
            SELECT 
210
                sc.id,
211
                sc.symbol,
212
                sc.description,
213
                sc.classification,
214
                sc.primary_exchange,
215
                sc.currency,
216
                sc.tick_size,
217
                sc.lot_size,
218
                sc.min_order_size,
219
                sc.max_order_size,
220
                sc.sector,
221
                sc.industry,
222
                sc.market_cap,
223
                sc.avg_daily_volume,
224
                sc.margin_requirement,
225
                sc.position_limit,
226
                sc.risk_multiplier,
227
                sc.is_active,
228
                sc.data_source,
229
                sc.created_at,
230
                sc.updated_at,
231
                sc.last_validated
232
            FROM symbol_config sc
233
            WHERE sc.symbol = $1 AND sc.is_active = true
234
        ";
235
236
        let row = sqlx::query(query)
237
            .bind(symbol)
238
            .fetch_optional(&self.pool)
239
            .await?;
240
241
        if let Some(row) = row {
242
            // Create a basic symbol config from the row
243
            let symbol_name: String = row.get("symbol");
244
            let description: String = row.get("description");
245
            let classification_str: String = row.get("classification");
246
247
            let classification = match classification_str.as_str() {
248
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
249
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
250
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
251
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
252
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
253
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
254
                "OPTION" => crate::symbol_config::AssetClassification::Option,
255
                "ETF" => crate::symbol_config::AssetClassification::Etf,
256
                "INDEX" => crate::symbol_config::AssetClassification::Index,
257
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
258
                _ => crate::symbol_config::AssetClassification::Equity,
259
            };
260
261
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
262
            config.description = description;
263
            config.primary_exchange = row.get("primary_exchange");
264
            config.currency = row.get("currency");
265
266
            // Handle decimal conversions safely
267
            if let Ok(tick_size) = row.try_get::<rust_decimal::Decimal, _>("tick_size") {
268
                if let Ok(f) = tick_size.try_into() {
269
                    config.tick_size = f;
270
                }
271
            }
272
273
            Ok(Some(config))
274
        } else {
275
            Ok(None)
276
        }
277
    }
278
279
    /// Loads all active symbol configurations.
280
    pub async fn load_all_symbols(
281
        &self,
282
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
283
        let query = "
284
            SELECT symbol, description, classification
285
            FROM symbol_config 
286
            WHERE is_active = true
287
            ORDER BY symbol
288
        ";
289
290
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
291
292
        let mut configs = Vec::new();
293
        for row in rows {
294
            let symbol_name: String = row.get("symbol");
295
            let description: String = row.get("description");
296
            let classification_str: String = row.get("classification");
297
298
            let classification = match classification_str.as_str() {
299
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
300
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
301
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
302
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
303
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
304
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
305
                "OPTION" => crate::symbol_config::AssetClassification::Option,
306
                "ETF" => crate::symbol_config::AssetClassification::Etf,
307
                "INDEX" => crate::symbol_config::AssetClassification::Index,
308
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
309
                _ => crate::symbol_config::AssetClassification::Equity,
310
            };
311
312
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
313
            config.description = description;
314
            configs.push(config);
315
        }
316
317
        Ok(configs)
318
    }
319
320
    /// Loads symbols filtered by asset classification.
321
    pub async fn load_symbols_by_classification(
322
        &self,
323
        classification: crate::symbol_config::AssetClassification,
324
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
325
        let class_str = classification.regulatory_class();
326
327
        let query = "
328
            SELECT symbol, description, classification
329
            FROM symbol_config 
330
            WHERE is_active = true AND classification = $1
331
            ORDER BY symbol
332
        ";
333
334
        let rows = sqlx::query(query)
335
            .bind(class_str)
336
            .fetch_all(&self.pool)
337
            .await?;
338
339
        let mut configs = Vec::new();
340
        for row in rows {
341
            let symbol_name: String = row.get("symbol");
342
            let description: String = row.get("description");
343
            let mut config =
344
                crate::symbol_config::SymbolConfig::new(symbol_name, classification.clone());
345
            config.description = description;
346
            configs.push(config);
347
        }
348
349
        Ok(configs)
350
    }
351
    /// Saves or updates a symbol configuration.
352
    pub async fn save_symbol_config(
353
        &self,
354
        config: &crate::symbol_config::SymbolConfig,
355
    ) -> Result<(), sqlx::Error> {
356
        let query = "
357
            INSERT INTO symbol_config (
358
                symbol, description, classification, primary_exchange, currency
359
            ) VALUES ($1, $2, $3, $4, $5)
360
            ON CONFLICT (symbol) DO UPDATE SET
361
                description = EXCLUDED.description,
362
                classification = EXCLUDED.classification,
363
                primary_exchange = EXCLUDED.primary_exchange,
364
                currency = EXCLUDED.currency,
365
                updated_at = NOW()
366
        ";
367
368
        sqlx::query(query)
369
            .bind(&config.symbol)
370
            .bind(&config.description)
371
            .bind(config.classification.regulatory_class())
372
            .bind(&config.primary_exchange)
373
            .bind(&config.currency)
374
            .execute(&self.pool)
375
            .await?;
376
377
        Ok(())
378
    }
379
380
    /// Initializes PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
381
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
382
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
383
        listener.listen("symbol_config_changed").await?;
384
        self.listener = Some(listener);
385
        Ok(())
386
    }
387
388
    /// Checks for configuration change notifications.
389
    pub async fn check_for_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
390
        if let Some(listener) = &mut self.listener {
391
            if let Some(notification) = listener.try_recv().await? {
392
                return Ok(Some(notification.payload().to_string()));
393
            }
394
        }
395
        Ok(None)
396
    }
397
}
398
399
/// Database integration for comprehensive asset classification system.
400
///
401
/// Provides PostgreSQL-backed storage and retrieval for asset classification
402
/// configurations with support for pattern matching, caching, and hot-reload.
403
#[cfg(feature = "postgres")]
404
pub struct PostgresAssetClassificationLoader {
405
    /// Database connection pool
406
    pool: sqlx::PgPool,
407
    /// Configuration cache timeout
408
    cache_timeout: Duration,
409
    /// PostgreSQL listener for configuration changes
410
    listener: Option<sqlx::postgres::PgListener>,
411
}
412
413
#[cfg(feature = "postgres")]
414
impl PostgresAssetClassificationLoader {
415
    /// Creates a new PostgreSQL asset classification loader.
416
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
417
        let pool = sqlx::PgPool::connect(database_url).await?;
418
419
        Ok(Self {
420
            pool,
421
            cache_timeout: Duration::from_secs(300), // 5 minutes
422
            listener: None,
423
        })
424
    }
425
426
    /// Creates a new loader with an existing connection pool.
427
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
428
        Self {
429
            pool,
430
            cache_timeout: Duration::from_secs(300),
431
            listener: None,
432
        }
433
    }
434
435
    /// Loads all active asset configurations ordered by priority.
436
    pub async fn load_asset_configurations(
437
        &self,
438
    ) -> Result<Vec<crate::asset_classification::AssetConfig>, sqlx::Error> {
439
        let query = "
440
                SELECT 
441
                    id,
442
                    name,
443
                    symbol_pattern,
444
                    asset_class_data,
445
                    volatility_profile,
446
                    trading_parameters,
447
                    priority,
448
                    is_active,
449
                    created_at,
450
                    updated_at,
451
                    trading_hours,
452
                    settlement_config
453
                FROM asset_configurations
454
                WHERE is_active = true
455
                ORDER BY priority DESC
456
            ";
457
458
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
459
460
        let mut configs = Vec::new();
461
        for row in rows {
462
            if let Ok(config) = self.row_to_asset_config(row) {
463
                configs.push(config);
464
            }
465
        }
466
467
        Ok(configs)
468
    }
469
470
    /// Loads a specific asset configuration by ID.
471
    pub async fn load_asset_configuration_by_id(
472
        &self,
473
        id: uuid::Uuid,
474
    ) -> Result<Option<crate::asset_classification::AssetConfig>, sqlx::Error> {
475
        let query = "
476
                SELECT 
477
                    id,
478
                    name,
479
                    symbol_pattern,
480
                    asset_class_data,
481
                    volatility_profile,
482
                    trading_parameters,
483
                    priority,
484
                    is_active,
485
                    created_at,
486
                    updated_at,
487
                    trading_hours,
488
                    settlement_config
489
                FROM asset_configurations
490
                WHERE id = $1
491
            ";
492
493
        let row = sqlx::query(query)
494
            .bind(id)
495
            .fetch_optional(&self.pool)
496
            .await?;
497
498
        if let Some(row) = row {
499
            Ok(Some(self.row_to_asset_config(row)?))
500
        } else {
501
            Ok(None)
502
        }
503
    }
504
505
    /// Saves or updates an asset configuration.
506
    pub async fn save_asset_configuration(
507
        &self,
508
        config: &crate::asset_classification::AssetConfig,
509
    ) -> Result<(), sqlx::Error> {
510
        let query = "
511
                INSERT INTO asset_configurations (
512
                    id, name, symbol_pattern, asset_class_data, volatility_profile,
513
                    trading_parameters, priority, is_active, created_at, updated_at,
514
                    trading_hours, settlement_config
515
                ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
516
                ON CONFLICT (id) DO UPDATE SET
517
                    name = EXCLUDED.name,
518
                    symbol_pattern = EXCLUDED.symbol_pattern,
519
                    asset_class_data = EXCLUDED.asset_class_data,
520
                    volatility_profile = EXCLUDED.volatility_profile,
521
                    trading_parameters = EXCLUDED.trading_parameters,
522
                    priority = EXCLUDED.priority,
523
                    is_active = EXCLUDED.is_active,
524
                    updated_at = NOW(),
525
                    trading_hours = EXCLUDED.trading_hours,
526
                    settlement_config = EXCLUDED.settlement_config
527
            ";
528
529
        let asset_class_json = serde_json::to_value(&config.asset_class)
530
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
531
        let volatility_json = serde_json::to_value(&config.volatility_profile)
532
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
533
        let trading_params_json = serde_json::to_value(&config.trading_parameters)
534
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
535
        let trading_hours_json = serde_json::to_value(&config.trading_hours)
536
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
537
        let settlement_json = serde_json::to_value(&config.settlement_config)
538
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
539
540
        sqlx::query(query)
541
            .bind(config.id)
542
            .bind(&config.name)
543
            .bind(&config.symbol_pattern)
544
            .bind(asset_class_json)
545
            .bind(volatility_json)
546
            .bind(trading_params_json)
547
            .bind(config.priority as i32)
548
            .bind(config.is_active)
549
            .bind(config.created_at)
550
            .bind(config.updated_at)
551
            .bind(trading_hours_json)
552
            .bind(settlement_json)
553
            .execute(&self.pool)
554
            .await?;
555
556
        Ok(())
557
    }
558
559
    /// Loads explicit symbol mappings.
560
    pub async fn load_symbol_mappings(
561
        &self,
562
    ) -> Result<
563
        std::collections::HashMap<String, crate::asset_classification::AssetClass>,
564
        sqlx::Error,
565
    > {
566
        let query = "
567
                SELECT symbol, asset_class_data
568
                FROM symbol_mappings
569
                WHERE is_active = true AND (expires_at IS NULL OR expires_at > NOW())
570
            ";
571
572
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
573
574
        let mut mappings = std::collections::HashMap::new();
575
        for row in rows {
576
            let symbol: String = row.get("symbol");
577
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
578
579
            if let Ok(asset_class) =
580
                serde_json::from_value::<crate::asset_classification::AssetClass>(asset_class_json)
581
            {
582
                mappings.insert(symbol.to_uppercase(), asset_class);
583
            }
584
        }
585
586
        Ok(mappings)
587
    }
588
589
    /// Saves a symbol mapping.
590
    pub async fn save_symbol_mapping(
591
        &self,
592
        symbol: &str,
593
        asset_class: &crate::asset_classification::AssetClass,
594
        source: &str,
595
        confidence_score: f64,
596
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
597
    ) -> Result<(), sqlx::Error> {
598
        let query = "
599
                INSERT INTO symbol_mappings (
600
                    symbol, asset_class_data, source, confidence_score, expires_at
601
                ) VALUES ($1, $2, $3, $4, $5)
602
                ON CONFLICT (symbol) DO UPDATE SET
603
                    asset_class_data = EXCLUDED.asset_class_data,
604
                    source = EXCLUDED.source,
605
                    confidence_score = EXCLUDED.confidence_score,
606
                    expires_at = EXCLUDED.expires_at,
607
                    updated_at = NOW()
608
            ";
609
610
        let asset_class_json =
611
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
612
613
        sqlx::query(query)
614
            .bind(symbol.to_uppercase())
615
            .bind(asset_class_json)
616
            .bind(source)
617
            .bind(confidence_score)
618
            .bind(expires_at)
619
            .execute(&self.pool)
620
            .await?;
621
622
        Ok(())
623
    }
624
625
    /// Loads volatility profiles.
626
    pub async fn load_volatility_profiles(
627
        &self,
628
    ) -> Result<
629
        std::collections::HashMap<String, crate::asset_classification::VolatilityProfile>,
630
        sqlx::Error,
631
    > {
632
        let query = "
633
                SELECT 
634
                    name,
635
                    base_annual_volatility,
636
                    stress_volatility_multiplier,
637
                    intraday_pattern,
638
                    volatility_persistence,
639
                    jump_risk
640
                FROM volatility_profiles
641
                WHERE is_active = true
642
            ";
643
644
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
645
646
        let mut profiles = std::collections::HashMap::new();
647
        for row in rows {
648
            let name: String = row.get("name");
649
            let base_volatility: rust_decimal::Decimal = row.get("base_annual_volatility");
650
            let stress_multiplier: rust_decimal::Decimal = row.get("stress_volatility_multiplier");
651
            let persistence: rust_decimal::Decimal = row.get("volatility_persistence");
652
            let intraday_json: serde_json::Value = row.get("intraday_pattern");
653
            let jump_risk_json: serde_json::Value = row.get("jump_risk");
654
655
            if let (Ok(base_vol), Ok(stress_mult), Ok(persist), Ok(intraday), Ok(jump_risk)) = (
656
                f64::try_from(base_volatility),
657
                f64::try_from(stress_multiplier),
658
                f64::try_from(persistence),
659
                serde_json::from_value::<Vec<f64>>(intraday_json),
660
                serde_json::from_value::<crate::asset_classification::JumpRiskProfile>(
661
                    jump_risk_json,
662
                ),
663
            ) {
664
                let profile = crate::asset_classification::VolatilityProfile {
665
                    base_annual_volatility: base_vol,
666
                    stress_volatility_multiplier: stress_mult,
667
                    intraday_pattern: intraday,
668
                    volatility_persistence: persist,
669
                    jump_risk,
670
                };
671
                profiles.insert(name, profile);
672
            }
673
        }
674
675
        Ok(profiles)
676
    }
677
678
    /// Caches symbol classification for performance.
679
    pub async fn cache_symbol_classification(
680
        &self,
681
        symbol: &str,
682
        asset_class: &crate::asset_classification::AssetClass,
683
        configuration_id: Option<uuid::Uuid>,
684
    ) -> Result<(), sqlx::Error> {
685
        let query = "
686
                INSERT INTO asset_classification_cache (symbol, asset_class_data, configuration_id)
687
                VALUES ($1, $2, $3)
688
                ON CONFLICT (symbol) DO UPDATE SET
689
                    asset_class_data = EXCLUDED.asset_class_data,
690
                    configuration_id = EXCLUDED.configuration_id,
691
                    cached_at = NOW(),
692
                    expires_at = NOW() + INTERVAL '1 hour'
693
            ";
694
695
        let asset_class_json =
696
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
697
698
        sqlx::query(query)
699
            .bind(symbol.to_uppercase())
700
            .bind(asset_class_json)
701
            .bind(configuration_id)
702
            .execute(&self.pool)
703
            .await?;
704
705
        Ok(())
706
    }
707
708
    /// Retrieves cached symbol classification.
709
    pub async fn get_cached_classification(
710
        &self,
711
        symbol: &str,
712
    ) -> Result<Option<crate::asset_classification::AssetClass>, sqlx::Error> {
713
        let query = "
714
                SELECT asset_class_data
715
                FROM asset_classification_cache
716
                WHERE symbol = $1 AND expires_at > NOW()
717
            ";
718
719
        let row = sqlx::query(query)
720
            .bind(symbol.to_uppercase())
721
            .fetch_optional(&self.pool)
722
            .await?;
723
724
        if let Some(row) = row {
725
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
726
            Ok(serde_json::from_value(asset_class_json).ok())
727
        } else {
728
            Ok(None)
729
        }
730
    }
731
732
    /// Cleans up expired cache entries.
733
    pub async fn cleanup_cache(&self) -> Result<u64, sqlx::Error> {
734
        let query = "DELETE FROM asset_classification_cache WHERE expires_at < NOW()";
735
        let result = sqlx::query(query).execute(&self.pool).await?;
736
        Ok(result.rows_affected())
737
    }
738
739
    /// Logs asset classification changes for audit.
740
    pub async fn log_classification_change(
741
        &self,
742
        symbol: &str,
743
        old_classification: Option<&crate::asset_classification::AssetClass>,
744
        new_classification: &crate::asset_classification::AssetClass,
745
        changed_by: &str,
746
        reason: &str,
747
    ) -> Result<(), sqlx::Error> {
748
        let query = "
749
                INSERT INTO asset_classification_audit (
750
                    symbol, old_classification, new_classification, changed_by, change_reason
751
                ) VALUES ($1, $2, $3, $4, $5)
752
            ";
753
754
        let old_json = old_classification
755
            .map(|c| serde_json::to_value(c).ok())
756
            .flatten();
757
        let new_json = serde_json::to_value(new_classification)
758
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
759
760
        sqlx::query(query)
761
            .bind(symbol)
762
            .bind(old_json)
763
            .bind(new_json)
764
            .bind(changed_by)
765
            .bind(reason)
766
            .execute(&self.pool)
767
            .await?;
768
769
        Ok(())
770
    }
771
772
    /// Enables PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
773
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
774
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
775
        listener.listen("config_change").await?;
776
        self.listener = Some(listener);
777
        Ok(())
778
    }
779
780
    /// Checks for configuration change notifications.
781
    pub async fn check_for_config_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
782
        if let Some(listener) = &mut self.listener {
783
            if let Some(notification) = listener.try_recv().await? {
784
                return Ok(Some(notification.payload().to_string()));
785
            }
786
        }
787
        Ok(None)
788
    }
789
790
    /// Converts a database row to AssetConfig.
791
    fn row_to_asset_config(
792
        &self,
793
        row: sqlx::postgres::PgRow,
794
    ) -> Result<crate::asset_classification::AssetConfig, sqlx::Error> {
795
        let id: uuid::Uuid = row.get("id");
796
        let name: String = row.get("name");
797
        let symbol_pattern: String = row.get("symbol_pattern");
798
        let priority: i32 = row.get("priority");
799
        let is_active: bool = row.get("is_active");
800
        let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
801
        let updated_at: chrono::DateTime<chrono::Utc> = row.get("updated_at");
802
803
        let asset_class_json: serde_json::Value = row.get("asset_class_data");
804
        let volatility_json: serde_json::Value = row.get("volatility_profile");
805
        let trading_params_json: serde_json::Value = row.get("trading_parameters");
806
        let trading_hours_json: Option<serde_json::Value> = row.get("trading_hours");
807
        let settlement_json: serde_json::Value = row.get("settlement_config");
808
809
        let asset_class = serde_json::from_value(asset_class_json)
810
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
811
        let volatility_profile = serde_json::from_value(volatility_json)
812
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
813
        let trading_parameters = serde_json::from_value(trading_params_json)
814
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
815
        let trading_hours = trading_hours_json
816
            .map(|json| serde_json::from_value(json).ok())
817
            .flatten();
818
        let settlement_config = serde_json::from_value(settlement_json)
819
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
820
821
        Ok(crate::asset_classification::AssetConfig {
822
            id,
823
            name,
824
            symbol_pattern,
825
            compiled_pattern: None, // Will be compiled when loaded
826
            asset_class,
827
            volatility_profile,
828
            trading_parameters,
829
            priority: priority as u32,
830
            is_active,
831
            created_at,
832
            updated_at,
833
            trading_hours,
834
            settlement_config,
835
        })
836
    }
837
}
838
839
/// General-purpose PostgreSQL configuration loader for various configuration types.
840
///
841
/// Provides a unified interface for loading configurations from PostgreSQL with
842
/// support for hot-reload through NOTIFY/LISTEN and caching for performance.
843
#[cfg(feature = "postgres")]
844
pub struct PostgresConfigLoader {
845
    /// Database connection pool
846
    pool: sqlx::PgPool,
847
    /// Configuration cache timeout
848
    cache_timeout: Duration,
849
}
850
851
#[cfg(feature = "postgres")]
852
impl PostgresConfigLoader {
853
    /// Creates a new PostgreSQL configuration loader.
854
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
855
        let pool = sqlx::PgPool::connect(database_url).await?;
856
857
        Ok(Self {
858
            pool,
859
            cache_timeout: Duration::from_secs(300), // 5 minutes
860
        })
861
    }
862
863
    /// Creates a new loader with an existing connection pool.
864
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
865
        Self {
866
            pool,
867
            cache_timeout: Duration::from_secs(300),
868
        }
869
    }
870
871
    /// Get the underlying connection pool.
872
    pub fn pool(&self) -> &sqlx::PgPool {
873
        &self.pool
874
    }
875
876
    // ============================================================================
877
    // ADAPTIVE STRATEGY CONFIGURATION METHODS
878
    // ============================================================================
879
880
    /// Get adaptive strategy configuration by strategy ID.
881
    ///
882
    /// Loads the complete configuration including main settings, models, and features
883
    /// from the PostgreSQL database. Returns None if the strategy doesn't exist.
884
    ///
885
    /// # Arguments
886
    /// * `strategy_id` - Unique identifier for the strategy (e.g., "default", "prod_v1")
887
    ///
888
    /// # Returns
889
    /// - `Ok(Some(config))` - Configuration found and loaded successfully
890
    /// - `Ok(None)` - Strategy ID not found in database
891
    /// - `Err(sqlx::Error)` - Database error occurred
892
    ///
893
    /// # Example
894
    /// ```no_run
895
    /// # use config::PostgresConfigLoader;
896
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
897
    /// let config = loader.get_adaptive_strategy_config("default").await?;
898
    /// if let Some(cfg) = config {
899
    ///     println!("Loaded strategy: {}", cfg.name);
900
    /// }
901
    /// # Ok(())
902
    /// # }
903
    /// ```
904
    pub async fn get_adaptive_strategy_config(
905
        &self,
906
        strategy_id: &str,
907
    ) -> Result<Option<serde_json::Value>, sqlx::Error> {
908
        // Query main configuration
909
        let row = sqlx::query(
910
            r#"
911
            SELECT
912
                id, strategy_id, name, description,
913
                execution_interval_ms, error_backoff_duration_secs,
914
                max_concurrent_operations, strategy_timeout_secs,
915
                max_parallel_models, rebalancing_interval_secs,
916
                min_model_weight, max_model_weight,
917
                max_position_size, max_leverage, stop_loss_pct,
918
                position_sizing_method, max_portfolio_var,
919
                max_drawdown_threshold, kelly_fraction,
920
                book_depth, vpin_window, trade_classification_threshold,
921
                trade_size_buckets, microstructure_features,
922
                regime_detection_method, regime_lookback_window,
923
                regime_transition_threshold, regime_features,
924
                execution_algorithm, max_order_size, min_order_size,
925
                order_timeout_secs, max_slippage_bps,
926
                smart_routing_enabled, dark_pool_preference,
927
                active, version, created_at, updated_at,
928
                created_by, updated_by, metadata
929
            FROM adaptive_strategy_config
930
            WHERE strategy_id = $1 AND active = true
931
            "#,
932
        )
933
        .bind(strategy_id)
934
        .fetch_optional(&self.pool)
935
        .await?;
936
937
        let Some(row) = row else {
938
            return Ok(None);
939
        };
940
941
        let config_id: uuid::Uuid = row.try_get("id")?;
942
943
        // Query associated models
944
        let models = sqlx::query(
945
            r#"
946
            SELECT
947
                id, strategy_config_id, model_id, model_name, model_type,
948
                parameters, initial_weight, enabled, display_order,
949
                created_at, updated_at
950
            FROM adaptive_strategy_models
951
            WHERE strategy_config_id = $1
952
            ORDER BY display_order, created_at
953
            "#,
954
        )
955
        .bind(config_id)
956
        .fetch_all(&self.pool)
957
        .await?;
958
959
        // Query associated features
960
        let features = sqlx::query(
961
            r#"
962
            SELECT
963
                id, strategy_config_id, feature_name, feature_type,
964
                parameters, enabled, required,
965
                created_at, updated_at
966
            FROM adaptive_strategy_features
967
            WHERE strategy_config_id = $1
968
            ORDER BY feature_name
969
            "#,
970
        )
971
        .bind(config_id)
972
        .fetch_all(&self.pool)
973
        .await?;
974
975
        // Convert to JSON for flexibility
976
        // In production, you'd convert to a proper struct type
977
        let config = serde_json::json!({
978
            "id": row.try_get::<uuid::Uuid, _>("id")?,
979
            "strategy_id": row.try_get::<String, _>("strategy_id")?,
980
            "name": row.try_get::<String, _>("name")?,
981
            "description": row.try_get::<Option<String>, _>("description")?,
982
            "general": {
983
                "execution_interval_ms": row.try_get::<i32, _>("execution_interval_ms")?,
984
                "error_backoff_duration_secs": row.try_get::<i32, _>("error_backoff_duration_secs")?,
985
                "max_concurrent_operations": row.try_get::<i32, _>("max_concurrent_operations")?,
986
                "strategy_timeout_secs": row.try_get::<i32, _>("strategy_timeout_secs")?,
987
            },
988
            "ensemble": {
989
                "max_parallel_models": row.try_get::<i32, _>("max_parallel_models")?,
990
                "rebalancing_interval_secs": row.try_get::<i32, _>("rebalancing_interval_secs")?,
991
                "min_model_weight": row.try_get::<f64, _>("min_model_weight")?,
992
                "max_model_weight": row.try_get::<f64, _>("max_model_weight")?,
993
            },
994
            "risk": {
995
                "max_position_size": row.try_get::<f64, _>("max_position_size")?,
996
                "max_leverage": row.try_get::<f64, _>("max_leverage")?,
997
                "stop_loss_pct": row.try_get::<f64, _>("stop_loss_pct")?,
998
                "position_sizing_method": row.try_get::<String, _>("position_sizing_method")?,
999
                "max_portfolio_var": row.try_get::<f64, _>("max_portfolio_var")?,
1000
                "max_drawdown_threshold": row.try_get::<f64, _>("max_drawdown_threshold")?,
1001
                "kelly_fraction": row.try_get::<f64, _>("kelly_fraction")?,
1002
            },
1003
            "microstructure": {
1004
                "book_depth": row.try_get::<i32, _>("book_depth")?,
1005
                "vpin_window": row.try_get::<i32, _>("vpin_window")?,
1006
                "trade_classification_threshold": row.try_get::<f64, _>("trade_classification_threshold")?,
1007
                "trade_size_buckets": row.try_get::<Vec<f64>, _>("trade_size_buckets")?,
1008
                "features": row.try_get::<Vec<String>, _>("microstructure_features")?,
1009
            },
1010
            "regime": {
1011
                "detection_method": row.try_get::<String, _>("regime_detection_method")?,
1012
                "lookback_window": row.try_get::<i32, _>("regime_lookback_window")?,
1013
                "transition_threshold": row.try_get::<f64, _>("regime_transition_threshold")?,
1014
                "features": row.try_get::<Vec<String>, _>("regime_features")?,
1015
            },
1016
            "execution": {
1017
                "algorithm": row.try_get::<String, _>("execution_algorithm")?,
1018
                "max_order_size": row.try_get::<f64, _>("max_order_size")?,
1019
                "min_order_size": row.try_get::<f64, _>("min_order_size")?,
1020
                "order_timeout_secs": row.try_get::<i32, _>("order_timeout_secs")?,
1021
                "max_slippage_bps": row.try_get::<f64, _>("max_slippage_bps")?,
1022
                "smart_routing_enabled": row.try_get::<bool, _>("smart_routing_enabled")?,
1023
                "dark_pool_preference": row.try_get::<f64, _>("dark_pool_preference")?,
1024
            },
1025
            "models": models.iter().map(|m| serde_json::json!({
1026
                "id": m.try_get::<uuid::Uuid, _>("id").unwrap(),
1027
                "model_id": m.try_get::<String, _>("model_id").unwrap(),
1028
                "model_name": m.try_get::<String, _>("model_name").unwrap(),
1029
                "model_type": m.try_get::<String, _>("model_type").unwrap(),
1030
                "parameters": m.try_get::<serde_json::Value, _>("parameters").unwrap(),
1031
                "initial_weight": m.try_get::<f64, _>("initial_weight").unwrap(),
1032
                "enabled": m.try_get::<bool, _>("enabled").unwrap(),
1033
            })).collect::<Vec<_>>(),
1034
            "features": features.iter().map(|f| serde_json::json!({
1035
                "name": f.try_get::<String, _>("feature_name").unwrap(),
1036
                "feature_type": f.try_get::<String, _>("feature_type").unwrap(),
1037
                "parameters": f.try_get::<serde_json::Value, _>("parameters").unwrap(),
1038
                "enabled": f.try_get::<bool, _>("enabled").unwrap(),
1039
                "required": f.try_get::<bool, _>("required").unwrap(),
1040
            })).collect::<Vec<_>>(),
1041
            "version": row.try_get::<i32, _>("version")?,
1042
            "created_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")?,
1043
            "updated_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("updated_at")?,
1044
        });
1045
1046
        Ok(Some(config))
1047
    }
1048
1049
    /// Upsert (insert or update) adaptive strategy configuration.
1050
    ///
1051
    /// Creates a new strategy configuration if it doesn't exist, or updates
1052
    /// the existing one. Automatically handles version tracking and audit trail.
1053
    ///
1054
    /// # Arguments
1055
    /// * `config` - Configuration data as JSON (allows flexibility in structure)
1056
    ///
1057
    /// # Returns
1058
    /// - `Ok(strategy_id)` - Strategy ID of the created/updated configuration
1059
    /// - `Err(sqlx::Error)` - Database error occurred
1060
    ///
1061
    /// # Example
1062
    /// ```no_run
1063
    /// # use config::PostgresConfigLoader;
1064
    /// # use serde_json::json;
1065
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
1066
    /// let config = json!({
1067
    ///     "strategy_id": "my_strategy",
1068
    ///     "name": "My Trading Strategy",
1069
    ///     "risk": {
1070
    ///         "max_position_size": 0.15,
1071
    ///         "max_leverage": 3.0
1072
    ///     }
1073
    /// });
1074
    /// let id = loader.upsert_adaptive_strategy_config(&config).await?;
1075
    /// # Ok(())
1076
    /// # }
1077
    /// ```
1078
    pub async fn upsert_adaptive_strategy_config(
1079
        &self,
1080
        config: &serde_json::Value,
1081
    ) -> Result<String, sqlx::Error> {
1082
        let strategy_id = config
1083
            .get("strategy_id")
1084
            .and_then(|v| v.as_str())
1085
            .ok_or_else(|| {
1086
                sqlx::Error::Decode(Box::new(std::io::Error::new(
1087
                    std::io::ErrorKind::InvalidData,
1088
                    "Missing strategy_id in config",
1089
                )))
1090
            })?;
1091
    
1092
        // Extract all configuration fields
1093
        let name = config.get("name").and_then(|v| v.as_str()).unwrap_or("Unnamed Strategy");
1094
        let description = config.get("description").and_then(|v| v.as_str());
1095
    
1096
        // Helper macro for extracting fields with defaults
1097
        macro_rules! get_i32 {
1098
            ($field:expr, $default:expr) => {
1099
                config.get($field).and_then(|v| v.as_i64()).map(|v| v as i32).unwrap_or($default)
1100
            };
1101
        }
1102
        macro_rules! get_f64 {
1103
            ($field:expr, $default:expr) => {
1104
                config.get($field).and_then(|v| v.as_f64()).unwrap_or($default)
1105
            };
1106
        }
1107
        macro_rules! get_bool {
1108
            ($field:expr, $default:expr) => {
1109
                config.get($field).and_then(|v| v.as_bool()).unwrap_or($default)
1110
            };
1111
        }
1112
        macro_rules! get_str {
1113
            ($field:expr, $default:expr) => {
1114
                config.get($field).and_then(|v| v.as_str()).unwrap_or($default)
1115
            };
1116
        }
1117
    
1118
        // Full upsert with all 50+ fields
1119
        let query = r#"
1120
            INSERT INTO adaptive_strategy_config (
1121
                strategy_id, name, description,
1122
                -- General config
1123
                execution_interval_ms, error_backoff_duration_secs,
1124
                max_concurrent_operations, strategy_timeout_secs,
1125
                -- Ensemble config
1126
                max_parallel_models, rebalancing_interval_secs,
1127
                min_model_weight, max_model_weight,
1128
                -- Risk config
1129
                max_position_size, max_leverage, stop_loss_pct,
1130
                position_sizing_method, max_portfolio_var,
1131
                max_drawdown_threshold, kelly_fraction,
1132
                -- Microstructure config
1133
                book_depth, vpin_window, trade_classification_threshold,
1134
                trade_size_buckets, microstructure_features,
1135
                -- Regime config
1136
                regime_detection_method, regime_lookback_window,
1137
                regime_transition_threshold, regime_features,
1138
                -- Execution config
1139
                execution_algorithm, max_order_size, min_order_size,
1140
                order_timeout_secs, max_slippage_bps,
1141
                smart_routing_enabled, dark_pool_preference
1142
            ) VALUES (
1143
                $1, $2, $3,
1144
                $4, $5, $6, $7,
1145
                $8, $9, $10, $11,
1146
                $12, $13, $14, $15, $16, $17, $18,
1147
                $19, $20, $21, $22, $23,
1148
                $24, $25, $26, $27,
1149
                $28, $29, $30, $31, $32, $33, $34
1150
            )
1151
            ON CONFLICT (strategy_id)
1152
            DO UPDATE SET
1153
                name = EXCLUDED.name,
1154
                description = EXCLUDED.description,
1155
                execution_interval_ms = EXCLUDED.execution_interval_ms,
1156
                error_backoff_duration_secs = EXCLUDED.error_backoff_duration_secs,
1157
                max_concurrent_operations = EXCLUDED.max_concurrent_operations,
1158
                strategy_timeout_secs = EXCLUDED.strategy_timeout_secs,
1159
                max_parallel_models = EXCLUDED.max_parallel_models,
1160
                rebalancing_interval_secs = EXCLUDED.rebalancing_interval_secs,
1161
                min_model_weight = EXCLUDED.min_model_weight,
1162
                max_model_weight = EXCLUDED.max_model_weight,
1163
                max_position_size = EXCLUDED.max_position_size,
1164
                max_leverage = EXCLUDED.max_leverage,
1165
                stop_loss_pct = EXCLUDED.stop_loss_pct,
1166
                position_sizing_method = EXCLUDED.position_sizing_method,
1167
                max_portfolio_var = EXCLUDED.max_portfolio_var,
1168
                max_drawdown_threshold = EXCLUDED.max_drawdown_threshold,
1169
                kelly_fraction = EXCLUDED.kelly_fraction,
1170
                book_depth = EXCLUDED.book_depth,
1171
                vpin_window = EXCLUDED.vpin_window,
1172
                trade_classification_threshold = EXCLUDED.trade_classification_threshold,
1173
                trade_size_buckets = EXCLUDED.trade_size_buckets,
1174
                microstructure_features = EXCLUDED.microstructure_features,
1175
                regime_detection_method = EXCLUDED.regime_detection_method,
1176
                regime_lookback_window = EXCLUDED.regime_lookback_window,
1177
                regime_transition_threshold = EXCLUDED.regime_transition_threshold,
1178
                regime_features = EXCLUDED.regime_features,
1179
                execution_algorithm = EXCLUDED.execution_algorithm,
1180
                max_order_size = EXCLUDED.max_order_size,
1181
                min_order_size = EXCLUDED.min_order_size,
1182
                order_timeout_secs = EXCLUDED.order_timeout_secs,
1183
                max_slippage_bps = EXCLUDED.max_slippage_bps,
1184
                smart_routing_enabled = EXCLUDED.smart_routing_enabled,
1185
                dark_pool_preference = EXCLUDED.dark_pool_preference,
1186
                updated_at = NOW()
1187
            RETURNING strategy_id
1188
        "#;
1189
    
1190
        // Extract trade_size_buckets and features arrays
1191
        let trade_size_buckets: Vec<f64> = config.get("trade_size_buckets")
1192
            .and_then(|v| v.as_array())
1193
            .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
1194
            .unwrap_or_else(|| vec![10.0, 100.0, 1000.0, 10000.0]);
1195
    
1196
        let microstructure_features: Vec<String> = config.get("microstructure_features")
1197
            .and_then(|v| v.as_array())
1198
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1199
            .unwrap_or_else(|| vec!["vpin".to_string(), "order_flow".to_string(), "bid_ask_spread".to_string()]);
1200
    
1201
        let regime_features: Vec<String> = config.get("regime_features")
1202
            .and_then(|v| v.as_array())
1203
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1204
            .unwrap_or_else(|| vec!["volatility".to_string(), "momentum".to_string(), "volume".to_string()]);
1205
    
1206
        let row = sqlx::query(query)
1207
            .bind(strategy_id)
1208
            .bind(name)
1209
            .bind(description)
1210
            // General config (4 fields)
1211
            .bind(get_i32!("execution_interval_ms", 100))
1212
            .bind(get_i32!("error_backoff_duration_secs", 1))
1213
            .bind(get_i32!("max_concurrent_operations", 10))
1214
            .bind(get_i32!("strategy_timeout_secs", 30))
1215
            // Ensemble config (4 fields)
1216
            .bind(get_i32!("max_parallel_models", 4))
1217
            .bind(get_i32!("rebalancing_interval_secs", 300))
1218
            .bind(get_f64!("min_model_weight", 0.01))
1219
            .bind(get_f64!("max_model_weight", 0.5))
1220
            // Risk config (7 fields)
1221
            .bind(get_f64!("max_position_size", 0.1))
1222
            .bind(get_f64!("max_leverage", 2.0))
1223
            .bind(get_f64!("stop_loss_pct", 0.02))
1224
            .bind(get_str!("position_sizing_method", "KELLY"))
1225
            .bind(get_f64!("max_portfolio_var", 0.02))
1226
            .bind(get_f64!("max_drawdown_threshold", 0.05))
1227
            .bind(get_f64!("kelly_fraction", 0.1))
1228
            // Microstructure config (5 fields)
1229
            .bind(get_i32!("book_depth", 10))
1230
            .bind(get_i32!("vpin_window", 50))
1231
            .bind(get_f64!("trade_classification_threshold", 0.5))
1232
            .bind(&trade_size_buckets)
1233
            .bind(&microstructure_features)
1234
            // Regime config (4 fields)
1235
            .bind(get_str!("regime_detection_method", "HMM"))
1236
            .bind(get_i32!("regime_lookback_window", 252))
1237
            .bind(get_f64!("regime_transition_threshold", 0.7))
1238
            .bind(&regime_features)
1239
            // Execution config (7 fields)
1240
            .bind(get_str!("execution_algorithm", "TWAP"))
1241
            .bind(get_f64!("max_order_size", 10000.0))
1242
            .bind(get_f64!("min_order_size", 100.0))
1243
            .bind(get_i32!("order_timeout_secs", 30))
1244
            .bind(get_f64!("max_slippage_bps", 10.0))
1245
            .bind(get_bool!("smart_routing_enabled", true))
1246
            .bind(get_f64!("dark_pool_preference", 0.3))
1247
            .fetch_one(&self.pool)
1248
            .await?;
1249
    
1250
                let result: String = row.try_get("strategy_id")?;
1251
                Ok(result)
1252
            }
1253
        
1254
            // ========================================================================
1255
            // MODEL CRUD OPERATIONS
1256
            // ========================================================================
1257
        
1258
            /// Add a model configuration to a strategy
1259
            ///
1260
            /// # Arguments
1261
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1262
            /// * `model` - Model configuration as JSON
1263
            ///
1264
            /// # Returns
1265
            /// UUID of the created model configuration
1266
            pub async fn add_model_config(
1267
                &self,
1268
                strategy_config_id: uuid::Uuid,
1269
                model: &serde_json::Value,
1270
            ) -> Result<uuid::Uuid, sqlx::Error> {
1271
                let model_id = model.get("model_id")
1272
                    .and_then(|v| v.as_str())
1273
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1274
                        std::io::ErrorKind::InvalidData,
1275
                        "Missing model_id"
1276
                    ))))?;
1277
        
1278
                let query = r#"
1279
                    INSERT INTO adaptive_strategy_models (
1280
                        strategy_config_id, model_id, model_name, model_type,
1281
                        parameters, initial_weight, enabled, display_order
1282
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1283
                    RETURNING id
1284
                "#;
1285
        
1286
                let row = sqlx::query(query)
1287
                    .bind(strategy_config_id)
1288
                    .bind(model_id)
1289
                    .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or(model_id))
1290
                    .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1291
                    .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1292
                    .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1293
                    .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1294
                    .bind(model.get("display_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32)
1295
                    .fetch_one(&self.pool)
1296
                    .await?;
1297
        
1298
                row.try_get("id")
1299
            }
1300
        
1301
            /// Update a model configuration
1302
            ///
1303
            /// # Arguments
1304
            /// * `model_id` - UUID of the model to update
1305
            /// * `updates` - Fields to update as JSON
1306
            pub async fn update_model_config(
1307
                &self,
1308
                model_id: uuid::Uuid,
1309
                updates: &serde_json::Value,
1310
            ) -> Result<(), sqlx::Error> {
1311
                let query = r#"
1312
                    UPDATE adaptive_strategy_models
1313
                    SET
1314
                        model_name = COALESCE($1, model_name),
1315
                        model_type = COALESCE($2, model_type),
1316
                        parameters = COALESCE($3, parameters),
1317
                        initial_weight = COALESCE($4, initial_weight),
1318
                        enabled = COALESCE($5, enabled),
1319
                        display_order = COALESCE($6, display_order),
1320
                        updated_at = NOW()
1321
                    WHERE id = $7
1322
                "#;
1323
        
1324
                sqlx::query(query)
1325
                    .bind(updates.get("model_name").and_then(|v| v.as_str()))
1326
                    .bind(updates.get("model_type").and_then(|v| v.as_str()))
1327
                    .bind(updates.get("parameters"))
1328
                    .bind(updates.get("initial_weight").and_then(|v| v.as_f64()))
1329
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1330
                    .bind(updates.get("display_order").and_then(|v| v.as_i64()).map(|v| v as i32))
1331
                    .bind(model_id)
1332
                    .execute(&self.pool)
1333
                    .await?;
1334
        
1335
                Ok(())
1336
            }
1337
        
1338
            /// Remove a model configuration
1339
            ///
1340
            /// # Arguments
1341
            /// * `model_id` - UUID of the model to remove
1342
            pub async fn remove_model_config(
1343
                &self,
1344
                model_id: uuid::Uuid,
1345
            ) -> Result<(), sqlx::Error> {
1346
                let query = "DELETE FROM adaptive_strategy_models WHERE id = $1";
1347
                sqlx::query(query)
1348
                    .bind(model_id)
1349
                    .execute(&self.pool)
1350
                    .await?;
1351
                Ok(())
1352
            }
1353
        
1354
            // ========================================================================
1355
            // FEATURE CRUD OPERATIONS
1356
            // ========================================================================
1357
        
1358
            /// Add a feature configuration to a strategy
1359
            ///
1360
            /// # Arguments
1361
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1362
            /// * `feature` - Feature configuration as JSON
1363
            ///
1364
            /// # Returns
1365
            /// UUID of the created feature configuration
1366
            pub async fn add_feature_config(
1367
                &self,
1368
                strategy_config_id: uuid::Uuid,
1369
                feature: &serde_json::Value,
1370
            ) -> Result<uuid::Uuid, sqlx::Error> {
1371
                let feature_name = feature.get("feature_name")
1372
                    .and_then(|v| v.as_str())
1373
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1374
                        std::io::ErrorKind::InvalidData,
1375
                        "Missing feature_name"
1376
                    ))))?;
1377
        
1378
                let query = r#"
1379
                    INSERT INTO adaptive_strategy_features (
1380
                        strategy_config_id, feature_name, feature_type,
1381
                        parameters, enabled, required
1382
                    ) VALUES ($1, $2, $3, $4, $5, $6)
1383
                    RETURNING id
1384
                "#;
1385
        
1386
                let row = sqlx::query(query)
1387
                    .bind(strategy_config_id)
1388
                    .bind(feature_name)
1389
                    .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1390
                    .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1391
                    .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1392
                    .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1393
                    .fetch_one(&self.pool)
1394
                    .await?;
1395
        
1396
                row.try_get("id")
1397
            }
1398
        
1399
            /// Update a feature configuration
1400
            ///
1401
            /// # Arguments
1402
            /// * `feature_id` - UUID of the feature to update
1403
            /// * `updates` - Fields to update as JSON
1404
            pub async fn update_feature_config(
1405
                &self,
1406
                feature_id: uuid::Uuid,
1407
                updates: &serde_json::Value,
1408
            ) -> Result<(), sqlx::Error> {
1409
                let query = r#"
1410
                    UPDATE adaptive_strategy_features
1411
                    SET
1412
                        feature_type = COALESCE($1, feature_type),
1413
                        parameters = COALESCE($2, parameters),
1414
                        enabled = COALESCE($3, enabled),
1415
                        required = COALESCE($4, required),
1416
                        updated_at = NOW()
1417
                    WHERE id = $5
1418
                "#;
1419
        
1420
                sqlx::query(query)
1421
                    .bind(updates.get("feature_type").and_then(|v| v.as_str()))
1422
                    .bind(updates.get("parameters"))
1423
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1424
                    .bind(updates.get("required").and_then(|v| v.as_bool()))
1425
                    .bind(feature_id)
1426
                    .execute(&self.pool)
1427
                    .await?;
1428
        
1429
                Ok(())
1430
            }
1431
        
1432
            /// Remove a feature configuration
1433
            ///
1434
            /// # Arguments
1435
            /// * `feature_id` - UUID of the feature to remove
1436
            pub async fn remove_feature_config(
1437
                &self,
1438
                feature_id: uuid::Uuid,
1439
            ) -> Result<(), sqlx::Error> {
1440
                let query = "DELETE FROM adaptive_strategy_features WHERE id = $1";
1441
                sqlx::query(query)
1442
                    .bind(feature_id)
1443
                    .execute(&self.pool)
1444
                    .await?;
1445
                Ok(())
1446
            }
1447
        
1448
            // ========================================================================
1449
            // TRANSACTION SUPPORT
1450
            // ========================================================================
1451
        
1452
            /// Update strategy configuration with models and features in a single transaction
1453
            ///
1454
            /// Provides atomic updates across all three tables:
1455
            /// - adaptive_strategy_config (main configuration)
1456
            /// - adaptive_strategy_models (model configurations)
1457
            /// - adaptive_strategy_features (feature configurations)
1458
            ///
1459
            /// # Arguments
1460
            /// * `config` - Full configuration including models and features
1461
            ///
1462
            /// # Returns
1463
            /// Strategy ID of the updated configuration
1464
            pub async fn update_strategy_atomic(
1465
                &self,
1466
                config: &serde_json::Value,
1467
            ) -> Result<String, sqlx::Error> {
1468
                // Start transaction
1469
                let mut tx = self.pool.begin().await?;
1470
        
1471
                // 1. Upsert main configuration
1472
                let strategy_id = config.get("strategy_id")
1473
                    .and_then(|v| v.as_str())
1474
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1475
                        std::io::ErrorKind::InvalidData,
1476
                        "Missing strategy_id"
1477
                    ))))?;
1478
        
1479
                // Get or create config_id
1480
                let config_id: uuid::Uuid = sqlx::query_scalar(
1481
                    "SELECT id FROM adaptive_strategy_config WHERE strategy_id = $1"
1482
                )
1483
                .bind(strategy_id)
1484
                .fetch_optional(&mut *tx)
1485
                .await?
1486
                .unwrap_or_else(uuid::Uuid::new_v4);
1487
        
1488
                // 2. Update models if provided
1489
                if let Some(models) = config.get("models").and_then(|v| v.as_array()) {
1490
                    // Delete existing models
1491
                    sqlx::query("DELETE FROM adaptive_strategy_models WHERE strategy_config_id = $1")
1492
                        .bind(config_id)
1493
                        .execute(&mut *tx)
1494
                        .await?;
1495
        
1496
                    // Insert new models
1497
                    for model in models {
1498
                        sqlx::query(r#"
1499
                            INSERT INTO adaptive_strategy_models (
1500
                                strategy_config_id, model_id, model_name, model_type,
1501
                                parameters, initial_weight, enabled
1502
                            ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1503
                        "#)
1504
                        .bind(config_id)
1505
                        .bind(model.get("model_id").and_then(|v| v.as_str()).unwrap_or("unknown"))
1506
                        .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or("Unknown Model"))
1507
                        .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1508
                        .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1509
                        .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1510
                        .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1511
                        .execute(&mut *tx)
1512
                        .await?;
1513
                    }
1514
                }
1515
        
1516
                // 3. Update features if provided
1517
                if let Some(features) = config.get("features").and_then(|v| v.as_array()) {
1518
                    // Delete existing features
1519
                    sqlx::query("DELETE FROM adaptive_strategy_features WHERE strategy_config_id = $1")
1520
                        .bind(config_id)
1521
                        .execute(&mut *tx)
1522
                        .await?;
1523
        
1524
                    // Insert new features
1525
                    for feature in features {
1526
                        sqlx::query(r#"
1527
                            INSERT INTO adaptive_strategy_features (
1528
                                strategy_config_id, feature_name, feature_type,
1529
                                parameters, enabled, required
1530
                            ) VALUES ($1, $2, $3, $4, $5, $6)
1531
                        "#)
1532
                        .bind(config_id)
1533
                        .bind(feature.get("feature_name").and_then(|v| v.as_str()).unwrap_or("unknown"))
1534
                        .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1535
                        .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1536
                        .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1537
                        .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1538
                        .execute(&mut *tx)
1539
                        .await?;
1540
                    }
1541
                }
1542
        
1543
                // Commit transaction
1544
                tx.commit().await?;
1545
        
1546
                Ok(strategy_id.to_string())
1547
            }
1548
        }
1549
        
1550
        #[cfg(test)]
1551
mod tests {
1552
    use super::*;
1553
1554
    #[test]
1555
1
    fn test_database_config_new() {
1556
1
        let config = DatabaseConfig::new();
1557
1
        assert!(!config.url.is_empty());
1558
1
        assert_eq!(config.max_connections, 10);
1559
1
        assert_eq!(config.min_connections, 1);
1560
1
        assert!(config.application_name.is_some());
1561
1
    }
1562
1563
    #[test]
1564
1
    fn test_database_config_validate_success() {
1565
1
        let config = DatabaseConfig::new();
1566
1
        assert!(config.validate().is_ok());
1567
1
    }
1568
1569
    #[test]
1570
1
    fn test_database_config_validate_empty_url() {
1571
1
        let mut config = DatabaseConfig::new();
1572
1
        config.url = String::new();
1573
1
        assert!(config.validate().is_err());
1574
1
    }
1575
1576
    #[test]
1577
1
    fn test_pool_config_default() {
1578
1
        let pool_config = PoolConfig::default();
1579
1
        assert_eq!(pool_config.min_connections, 1);
1580
1
        assert_eq!(pool_config.max_connections, 10);
1581
1
        assert!(pool_config.test_before_acquire);
1582
1
    }
1583
1584
    #[test]
1585
1
    fn test_transaction_config_default() {
1586
1
        let tx_config = TransactionConfig::default();
1587
1
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1588
1
        assert_eq!(tx_config.default_timeout_secs, 30);
1589
1
        assert!(tx_config.enable_retry);
1590
1
    }
1591
1592
    #[test]
1593
1
    fn test_transaction_config_serialization() {
1594
1
        let tx_config = TransactionConfig::default();
1595
1
        let serialized = serde_json::to_string(&tx_config).unwrap();
1596
1
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1597
1
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1598
1
    }
1599
1600
    #[test]
1601
1
    fn test_database_config_with_custom_values() {
1602
1
        let mut config = DatabaseConfig::new();
1603
1
        config.max_connections = 50;
1604
1
        config.min_connections = 5;
1605
1
        config.enable_query_logging = true;
1606
1607
1
        assert_eq!(config.max_connections, 50);
1608
1
        assert_eq!(config.min_connections, 5);
1609
1
        assert!(config.enable_query_logging);
1610
1
    }
1611
1612
    #[test]
1613
1
    fn test_pool_config_timeouts() {
1614
1
        let pool_config = PoolConfig {
1615
1
            acquire_timeout_secs: 30,
1616
1
            max_lifetime_secs: 1800,
1617
1
            idle_timeout_secs: 600,
1618
1
            ..Default::default()
1619
1
        };
1620
1621
1
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1622
1
        assert_eq!(pool_config.max_lifetime_secs, 1800);
1623
1
        assert_eq!(pool_config.idle_timeout_secs, 600);
1624
1
    }
1625
1626
    #[test]
1627
1
    fn test_transaction_config_isolation_levels() {
1628
1
        let levels = vec![
1629
            "READ_UNCOMMITTED",
1630
1
            "READ_COMMITTED",
1631
1
            "REPEATABLE_READ",
1632
1
            "SERIALIZABLE",
1633
        ];
1634
1635
5
        for 
level4
in levels {
1636
4
            let tx_config = TransactionConfig {
1637
4
                isolation_level: level.to_string(),
1638
4
                ..Default::default()
1639
4
            };
1640
4
            assert_eq!(tx_config.isolation_level, level);
1641
        }
1642
1
    }
1643
1644
    #[test]
1645
1
    fn test_database_config_clone() {
1646
1
        let config1 = DatabaseConfig::new();
1647
1
        let config2 = config1.clone();
1648
1649
1
        assert_eq!(config1.url, config2.url);
1650
1
        assert_eq!(config1.max_connections, config2.max_connections);
1651
1
        assert_eq!(config1.min_connections, config2.min_connections);
1652
1
    }
1653
1654
    #[test]
1655
1
    fn test_pool_config_validation() {
1656
1
        let pool_config = PoolConfig::default();
1657
1
        assert!(pool_config.min_connections <= pool_config.max_connections);
1658
1
    }
1659
1660
    #[test]
1661
1
    fn test_database_url_format() {
1662
1
        let config = DatabaseConfig::new();
1663
1
        assert!(config.url.starts_with("postgresql://"));
1664
1
    }
1665
1666
    #[test]
1667
1
    fn test_transaction_config_retry_settings() {
1668
1
        let tx_config = TransactionConfig {
1669
1
            enable_retry: true,
1670
1
            max_retries: 5,
1671
1
            ..Default::default()
1672
1
        };
1673
1
        assert!(tx_config.enable_retry);
1674
1
        assert_eq!(tx_config.max_retries, 5);
1675
1676
1
        let tx_config_no_retry = TransactionConfig {
1677
1
            enable_retry: false,
1678
1
            ..Default::default()
1679
1
        };
1680
1
        assert!(!tx_config_no_retry.enable_retry);
1681
1
    }
1682
1683
    #[test]
1684
1
    fn test_pool_config_connection_settings() {
1685
1
        let pool_config = PoolConfig {
1686
1
            test_before_acquire: true,
1687
1
            acquire_timeout_secs: 30,
1688
1
            ..Default::default()
1689
1
        };
1690
1691
1
        assert!(pool_config.test_before_acquire);
1692
1
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1693
1
    }
1694
1695
    #[test]
1696
1
    fn test_database_config_application_name() {
1697
1
        let config = DatabaseConfig::new();
1698
1
        assert_eq!(config.application_name, Some("foxhunt".to_string()));
1699
1
    }
1700
1701
    #[test]
1702
1
    fn test_database_config_query_logging() {
1703
1
        let mut config = DatabaseConfig::new();
1704
1
        config.enable_query_logging = true;
1705
1
        assert!(config.enable_query_logging);
1706
1
    }
1707
1708
    #[test]
1709
1
    fn test_pool_config_connection_limits() {
1710
1
        let pool_config = PoolConfig {
1711
1
            max_connections: 100,
1712
1
            min_connections: 10,
1713
1
            ..Default::default()
1714
1
        };
1715
1716
1
        assert_eq!(pool_config.max_connections, 100);
1717
1
        assert_eq!(pool_config.min_connections, 10);
1718
1
    }
1719
1720
    #[test]
1721
1
    fn test_transaction_timeout() {
1722
1
        let tx_config = TransactionConfig {
1723
1
            default_timeout_secs: 60,
1724
1
            timeout: Duration::from_secs(60),
1725
1
            ..Default::default()
1726
1
        };
1727
1
        assert_eq!(tx_config.default_timeout_secs, 60);
1728
1
        assert_eq!(tx_config.timeout, Duration::from_secs(60));
1729
1
    }
1730
1731
    #[test]
1732
1
    fn test_database_config_connect_timeout() {
1733
1
        let config = DatabaseConfig::new();
1734
1
        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1735
1
    }
1736
1737
    #[test]
1738
1
    fn test_database_config_query_timeout() {
1739
1
        let config = DatabaseConfig::new();
1740
1
        assert_eq!(config.query_timeout, Duration::from_secs(60));
1741
1
    }
1742
1743
    #[test]
1744
1
    fn test_pool_config_test_before_acquire() {
1745
1
        let pool_config = PoolConfig {
1746
1
            test_before_acquire: false,
1747
1
            ..Default::default()
1748
1
        };
1749
1
        assert!(!pool_config.test_before_acquire);
1750
1751
1
        let pool_config_enabled = PoolConfig {
1752
1
            test_before_acquire: true,
1753
1
            ..Default::default()
1754
1
        };
1755
1
        assert!(pool_config_enabled.test_before_acquire);
1756
1
    }
1757
1758
    #[test]
1759
1
    fn test_database_config_validation_empty_url() {
1760
1
        let mut config = DatabaseConfig::new();
1761
1
        config.url = String::new();
1762
1
        assert!(config.validate().is_err());
1763
1
        assert_eq!(
1764
1
            config.validate().unwrap_err(),
1765
            "Database URL cannot be empty"
1766
        );
1767
1
    }
1768
1769
    #[test]
1770
1
    fn test_database_config_validation_valid() {
1771
1
        let config = DatabaseConfig::new();
1772
1
        assert!(config.validate().is_ok());
1773
1
    }
1774
1775
    #[test]
1776
1
    fn test_pool_config_defaults() {
1777
1
        let pool_config = PoolConfig::default();
1778
1
        assert_eq!(pool_config.min_connections, 1);
1779
1
        assert_eq!(pool_config.max_connections, 10);
1780
1
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1781
1
        assert_eq!(pool_config.max_lifetime_secs, 1800);
1782
1
        assert_eq!(pool_config.idle_timeout_secs, 600);
1783
1
        assert!(pool_config.test_before_acquire);
1784
1
        assert!(pool_config.health_check_enabled);
1785
1
        assert_eq!(pool_config.health_check_interval_secs, 60);
1786
1
    }
1787
1788
    #[test]
1789
1
    fn test_transaction_config_defaults() {
1790
1
        let tx_config = TransactionConfig::default();
1791
1
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1792
1
        assert_eq!(tx_config.timeout, Duration::from_secs(30));
1793
1
        assert_eq!(tx_config.default_timeout_secs, 30);
1794
1
        assert!(tx_config.enable_retry);
1795
1
        assert_eq!(tx_config.max_retries, 3);
1796
1
        assert_eq!(tx_config.retry_delay_ms, 100);
1797
1
        assert_eq!(tx_config.max_savepoints, 10);
1798
1
    }
1799
1800
    #[test]
1801
1
    fn test_transaction_config_custom_isolation() {
1802
1
        let tx_config = TransactionConfig {
1803
1
            isolation_level: "SERIALIZABLE".to_string(),
1804
1
            ..Default::default()
1805
1
        };
1806
1
        assert_eq!(tx_config.isolation_level, "SERIALIZABLE");
1807
1
    }
1808
1809
    #[test]
1810
1
    fn test_pool_config_extreme_values() {
1811
1
        let pool_config = PoolConfig {
1812
1
            max_connections: 1000,
1813
1
            min_connections: 0,
1814
1
            ..Default::default()
1815
1
        };
1816
1
        assert_eq!(pool_config.max_connections, 1000);
1817
1
        assert_eq!(pool_config.min_connections, 0);
1818
1
    }
1819
1820
    #[test]
1821
1
    fn test_database_config_custom_application_name() {
1822
1
        let mut config = DatabaseConfig::new();
1823
1
        config.application_name = Some("custom_app".to_string());
1824
1
        assert_eq!(config.application_name.unwrap(), "custom_app");
1825
1
    }
1826
1827
    #[test]
1828
1
    fn test_database_config_no_application_name() {
1829
1
        let mut config = DatabaseConfig::new();
1830
1
        config.application_name = None;
1831
1
        assert!(config.application_name.is_none());
1832
1
    }
1833
1834
    #[test]
1835
1
    fn test_transaction_config_retry_disabled() {
1836
1
        let tx_config = TransactionConfig {
1837
1
            enable_retry: false,
1838
1
            ..Default::default()
1839
1
        };
1840
1
        assert!(!tx_config.enable_retry);
1841
1
    }
1842
1843
    #[test]
1844
1
    fn test_pool_config_serialization() {
1845
1
        let pool_config = PoolConfig::default();
1846
1
        let serialized = serde_json::to_string(&pool_config).unwrap();
1847
1
        let deserialized: PoolConfig = serde_json::from_str(&serialized).unwrap();
1848
1
        assert_eq!(pool_config.max_connections, deserialized.max_connections);
1849
1
        assert_eq!(pool_config.min_connections, deserialized.min_connections);
1850
1
    }
1851
1852
    #[test]
1853
1
    fn test_transaction_config_serde_roundtrip() {
1854
1
        let tx_config = TransactionConfig::default();
1855
1
        let serialized = serde_json::to_string(&tx_config).unwrap();
1856
1
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1857
1
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1858
1
        assert_eq!(tx_config.max_retries, deserialized.max_retries);
1859
1
    }
1860
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/error.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/error.rs.html deleted file mode 100644 index 9c670d2bf..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/error.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/error.rs
Line
Count
Source
1
//! Configuration error types and result handling.
2
//!
3
//! This module defines comprehensive error types for configuration management
4
//! operations, including database errors, vault integration failures, parsing
5
//! errors, and validation issues. Uses thiserror for ergonomic error handling.
6
7
use thiserror::Error;
8
9
/// Comprehensive error type for configuration management operations.
10
///
11
/// Covers all possible error conditions that can occur during configuration
12
/// loading, validation, and management operations. Each variant provides
13
/// specific context about the failure to aid in debugging and error handling.
14
#[derive(Error, Debug)]
15
pub enum ConfigError {
16
    /// Database operation failed (connection, query, or transaction error)
17
    #[error("Database error: {0}")]
18
    Database(#[from] sqlx::Error),
19
20
    /// HashiCorp Vault integration error (authentication, secret retrieval, etc.)
21
    #[error("Vault error: {0}")]
22
    Vault(String),
23
24
    /// Configuration parsing error (invalid TOML, JSON, or environment variables)
25
    #[error("Parse error: {0}")]
26
    Parse(String),
27
28
    /// Requested configuration key or resource was not found
29
    #[error("Not found: {0}")]
30
    NotFound(String),
31
32
    /// Configuration validation failed (invalid values, missing required fields)
33
    #[error("Invalid configuration: {0}")]
34
    Invalid(String),
35
}
36
37
/// Result type alias for configuration operations.
38
///
39
/// Provides a convenient Result type that uses ConfigError as the error type.
40
/// Used throughout the configuration system for consistent error handling.
41
pub type ConfigResult<T> = Result<T, ConfigError>;
42
43
#[cfg(test)]
44
mod tests {
45
    use super::*;
46
47
    #[test]
48
1
    fn test_vault_error_display() {
49
1
        let error = ConfigError::Vault("Connection failed".to_string());
50
1
        assert_eq!(format!("{}", error), "Vault error: Connection failed");
51
1
    }
52
53
    #[test]
54
1
    fn test_parse_error_display() {
55
1
        let error = ConfigError::Parse("Invalid JSON".to_string());
56
1
        assert_eq!(format!("{}", error), "Parse error: Invalid JSON");
57
1
    }
58
59
    #[test]
60
1
    fn test_not_found_error_display() {
61
1
        let error = ConfigError::NotFound("config_key".to_string());
62
1
        assert_eq!(format!("{}", error), "Not found: config_key");
63
1
    }
64
65
    #[test]
66
1
    fn test_invalid_error_display() {
67
1
        let error = ConfigError::Invalid("Missing required field".to_string());
68
1
        assert_eq!(
69
1
            format!("{}", error),
70
            "Invalid configuration: Missing required field"
71
        );
72
1
    }
73
74
    #[test]
75
1
    fn test_error_debug_format() {
76
1
        let error = ConfigError::Vault("Test error".to_string());
77
1
        let debug_output = format!("{:?}", error);
78
1
        assert!(debug_output.contains("Vault"));
79
1
        assert!(debug_output.contains("Test error"));
80
1
    }
81
82
    #[test]
83
1
    fn test_config_result_ok() {
84
1
        let result: ConfigResult<i32> = Ok(42);
85
1
        assert!(result.is_ok());
86
1
        match result {
87
1
            Ok(value) => assert_eq!(value, 42),
88
0
            Err(_) => panic!("Expected Ok value"),
89
        }
90
1
    }
91
92
    #[test]
93
1
    fn test_config_result_err() {
94
1
        let result: ConfigResult<i32> = Err(ConfigError::NotFound("test".to_string()));
95
1
        assert!(result.is_err());
96
1
    }
97
98
    #[test]
99
1
    fn test_error_type_matching() {
100
1
        let error = ConfigError::Parse("syntax error".to_string());
101
1
        match error {
102
1
            ConfigError::Parse(msg) => assert_eq!(msg, "syntax error"),
103
0
            _ => panic!("Expected Parse error"),
104
        }
105
1
    }
106
107
    #[test]
108
1
    fn test_vault_error_creation() {
109
1
        let error = ConfigError::Vault("Token expired".to_string());
110
1
        if let ConfigError::Vault(msg) = error {
111
1
            assert_eq!(msg, "Token expired");
112
        } else {
113
0
            panic!("Expected Vault error");
114
        }
115
1
    }
116
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html deleted file mode 100644 index 1f00470e6..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/lib.rs
Line
Count
Source
1
#![warn(missing_docs)]
2
//! Configuration management for Foxhunt HFT trading system
3
4
#![allow(missing_docs)] // Internal implementation details don't require documentation
5
#![allow(missing_debug_implementations)] // Not all types need Debug
6
7
// Allow pedantic lints for configuration management
8
#![allow(clippy::type_complexity)]
9
#![allow(clippy::unnecessary_map_or)]
10
#![allow(clippy::map_flatten)]
11
#![allow(dead_code)]
12
13
use serde::{Deserialize, Serialize};
14
15
// Module declarations
16
pub mod asset_classification;
17
pub mod compliance_config;
18
pub mod data_config;
19
pub mod data_providers;
20
pub mod database;
21
pub mod error;
22
pub mod manager;
23
pub mod ml_config;
24
pub mod risk_config;
25
pub mod runtime;
26
pub mod schemas;
27
pub mod storage_config;
28
pub mod structures;
29
pub mod symbol_config;
30
pub mod vault;
31
32
// Re-export commonly used types
33
pub use asset_classification::{
34
    create_default_configurations, AssetClass, AssetClassificationManager, AssetConfig,
35
    CommodityType, CryptoType, DerivativeType, EquitySector, ExecutionConfig, FixedIncomeType,
36
    ForexPairType, FutureType, GeographicRegion, JumpRiskProfile, MarketCapTier, MarketMakingConfig, OrderType,
37
    PositionLimits, RiskThresholds, SettlementConfig, TimeInForce,
38
    TradingHours as DetailedTradingHours, TradingParameters,
39
    VolatilityProfile as DetailedVolatilityProfile,
40
};
41
pub use data_config::{
42
    DataCompressionAlgorithm, DataCompressionConfig, DataConfig, DataRetentionConfig,
43
    DataStorageConfig, DataStorageFormat, DataVersioningConfig, MissingDataHandling,
44
};
45
pub use data_providers::{
46
    AlpacaEndpoints, BenzingaEndpoints, DataProviderConfig, DataProviderEnvironment,
47
    DatabentoEndpoints, IBGatewayConfig,
48
};
49
pub use compliance_config::ComplianceRuleConfig;
50
#[cfg(feature = "postgres")]
51
pub use compliance_config::PostgresComplianceRuleLoader;
52
pub use database::{DatabaseConfig, PoolConfig, TransactionConfig};
53
#[cfg(feature = "postgres")]
54
pub use database::{
55
    PostgresAssetClassificationLoader, PostgresConfigLoader, PostgresSymbolConfigLoader,
56
};
57
pub use error::{ConfigError, ConfigResult};
58
pub use manager::{ConfigManager, ServiceConfig};
59
pub use ml_config::{
60
    MLConfig, Mamba2Config, MarketState, ModelArchitectureConfig, SimulationConfig,
61
    SymbolConfig as MLSymbolConfig, TrainingConfig,
62
};
63
pub use risk_config::{
64
    AssetClass as RiskAssetClass, AssetClassMapping, RiskConfig, StressScenarioConfig,
65
};
66
pub use runtime::{
67
    CacheRuntimeConfig, DatabaseRuntimeConfig, Environment, LimitsConfig, RuntimeConfig,
68
    TimeoutConfig,
69
};
70
pub use schemas::*;
71
pub use storage_config::{ModelArchitecture, ModelMetadata, StorageConfig, TrainingMetrics};
72
pub use structures::{
73
    AssetClass as SimpleAssetClass, AssetClassificationConfig, BacktestingDatabaseConfig,
74
    BacktestingPerformanceConfig, BacktestingStrategyConfig, BrokerConfig, BrokerRoutingRule,
75
    CommissionConfig, EncryptionConfig, MarketDataConfig, TlsConfig, TradingConfig, VolatilityProfile as SimpleVolatilityProfile,
76
};
77
pub use symbol_config::{
78
    AssetClassification, SymbolConfig, SymbolConfigManager, SymbolMetadata, TradingHours,
79
    VolatilityProfile, VolatilityRegime,
80
};
81
pub use vault::VaultConfig;
82
83
/// Configuration categories for organizing different aspects of the trading system.
84
///
85
/// This enum categorizes different types of configurations to enable organized
86
/// access and management of system settings across various functional domains.
87
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
88
pub enum ConfigCategory {
89
    /// Trading system configuration including order management and execution
90
    Trading,
91
    /// Risk management configuration including position limits and VaR settings
92
    Risk,
93
    /// Market data configuration for data providers and feeds
94
    MarketData,
95
    /// Machine learning model configuration and training parameters
96
    MachineLearning,
97
    /// Broker connectivity and execution configuration
98
    Brokers,
99
    /// Performance monitoring and optimization configuration
100
    Performance,
101
    /// Symbol classification and trading parameters configuration
102
    Symbols,
103
    /// Comprehensive asset classification with advanced features
104
    AssetClassification,
105
}
106
107
/// Production-ready asset classification system integration.
108
///
109
/// This module provides a comprehensive asset classification system that integrates
110
/// with the existing config infrastructure while offering advanced features like:
111
/// - Dynamic pattern-based classification
112
/// - Regime-aware volatility profiling  
113
/// - Hot-reload configuration management
114
/// - Performance caching and audit trails
115
///
116
/// # Usage
117
///
118
/// ```rust,no_run
119
/// use config::{AssetClassificationManager, create_default_configurations};
120
///
121
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
122
/// let mut manager = AssetClassificationManager::new();
123
/// let configs = create_default_configurations();
124
/// manager.load_configurations(configs).await?;
125
///
126
/// // Classify a symbol
127
/// let asset_class = manager.classify_symbol("AAPL");
128
///
129
/// // Get trading parameters
130
/// if let Some(params) = manager.get_trading_parameters("AAPL") {
131
///     let max_position = params.position_limits.max_position_fraction;
132
///     println!("Max position fraction for AAPL: {}", max_position);
133
/// }
134
/// # Ok(())
135
/// # }
136
/// ```
137
pub mod asset_classification_integration {
138
    pub use crate::asset_classification::*;
139
140
    /// Convenience function to create a fully configured asset classification manager
141
    /// with default configurations suitable for production use.
142
0
    pub async fn create_production_manager(
143
0
        database_pool: Option<sqlx::PgPool>,
144
0
    ) -> Result<AssetClassificationManager, Box<dyn std::error::Error + Send + Sync>> {
145
0
        let mut manager = AssetClassificationManager::new();
146
147
        // Load configurations from database if available, otherwise use defaults
148
0
        let configs = if let Some(_pool) = database_pool {
149
            // In production, load from database
150
            // let loader = crate::database::PostgresAssetClassificationLoader::with_pool(pool);
151
            // loader.load_asset_configurations().await?
152
0
            create_default_configurations()
153
        } else {
154
0
            create_default_configurations()
155
        };
156
157
0
        manager.load_configurations(configs).await?;
158
0
        Ok(manager)
159
0
    }
160
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html deleted file mode 100644 index 742ec7ed2..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/manager.rs
Line
Count
Source
1
/// Builder for ConfigManager with advanced configuration options.
2
///
3
/// Provides a fluent interface for constructing ConfigManager instances
4
/// with optional asset classification, caching, and database integration.
5
pub struct ConfigManagerBuilder {
6
    config: ServiceConfig,
7
    asset_manager: Option<crate::asset_classification::AssetClassificationManager>,
8
    cache_timeout: std::time::Duration,
9
}
10
11
impl ConfigManagerBuilder {
12
    /// Creates a new ConfigManagerBuilder with the specified service configuration.
13
7
    pub fn new(config: ServiceConfig) -> Self {
14
7
        Self {
15
7
            config,
16
7
            asset_manager: None,
17
7
            cache_timeout: std::time::Duration::from_secs(300),
18
7
        }
19
7
    }
20
21
    /// Sets the asset classification manager.
22
2
    pub fn with_asset_classification(
23
2
        mut self,
24
2
        manager: crate::asset_classification::AssetClassificationManager,
25
2
    ) -> Self {
26
2
        self.asset_manager = Some(manager);
27
2
        self
28
2
    }
29
30
    /// Sets the cache timeout duration.
31
4
    pub fn with_cache_timeout(mut self, timeout: std::time::Duration) -> Self {
32
4
        self.cache_timeout = timeout;
33
4
        self
34
4
    }
35
36
    /// Builds the ConfigManager with the specified configuration.
37
7
    pub fn build(self) -> ConfigManager {
38
7
        ConfigManager {
39
7
            config: Arc::new(self.config),
40
7
            asset_classification: Arc::new(RwLock::new(self.asset_manager)),
41
7
            cache: Arc::new(RwLock::new(HashMap::new())),
42
7
            cache_timeout: self.cache_timeout,
43
7
        }
44
7
    }
45
46
    /// Builds the ConfigManager with database integration.
47
    #[cfg(feature = "postgres")]
48
    pub async fn build_with_database(
49
        self,
50
        database_pool: sqlx::PgPool,
51
    ) -> Result<ConfigManager, Box<dyn std::error::Error + Send + Sync>> {
52
        let manager = self.build();
53
        manager
54
            .initialize_asset_classification(database_pool)
55
            .await?;
56
        Ok(manager)
57
    }
58
}
59
60
// Configuration management and service configuration structures.
61
//
62
// This module provides the core configuration management infrastructure for
63
// the Foxhunt trading system. It handles service-specific configuration,
64
// environment management, and provides thread-safe access to configuration
65
// data across the application.
66
67
use chrono::{DateTime, Utc};
68
use serde::{Deserialize, Serialize};
69
use std::collections::HashMap;
70
use std::sync::{Arc, RwLock};
71
72
/// Service-specific configuration structure.
73
///
74
/// Contains metadata and settings for a specific service in the Foxhunt
75
/// trading system. Supports environment-specific configuration and
76
/// versioning for configuration management and deployment tracking.
77
#[derive(Debug, Clone, Serialize, Deserialize)]
78
pub struct ServiceConfig {
79
    /// Service name (e.g., "trading_service", "ml_training_service")
80
    pub name: String,
81
    /// Deployment environment (e.g., "development", "staging", "production")
82
    pub environment: String,
83
    /// Service version for deployment tracking
84
    pub version: String,
85
    /// Service-specific configuration settings as JSON
86
    pub settings: serde_json::Value,
87
}
88
89
/// Thread-safe configuration manager for comprehensive service configuration.
90
///
91
/// Provides centralized access to service configuration with support for:
92
/// - Asset classification management
93
/// - Hot-reload capabilities
94
/// - Environment-specific settings
95
/// - Thread-safe access patterns
96
///
97
/// Ensures configuration consistency across all components of a service.
98
pub struct ConfigManager {
99
    config: Arc<ServiceConfig>,
100
    /// Asset classification manager for symbol-based configuration
101
    asset_classification:
102
        Arc<RwLock<Option<crate::asset_classification::AssetClassificationManager>>>,
103
    /// Configuration cache for performance
104
    cache: Arc<RwLock<HashMap<String, (serde_json::Value, DateTime<Utc>)>>>,
105
    /// Cache timeout duration
106
    cache_timeout: std::time::Duration,
107
}
108
109
impl ConfigManager {
110
    /// Creates a new ConfigManager with the provided service configuration.
111
    ///
112
    /// The configuration is wrapped in an Arc for efficient sharing across
113
    /// multiple threads and components within the service.
114
    ///
115
    /// # Arguments
116
    ///
117
    /// * `config` - The service configuration to manage
118
18
    pub fn new(config: ServiceConfig) -> Self {
119
18
        Self {
120
18
            config: Arc::new(config),
121
18
            asset_classification: Arc::new(RwLock::new(None)),
122
18
            cache: Arc::new(RwLock::new(HashMap::new())),
123
18
            cache_timeout: std::time::Duration::from_secs(300), // 5 minutes
124
18
        }
125
18
    }
126
127
    /// Creates a new ConfigManager with asset classification support.
128
    ///
129
    /// Initializes the manager with both service configuration and
130
    /// asset classification capabilities for comprehensive trading
131
    /// parameter management.
132
    ///
133
    /// # Arguments
134
    ///
135
    /// * `config` - The service configuration to manage
136
    /// * `asset_manager` - Pre-configured asset classification manager
137
1
    pub fn with_asset_classification(
138
1
        config: ServiceConfig,
139
1
        asset_manager: crate::asset_classification::AssetClassificationManager,
140
1
    ) -> Self {
141
1
        Self {
142
1
            config: Arc::new(config),
143
1
            asset_classification: Arc::new(RwLock::new(Some(asset_manager))),
144
1
            cache: Arc::new(RwLock::new(HashMap::new())),
145
1
            cache_timeout: std::time::Duration::from_secs(300),
146
1
        }
147
1
    }
148
149
    /// Returns a shared reference to the service configuration.
150
    ///
151
    /// Provides thread-safe access to the configuration data through Arc cloning.
152
    /// The returned Arc can be shared across threads without additional locking.
153
    ///
154
    /// # Returns
155
    ///
156
    /// An Arc containing the service configuration
157
8
    pub fn get_config(&self) -> Arc<ServiceConfig> {
158
8
        Arc::clone(&self.config)
159
8
    }
160
161
    /// Initializes asset classification with database-backed configurations.
162
    ///
163
    /// Loads asset classification configurations from the database and
164
    /// initializes the asset classification manager for dynamic symbol
165
    /// classification and trading parameter retrieval.
166
    #[cfg(feature = "postgres")]
167
    pub async fn initialize_asset_classification(
168
        &self,
169
        database_pool: sqlx::PgPool,
170
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
171
        let loader = crate::database::PostgresAssetClassificationLoader::with_pool(database_pool);
172
        let configs = loader.load_asset_configurations().await?;
173
174
        let mut manager = crate::asset_classification::AssetClassificationManager::new();
175
        manager.load_configurations(configs).await?;
176
177
        if let Ok(mut asset_classification) = self.asset_classification.write() {
178
            *asset_classification = Some(manager);
179
        }
180
181
        Ok(())
182
    }
183
184
    /// Classifies a symbol using the asset classification manager.
185
    ///
186
    /// Returns the asset class for the given symbol based on configured
187
    /// pattern matching rules and explicit mappings.
188
    ///
189
    /// # Arguments
190
    ///
191
    /// * `symbol` - The trading symbol to classify
192
    ///
193
    /// # Returns
194
    ///
195
    /// The asset class or Unknown if classification fails
196
2
    pub fn classify_symbol(&self, symbol: &str) -> crate::asset_classification::AssetClass {
197
2
        if let Ok(asset_classification) = self.asset_classification.read() {
198
2
            if let Some(
ref manager1
) = *asset_classification {
199
1
                return manager.classify_symbol(symbol);
200
1
            }
201
0
        }
202
1
        crate::asset_classification::AssetClass::Unknown
203
2
    }
204
205
    /// Gets trading parameters for a symbol.
206
    ///
207
    /// Retrieves comprehensive trading parameters including position limits,
208
    /// risk thresholds, and execution configuration for the specified symbol.
209
    ///
210
    /// # Arguments
211
    ///
212
    /// * `symbol` - The trading symbol
213
    ///
214
    /// # Returns
215
    ///
216
    /// Trading parameters if available, None otherwise
217
2
    pub fn get_trading_parameters(
218
2
        &self,
219
2
        symbol: &str,
220
2
    ) -> Option<crate::asset_classification::TradingParameters> {
221
2
        if let Ok(asset_classification) = self.asset_classification.read() {
222
2
            if let Some(
ref manager1
) = *asset_classification {
223
1
                return manager.get_trading_parameters(symbol).cloned();
224
1
            }
225
0
        }
226
1
        None
227
2
    }
228
229
    /// Gets volatility profile for a symbol.
230
    ///
231
    /// Retrieves the volatility profile including base volatility,
232
    /// stress multipliers, and jump risk characteristics.
233
    ///
234
    /// # Arguments
235
    ///
236
    /// * `symbol` - The trading symbol
237
    ///
238
    /// # Returns
239
    ///
240
    /// Volatility profile if available, None otherwise
241
1
    pub fn get_volatility_profile(
242
1
        &self,
243
1
        symbol: &str,
244
1
    ) -> Option<crate::asset_classification::VolatilityProfile> {
245
1
        if let Ok(asset_classification) = self.asset_classification.read() {
246
1
            if let Some(
ref manager0
) = *asset_classification {
247
0
                return manager.get_volatility_profile(symbol).cloned();
248
1
            }
249
0
        }
250
1
        None
251
1
    }
252
253
    /// Gets daily volatility estimate for a symbol.
254
    ///
255
    /// Calculates the daily volatility from the annual volatility
256
    /// using standard financial mathematics (annual / sqrt(252)).
257
    ///
258
    /// # Arguments
259
    ///
260
    /// * `symbol` - The trading symbol
261
    ///
262
    /// # Returns
263
    ///
264
    /// Daily volatility estimate as a decimal
265
3
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
266
3
        if let Ok(asset_classification) = self.asset_classification.read() {
267
3
            if let Some(
ref manager1
) = *asset_classification {
268
1
                return manager.get_daily_volatility(symbol);
269
2
            }
270
0
        }
271
2
        0.05 // Default 5% daily volatility for unknown symbols
272
3
    }
273
274
    /// Gets position size recommendation for a symbol.
275
    ///
276
    /// Calculates recommended position size based on portfolio NAV
277
    /// and the symbol's configured position limits.
278
    ///
279
    /// # Arguments
280
    ///
281
    /// * `symbol` - The trading symbol
282
    /// * `portfolio_nav` - Current portfolio net asset value
283
    ///
284
    /// # Returns
285
    ///
286
    /// Recommended position size if available
287
3
    pub fn get_position_size_recommendation(
288
3
        &self,
289
3
        symbol: &str,
290
3
        portfolio_nav: rust_decimal::Decimal,
291
3
    ) -> Option<rust_decimal::Decimal> {
292
3
        if let Ok(asset_classification) = self.asset_classification.read() {
293
3
            if let Some(
ref manager1
) = *asset_classification {
294
1
                return manager.get_position_size_recommendation(symbol, portfolio_nav);
295
2
            }
296
0
        }
297
2
        None
298
3
    }
299
300
    /// Checks if trading is active for a symbol at the given time.
301
    ///
302
    /// Validates trading hours and market schedule for the symbol.
303
    ///
304
    /// # Arguments
305
    ///
306
    /// * `symbol` - The trading symbol
307
    /// * `timestamp` - The timestamp to check
308
    ///
309
    /// # Returns
310
    ///
311
    /// True if trading is active, false otherwise
312
1
    pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime<Utc>) -> bool {
313
1
        if let Ok(asset_classification) = self.asset_classification.read() {
314
1
            if let Some(
ref manager0
) = *asset_classification {
315
0
                return manager.is_trading_active(symbol, timestamp);
316
1
            }
317
0
        }
318
1
        true // Default to always active if no classification available
319
1
    }
320
321
    /// Reloads asset classification configurations.
322
    ///
323
    /// Triggers a reload of asset classification configurations
324
    /// for hot-reload functionality in production environments.
325
    #[cfg(feature = "postgres")]
326
    pub async fn reload_asset_classification(
327
        &self,
328
        database_pool: sqlx::PgPool,
329
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
330
        let needs_reload = {
331
            let asset_classification = self.asset_classification.read().ok();
332
            asset_classification
333
                .as_ref()
334
                .and_then(|ac| ac.as_ref())
335
                .map(|manager| manager.needs_reload())
336
                .unwrap_or(false)
337
        }; // Lock released here
338
339
        if needs_reload {
340
            self.initialize_asset_classification(database_pool).await?;
341
        }
342
        Ok(())
343
    }
344
345
    /// Gets cached configuration value.
346
    ///
347
    /// Retrieves a cached configuration value with automatic expiration.
348
    ///
349
    /// # Arguments
350
    ///
351
    /// * `key` - Cache key
352
    ///
353
    /// # Returns
354
    ///
355
    /// Cached value if available and not expired
356
31
    pub fn get_cached_config(&self, key: &str) -> Option<serde_json::Value> {
357
31
        if let Ok(cache) = self.cache.read() {
358
31
            if let Some((
value28
,
timestamp28
)) = cache.get(key) {
359
28
                let elapsed = Utc::now().signed_duration_since(*timestamp);
360
28
                if elapsed.to_std().unwrap_or_default() < self.cache_timeout {
361
27
                    return Some(value.clone());
362
1
                }
363
3
            }
364
0
        }
365
4
        None
366
31
    }
367
368
    /// Sets cached configuration value.
369
    ///
370
    /// Stores a configuration value in the cache with timestamp.
371
    ///
372
    /// # Arguments
373
    ///
374
    /// * `key` - Cache key
375
    /// * `value` - Value to cache
376
28
    pub fn set_cached_config(&self, key: String, value: serde_json::Value) {
377
28
        if let Ok(mut cache) = self.cache.write() {
378
28
            cache.insert(key, (value, Utc::now()));
379
28
        
}0
380
28
    }
381
382
    /// Clears expired cache entries.
383
    ///
384
    /// Removes cache entries that have exceeded the timeout duration.
385
1
    pub fn cleanup_cache(&self) {
386
1
        if let Ok(mut cache) = self.cache.write() {
387
1
            let now = Utc::now();
388
1
            cache.retain(|_, (_, timestamp)| {
389
1
                let elapsed = now.signed_duration_since(*timestamp);
390
1
                elapsed.to_std().unwrap_or_default() < self.cache_timeout
391
1
            });
392
0
        }
393
1
    }
394
}
395
396
#[cfg(test)]
397
mod tests {
398
    use super::*;
399
    use serde_json::json;
400
401
28
    fn create_test_config() -> ServiceConfig {
402
28
        ServiceConfig {
403
28
            name: "test_service".to_string(),
404
28
            environment: "test".to_string(),
405
28
            version: "1.0.0".to_string(),
406
28
            settings: json!({"test_key": "test_value"}),
407
28
        }
408
28
    }
409
410
    #[test]
411
1
    fn test_service_config_creation() {
412
1
        let config = create_test_config();
413
1
        assert_eq!(config.name, "test_service");
414
1
        assert_eq!(config.environment, "test");
415
1
        assert_eq!(config.version, "1.0.0");
416
1
    }
417
418
    #[test]
419
1
    fn test_config_manager_new() {
420
1
        let config = create_test_config();
421
1
        let manager = ConfigManager::new(config);
422
1
        let retrieved_config = manager.get_config();
423
1
        assert_eq!(retrieved_config.name, "test_service");
424
1
    }
425
426
    #[test]
427
1
    fn test_config_manager_builder() {
428
1
        let config = create_test_config();
429
1
        let manager = ConfigManagerBuilder::new(config)
430
1
            .with_cache_timeout(std::time::Duration::from_secs(60))
431
1
            .build();
432
433
1
        let retrieved_config = manager.get_config();
434
1
        assert_eq!(retrieved_config.name, "test_service");
435
1
    }
436
437
    #[test]
438
1
    fn test_config_manager_cache_set_and_get() {
439
1
        let config = create_test_config();
440
1
        let manager = ConfigManager::new(config);
441
442
1
        let test_value = json!({"cached": "data"});
443
1
        manager.set_cached_config("test_key".to_string(), test_value.clone());
444
445
1
        let retrieved = manager.get_cached_config("test_key");
446
1
        assert!(retrieved.is_some());
447
1
        assert_eq!(retrieved.unwrap(), test_value);
448
1
    }
449
450
    #[test]
451
1
    fn test_config_manager_cache_miss() {
452
1
        let config = create_test_config();
453
1
        let manager = ConfigManager::new(config);
454
455
1
        let retrieved = manager.get_cached_config("nonexistent_key");
456
1
        assert!(retrieved.is_none());
457
1
    }
458
459
    #[test]
460
1
    fn test_config_manager_cleanup_cache() {
461
1
        let config = create_test_config();
462
1
        let manager = ConfigManager::new(config);
463
464
1
        let test_value = json!({"cached": "data"});
465
1
        manager.set_cached_config("test_key".to_string(), test_value);
466
467
1
        manager.cleanup_cache();
468
469
        // Cache entry should still exist since it was just created
470
1
        let retrieved = manager.get_cached_config("test_key");
471
1
        assert!(retrieved.is_some());
472
1
    }
473
474
    #[test]
475
1
    fn test_config_manager_classify_symbol_without_asset_manager() {
476
1
        let config = create_test_config();
477
1
        let manager = ConfigManager::new(config);
478
479
1
        let asset_class = manager.classify_symbol("AAPL");
480
1
        assert_eq!(
481
            asset_class,
482
            crate::asset_classification::AssetClass::Unknown
483
        );
484
1
    }
485
486
    #[test]
487
1
    fn test_config_manager_get_daily_volatility_default() {
488
1
        let config = create_test_config();
489
1
        let manager = ConfigManager::new(config);
490
491
1
        let volatility = manager.get_daily_volatility("AAPL");
492
1
        assert_eq!(volatility, 0.05); // Default value
493
1
    }
494
495
    #[test]
496
1
    fn test_config_manager_is_trading_active_default() {
497
1
        let config = create_test_config();
498
1
        let manager = ConfigManager::new(config);
499
500
1
        let now = chrono::Utc::now();
501
1
        let is_active = manager.is_trading_active("AAPL", now);
502
1
        assert!(is_active); // Default to always active
503
1
    }
504
505
    #[test]
506
1
    fn test_config_manager_get_trading_parameters_none() {
507
1
        let config = create_test_config();
508
1
        let manager = ConfigManager::new(config);
509
510
1
        let params = manager.get_trading_parameters("AAPL");
511
1
        assert!(params.is_none());
512
1
    }
513
514
    #[test]
515
1
    fn test_config_manager_get_volatility_profile_none() {
516
1
        let config = create_test_config();
517
1
        let manager = ConfigManager::new(config);
518
519
1
        let profile = manager.get_volatility_profile("AAPL");
520
1
        assert!(profile.is_none());
521
1
    }
522
523
    #[test]
524
1
    fn test_config_manager_get_position_size_recommendation_none() {
525
1
        let config = create_test_config();
526
1
        let manager = ConfigManager::new(config);
527
528
1
        let recommendation =
529
1
            manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0));
530
1
        assert!(recommendation.is_none());
531
1
    }
532
533
    #[test]
534
1
    fn test_config_manager_with_asset_classification() {
535
1
        let config = create_test_config();
536
1
        let asset_manager = crate::asset_classification::AssetClassificationManager::new();
537
1
        let manager = ConfigManager::with_asset_classification(config, asset_manager);
538
539
1
        let retrieved_config = manager.get_config();
540
1
        assert_eq!(retrieved_config.name, "test_service");
541
1
    }
542
543
    #[test]
544
1
    fn test_builder_with_asset_classification() {
545
1
        let config = create_test_config();
546
1
        let asset_manager = crate::asset_classification::AssetClassificationManager::new();
547
548
1
        let manager = ConfigManagerBuilder::new(config)
549
1
            .with_asset_classification(asset_manager)
550
1
            .build();
551
552
1
        let retrieved_config = manager.get_config();
553
1
        assert_eq!(retrieved_config.name, "test_service");
554
1
    }
555
556
    #[test]
557
1
    fn test_service_config_serialization() {
558
1
        let config = create_test_config();
559
1
        let serialized = serde_json::to_string(&config).unwrap();
560
1
        let deserialized: ServiceConfig = serde_json::from_str(&serialized).unwrap();
561
562
1
        assert_eq!(config.name, deserialized.name);
563
1
        assert_eq!(config.environment, deserialized.environment);
564
1
        assert_eq!(config.version, deserialized.version);
565
1
    }
566
567
    #[test]
568
1
    fn test_config_manager_multiple_cache_entries() {
569
1
        let config = create_test_config();
570
1
        let manager = ConfigManager::new(config);
571
572
11
        for 
i10
in 0..10 {
573
10
            manager.set_cached_config(format!("key_{}", i), json!({"value": i}));
574
10
        }
575
576
11
        for 
i10
in 0..10 {
577
10
            let retrieved = manager.get_cached_config(&format!("key_{}", i));
578
10
            assert!(retrieved.is_some());
579
        }
580
1
    }
581
582
    #[test]
583
1
    fn test_config_manager_cache_overwrite() {
584
1
        let config = create_test_config();
585
1
        let manager = ConfigManager::new(config);
586
587
1
        manager.set_cached_config("key".to_string(), json!({"value": 1}));
588
1
        manager.set_cached_config("key".to_string(), json!({"value": 2}));
589
590
1
        let retrieved = manager.get_cached_config("key");
591
1
        assert_eq!(retrieved.unwrap(), json!({"value": 2}));
592
1
    }
593
594
    #[test]
595
1
    fn test_builder_custom_cache_timeout() {
596
1
        let config = create_test_config();
597
1
        let custom_timeout = std::time::Duration::from_secs(120);
598
599
1
        let manager = ConfigManagerBuilder::new(config)
600
1
            .with_cache_timeout(custom_timeout)
601
1
            .build();
602
603
        // Cache timeout is set internally
604
1
        let retrieved_config = manager.get_config();
605
1
        assert_eq!(retrieved_config.name, "test_service");
606
1
    }
607
608
    #[test]
609
1
    fn test_config_manager_shared_config() {
610
1
        let config = create_test_config();
611
1
        let manager = ConfigManager::new(config);
612
613
1
        let config1 = manager.get_config();
614
1
        let config2 = manager.get_config();
615
616
        // Both should point to the same Arc
617
1
        assert_eq!(config1.name, config2.name);
618
1
    }
619
620
    #[test]
621
1
    fn test_service_config_clone() {
622
1
        let config1 = create_test_config();
623
1
        let config2 = config1.clone();
624
625
1
        assert_eq!(config1.name, config2.name);
626
1
        assert_eq!(config1.environment, config2.environment);
627
1
        assert_eq!(config1.version, config2.version);
628
1
    }
629
630
    #[test]
631
1
    fn test_config_manager_cache_timeout_configuration() {
632
1
        let config = create_test_config();
633
1
        let custom_timeout = std::time::Duration::from_millis(10);
634
1
        let manager = ConfigManagerBuilder::new(config)
635
1
            .with_cache_timeout(custom_timeout)
636
1
            .build();
637
638
        // Cache timeout is configured internally
639
1
        assert_eq!(manager.cache_timeout, custom_timeout);
640
641
        // Test that cache still works normally
642
1
        manager.set_cached_config("test_key".to_string(), json!({"value": 42}));
643
1
        assert!(manager.get_cached_config("test_key").is_some());
644
1
    }
645
646
    #[test]
647
1
    fn test_config_manager_concurrent_access() {
648
        use std::sync::Arc;
649
        use std::thread;
650
651
1
        let config = create_test_config();
652
1
        let manager = Arc::new(ConfigManager::new(config));
653
654
1
        let mut handles = vec![];
655
656
11
        for 
i10
in 0..10 {
657
10
            let manager_clone = Arc::clone(&manager);
658
10
            let handle = thread::spawn(move || {
659
10
                manager_clone
660
10
                    .set_cached_config(format!("concurrent_key_{}", i), json!({"thread_id": i}));
661
10
                manager_clone.get_cached_config(&format!("concurrent_key_{}", i))
662
10
            });
663
10
            handles.push(handle);
664
        }
665
666
11
        for 
handle10
in handles {
667
10
            assert!(handle.join().unwrap().is_some());
668
        }
669
1
    }
670
671
    #[test]
672
1
    fn test_config_manager_daily_volatility_fallback() {
673
1
        let config = create_test_config();
674
1
        let manager = ConfigManager::new(config);
675
676
        // Should return default 5% for unknown symbols
677
1
        let vol = manager.get_daily_volatility("UNKNOWN_SYMBOL");
678
1
        assert_eq!(vol, 0.05);
679
1
    }
680
681
    #[test]
682
1
    fn test_config_manager_position_size_none() {
683
1
        let config = create_test_config();
684
1
        let manager = ConfigManager::new(config);
685
686
        // Should return None without asset classification
687
1
        let size =
688
1
            manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0));
689
1
        assert!(size.is_none());
690
1
    }
691
692
    #[test]
693
1
    fn test_service_config_validation() {
694
1
        let mut config = create_test_config();
695
696
        // Valid config
697
1
        assert!(!config.name.is_empty());
698
1
        assert!(!config.environment.is_empty());
699
700
        // Test with empty name
701
1
        config.name = String::new();
702
1
        assert!(config.name.is_empty());
703
1
    }
704
705
    #[test]
706
1
    fn test_config_manager_cache_clear() {
707
1
        let config = create_test_config();
708
1
        let manager = ConfigManager::new(config);
709
710
        // Add some cache entries
711
1
        manager.set_cached_config("key1".to_string(), json!({"value": 1}));
712
1
        manager.set_cached_config("key2".to_string(), json!({"value": 2}));
713
714
1
        assert!(manager.get_cached_config("key1").is_some());
715
1
        assert!(manager.get_cached_config("key2").is_some());
716
717
        // Manual clear
718
1
        if let Ok(mut cache) = manager.cache.write() {
719
1
            cache.clear();
720
1
        
}0
721
722
1
        assert!(manager.get_cached_config("key1").is_none());
723
1
        assert!(manager.get_cached_config("key2").is_none());
724
1
    }
725
726
    #[test]
727
1
    fn test_builder_default_values() {
728
1
        let config = create_test_config();
729
1
        let manager = ConfigManagerBuilder::new(config.clone()).build();
730
731
1
        let retrieved = manager.get_config();
732
1
        assert_eq!(retrieved.name, config.name);
733
1
        assert_eq!(retrieved.environment, config.environment);
734
1
    }
735
736
    #[test]
737
1
    fn test_config_manager_arc_cloning() {
738
1
        let config = create_test_config();
739
1
        let manager = ConfigManager::new(config);
740
741
1
        let config1 = Arc::clone(&manager.config);
742
1
        let config2 = Arc::clone(&manager.config);
743
744
1
        assert_eq!(config1.name, config2.name);
745
1
        assert_eq!(Arc::strong_count(&manager.config), 3); // Original + 2 clones
746
1
    }
747
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html deleted file mode 100644 index 4c2bda200..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs
Line
Count
Source
1
//! Machine learning configuration
2
3
use serde::{Deserialize, Serialize};
4
use std::collections::HashMap;
5
6
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7
pub struct MLConfig {
8
    pub model_config: ModelArchitectureConfig,
9
    pub training_config: TrainingConfig,
10
    pub simulation_config: SimulationConfig,
11
}
12
13
/// Configuration for market data simulation and stress testing
14
#[derive(Debug, Clone, Serialize, Deserialize)]
15
pub struct SimulationConfig {
16
    /// Initial market state with configurable symbol prices
17
    pub initial_market_state: MarketState,
18
    /// Simulation parameters
19
    pub parameters: SimulationParameters,
20
    /// Test symbol configuration for generic testing
21
    pub test_symbols: TestSymbolConfig,
22
}
23
24
/// Initial market state configuration
25
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct MarketState {
27
    /// Symbol-specific initial prices and configuration
28
    pub symbols: HashMap<String, SymbolConfig>,
29
    /// Default configuration for unlisted symbols
30
    pub default_symbol: SymbolConfig,
31
}
32
33
/// Configuration for individual symbols
34
#[derive(Debug, Clone, Serialize, Deserialize)]
35
pub struct SymbolConfig {
36
    /// Initial price for the symbol
37
    pub initial_price: f64,
38
    /// Base volatility for the symbol
39
    pub volatility: f64,
40
    /// Base trading volume
41
    pub base_volume: f64,
42
    /// Minimum spread in basis points
43
    pub min_spread_bps: f64,
44
    /// Maximum spread in basis points
45
    pub max_spread_bps: f64,
46
    /// Market capitalization tier (affects behavior)
47
    pub market_cap_tier: MarketCapTier,
48
}
49
50
/// Market capitalization tiers for different symbol behaviors
51
#[derive(Debug, Clone, Serialize, Deserialize)]
52
pub enum MarketCapTier {
53
    /// Large cap stocks (>$10B)
54
    LargeCap,
55
    /// Mid cap stocks ($2B-$10B)
56
    MidCap,
57
    /// Small cap stocks (<$2B)
58
    SmallCap,
59
    /// Generic test symbol
60
    Test,
61
}
62
63
/// Simulation parameters
64
#[derive(Debug, Clone, Serialize, Deserialize)]
65
pub struct SimulationParameters {
66
    /// Update rate in Hz
67
    pub update_rate_hz: u32,
68
    /// Base market volatility
69
    pub base_volatility: f64,
70
    /// Market trend direction (-1.0 to 1.0)
71
    pub trend: f64,
72
    /// Enable realistic market microstructure
73
    pub enable_microstructure: bool,
74
    /// Enable correlated movements between symbols
75
    pub enable_correlation: bool,
76
}
77
78
/// Test symbol configuration for generic testing
79
#[derive(Debug, Clone, Serialize, Deserialize)]
80
pub struct TestSymbolConfig {
81
    /// Prefix for test symbols (e.g., "TEST")
82
    pub symbol_prefix: String,
83
    /// Number of test symbols to generate
84
    pub count: usize,
85
    /// Price range for test symbols
86
    pub price_range: (f64, f64),
87
    /// Volume range for test symbols
88
    pub volume_range: (f64, f64),
89
}
90
91
/// Default simulation configuration
92
impl Default for SimulationConfig {
93
0
    fn default() -> Self {
94
0
        let mut symbols = HashMap::new();
95
96
        // Production-ready major symbols with realistic configurations
97
0
        symbols.insert(
98
0
            "AAPL".to_string(),
99
0
            SymbolConfig {
100
0
                initial_price: 150.0,
101
0
                volatility: 0.25,
102
0
                base_volume: 50000000.0,
103
0
                min_spread_bps: 1.0,
104
0
                max_spread_bps: 5.0,
105
0
                market_cap_tier: MarketCapTier::LargeCap,
106
0
            },
107
        );
108
109
0
        symbols.insert(
110
0
            "MSFT".to_string(),
111
0
            SymbolConfig {
112
0
                initial_price: 300.0,
113
0
                volatility: 0.22,
114
0
                base_volume: 30000000.0,
115
0
                min_spread_bps: 1.0,
116
0
                max_spread_bps: 5.0,
117
0
                market_cap_tier: MarketCapTier::LargeCap,
118
0
            },
119
        );
120
121
0
        symbols.insert(
122
0
            "GOOGL".to_string(),
123
0
            SymbolConfig {
124
0
                initial_price: 2500.0,
125
0
                volatility: 0.28,
126
0
                base_volume: 20000000.0,
127
0
                min_spread_bps: 2.0,
128
0
                max_spread_bps: 8.0,
129
0
                market_cap_tier: MarketCapTier::LargeCap,
130
0
            },
131
        );
132
133
0
        symbols.insert(
134
0
            "TSLA".to_string(),
135
0
            SymbolConfig {
136
0
                initial_price: 800.0,
137
0
                volatility: 0.45,
138
0
                base_volume: 80000000.0,
139
0
                min_spread_bps: 2.0,
140
0
                max_spread_bps: 10.0,
141
0
                market_cap_tier: MarketCapTier::LargeCap,
142
0
            },
143
        );
144
145
0
        symbols.insert(
146
0
            "AMZN".to_string(),
147
0
            SymbolConfig {
148
0
                initial_price: 3200.0,
149
0
                volatility: 0.30,
150
0
                base_volume: 25000000.0,
151
0
                min_spread_bps: 2.0,
152
0
                max_spread_bps: 8.0,
153
0
                market_cap_tier: MarketCapTier::LargeCap,
154
0
            },
155
        );
156
157
0
        symbols.insert(
158
0
            "NVDA".to_string(),
159
0
            SymbolConfig {
160
0
                initial_price: 500.0,
161
0
                volatility: 0.40,
162
0
                base_volume: 40000000.0,
163
0
                min_spread_bps: 2.0,
164
0
                max_spread_bps: 8.0,
165
0
                market_cap_tier: MarketCapTier::LargeCap,
166
0
            },
167
        );
168
169
0
        Self {
170
0
            initial_market_state: MarketState {
171
0
                symbols,
172
0
                default_symbol: SymbolConfig {
173
0
                    initial_price: 100.0,
174
0
                    volatility: 0.30,
175
0
                    base_volume: 1000000.0,
176
0
                    min_spread_bps: 5.0,
177
0
                    max_spread_bps: 20.0,
178
0
                    market_cap_tier: MarketCapTier::Test,
179
0
                },
180
0
            },
181
0
            parameters: SimulationParameters {
182
0
                update_rate_hz: 1000,
183
0
                base_volatility: 0.02,
184
0
                trend: 0.0,
185
0
                enable_microstructure: true,
186
0
                enable_correlation: false,
187
0
            },
188
0
            test_symbols: TestSymbolConfig {
189
0
                symbol_prefix: "TEST".to_string(),
190
0
                count: 10,
191
0
                price_range: (50.0, 500.0),
192
0
                volume_range: (100000.0, 10000000.0),
193
0
            },
194
0
        }
195
0
    }
196
}
197
198
#[derive(Debug, Clone, Serialize, Deserialize)]
199
pub struct ModelArchitectureConfig {
200
    pub model_type: String,
201
    pub hidden_dims: Vec<usize>,
202
    pub dropout_rate: f64,
203
    pub activation: String,
204
}
205
206
impl Default for ModelArchitectureConfig {
207
0
    fn default() -> Self {
208
0
        Self {
209
0
            model_type: "transformer".to_string(),
210
0
            hidden_dims: vec![256, 128, 64],
211
0
            dropout_rate: 0.1,
212
0
            activation: "relu".to_string(),
213
0
        }
214
0
    }
215
}
216
217
#[derive(Debug, Clone, Serialize, Deserialize)]
218
pub struct TrainingConfig {
219
    pub batch_size: usize,
220
    pub learning_rate: f64,
221
    pub epochs: u32,
222
    pub early_stopping_patience: u32,
223
}
224
225
impl Default for TrainingConfig {
226
0
    fn default() -> Self {
227
0
        Self {
228
0
            batch_size: 32,
229
0
            learning_rate: 0.001,
230
0
            epochs: 100,
231
0
            early_stopping_patience: 10,
232
0
        }
233
0
    }
234
}
235
236
#[derive(Debug, Clone, Serialize, Deserialize)]
237
pub struct Mamba2Config {
238
    pub d_model: usize,
239
    pub d_state: usize,
240
    pub d_conv: usize,
241
    pub expand: usize,
242
    pub dt_rank: Option<usize>,
243
    pub dt_min: f64,
244
    pub dt_max: f64,
245
    pub dt_init: String,
246
    pub dt_scale: f64,
247
    pub dt_init_floor: f64,
248
    pub conv_bias: bool,
249
    pub bias: bool,
250
    pub use_fast_path: bool,
251
    pub layer_idx: Option<usize>,
252
    pub device: Option<String>,
253
    pub dtype: Option<String>,
254
    pub d_head: usize,
255
    pub num_heads: usize,
256
    pub num_layers: usize,
257
    pub target_latency_us: u64,
258
    pub hardware_aware: bool,
259
    pub use_ssd: bool,
260
    pub use_selective_state: bool,
261
    pub max_seq_len: usize,
262
    pub batch_size: usize,
263
    pub seq_len: usize,
264
    pub dropout: f64,
265
}
266
267
impl Default for Mamba2Config {
268
0
    fn default() -> Self {
269
0
        Self {
270
0
            d_model: 768,
271
0
            d_state: 128,
272
0
            d_conv: 4,
273
0
            expand: 2,
274
0
            dt_rank: None, // Auto-calculated as ceil(d_model / 16)
275
0
            dt_min: 0.001,
276
0
            dt_max: 0.1,
277
0
            dt_init: "random".to_string(),
278
0
            dt_scale: 1.0,
279
0
            dt_init_floor: 1e-4,
280
0
            conv_bias: true,
281
0
            bias: false,
282
0
            use_fast_path: true,
283
0
            layer_idx: None,
284
0
            device: None,
285
0
            dtype: None,
286
0
            d_head: 32,
287
0
            num_heads: 8,
288
0
            num_layers: 4,
289
0
            target_latency_us: 3,
290
0
            hardware_aware: true,
291
0
            use_ssd: true,
292
0
            use_selective_state: true,
293
0
            max_seq_len: 1024,
294
0
            batch_size: 1,
295
0
            seq_len: 256,
296
0
            dropout: 0.0,
297
0
        }
298
0
    }
299
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html deleted file mode 100644 index 5d2bd8b60..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs
Line
Count
Source
1
//! Risk management configuration structures
2
//!
3
//! Provides configuration types for risk management components including
4
//! stress testing scenarios, asset class definitions, and market shock parameters.
5
6
use serde::{Deserialize, Serialize};
7
use std::collections::HashMap;
8
9
/// Configuration for stress testing scenarios
10
///
11
/// Defines how stress scenarios are configured and applied to portfolios.
12
/// Supports both individual instrument shocks and asset class-based shocks
13
/// for more flexible and maintainable stress testing.
14
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15
pub struct StressScenarioConfig {
16
    /// Unique identifier for this stress test scenario
17
    pub id: String,
18
    /// Human-readable name describing the scenario
19
    pub name: String,
20
    /// Description of the stress scenario and its historical context
21
    pub description: String,
22
    /// Individual instrument-specific shocks (symbol -> shock percentage)
23
    pub instrument_shocks: HashMap<String, f64>,
24
    /// Asset class-based shocks that apply to all instruments in a class
25
    pub asset_class_shocks: HashMap<AssetClass, f64>,
26
    /// Global volatility multiplier to apply across all instruments
27
    pub volatility_multiplier: f64,
28
    /// Asset class-specific volatility multipliers
29
    pub volatility_multipliers: HashMap<AssetClass, f64>,
30
    /// Correlation adjustments between asset classes
31
    pub correlation_adjustments: HashMap<String, f64>,
32
    /// Liquidity haircuts to apply per asset class
33
    pub liquidity_haircuts: HashMap<AssetClass, f64>,
34
    /// Whether this scenario is active and available for use
35
    pub is_active: bool,
36
}
37
38
/// Asset class definitions for grouping instruments
39
///
40
/// Provides a hierarchical way to apply stress shocks to groups
41
/// of related instruments rather than hardcoding individual symbols.
42
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43
pub enum AssetClass {
44
    /// Large-cap US equities (S&P 500 companies)
45
    LargeCapEquity,
46
    /// Small-cap US equities
47
    SmallCapEquity,
48
    /// Technology sector equities
49
    Technology,
50
    /// Financial sector equities
51
    Financials,
52
    /// Healthcare sector equities
53
    Healthcare,
54
    /// Energy sector equities
55
    Energy,
56
    /// Consumer discretionary equities
57
    ConsumerDiscretionary,
58
    /// Consumer staples equities
59
    ConsumerStaples,
60
    /// Industrial sector equities
61
    Industrials,
62
    /// Materials sector equities
63
    Materials,
64
    /// Real estate sector equities
65
    RealEstate,
66
    /// Utilities sector equities
67
    Utilities,
68
    /// Communication services sector equities
69
    CommunicationServices,
70
    /// US Treasury bonds
71
    USBonds,
72
    /// Corporate bonds
73
    CorporateBonds,
74
    /// High-yield bonds
75
    HighYieldBonds,
76
    /// International developed market equities
77
    InternationalEquity,
78
    /// Emerging market equities
79
    EmergingMarkets,
80
    /// Commodities
81
    Commodities,
82
    /// Foreign exchange
83
    ForeignExchange,
84
    /// Cryptocurrencies
85
    Crypto,
86
    /// Alternative investments
87
    Alternatives,
88
}
89
90
/// Asset class mapping configuration
91
///
92
/// Maps individual instrument symbols to their asset classes for
93
/// applying class-based stress shocks and risk calculations.
94
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95
pub struct AssetClassMapping {
96
    /// Symbol to asset class mappings
97
    pub mappings: HashMap<String, AssetClass>,
98
    /// Default asset class for unmapped symbols
99
    pub default_class: AssetClass,
100
}
101
102
/// Complete risk configuration containing all risk-related settings
103
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104
pub struct RiskConfig {
105
    /// Available stress test scenarios
106
    pub stress_scenarios: Vec<StressScenarioConfig>,
107
    /// Asset class mappings for instruments
108
    pub asset_class_mapping: AssetClassMapping,
109
    /// Default volatility settings
110
    pub default_volatility_multiplier: f64,
111
    /// Maximum allowed portfolio loss percentage
112
    pub max_portfolio_loss_pct: f64,
113
    /// VaR confidence level (e.g., 0.95 for 95% confidence)
114
    pub var_confidence_level: f64,
115
    /// Time horizon for VaR calculations in days
116
    pub var_time_horizon_days: u32,
117
}
118
119
impl Default for RiskConfig {
120
0
    fn default() -> Self {
121
0
        Self {
122
0
            stress_scenarios: create_default_stress_scenarios(),
123
0
            asset_class_mapping: create_default_asset_class_mapping(),
124
0
            default_volatility_multiplier: 1.0,
125
0
            max_portfolio_loss_pct: 20.0,
126
0
            var_confidence_level: 0.95,
127
0
            var_time_horizon_days: 1,
128
0
        }
129
0
    }
130
}
131
132
impl StressScenarioConfig {
133
    /// Get the effective shock for a given instrument symbol
134
    ///
135
    /// Returns the instrument-specific shock if available, otherwise
136
    /// returns the asset class shock based on the symbol's asset class mapping.
137
3
    pub fn get_shock_for_symbol(
138
3
        &self,
139
3
        symbol: &str,
140
3
        asset_mapping: &AssetClassMapping,
141
3
    ) -> Option<f64> {
142
        // First check for instrument-specific shock
143
3
        if let Some(
shock1
) = self.instrument_shocks.get(symbol) {
144
1
            return Some(*shock);
145
2
        }
146
147
        // Then check for asset class shock
148
2
        if let Some(
asset_class1
) = asset_mapping.mappings.get(symbol) {
149
1
            return self.asset_class_shocks.get(asset_class).copied();
150
1
        }
151
152
        // Fall back to default asset class shock
153
1
        self.asset_class_shocks
154
1
            .get(&asset_mapping.default_class)
155
1
            .copied()
156
3
    }
157
158
    /// Get volatility multiplier for a given instrument symbol
159
0
    pub fn get_volatility_multiplier_for_symbol(
160
0
        &self,
161
0
        symbol: &str,
162
0
        asset_mapping: &AssetClassMapping,
163
0
    ) -> f64 {
164
        // Check for asset class-specific volatility multiplier
165
0
        if let Some(asset_class) = asset_mapping.mappings.get(symbol) {
166
0
            if let Some(multiplier) = self.volatility_multipliers.get(asset_class) {
167
0
                return *multiplier;
168
0
            }
169
0
        }
170
171
        // Fall back to default asset class
172
0
        if let Some(multiplier) = self
173
0
            .volatility_multipliers
174
0
            .get(&asset_mapping.default_class)
175
        {
176
0
            return *multiplier;
177
0
        }
178
179
        // Fall back to global multiplier
180
0
        self.volatility_multiplier
181
0
    }
182
}
183
184
/// Create default stress test scenarios based on historical events
185
0
fn create_default_stress_scenarios() -> Vec<StressScenarioConfig> {
186
0
    vec![
187
0
        StressScenarioConfig {
188
0
            id: "market_crash_2008".to_string(),
189
0
            name: "2008 Financial Crisis".to_string(),
190
0
            description: "Simulates the market conditions during the 2008 financial crisis with severe equity declines and financial sector stress".to_string(),
191
0
            instrument_shocks: HashMap::new(),
192
0
            asset_class_shocks: {
193
0
                let mut shocks = HashMap::new();
194
0
                shocks.insert(AssetClass::LargeCapEquity, -37.0);
195
0
                shocks.insert(AssetClass::SmallCapEquity, -45.0);
196
0
                shocks.insert(AssetClass::Financials, -55.0);
197
0
                shocks.insert(AssetClass::Technology, -40.0);
198
0
                shocks.insert(AssetClass::RealEstate, -60.0);
199
0
                shocks.insert(AssetClass::EmergingMarkets, -50.0);
200
0
                shocks.insert(AssetClass::HighYieldBonds, -25.0);
201
0
                shocks
202
0
            },
203
0
            volatility_multiplier: 2.5,
204
0
            volatility_multipliers: HashMap::new(),
205
0
            correlation_adjustments: HashMap::new(),
206
0
            liquidity_haircuts: {
207
0
                let mut haircuts = HashMap::new();
208
0
                haircuts.insert(AssetClass::SmallCapEquity, 0.15);
209
0
                haircuts.insert(AssetClass::EmergingMarkets, 0.20);
210
0
                haircuts.insert(AssetClass::HighYieldBonds, 0.10);
211
0
                haircuts
212
0
            },
213
0
            is_active: true,
214
0
        },
215
0
        StressScenarioConfig {
216
0
            id: "covid_crash_2020".to_string(),
217
0
            name: "COVID-19 Market Crash".to_string(),
218
0
            description: "Simulates the market crash of March 2020 due to COVID-19 pandemic with broad-based equity declines".to_string(),
219
0
            instrument_shocks: HashMap::new(),
220
0
            asset_class_shocks: {
221
0
                let mut shocks = HashMap::new();
222
0
                shocks.insert(AssetClass::LargeCapEquity, -34.0);
223
0
                shocks.insert(AssetClass::SmallCapEquity, -40.0);
224
0
                shocks.insert(AssetClass::Energy, -50.0);
225
0
                shocks.insert(AssetClass::Financials, -45.0);
226
0
                shocks.insert(AssetClass::RealEstate, -35.0);
227
0
                shocks.insert(AssetClass::Technology, -25.0);
228
0
                shocks.insert(AssetClass::EmergingMarkets, -45.0);
229
0
                shocks
230
0
            },
231
0
            volatility_multiplier: 3.0,
232
0
            volatility_multipliers: HashMap::new(),
233
0
            correlation_adjustments: HashMap::new(),
234
0
            liquidity_haircuts: HashMap::new(),
235
0
            is_active: true,
236
0
        },
237
0
        StressScenarioConfig {
238
0
            id: "flash_crash_2010".to_string(),
239
0
            name: "Flash Crash 2010".to_string(),
240
0
            description: "Simulates the May 6, 2010 flash crash with rapid market decline and liquidity issues".to_string(),
241
0
            instrument_shocks: HashMap::new(),
242
0
            asset_class_shocks: {
243
0
                let mut shocks = HashMap::new();
244
0
                shocks.insert(AssetClass::LargeCapEquity, -9.0);
245
0
                shocks.insert(AssetClass::SmallCapEquity, -15.0);
246
0
                shocks.insert(AssetClass::Technology, -12.0);
247
0
                shocks
248
0
            },
249
0
            volatility_multiplier: 5.0,
250
0
            volatility_multipliers: HashMap::new(),
251
0
            correlation_adjustments: HashMap::new(),
252
0
            liquidity_haircuts: {
253
0
                let mut haircuts = HashMap::new();
254
0
                haircuts.insert(AssetClass::LargeCapEquity, 0.05);
255
0
                haircuts.insert(AssetClass::SmallCapEquity, 0.20);
256
0
                haircuts.insert(AssetClass::Technology, 0.10);
257
0
                haircuts
258
0
            },
259
0
            is_active: true,
260
0
        },
261
0
        StressScenarioConfig {
262
0
            id: "volatility_spike".to_string(),
263
0
            name: "Volatility Spike".to_string(),
264
0
            description: "Simulates a sudden spike in market volatility without significant price moves".to_string(),
265
0
            instrument_shocks: HashMap::new(),
266
0
            asset_class_shocks: HashMap::new(),
267
0
            volatility_multiplier: 3.0,
268
0
            volatility_multipliers: {
269
0
                let mut multipliers = HashMap::new();
270
0
                multipliers.insert(AssetClass::SmallCapEquity, 4.0);
271
0
                multipliers.insert(AssetClass::EmergingMarkets, 3.5);
272
0
                multipliers.insert(AssetClass::HighYieldBonds, 2.5);
273
0
                multipliers
274
0
            },
275
0
            correlation_adjustments: HashMap::new(),
276
0
            liquidity_haircuts: HashMap::new(),
277
0
            is_active: true,
278
0
        },
279
0
        StressScenarioConfig {
280
0
            id: "interest_rate_shock".to_string(),
281
0
            name: "Interest Rate Shock".to_string(),
282
0
            description: "Simulates a sudden rise in interest rates affecting bonds and rate-sensitive sectors".to_string(),
283
0
            instrument_shocks: HashMap::new(),
284
0
            asset_class_shocks: {
285
0
                let mut shocks = HashMap::new();
286
0
                shocks.insert(AssetClass::USBonds, -8.0);
287
0
                shocks.insert(AssetClass::CorporateBonds, -12.0);
288
0
                shocks.insert(AssetClass::RealEstate, -15.0);
289
0
                shocks.insert(AssetClass::Utilities, -10.0);
290
0
                shocks.insert(AssetClass::Financials, 5.0); // Banks benefit from higher rates
291
0
                shocks
292
0
            },
293
0
            volatility_multiplier: 1.5,
294
0
            volatility_multipliers: HashMap::new(),
295
0
            correlation_adjustments: HashMap::new(),
296
0
            liquidity_haircuts: HashMap::new(),
297
0
            is_active: true,
298
0
        },
299
    ]
300
0
}
301
302
/// Create default asset class mapping for common symbols
303
2
fn create_default_asset_class_mapping() -> AssetClassMapping {
304
2
    let mut mappings = HashMap::new();
305
306
    // Large Cap Technology
307
2
    mappings.insert("AAPL".to_string(), AssetClass::Technology);
308
2
    mappings.insert("MSFT".to_string(), AssetClass::Technology);
309
2
    mappings.insert("GOOGL".to_string(), AssetClass::Technology);
310
2
    mappings.insert("GOOG".to_string(), AssetClass::Technology);
311
2
    mappings.insert("AMZN".to_string(), AssetClass::Technology);
312
2
    mappings.insert("META".to_string(), AssetClass::Technology);
313
2
    mappings.insert("TSLA".to_string(), AssetClass::Technology);
314
2
    mappings.insert("NVDA".to_string(), AssetClass::Technology);
315
316
    // Large Cap Financials
317
2
    mappings.insert("JPM".to_string(), AssetClass::Financials);
318
2
    mappings.insert("BAC".to_string(), AssetClass::Financials);
319
2
    mappings.insert("WFC".to_string(), AssetClass::Financials);
320
2
    mappings.insert("GS".to_string(), AssetClass::Financials);
321
2
    mappings.insert("MS".to_string(), AssetClass::Financials);
322
323
    // ETFs
324
2
    mappings.insert("SPY".to_string(), AssetClass::LargeCapEquity);
325
2
    mappings.insert("QQQ".to_string(), AssetClass::Technology);
326
2
    mappings.insert("IWM".to_string(), AssetClass::SmallCapEquity);
327
2
    mappings.insert("VTI".to_string(), AssetClass::LargeCapEquity);
328
2
    mappings.insert("EEM".to_string(), AssetClass::EmergingMarkets);
329
2
    mappings.insert("VEA".to_string(), AssetClass::InternationalEquity);
330
2
    mappings.insert("TLT".to_string(), AssetClass::USBonds);
331
2
    mappings.insert("HYG".to_string(), AssetClass::HighYieldBonds);
332
333
    // Healthcare
334
2
    mappings.insert("JNJ".to_string(), AssetClass::Healthcare);
335
2
    mappings.insert("PFE".to_string(), AssetClass::Healthcare);
336
2
    mappings.insert("UNH".to_string(), AssetClass::Healthcare);
337
338
    // Energy
339
2
    mappings.insert("XOM".to_string(), AssetClass::Energy);
340
2
    mappings.insert("CVX".to_string(), AssetClass::Energy);
341
342
2
    AssetClassMapping {
343
2
        mappings,
344
2
        default_class: AssetClass::LargeCapEquity,
345
2
    }
346
2
}
347
348
#[cfg(test)]
349
mod tests {
350
    use super::*;
351
352
    #[test]
353
1
    fn test_stress_scenario_config_creation() {
354
1
        let config = StressScenarioConfig {
355
1
            id: "test".to_string(),
356
1
            name: "Test Scenario".to_string(),
357
1
            description: "Test description".to_string(),
358
1
            instrument_shocks: HashMap::new(),
359
1
            asset_class_shocks: {
360
1
                let mut shocks = HashMap::new();
361
1
                shocks.insert(AssetClass::Technology, -10.0);
362
1
                shocks
363
1
            },
364
1
            volatility_multiplier: 2.0,
365
1
            volatility_multipliers: HashMap::new(),
366
1
            correlation_adjustments: HashMap::new(),
367
1
            liquidity_haircuts: HashMap::new(),
368
1
            is_active: true,
369
1
        };
370
371
1
        assert_eq!(config.id, "test");
372
1
        assert_eq!(config.volatility_multiplier, 2.0);
373
1
    }
374
375
    #[test]
376
1
    fn test_asset_class_mapping() {
377
1
        let mapping = create_default_asset_class_mapping();
378
379
1
        assert_eq!(mapping.mappings.get("AAPL"), Some(&AssetClass::Technology));
380
1
        assert_eq!(
381
1
            mapping.mappings.get("SPY"),
382
            Some(&AssetClass::LargeCapEquity)
383
        );
384
1
        assert_eq!(mapping.default_class, AssetClass::LargeCapEquity);
385
1
    }
386
387
    #[test]
388
1
    fn test_get_shock_for_symbol() {
389
1
        let config = StressScenarioConfig {
390
1
            id: "test".to_string(),
391
1
            name: "Test".to_string(),
392
1
            description: "Test".to_string(),
393
1
            instrument_shocks: {
394
1
                let mut shocks = HashMap::new();
395
1
                shocks.insert("AAPL".to_string(), -15.0);
396
1
                shocks
397
1
            },
398
1
            asset_class_shocks: {
399
1
                let mut shocks = HashMap::new();
400
1
                shocks.insert(AssetClass::Technology, -10.0);
401
1
                shocks.insert(AssetClass::LargeCapEquity, -5.0);
402
1
                shocks
403
1
            },
404
1
            volatility_multiplier: 1.0,
405
1
            volatility_multipliers: HashMap::new(),
406
1
            correlation_adjustments: HashMap::new(),
407
1
            liquidity_haircuts: HashMap::new(),
408
1
            is_active: true,
409
1
        };
410
411
1
        let mapping = create_default_asset_class_mapping();
412
413
        // Should get instrument-specific shock
414
1
        assert_eq!(config.get_shock_for_symbol("AAPL", &mapping), Some(-15.0));
415
416
        // Should get asset class shock for GOOGL (Technology)
417
1
        assert_eq!(config.get_shock_for_symbol("GOOGL", &mapping), Some(-10.0));
418
419
        // Should get default class shock for unknown symbol
420
1
        assert_eq!(config.get_shock_for_symbol("UNKNOWN", &mapping), Some(-5.0));
421
1
    }
422
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html deleted file mode 100644 index c4f2712f3..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/runtime.rs
Line
Count
Source
1
//! Runtime configuration layer for environment-aware defaults.
2
//!
3
//! This module provides Tier 2 runtime configuration that complements the
4
//! compile-time constants in `common::thresholds`. Values here can be overridden
5
//! via environment variables to support different deployment environments
6
//! (development, staging, production) without recompilation.
7
//!
8
//! # Architecture
9
//!
10
//! - Tier 1 (Compile-time): `common::thresholds` - Performance-critical constants
11
//! - Tier 2 (Runtime): This module - Environment-aware operational parameters
12
//! - Tier 3 (Database): Hot-reload via PostgreSQL NOTIFY/LISTEN
13
//!
14
//! # Environment Variables
15
//!
16
//! ## Database Configuration
17
//! - `DATABASE_QUERY_TIMEOUT_MS` - Query timeout in milliseconds (default: environment-aware)
18
//! - `DATABASE_CONNECTION_TIMEOUT_MS` - Connection timeout in milliseconds
19
//! - `DATABASE_POOL_SIZE` - Connection pool size
20
//! - `DATABASE_MAX_POOL_SIZE` - Maximum pool size
21
//! - `DATABASE_ACQUIRE_TIMEOUT_MS` - Pool acquire timeout in milliseconds
22
//!
23
//! ## Cache Configuration
24
//! - `CACHE_POSITION_TTL_SECS` - Position cache TTL in seconds
25
//! - `CACHE_VAR_TTL_SECS` - VaR calculation cache TTL in seconds
26
//! - `CACHE_COMPLIANCE_TTL_SECS` - Compliance check cache TTL in seconds
27
//! - `CACHE_MARKET_DATA_TTL_SECS` - Market data cache TTL in seconds
28
//! - `CACHE_MODEL_PREDICTION_TTL_SECS` - Model prediction cache TTL in seconds
29
//!
30
//! ## Network Configuration
31
//! - `NETWORK_GRPC_CONNECT_TIMEOUT_SECS` - gRPC connect timeout in seconds
32
//! - `NETWORK_GRPC_REQUEST_TIMEOUT_SECS` - gRPC request timeout in seconds
33
//! - `NETWORK_KEEP_ALIVE_INTERVAL_SECS` - Keep-alive interval in seconds
34
//! - `NETWORK_KEEP_ALIVE_TIMEOUT_SECS` - Keep-alive timeout in seconds
35
//! - `NETWORK_MAX_CONCURRENT_CONNECTIONS` - Maximum concurrent connections
36
//!
37
//! ## Retry Configuration
38
//! - `RETRY_INITIAL_DELAY_MS` - Initial retry delay in milliseconds
39
//! - `RETRY_MAX_DELAY_SECS` - Maximum retry delay in seconds
40
//! - `RETRY_MAX_ATTEMPTS` - Maximum retry attempts
41
//! - `RETRY_BACKOFF_MULTIPLIER` - Backoff multiplier for exponential backoff
42
//!
43
//! ## Safety Configuration
44
//! - `SAFETY_CHECK_TIMEOUT_MS` - Safety check timeout in milliseconds
45
//! - `SAFETY_AUTO_RECOVERY_DELAY_SECS` - Auto-recovery delay in seconds
46
//! - `SAFETY_LOSS_CHECK_INTERVAL_SECS` - Loss check interval in seconds
47
//! - `SAFETY_POSITION_CHECK_INTERVAL_SECS` - Position check interval in seconds
48
//!
49
//! ## ML Configuration
50
//! - `ML_MAX_BATCH_SIZE` - Maximum batch size for ML inference
51
//! - `ML_INFERENCE_TIMEOUT_MS` - ML inference timeout in milliseconds
52
//! - `ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS` - Model cache cleanup interval
53
//! - `ML_DRIFT_CHECK_INTERVAL_SECS` - Drift detection check interval
54
//!
55
//! ## Risk Configuration
56
//! - `RISK_VAR_LOOKBACK_DAYS` - VaR lookback period in trading days
57
//! - `RISK_VAR_CONFIDENCE` - VaR confidence level (0.0-1.0)
58
//! - `RISK_MAX_DRAWDOWN_WARNING_PCT` - Max drawdown warning threshold
59
//!
60
//! # Example
61
//!
62
//! ```rust,no_run
63
//! use config::runtime::{RuntimeConfig, Environment};
64
//!
65
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
66
//! // Auto-detect environment and load from env vars
67
//! let config = RuntimeConfig::from_env()?;
68
//!
69
//! // Or specify environment explicitly
70
//! let prod_config = RuntimeConfig::from_env_with_environment(Environment::Production)?;
71
//!
72
//! // Or use defaults for specific environment
73
//! let dev_config = RuntimeConfig::with_defaults(Environment::Development);
74
//!
75
//! println!("Database query timeout: {:?}", config.database.query_timeout);
76
//! println!("Position cache TTL: {:?}", config.cache.position_ttl);
77
//! # Ok(())
78
//! # }
79
//! ```
80
81
use crate::error::{ConfigError, ConfigResult};
82
use serde::{Deserialize, Serialize};
83
use std::time::Duration;
84
85
/// Deployment environment enumeration.
86
///
87
/// Determines default values for runtime configuration parameters.
88
/// Different environments have different performance vs safety trade-offs.
89
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90
pub enum Environment {
91
    /// Development environment - Relaxed timeouts, verbose logging
92
    Development,
93
    /// Staging environment - Production-like settings with some debug features
94
    Staging,
95
    /// Production environment - Optimized for performance and reliability
96
    Production,
97
}
98
99
impl Environment {
100
    /// Detects the environment from the ENVIRONMENT environment variable.
101
    ///
102
    /// Falls back to Development if not set or invalid.
103
1
    pub fn detect() -> Self {
104
1
        match std::env::var("ENVIRONMENT")
105
1
            .unwrap_or_else(|_| "development".to_string())
106
1
            .to_lowercase()
107
1
            .as_str()
108
        {
109
1
            "production" | "prod" => 
Environment::Production0
,
110
1
            "staging" | "stage" => 
Environment::Staging0
,
111
1
            _ => Environment::Development,
112
        }
113
1
    }
114
115
    /// Returns true if this is a production environment.
116
3
    pub fn is_production(&self) -> bool {
117
3
        
matches!2
(self, Environment::Production)
118
3
    }
119
120
    /// Returns true if this is a development environment.
121
3
    pub fn is_development(&self) -> bool {
122
3
        
matches!2
(self, Environment::Development)
123
3
    }
124
}
125
126
/// Database runtime configuration.
127
///
128
/// Controls database connection pooling, timeouts, and query execution limits.
129
#[derive(Debug, Clone, Serialize, Deserialize)]
130
pub struct DatabaseRuntimeConfig {
131
    /// Query timeout for standard operations
132
    pub query_timeout: Duration,
133
    /// Connection establishment timeout
134
    pub connection_timeout: Duration,
135
    /// Pool acquire timeout
136
    pub acquire_timeout: Duration,
137
    /// Default pool size
138
    pub pool_size: u32,
139
    /// Maximum pool size
140
    pub max_pool_size: u32,
141
    /// Connection lifetime
142
    pub connection_lifetime: Duration,
143
    /// Idle timeout
144
    pub idle_timeout: Duration,
145
}
146
147
impl DatabaseRuntimeConfig {
148
    /// Creates configuration with environment-aware defaults.
149
10
    pub fn with_defaults(env: Environment) -> Self {
150
10
        match env {
151
3
            Environment::Development => Self {
152
3
                query_timeout: Duration::from_millis(5000), // More relaxed for debugging
153
3
                connection_timeout: Duration::from_millis(500),
154
3
                acquire_timeout: Duration::from_millis(200),
155
3
                pool_size: 10,
156
3
                max_pool_size: 50,
157
3
                connection_lifetime: Duration::from_secs(1800), // 30 minutes
158
3
                idle_timeout: Duration::from_secs(600), // 10 minutes
159
3
            },
160
1
            Environment::Staging => Self {
161
1
                query_timeout: Duration::from_millis(2000),
162
1
                connection_timeout: Duration::from_millis(200),
163
1
                acquire_timeout: Duration::from_millis(100),
164
1
                pool_size: 15,
165
1
                max_pool_size: 75,
166
1
                connection_lifetime: Duration::from_secs(3600), // 1 hour
167
1
                idle_timeout: Duration::from_secs(300), // 5 minutes
168
1
            },
169
6
            Environment::Production => Self {
170
6
                query_timeout: Duration::from_millis(1000), // Tight timeout for HFT
171
6
                connection_timeout: Duration::from_millis(100),
172
6
                acquire_timeout: Duration::from_millis(50),
173
6
                pool_size: 20,
174
6
                max_pool_size: 100,
175
6
                connection_lifetime: Duration::from_secs(3600), // 1 hour
176
6
                idle_timeout: Duration::from_secs(300), // 5 minutes
177
6
            },
178
        }
179
10
    }
180
181
    /// Loads from environment variables with fallback to defaults.
182
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
183
0
        let defaults = Self::with_defaults(env);
184
185
        Ok(Self {
186
0
            query_timeout: parse_env_duration_ms("DATABASE_QUERY_TIMEOUT_MS", defaults.query_timeout)?,
187
0
            connection_timeout: parse_env_duration_ms("DATABASE_CONNECTION_TIMEOUT_MS", defaults.connection_timeout)?,
188
0
            acquire_timeout: parse_env_duration_ms("DATABASE_ACQUIRE_TIMEOUT_MS", defaults.acquire_timeout)?,
189
0
            pool_size: parse_env_u32("DATABASE_POOL_SIZE", defaults.pool_size)?,
190
0
            max_pool_size: parse_env_u32("DATABASE_MAX_POOL_SIZE", defaults.max_pool_size)?,
191
0
            connection_lifetime: parse_env_duration_secs("DATABASE_CONNECTION_LIFETIME_SECS", defaults.connection_lifetime)?,
192
0
            idle_timeout: parse_env_duration_secs("DATABASE_IDLE_TIMEOUT_SECS", defaults.idle_timeout)?,
193
        })
194
0
    }
195
196
    /// Validates the configuration.
197
5
    pub fn validate(&self) -> ConfigResult<()> {
198
5
        if self.query_timeout.as_millis() == 0 {
199
1
            return Err(ConfigError::Invalid("Query timeout must be positive".into()));
200
4
        }
201
4
        if self.pool_size == 0 {
202
1
            return Err(ConfigError::Invalid("Pool size must be positive".into()));
203
3
        }
204
3
        if self.pool_size > self.max_pool_size {
205
1
            return Err(ConfigError::Invalid("Pool size cannot exceed max pool size".into()));
206
2
        }
207
2
        Ok(())
208
5
    }
209
}
210
211
/// Cache TTL runtime configuration.
212
///
213
/// Controls time-to-live values for various cache types.
214
#[derive(Debug, Clone, Serialize, Deserialize)]
215
pub struct CacheRuntimeConfig {
216
    /// Position cache TTL
217
    pub position_ttl: Duration,
218
    /// VaR calculation cache TTL
219
    pub var_ttl: Duration,
220
    /// Compliance check cache TTL
221
    pub compliance_ttl: Duration,
222
    /// Market data cache TTL
223
    pub market_data_ttl: Duration,
224
    /// Model prediction cache TTL
225
    pub model_prediction_ttl: Duration,
226
}
227
228
impl CacheRuntimeConfig {
229
    /// Creates configuration with environment-aware defaults.
230
8
    pub fn with_defaults(env: Environment) -> Self {
231
8
        match env {
232
3
            Environment::Development => Self {
233
3
                position_ttl: Duration::from_secs(120), // Longer TTL for debugging
234
3
                var_ttl: Duration::from_secs(7200), // 2 hours
235
3
                compliance_ttl: Duration::from_secs(172800), // 48 hours
236
3
                market_data_ttl: Duration::from_secs(600), // 10 minutes
237
3
                model_prediction_ttl: Duration::from_secs(120), // 2 minutes
238
3
            },
239
1
            Environment::Staging => Self {
240
1
                position_ttl: Duration::from_secs(90),
241
1
                var_ttl: Duration::from_secs(5400), // 1.5 hours
242
1
                compliance_ttl: Duration::from_secs(129600), // 36 hours
243
1
                market_data_ttl: Duration::from_secs(450), // 7.5 minutes
244
1
                model_prediction_ttl: Duration::from_secs(90),
245
1
            },
246
4
            Environment::Production => Self {
247
4
                position_ttl: Duration::from_secs(60), // 1 minute for HFT
248
4
                var_ttl: Duration::from_secs(3600), // 1 hour
249
4
                compliance_ttl: Duration::from_secs(86400), // 24 hours
250
4
                market_data_ttl: Duration::from_secs(300), // 5 minutes
251
4
                model_prediction_ttl: Duration::from_secs(60), // 1 minute
252
4
            },
253
        }
254
8
    }
255
256
    /// Loads from environment variables with fallback to defaults.
257
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
258
0
        let defaults = Self::with_defaults(env);
259
260
        Ok(Self {
261
0
            position_ttl: parse_env_duration_secs("CACHE_POSITION_TTL_SECS", defaults.position_ttl)?,
262
0
            var_ttl: parse_env_duration_secs("CACHE_VAR_TTL_SECS", defaults.var_ttl)?,
263
0
            compliance_ttl: parse_env_duration_secs("CACHE_COMPLIANCE_TTL_SECS", defaults.compliance_ttl)?,
264
0
            market_data_ttl: parse_env_duration_secs("CACHE_MARKET_DATA_TTL_SECS", defaults.market_data_ttl)?,
265
0
            model_prediction_ttl: parse_env_duration_secs("CACHE_MODEL_PREDICTION_TTL_SECS", defaults.model_prediction_ttl)?,
266
        })
267
0
    }
268
269
    /// Validates the configuration.
270
3
    pub fn validate(&self) -> ConfigResult<()> {
271
3
        if self.position_ttl.as_secs() == 0 {
272
1
            return Err(ConfigError::Invalid("Position TTL must be positive".into()));
273
2
        }
274
2
        if self.var_ttl.as_secs() == 0 {
275
0
            return Err(ConfigError::Invalid("VaR TTL must be positive".into()));
276
2
        }
277
2
        Ok(())
278
3
    }
279
}
280
281
/// Network timeout runtime configuration.
282
///
283
/// Controls gRPC and network-related timeouts.
284
#[derive(Debug, Clone, Serialize, Deserialize)]
285
pub struct TimeoutConfig {
286
    /// gRPC connect timeout
287
    pub grpc_connect_timeout: Duration,
288
    /// gRPC request timeout
289
    pub grpc_request_timeout: Duration,
290
    /// Keep-alive interval
291
    pub keep_alive_interval: Duration,
292
    /// Keep-alive timeout
293
    pub keep_alive_timeout: Duration,
294
    /// Maximum concurrent connections
295
    pub max_concurrent_connections: u32,
296
}
297
298
impl TimeoutConfig {
299
    /// Creates configuration with environment-aware defaults.
300
7
    pub fn with_defaults(env: Environment) -> Self {
301
7
        match env {
302
3
            Environment::Development => Self {
303
3
                grpc_connect_timeout: Duration::from_secs(10),
304
3
                grpc_request_timeout: Duration::from_secs(30),
305
3
                keep_alive_interval: Duration::from_secs(60),
306
3
                keep_alive_timeout: Duration::from_secs(10),
307
3
                max_concurrent_connections: 50,
308
3
            },
309
1
            Environment::Staging => Self {
310
1
                grpc_connect_timeout: Duration::from_secs(7),
311
1
                grpc_request_timeout: Duration::from_secs(20),
312
1
                keep_alive_interval: Duration::from_secs(45),
313
1
                keep_alive_timeout: Duration::from_secs(7),
314
1
                max_concurrent_connections: 75,
315
1
            },
316
3
            Environment::Production => Self {
317
3
                grpc_connect_timeout: Duration::from_secs(5),
318
3
                grpc_request_timeout: Duration::from_secs(10),
319
3
                keep_alive_interval: Duration::from_secs(30),
320
3
                keep_alive_timeout: Duration::from_secs(5),
321
3
                max_concurrent_connections: 100,
322
3
            },
323
        }
324
7
    }
325
326
    /// Loads from environment variables with fallback to defaults.
327
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
328
0
        let defaults = Self::with_defaults(env);
329
330
        Ok(Self {
331
0
            grpc_connect_timeout: parse_env_duration_secs("NETWORK_GRPC_CONNECT_TIMEOUT_SECS", defaults.grpc_connect_timeout)?,
332
0
            grpc_request_timeout: parse_env_duration_secs("NETWORK_GRPC_REQUEST_TIMEOUT_SECS", defaults.grpc_request_timeout)?,
333
0
            keep_alive_interval: parse_env_duration_secs("NETWORK_KEEP_ALIVE_INTERVAL_SECS", defaults.keep_alive_interval)?,
334
0
            keep_alive_timeout: parse_env_duration_secs("NETWORK_KEEP_ALIVE_TIMEOUT_SECS", defaults.keep_alive_timeout)?,
335
0
            max_concurrent_connections: parse_env_u32("NETWORK_MAX_CONCURRENT_CONNECTIONS", defaults.max_concurrent_connections)?,
336
        })
337
0
    }
338
339
    /// Validates the configuration.
340
1
    pub fn validate(&self) -> ConfigResult<()> {
341
1
        if self.grpc_connect_timeout.as_secs() == 0 {
342
0
            return Err(ConfigError::Invalid("gRPC connect timeout must be positive".into()));
343
1
        }
344
1
        if self.max_concurrent_connections == 0 {
345
0
            return Err(ConfigError::Invalid("Max concurrent connections must be positive".into()));
346
1
        }
347
1
        Ok(())
348
1
    }
349
}
350
351
/// Operational limits runtime configuration.
352
///
353
/// Controls retry behavior, safety checks, ML parameters, and risk calculations.
354
#[derive(Debug, Clone, Serialize, Deserialize)]
355
pub struct LimitsConfig {
356
    // Retry configuration
357
    /// Initial retry delay
358
    pub retry_initial_delay: Duration,
359
    /// Maximum retry delay
360
    pub retry_max_delay: Duration,
361
    /// Maximum retry attempts
362
    pub retry_max_attempts: u32,
363
    /// Backoff multiplier
364
    pub retry_backoff_multiplier: f32,
365
366
    // Safety configuration
367
    /// Safety check timeout
368
    pub safety_check_timeout: Duration,
369
    /// Auto-recovery delay
370
    pub safety_auto_recovery_delay: Duration,
371
    /// Loss check interval
372
    pub safety_loss_check_interval: Duration,
373
    /// Position check interval
374
    pub safety_position_check_interval: Duration,
375
376
    // ML configuration
377
    /// Maximum batch size for ML inference
378
    pub ml_max_batch_size: usize,
379
    /// ML inference timeout
380
    pub ml_inference_timeout: Duration,
381
    /// Model cache cleanup interval
382
    pub ml_cache_cleanup_interval: Duration,
383
    /// Drift detection check interval
384
    pub ml_drift_check_interval: Duration,
385
386
    // Risk configuration
387
    /// VaR lookback period in trading days
388
    pub risk_var_lookback_days: usize,
389
    /// VaR confidence level
390
    pub risk_var_confidence: f64,
391
    /// Max drawdown warning threshold (percentage)
392
    pub risk_max_drawdown_warning_pct: u8,
393
}
394
395
impl LimitsConfig {
396
    /// Creates configuration with environment-aware defaults.
397
10
    pub fn with_defaults(env: Environment) -> Self {
398
10
        match env {
399
3
            Environment::Development => Self {
400
3
                // Retry
401
3
                retry_initial_delay: Duration::from_millis(200),
402
3
                retry_max_delay: Duration::from_secs(60),
403
3
                retry_max_attempts: 5,
404
3
                retry_backoff_multiplier: 2.0,
405
3
406
3
                // Safety
407
3
                safety_check_timeout: Duration::from_millis(50),
408
3
                safety_auto_recovery_delay: Duration::from_secs(60),
409
3
                safety_loss_check_interval: Duration::from_secs(30),
410
3
                safety_position_check_interval: Duration::from_secs(15),
411
3
412
3
                // ML
413
3
                ml_max_batch_size: 1024,
414
3
                ml_inference_timeout: Duration::from_millis(200),
415
3
                ml_cache_cleanup_interval: Duration::from_secs(7200), // 2 hours
416
3
                ml_drift_check_interval: Duration::from_secs(600), // 10 minutes
417
3
418
3
                // Risk
419
3
                risk_var_lookback_days: 252,
420
3
                risk_var_confidence: 0.95,
421
3
                risk_max_drawdown_warning_pct: 20,
422
3
            },
423
1
            Environment::Staging => Self {
424
1
                // Retry
425
1
                retry_initial_delay: Duration::from_millis(150),
426
1
                retry_max_delay: Duration::from_secs(45),
427
1
                retry_max_attempts: 4,
428
1
                retry_backoff_multiplier: 1.75,
429
1
430
1
                // Safety
431
1
                safety_check_timeout: Duration::from_millis(25),
432
1
                safety_auto_recovery_delay: Duration::from_secs(900), // 15 minutes
433
1
                safety_loss_check_interval: Duration::from_secs(15),
434
1
                safety_position_check_interval: Duration::from_secs(7),
435
1
436
1
                // ML
437
1
                ml_max_batch_size: 4096,
438
1
                ml_inference_timeout: Duration::from_millis(150),
439
1
                ml_cache_cleanup_interval: Duration::from_secs(5400), // 1.5 hours
440
1
                ml_drift_check_interval: Duration::from_secs(450), // 7.5 minutes
441
1
442
1
                // Risk
443
1
                risk_var_lookback_days: 252,
444
1
                risk_var_confidence: 0.95,
445
1
                risk_max_drawdown_warning_pct: 17,
446
1
            },
447
6
            Environment::Production => Self {
448
6
                // Retry
449
6
                retry_initial_delay: Duration::from_millis(100),
450
6
                retry_max_delay: Duration::from_secs(30),
451
6
                retry_max_attempts: 3,
452
6
                retry_backoff_multiplier: 1.5,
453
6
454
6
                // Safety
455
6
                safety_check_timeout: Duration::from_millis(5),
456
6
                safety_auto_recovery_delay: Duration::from_secs(1800), // 30 minutes
457
6
                safety_loss_check_interval: Duration::from_secs(5),
458
6
                safety_position_check_interval: Duration::from_secs(2),
459
6
460
6
                // ML
461
6
                ml_max_batch_size: 8192,
462
6
                ml_inference_timeout: Duration::from_millis(100),
463
6
                ml_cache_cleanup_interval: Duration::from_secs(3600), // 1 hour
464
6
                ml_drift_check_interval: Duration::from_secs(300), // 5 minutes
465
6
466
6
                // Risk
467
6
                risk_var_lookback_days: 252,
468
6
                risk_var_confidence: 0.95,
469
6
                risk_max_drawdown_warning_pct: 15,
470
6
            },
471
        }
472
10
    }
473
474
    /// Loads from environment variables with fallback to defaults.
475
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
476
0
        let defaults = Self::with_defaults(env);
477
478
        Ok(Self {
479
            // Retry
480
0
            retry_initial_delay: parse_env_duration_ms("RETRY_INITIAL_DELAY_MS", defaults.retry_initial_delay)?,
481
0
            retry_max_delay: parse_env_duration_secs("RETRY_MAX_DELAY_SECS", defaults.retry_max_delay)?,
482
0
            retry_max_attempts: parse_env_u32("RETRY_MAX_ATTEMPTS", defaults.retry_max_attempts)?,
483
0
            retry_backoff_multiplier: parse_env_f32("RETRY_BACKOFF_MULTIPLIER", defaults.retry_backoff_multiplier)?,
484
485
            // Safety
486
0
            safety_check_timeout: parse_env_duration_ms("SAFETY_CHECK_TIMEOUT_MS", defaults.safety_check_timeout)?,
487
0
            safety_auto_recovery_delay: parse_env_duration_secs("SAFETY_AUTO_RECOVERY_DELAY_SECS", defaults.safety_auto_recovery_delay)?,
488
0
            safety_loss_check_interval: parse_env_duration_secs("SAFETY_LOSS_CHECK_INTERVAL_SECS", defaults.safety_loss_check_interval)?,
489
0
            safety_position_check_interval: parse_env_duration_secs("SAFETY_POSITION_CHECK_INTERVAL_SECS", defaults.safety_position_check_interval)?,
490
491
            // ML
492
0
            ml_max_batch_size: parse_env_usize("ML_MAX_BATCH_SIZE", defaults.ml_max_batch_size)?,
493
0
            ml_inference_timeout: parse_env_duration_ms("ML_INFERENCE_TIMEOUT_MS", defaults.ml_inference_timeout)?,
494
0
            ml_cache_cleanup_interval: parse_env_duration_secs("ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS", defaults.ml_cache_cleanup_interval)?,
495
0
            ml_drift_check_interval: parse_env_duration_secs("ML_DRIFT_CHECK_INTERVAL_SECS", defaults.ml_drift_check_interval)?,
496
497
            // Risk
498
0
            risk_var_lookback_days: parse_env_usize("RISK_VAR_LOOKBACK_DAYS", defaults.risk_var_lookback_days)?,
499
0
            risk_var_confidence: parse_env_f64("RISK_VAR_CONFIDENCE", defaults.risk_var_confidence)?,
500
0
            risk_max_drawdown_warning_pct: parse_env_u8("RISK_MAX_DRAWDOWN_WARNING_PCT", defaults.risk_max_drawdown_warning_pct)?,
501
        })
502
0
    }
503
504
    /// Validates the configuration.
505
5
    pub fn validate(&self) -> ConfigResult<()> {
506
5
        if self.retry_max_attempts == 0 {
507
1
            return Err(ConfigError::Invalid("Retry max attempts must be positive".into()));
508
4
        }
509
4
        if self.retry_backoff_multiplier <= 1.0 {
510
1
            return Err(ConfigError::Invalid("Backoff multiplier must be > 1.0".into()));
511
3
        }
512
3
        if self.ml_max_batch_size == 0 {
513
0
            return Err(ConfigError::Invalid("ML max batch size must be positive".into()));
514
3
        }
515
3
        if self.risk_var_confidence < 0.0 || self.risk_var_confidence > 1.0 {
516
1
            return Err(ConfigError::Invalid("VaR confidence must be between 0.0 and 1.0".into()));
517
2
        }
518
2
        if self.risk_var_lookback_days == 0 {
519
0
            return Err(ConfigError::Invalid("VaR lookback days must be positive".into()));
520
2
        }
521
2
        Ok(())
522
5
    }
523
}
524
525
/// Complete runtime configuration for the Foxhunt trading system.
526
///
527
/// Aggregates all runtime configuration categories with environment-aware defaults
528
/// and environment variable overrides.
529
#[derive(Debug, Clone, Serialize, Deserialize)]
530
pub struct RuntimeConfig {
531
    /// Detected or specified environment
532
    pub environment: Environment,
533
    /// Database configuration
534
    pub database: DatabaseRuntimeConfig,
535
    /// Cache configuration
536
    pub cache: CacheRuntimeConfig,
537
    /// Timeout configuration
538
    pub timeouts: TimeoutConfig,
539
    /// Limits and operational parameters
540
    pub limits: LimitsConfig,
541
}
542
543
impl RuntimeConfig {
544
    /// Creates runtime configuration by auto-detecting environment and loading from env vars.
545
    ///
546
    /// # Errors
547
    ///
548
    /// Returns ConfigError if environment variables contain invalid values or
549
    /// if validation fails.
550
0
    pub fn from_env() -> ConfigResult<Self> {
551
0
        let environment = Environment::detect();
552
0
        Self::from_env_with_environment(environment)
553
0
    }
554
555
    /// Creates runtime configuration with specified environment and loads from env vars.
556
    ///
557
    /// # Arguments
558
    ///
559
    /// * `environment` - The deployment environment to use for defaults
560
    ///
561
    /// # Errors
562
    ///
563
    /// Returns ConfigError if environment variables contain invalid values or
564
    /// if validation fails.
565
0
    pub fn from_env_with_environment(environment: Environment) -> ConfigResult<Self> {
566
0
        let config = Self {
567
0
            environment,
568
0
            database: DatabaseRuntimeConfig::from_env(environment)?,
569
0
            cache: CacheRuntimeConfig::from_env(environment)?,
570
0
            timeouts: TimeoutConfig::from_env(environment)?,
571
0
            limits: LimitsConfig::from_env(environment)?,
572
        };
573
574
0
        config.validate()?;
575
0
        Ok(config)
576
0
    }
577
578
    /// Creates runtime configuration with environment-specific defaults.
579
    ///
580
    /// Does not read from environment variables. Useful for testing or
581
    /// when you want pure default values.
582
    ///
583
    /// # Arguments
584
    ///
585
    /// * `environment` - The deployment environment to use for defaults
586
5
    pub fn with_defaults(environment: Environment) -> Self {
587
5
        Self {
588
5
            environment,
589
5
            database: DatabaseRuntimeConfig::with_defaults(environment),
590
5
            cache: CacheRuntimeConfig::with_defaults(environment),
591
5
            timeouts: TimeoutConfig::with_defaults(environment),
592
5
            limits: LimitsConfig::with_defaults(environment),
593
5
        }
594
5
    }
595
596
    /// Validates the entire runtime configuration.
597
    ///
598
    /// # Errors
599
    ///
600
    /// Returns ConfigError if any configuration values are invalid.
601
1
    pub fn validate(&self) -> ConfigResult<()> {
602
1
        self.database.validate()
?0
;
603
1
        self.cache.validate()
?0
;
604
1
        self.timeouts.validate()
?0
;
605
1
        self.limits.validate()
?0
;
606
1
        Ok(())
607
1
    }
608
}
609
610
// Helper functions for parsing environment variables
611
612
0
fn parse_env_duration_ms(key: &str, default: Duration) -> ConfigResult<Duration> {
613
0
    match std::env::var(key) {
614
0
        Ok(val) => {
615
0
            let ms = val.parse::<u64>()
616
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
617
0
            Ok(Duration::from_millis(ms))
618
        }
619
0
        Err(_) => Ok(default),
620
    }
621
0
}
622
623
0
fn parse_env_duration_secs(key: &str, default: Duration) -> ConfigResult<Duration> {
624
0
    match std::env::var(key) {
625
0
        Ok(val) => {
626
0
            let secs = val.parse::<u64>()
627
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
628
0
            Ok(Duration::from_secs(secs))
629
        }
630
0
        Err(_) => Ok(default),
631
    }
632
0
}
633
634
0
fn parse_env_u32(key: &str, default: u32) -> ConfigResult<u32> {
635
0
    match std::env::var(key) {
636
0
        Ok(val) => val.parse::<u32>()
637
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u32 for {}: {}", key, e))),
638
0
        Err(_) => Ok(default),
639
    }
640
0
}
641
642
0
fn parse_env_u8(key: &str, default: u8) -> ConfigResult<u8> {
643
0
    match std::env::var(key) {
644
0
        Ok(val) => val.parse::<u8>()
645
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u8 for {}: {}", key, e))),
646
0
        Err(_) => Ok(default),
647
    }
648
0
}
649
650
0
fn parse_env_usize(key: &str, default: usize) -> ConfigResult<usize> {
651
0
    match std::env::var(key) {
652
0
        Ok(val) => val.parse::<usize>()
653
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid usize for {}: {}", key, e))),
654
0
        Err(_) => Ok(default),
655
    }
656
0
}
657
658
0
fn parse_env_f32(key: &str, default: f32) -> ConfigResult<f32> {
659
0
    match std::env::var(key) {
660
0
        Ok(val) => val.parse::<f32>()
661
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f32 for {}: {}", key, e))),
662
0
        Err(_) => Ok(default),
663
    }
664
0
}
665
666
0
fn parse_env_f64(key: &str, default: f64) -> ConfigResult<f64> {
667
0
    match std::env::var(key) {
668
0
        Ok(val) => val.parse::<f64>()
669
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f64 for {}: {}", key, e))),
670
0
        Err(_) => Ok(default),
671
    }
672
0
}
673
674
#[cfg(test)]
675
mod tests {
676
    use super::*;
677
678
    #[test]
679
1
    fn test_environment_detection() {
680
        // Should default to Development
681
1
        let env = Environment::detect();
682
1
        assert!(
matches!0
(env, Environment::Development | Environment::Production | Environment::Staging));
683
1
    }
684
685
    #[test]
686
1
    fn test_environment_is_production() {
687
1
        assert!(Environment::Production.is_production());
688
1
        assert!(!Environment::Development.is_production());
689
1
        assert!(!Environment::Staging.is_production());
690
1
    }
691
692
    #[test]
693
1
    fn test_environment_is_development() {
694
1
        assert!(Environment::Development.is_development());
695
1
        assert!(!Environment::Production.is_development());
696
1
        assert!(!Environment::Staging.is_development());
697
1
    }
698
699
    #[test]
700
1
    fn test_runtime_config_with_defaults() {
701
1
        let config = RuntimeConfig::with_defaults(Environment::Production);
702
1
        assert_eq!(config.environment, Environment::Production);
703
1
        assert!(config.database.query_timeout.as_millis() > 0);
704
1
        assert!(config.cache.position_ttl.as_secs() > 0);
705
1
    }
706
707
    #[test]
708
1
    fn test_runtime_config_validation() {
709
1
        let config = RuntimeConfig::with_defaults(Environment::Development);
710
1
        assert!(config.validate().is_ok());
711
1
    }
712
713
    #[test]
714
1
    fn test_database_config_defaults() {
715
1
        let dev_config = DatabaseRuntimeConfig::with_defaults(Environment::Development);
716
1
        let prod_config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
717
718
        // Production should have tighter timeouts
719
1
        assert!(prod_config.query_timeout < dev_config.query_timeout);
720
1
        assert!(prod_config.connection_timeout < dev_config.connection_timeout);
721
1
    }
722
723
    #[test]
724
1
    fn test_cache_config_defaults() {
725
1
        let dev_config = CacheRuntimeConfig::with_defaults(Environment::Development);
726
1
        let prod_config = CacheRuntimeConfig::with_defaults(Environment::Production);
727
728
        // Production should have shorter TTLs for HFT
729
1
        assert!(prod_config.position_ttl < dev_config.position_ttl);
730
1
        assert!(prod_config.var_ttl < dev_config.var_ttl);
731
1
    }
732
733
    #[test]
734
1
    fn test_timeout_config_defaults() {
735
1
        let dev_config = TimeoutConfig::with_defaults(Environment::Development);
736
1
        let prod_config = TimeoutConfig::with_defaults(Environment::Production);
737
738
        // Production should have tighter timeouts
739
1
        assert!(prod_config.grpc_request_timeout < dev_config.grpc_request_timeout);
740
1
        assert!(prod_config.grpc_connect_timeout < dev_config.grpc_connect_timeout);
741
1
    }
742
743
    #[test]
744
1
    fn test_limits_config_defaults() {
745
1
        let dev_config = LimitsConfig::with_defaults(Environment::Development);
746
1
        let prod_config = LimitsConfig::with_defaults(Environment::Production);
747
748
        // Production should have more aggressive settings
749
1
        assert!(prod_config.safety_check_timeout < dev_config.safety_check_timeout);
750
1
        assert!(prod_config.ml_inference_timeout < dev_config.ml_inference_timeout);
751
1
    }
752
753
    #[test]
754
1
    fn test_database_config_validation() {
755
1
        let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
756
1
        assert!(config.validate().is_ok());
757
758
1
        config.query_timeout = Duration::from_millis(0);
759
1
        assert!(config.validate().is_err());
760
761
1
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
762
1
        config.pool_size = 0;
763
1
        assert!(config.validate().is_err());
764
765
1
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
766
1
        config.pool_size = 200;
767
1
        config.max_pool_size = 100;
768
1
        assert!(config.validate().is_err());
769
1
    }
770
771
    #[test]
772
1
    fn test_cache_config_validation() {
773
1
        let mut config = CacheRuntimeConfig::with_defaults(Environment::Production);
774
1
        assert!(config.validate().is_ok());
775
776
1
        config.position_ttl = Duration::from_secs(0);
777
1
        assert!(config.validate().is_err());
778
1
    }
779
780
    #[test]
781
1
    fn test_limits_config_validation() {
782
1
        let mut config = LimitsConfig::with_defaults(Environment::Production);
783
1
        assert!(config.validate().is_ok());
784
785
1
        config.retry_max_attempts = 0;
786
1
        assert!(config.validate().is_err());
787
788
1
        config = LimitsConfig::with_defaults(Environment::Production);
789
1
        config.retry_backoff_multiplier = 0.5;
790
1
        assert!(config.validate().is_err());
791
792
1
        config = LimitsConfig::with_defaults(Environment::Production);
793
1
        config.risk_var_confidence = 1.5;
794
1
        assert!(config.validate().is_err());
795
1
    }
796
797
    #[test]
798
1
    fn test_staging_environment_defaults() {
799
1
        let config = RuntimeConfig::with_defaults(Environment::Staging);
800
801
        // Staging should be between dev and prod
802
1
        let dev_config = RuntimeConfig::with_defaults(Environment::Development);
803
1
        let prod_config = RuntimeConfig::with_defaults(Environment::Production);
804
805
1
        assert!(config.database.query_timeout > prod_config.database.query_timeout);
806
1
        assert!(config.database.query_timeout < dev_config.database.query_timeout);
807
1
    }
808
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html deleted file mode 100644 index 8177fefa8..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/schemas.rs
Line
Count
Source
1
//! Configuration schemas and cloud storage configurations.
2
//!
3
//! This module defines configuration schemas for various cloud storage backends
4
//! and configuration versioning. Primarily focused on S3-compatible storage
5
//! for model artifacts and configuration management in the Foxhunt trading system.
6
7
use chrono::{DateTime, Utc};
8
use serde::{Deserialize, Serialize};
9
use std::collections::HashMap;
10
use std::time::Duration;
11
use uuid::Uuid;
12
13
/// Configuration schema metadata for versioning and tracking.
14
///
15
/// Provides versioning and audit trail information for configuration schemas.
16
/// Used to track configuration changes over time and maintain compatibility
17
/// across different versions of the trading system.
18
#[derive(Debug, Clone, Serialize, Deserialize)]
19
pub struct ConfigSchema {
20
    /// Unique identifier for this configuration schema
21
    pub id: Uuid,
22
    /// Semantic version string (e.g., "1.2.3")
23
    pub version: String,
24
    /// Timestamp when this schema was created
25
    pub created_at: DateTime<Utc>,
26
    /// Timestamp when this schema was last updated
27
    pub updated_at: DateTime<Utc>,
28
}
29
30
/// Amazon S3 and S3-compatible storage configuration.
31
///
32
/// Configures access to S3 or S3-compatible storage services for storing
33
/// ML model artifacts, configuration backups, and other binary data.
34
/// Supports various authentication methods and connection options.
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct S3Config {
37
    /// S3 bucket name for storing model artifacts and data
38
    pub bucket_name: String,
39
    /// AWS region or S3-compatible service region
40
    pub region: String,
41
    /// AWS access key ID (optional, can use IAM roles or environment variables)
42
    pub access_key_id: Option<String>,
43
    /// AWS secret access key (optional, can use IAM roles or environment variables)
44
    pub secret_access_key: Option<String>,
45
    /// AWS session token for temporary credentials (optional)
46
    pub session_token: Option<String>,
47
    /// Custom S3-compatible endpoint URL (e.g., MinIO, DigitalOcean Spaces)
48
    pub endpoint_url: Option<String>,
49
    /// Force path-style URLs instead of virtual-hosted-style URLs
50
    pub force_path_style: bool,
51
    /// Request timeout duration for S3 operations
52
    pub timeout: Duration,
53
    /// Maximum number of retry attempts for failed requests
54
    pub max_retry_attempts: u32,
55
    /// Enable SSL/TLS for S3 connections
56
    pub use_ssl: bool,
57
}
58
59
impl S3Config {
60
    /// Validates the S3 configuration for correctness.
61
    ///
62
    /// Performs validation checks on the S3 configuration to ensure all
63
    /// required fields are present and have valid values before attempting
64
    /// to establish connections to S3 services.
65
    ///
66
    /// # Errors
67
    ///
68
    /// Returns an error string if the configuration is invalid:
69
    /// - Empty bucket name
70
    /// - Empty region
71
    /// - Invalid endpoint URL format
72
0
    pub fn validate(&self) -> Result<(), String> {
73
0
        if self.bucket_name.is_empty() {
74
0
            return Err("S3 bucket name cannot be empty".to_string());
75
0
        }
76
0
        if self.region.is_empty() {
77
0
            return Err("S3 region cannot be empty".to_string());
78
0
        }
79
0
        Ok(())
80
0
    }
81
}
82
83
/// Asset classification configuration for sector and type categorization.
84
///
85
/// Provides configuration-driven asset classification that replaces hardcoded
86
/// symbol-based classification logic. Supports flexible categorization rules
87
/// based on instrument properties rather than specific symbol names.
88
#[derive(Debug, Clone, Serialize, Deserialize)]
89
pub struct AssetClassificationConfig {
90
    /// Classification rules based on asset type patterns
91
    pub asset_type_rules: HashMap<String, String>,
92
    /// Default classifications for different asset categories
93
    pub default_sectors: HashMap<String, String>,
94
    /// Regex patterns for currency pair detection
95
    pub currency_patterns: Vec<String>,
96
    /// Regex patterns for cryptocurrency detection
97
    pub crypto_patterns: Vec<String>,
98
}
99
100
impl AssetClassificationConfig {
101
    /// Creates a new asset classification configuration with default rules.
102
0
    pub fn new() -> Self {
103
0
        let mut asset_type_rules = HashMap::new();
104
0
        asset_type_rules.insert("EQUITY".to_string(), "Equity".to_string());
105
0
        asset_type_rules.insert("FOREX".to_string(), "Currencies".to_string());
106
0
        asset_type_rules.insert("CRYPTO".to_string(), "Cryptocurrency".to_string());
107
0
        asset_type_rules.insert("COMMODITY".to_string(), "Commodities".to_string());
108
0
        asset_type_rules.insert("BOND".to_string(), "Fixed Income".to_string());
109
110
0
        let mut default_sectors = HashMap::new();
111
0
        default_sectors.insert("Equity".to_string(), "Other".to_string());
112
0
        default_sectors.insert("Currencies".to_string(), "Currencies".to_string());
113
0
        default_sectors.insert("Cryptocurrency".to_string(), "Cryptocurrency".to_string());
114
0
        default_sectors.insert("Commodities".to_string(), "Commodities".to_string());
115
0
        default_sectors.insert("Fixed Income".to_string(), "Fixed Income".to_string());
116
117
0
        Self {
118
0
            asset_type_rules,
119
0
            default_sectors,
120
0
            currency_patterns: vec![
121
0
                r"^[A-Z]{3}[A-Z]{3}$".to_string(), // USDEUR format
122
0
                r".*USD.*".to_string(),
123
0
                r".*EUR.*".to_string(),
124
0
                r".*GBP.*".to_string(),
125
0
                r".*JPY.*".to_string(),
126
0
            ],
127
0
            crypto_patterns: vec![
128
0
                r".*BTC.*".to_string(),
129
0
                r".*ETH.*".to_string(),
130
0
                r".*CRYPTO.*".to_string(),
131
0
            ],
132
0
        }
133
0
    }
134
135
    /// Classifies an instrument based on configuration rules.
136
0
    pub fn classify_sector(&self, instrument_id: &str, asset_type: Option<&str>) -> String {
137
        // First try to classify based on asset type if provided
138
0
        if let Some(asset_type) = asset_type {
139
0
            if let Some(sector) = self.asset_type_rules.get(asset_type) {
140
0
                return sector.clone();
141
0
            }
142
0
        }
143
144
        // Check for currency patterns
145
0
        for pattern in &self.currency_patterns {
146
0
            if let Ok(regex) = regex::Regex::new(pattern) {
147
0
                if regex.is_match(instrument_id) {
148
0
                    return "Currencies".to_string();
149
0
                }
150
0
            }
151
        }
152
153
        // Check for crypto patterns
154
0
        for pattern in &self.crypto_patterns {
155
0
            if let Ok(regex) = regex::Regex::new(pattern) {
156
0
                if regex.is_match(instrument_id) {
157
0
                    return "Cryptocurrency".to_string();
158
0
                }
159
0
            }
160
        }
161
162
        // Default classification
163
0
        "Other".to_string()
164
0
    }
165
}
166
167
impl Default for AssetClassificationConfig {
168
0
    fn default() -> Self {
169
0
        Self::new()
170
0
    }
171
}
172
173
impl Default for S3Config {
174
0
    fn default() -> Self {
175
0
        Self {
176
0
            bucket_name: "foxhunt-models".to_string(),
177
0
            region: "us-east-1".to_string(),
178
0
            access_key_id: None,
179
0
            secret_access_key: None,
180
0
            session_token: None,
181
0
            endpoint_url: None,
182
0
            force_path_style: false,
183
0
            timeout: Duration::from_secs(30),
184
0
            max_retry_attempts: 3,
185
0
            use_ssl: true,
186
0
        }
187
0
    }
188
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html deleted file mode 100644 index f6581a909..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs
Line
Count
Source
1
//! Model storage and metadata configuration structures.
2
//!
3
//! This module defines configuration structures for managing ML model metadata,
4
//! training metrics, and architectural information. Used for model versioning,
5
//! performance tracking, and deployment management in the Foxhunt trading system.
6
7
use chrono::{DateTime, Utc};
8
use serde::{Deserialize, Serialize};
9
use std::path::PathBuf;
10
use uuid::Uuid;
11
12
/// Comprehensive metadata for ML model storage and tracking.
13
///
14
/// Contains all information necessary for model identification, versioning,
15
/// and performance tracking. Used for model lifecycle management and
16
/// deployment coordination across the trading system.
17
#[derive(Debug, Clone, Serialize, Deserialize)]
18
pub struct ModelMetadata {
19
    /// Unique identifier for this model instance
20
    pub id: Uuid,
21
    /// Human-readable model name (e.g., "mamba2-price-prediction")
22
    pub name: String,
23
    /// Semantic version string (e.g., "1.2.3")
24
    pub version: String,
25
    /// Timestamp when this model was created/trained
26
    pub created_at: DateTime<Utc>,
27
    /// Timestamp when this model metadata was last updated
28
    pub updated_at: DateTime<Utc>,
29
    /// Training performance metrics for model evaluation
30
    pub training_metrics: TrainingMetrics,
31
    /// Model architecture and hyperparameter configuration
32
    pub architecture: ModelArchitecture,
33
}
34
35
/// Training performance metrics for model evaluation.
36
///
37
/// Captures key performance indicators from model training to enable
38
/// comparison between different model versions and architectures.
39
/// Essential for model selection and performance monitoring.
40
#[derive(Debug, Clone, Serialize, Deserialize)]
41
pub struct TrainingMetrics {
42
    /// Final training accuracy (0.0 to 1.0)
43
    pub accuracy: f64,
44
    /// Final training loss value
45
    pub loss: f64,
46
    /// Final validation accuracy (0.0 to 1.0)
47
    pub validation_accuracy: f64,
48
    /// Final validation loss value
49
    pub validation_loss: f64,
50
    /// Number of training epochs completed
51
    pub epochs: u32,
52
    /// Total training time in seconds
53
    pub training_time_seconds: f64,
54
}
55
56
/// Model architecture and hyperparameter specification.
57
///
58
/// Defines the structural configuration of ML models including layer
59
/// dimensions, activation functions, and optimization parameters.
60
/// Used for model reconstruction and hyperparameter tracking.
61
#[derive(Debug, Clone, Serialize, Deserialize)]
62
pub struct ModelArchitecture {
63
    /// Model type identifier (e.g., "mamba2", "transformer", "dqn")
64
    pub model_type: String,
65
    /// Input feature dimension size
66
    pub input_dim: usize,
67
    /// Output prediction dimension size
68
    pub output_dim: usize,
69
    /// Hidden layer sizes in order from input to output
70
    pub hidden_layers: Vec<usize>,
71
    /// Activation function name (e.g., "relu", "gelu", "swish")
72
    pub activation: String,
73
    /// Optimizer type (e.g., "adam", "sgd", "adamw")
74
    pub optimizer: String,
75
    /// Learning rate used during training
76
    pub learning_rate: f64,
77
}
78
79
/// Storage configuration for model artifacts
80
#[derive(Debug, Clone, Serialize, Deserialize)]
81
pub struct StorageConfig {
82
    /// Storage type (e.g., "local", "s3")
83
    pub storage_type: String,
84
    /// Local base path for file storage (required for "local" storage type)
85
    pub local_base_path: Option<PathBuf>,
86
    /// Enable compression for stored models
87
    pub enable_compression: bool,
88
}
89
90
impl Default for StorageConfig {
91
0
    fn default() -> Self {
92
0
        Self {
93
0
            storage_type: "local".to_string(),
94
0
            local_base_path: Some(PathBuf::from("/tmp/foxhunt/models")),
95
0
            enable_compression: false,
96
0
        }
97
0
    }
98
}
99
100
impl StorageConfig {
101
    /// Create StorageConfig from environment variables
102
0
    pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
103
0
        let storage_type = std::env::var("STORAGE_TYPE").unwrap_or_else(|_| "local".to_string());
104
0
        let local_base_path = std::env::var("STORAGE_LOCAL_PATH")
105
0
            .ok()
106
0
            .map(PathBuf::from)
107
0
            .or_else(|| Some(PathBuf::from("/tmp/foxhunt/models")));
108
0
        let enable_compression = std::env::var("STORAGE_ENABLE_COMPRESSION")
109
0
            .ok()
110
0
            .and_then(|v| v.parse().ok())
111
0
            .unwrap_or(false);
112
113
0
        Ok(Self {
114
0
            storage_type,
115
0
            local_base_path,
116
0
            enable_compression,
117
0
        })
118
0
    }
119
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html deleted file mode 100644 index 3e48cd174..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/structures.rs
Line
Count
Source
1
//! Configuration structures
2
3
use rust_decimal::Decimal;
4
use serde::{Deserialize, Serialize};
5
use std::collections::HashMap;
6
7
#[derive(Debug, Clone, Serialize, Deserialize)]
8
pub struct RiskConfig {
9
    /// Maximum single position size in base currency
10
    pub max_position_size: Decimal,
11
    /// Maximum total portfolio exposure in base currency
12
    pub max_portfolio_exposure: Decimal,
13
    /// Maximum concentration percentage for a single position (0.0-1.0)
14
    pub max_concentration_pct: Decimal,
15
    /// Maximum daily loss threshold in base currency
16
    pub max_daily_loss: Decimal,
17
    /// Maximum drawdown percentage allowed (0.0-1.0)
18
    pub max_drawdown_pct: Decimal,
19
    /// Stop loss threshold in base currency
20
    pub stop_loss_threshold: Decimal,
21
    /// VaR confidence level (e.g., 0.95 for 95%)
22
    pub var_confidence_level: f64,
23
    /// VaR time horizon in days
24
    pub var_time_horizon: u32,
25
    /// 1-day VaR limit in base currency
26
    pub var_limit_1d: Decimal,
27
    /// 10-day VaR limit in base currency
28
    pub var_limit_10d: Decimal,
29
    /// Maximum single order size in base currency
30
    pub max_order_size: Decimal,
31
    /// Maximum orders per second (rate limiting)
32
    pub max_orders_per_second: u64,
33
    /// Maximum notional value per hour in base currency
34
    pub max_notional_per_hour: Decimal,
35
    /// Kelly criterion fraction limit (0.0-1.0)
36
    pub kelly_fraction_limit: f64,
37
    /// Maximum Kelly criterion position size (0.0-1.0)
38
    pub max_kelly_position_size: f64,
39
    /// Emergency stop threshold as fraction of capital (0.0-1.0)
40
    pub emergency_stop_threshold: f64,
41
    /// VaR configuration
42
    pub var_config: VarConfig,
43
    /// Circuit breaker configuration
44
    pub circuit_breaker: CircuitBreakerConfig,
45
    /// Position limits configuration
46
    pub position_limits: PositionLimitsConfig,
47
    /// Asset classification configuration
48
    pub asset_classification: AssetClassificationConfig,
49
}
50
51
impl Default for RiskConfig {
52
0
    fn default() -> Self {
53
0
        Self {
54
0
            // Position and exposure limits
55
0
            max_position_size: Decimal::new(1_000_000, 0), // $1M max single position
56
0
            max_portfolio_exposure: Decimal::new(10_000_000, 0), // $10M total portfolio exposure
57
0
            max_concentration_pct: Decimal::new(25, 2), // 25% max concentration
58
0
            
59
0
            // Loss and drawdown limits
60
0
            max_daily_loss: Decimal::new(100_000, 0), // $100K max daily loss
61
0
            max_drawdown_pct: Decimal::new(15, 2), // 15% max drawdown
62
0
            stop_loss_threshold: Decimal::new(50_000, 0), // $50K stop loss threshold
63
0
            
64
0
            // VaR configuration
65
0
            var_confidence_level: 0.95, // 95% confidence
66
0
            var_time_horizon: 1, // 1-day horizon
67
0
            var_limit_1d: Decimal::new(50_000, 0), // $50K 1-day VaR limit
68
0
            var_limit_10d: Decimal::new(150_000, 0), // $150K 10-day VaR limit
69
0
            
70
0
            // Order limits and rate limiting
71
0
            max_order_size: Decimal::new(100_000, 0), // $100K max order size
72
0
            max_orders_per_second: 100, // 100 orders/sec
73
0
            max_notional_per_hour: Decimal::new(10_000_000, 0), // $10M hourly notional
74
0
            
75
0
            // Kelly criterion parameters
76
0
            kelly_fraction_limit: 0.25, // 25% Kelly fraction limit
77
0
            max_kelly_position_size: 0.20, // 20% max Kelly position
78
0
            
79
0
            // Emergency stop
80
0
            emergency_stop_threshold: 0.10, // 10% loss triggers emergency stop
81
0
            
82
0
            // Nested configurations
83
0
            var_config: VarConfig::default(),
84
0
            circuit_breaker: CircuitBreakerConfig::default(),
85
0
            position_limits: PositionLimitsConfig::default(),
86
0
            asset_classification: AssetClassificationConfig::default(),
87
0
        }
88
0
    }
89
}
90
91
#[derive(Debug, Clone, Serialize, Deserialize)]
92
pub struct VarConfig {
93
    /// VaR confidence level (0.0-1.0)
94
    pub confidence_level: f64,
95
    /// Time horizon in days
96
    pub time_horizon_days: u32,
97
    /// Historical lookback period in days
98
    pub lookback_period_days: u32,
99
    /// Calculation method (e.g., "historical", "monte_carlo")
100
    pub calculation_method: String,
101
    /// Maximum VaR limit
102
    pub max_var_limit: f64,
103
}
104
105
impl Default for VarConfig {
106
0
    fn default() -> Self {
107
0
        Self {
108
0
            confidence_level: 0.95,
109
0
            time_horizon_days: 1,
110
0
            lookback_period_days: 252,
111
0
            calculation_method: "historical".to_string(),
112
0
            max_var_limit: 100_000.0,
113
0
        }
114
0
    }
115
}
116
117
#[derive(Debug, Clone, Serialize, Deserialize)]
118
pub struct KellyConfig {
119
    pub kelly_fraction: f64,
120
    pub max_kelly_leverage: f64,
121
    pub min_kelly_leverage: f64,
122
    pub confidence_threshold: f64,
123
    pub lookback_periods: usize,
124
    pub default_position_fraction: f64,
125
    pub enabled: bool,
126
    pub fractional_kelly: f64,
127
    pub min_kelly_fraction: f64,
128
    pub max_kelly_fraction: f64,
129
}
130
131
impl Default for KellyConfig {
132
0
    fn default() -> Self {
133
0
        Self {
134
0
            kelly_fraction: 0.25,
135
0
            max_kelly_leverage: 2.0,
136
0
            min_kelly_leverage: 0.1,
137
0
            confidence_threshold: 0.95,
138
0
            lookback_periods: 252,
139
0
            default_position_fraction: 0.02,
140
0
            enabled: true,
141
0
            fractional_kelly: 0.5,
142
0
            min_kelly_fraction: 0.01,
143
0
            max_kelly_fraction: 0.5,
144
0
        }
145
0
    }
146
}
147
148
#[derive(Debug, Clone, Serialize, Deserialize)]
149
pub struct CircuitBreakerConfig {
150
    /// Enable circuit breaker
151
    pub enabled: bool,
152
    /// Price movement threshold to trigger halt (0.0-1.0)
153
    pub price_move_threshold: f64,
154
    /// Duration to halt trading in seconds
155
    pub halt_duration_seconds: u64,
156
}
157
158
impl Default for CircuitBreakerConfig {
159
0
    fn default() -> Self {
160
0
        Self {
161
0
            enabled: true,
162
0
            price_move_threshold: 0.05, // 5% price move
163
0
            halt_duration_seconds: 300, // 5 minutes
164
0
        }
165
0
    }
166
}
167
168
#[derive(Debug, Clone, Serialize, Deserialize)]
169
pub struct PositionLimitsConfig {
170
    /// Global position limit
171
    pub global_limit: f64,
172
    /// Maximum leverage allowed
173
    pub max_leverage: f64,
174
    /// Maximum VaR limit
175
    pub max_var_limit: f64,
176
}
177
178
impl Default for PositionLimitsConfig {
179
0
    fn default() -> Self {
180
0
        Self {
181
0
            global_limit: 10_000_000.0,
182
0
            max_leverage: 3.0,
183
0
            max_var_limit: 100_000.0,
184
0
        }
185
0
    }
186
}
187
188
/// Broker configuration for order routing and execution
189
#[derive(Debug, Clone, Serialize, Deserialize)]
190
pub struct BrokerConfig {
191
    /// Broker routing rules based on symbol patterns and sizes
192
    pub routing_rules: Vec<BrokerRoutingRule>,
193
    /// Default broker when no rules match
194
    pub default_broker: String,
195
    /// Commission rates by broker
196
    pub commission_rates: HashMap<String, CommissionConfig>,
197
}
198
199
/// Rule for routing orders to specific brokers
200
#[derive(Debug, Clone, Serialize, Deserialize)]
201
pub struct BrokerRoutingRule {
202
    /// Priority (higher numbers take precedence)
203
    pub priority: u32,
204
    /// Symbol pattern (regex)
205
    pub symbol_pattern: String,
206
    /// Minimum quantity for this rule
207
    pub min_quantity: Option<f64>,
208
    /// Maximum quantity for this rule
209
    pub max_quantity: Option<f64>,
210
    /// Target broker ID
211
    pub broker_id: String,
212
    /// Rule description for debugging
213
    pub description: String,
214
}
215
216
/// Commission configuration per broker
217
#[derive(Debug, Clone, Serialize, Deserialize)]
218
pub struct CommissionConfig {
219
    /// Commission rate (basis points, e.g., 0.00007 = 0.7 bps)
220
    pub rate_bps: f64,
221
    /// Minimum commission per trade
222
    pub min_commission: f64,
223
}
224
225
impl Default for BrokerConfig {
226
0
    fn default() -> Self {
227
0
        let mut commission_rates = HashMap::new();
228
229
0
        commission_rates.insert(
230
0
            "ICMARKETS".to_string(),
231
0
            CommissionConfig {
232
0
                rate_bps: 0.00007, // 0.7 bps
233
0
                min_commission: 0.0,
234
0
            },
235
        );
236
237
0
        commission_rates.insert(
238
0
            "IBKR".to_string(),
239
0
            CommissionConfig {
240
0
                rate_bps: 0.00005, // 0.5 bps
241
0
                min_commission: 1.0,
242
0
            },
243
        );
244
245
0
        let routing_rules = vec![
246
0
            BrokerRoutingRule {
247
0
                priority: 100,
248
0
                symbol_pattern: r"^(BTC|ETH).*".to_string(),
249
0
                min_quantity: None,
250
0
                max_quantity: None,
251
0
                broker_id: "ICMARKETS".to_string(),
252
0
                description: "Route all crypto symbols to ICMarkets".to_string(),
253
0
            },
254
0
            BrokerRoutingRule {
255
0
                priority: 90,
256
0
                symbol_pattern: r".*USD$".to_string(),
257
0
                min_quantity: None,
258
0
                max_quantity: Some(1_000_000.0),
259
0
                broker_id: "ICMARKETS".to_string(),
260
0
                description: "Route smaller USD pairs to ICMarkets".to_string(),
261
0
            },
262
0
            BrokerRoutingRule {
263
0
                priority: 50,
264
0
                symbol_pattern: r".*".to_string(), // Catch-all
265
0
                min_quantity: None,
266
0
                max_quantity: None,
267
0
                broker_id: "IBKR".to_string(),
268
0
                description: "Default routing to IBKR".to_string(),
269
0
            },
270
        ];
271
272
0
        Self {
273
0
            routing_rules,
274
0
            default_broker: "IBKR".to_string(),
275
0
            commission_rates,
276
0
        }
277
0
    }
278
}
279
280
impl BrokerConfig {
281
    /// Select optimal broker based on symbol and quantity using routing rules
282
0
    pub fn select_broker(&self, symbol: &str, quantity: f64) -> String {
283
0
        let symbol_upper = symbol.to_uppercase();
284
285
        // Sort rules by priority (highest first)
286
0
        let mut applicable_rules: Vec<_> = self
287
0
            .routing_rules
288
0
            .iter()
289
0
            .filter(|rule| {
290
                // Check symbol pattern
291
0
                let symbol_matches = if let Ok(regex) = regex::Regex::new(&rule.symbol_pattern) {
292
0
                    regex.is_match(&symbol_upper)
293
                } else {
294
0
                    false
295
                };
296
297
                // Check quantity bounds
298
0
                let quantity_matches = {
299
0
                    let min_ok = rule.min_quantity.map_or(true, |min| quantity >= min);
300
0
                    let max_ok = rule.max_quantity.map_or(true, |max| quantity <= max);
301
0
                    min_ok && max_ok
302
                };
303
304
0
                symbol_matches && quantity_matches
305
0
            })
306
0
            .collect();
307
308
0
        applicable_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
309
310
0
        if let Some(rule) = applicable_rules.first() {
311
0
            rule.broker_id.clone()
312
        } else {
313
0
            self.default_broker.clone()
314
        }
315
0
    }
316
317
    /// Calculate commission for a given broker and notional value
318
0
    pub fn calculate_commission(&self, broker_id: &str, notional: f64) -> f64 {
319
0
        if let Some(config) = self.commission_rates.get(broker_id) {
320
0
            (notional * config.rate_bps).max(config.min_commission)
321
        } else {
322
            // Default commission if broker not found
323
0
            notional * 0.0001 // 1 bps
324
        }
325
0
    }
326
}
327
328
/// Asset classification for risk management and volatility profiling
329
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
330
pub enum AssetClass {
331
    /// Equity securities and stocks
332
    Equities,
333
    /// Bonds and fixed income securities
334
    FixedIncome,
335
    /// Physical and financial commodities
336
    Commodities,
337
    /// Foreign exchange and currencies
338
    Currencies,
339
    /// Alternative investments
340
    Alternatives,
341
    /// Derivative instruments
342
    Derivatives,
343
    /// Cash and cash equivalents
344
    Cash,
345
}
346
347
/// Volatility and risk profile for an asset class
348
#[derive(Debug, Clone, Serialize, Deserialize)]
349
pub struct VolatilityProfile {
350
    /// Annual volatility (0.0 to 1.0, e.g., 0.25 = 25%)
351
    pub annual_volatility: f64,
352
    /// Maximum position size as fraction of portfolio (0.0 to 1.0)
353
    pub max_position_fraction: f64,
354
    /// Volatility threshold for risk alerts (0.0 to 1.0)
355
    pub volatility_threshold: f64,
356
    /// Maximum daily loss threshold (0.0 to 1.0)
357
    pub daily_loss_threshold: f64,
358
}
359
360
/// Asset classification configuration with symbol mappings and volatility profiles
361
#[derive(Debug, Clone, Serialize, Deserialize)]
362
pub struct AssetClassificationConfig {
363
    /// Explicit symbol to asset class mappings
364
    pub symbol_mappings: HashMap<String, AssetClass>,
365
    /// Volatility profiles for each asset class
366
    pub volatility_profiles: HashMap<AssetClass, VolatilityProfile>,
367
    /// Pattern-based classification rules (regex patterns)
368
    pub pattern_rules: Vec<PatternRule>,
369
}
370
371
/// Pattern-based rule for asset classification
372
#[derive(Debug, Clone, Serialize, Deserialize)]
373
pub struct PatternRule {
374
    /// Regex pattern to match against symbol
375
    pub pattern: String,
376
    /// Asset class to assign if pattern matches
377
    pub asset_class: AssetClass,
378
    /// Priority (higher numbers take precedence)
379
    pub priority: u32,
380
}
381
382
/// Encryption configuration for secure model storage
383
#[derive(Debug, Clone, Serialize, Deserialize)]
384
pub struct EncryptionConfig {
385
    /// Enable/disable encryption for model storage
386
    pub enable_encryption: bool,
387
    /// Encryption algorithm (e.g., "AES-256-GCM")
388
    pub algorithm: String,
389
    /// Key rotation period in days
390
    pub key_rotation_days: u64,
391
    /// Vault path for encryption keys (optional, can use local keys)
392
    pub encryption_keys_vault_path: Option<String>,
393
    /// Local key file path for development/testing
394
    pub local_key_file: Option<String>,
395
}
396
397
impl Default for EncryptionConfig {
398
0
    fn default() -> Self {
399
0
        Self {
400
0
            enable_encryption: false,
401
0
            algorithm: "AES-256-GCM".to_string(),
402
0
            key_rotation_days: 90,
403
0
            encryption_keys_vault_path: None,
404
0
            local_key_file: None,
405
0
        }
406
0
    }
407
}
408
409
impl Default for AssetClassificationConfig {
410
0
    fn default() -> Self {
411
0
        let mut symbol_mappings = HashMap::new();
412
413
        // Equity stocks
414
0
        for symbol in [
415
0
            "AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "JPM", "JNJ", "V",
416
0
        ] {
417
0
            symbol_mappings.insert(symbol.to_string(), AssetClass::Equities);
418
0
        }
419
420
        // Major cryptocurrencies
421
0
        for symbol in ["BTC", "ETH", "BTCUSD", "ETHUSD", "BTCUSDT", "ETHUSDT"] {
422
0
            symbol_mappings.insert(symbol.to_string(), AssetClass::Alternatives);
423
0
        }
424
425
0
        let mut volatility_profiles = HashMap::new();
426
427
0
        volatility_profiles.insert(
428
0
            AssetClass::Equities,
429
0
            VolatilityProfile {
430
0
                annual_volatility: 0.25,
431
0
                max_position_fraction: 0.20,
432
0
                volatility_threshold: 0.025,
433
0
                daily_loss_threshold: 0.03,
434
0
            },
435
        );
436
437
0
        volatility_profiles.insert(
438
0
            AssetClass::Alternatives,
439
0
            VolatilityProfile {
440
0
                annual_volatility: 0.80,
441
0
                max_position_fraction: 0.08,
442
0
                volatility_threshold: 0.15,
443
0
                daily_loss_threshold: 0.05,
444
0
            },
445
        );
446
447
0
        volatility_profiles.insert(
448
0
            AssetClass::Currencies,
449
0
            VolatilityProfile {
450
0
                annual_volatility: 0.15,
451
0
                max_position_fraction: 0.30,
452
0
                volatility_threshold: 0.02,
453
0
                daily_loss_threshold: 0.02,
454
0
            },
455
        );
456
457
0
        volatility_profiles.insert(
458
0
            AssetClass::Cash,
459
0
            VolatilityProfile {
460
0
                annual_volatility: 0.01,
461
0
                max_position_fraction: 1.00,
462
0
                volatility_threshold: 0.001,
463
0
                daily_loss_threshold: 0.001,
464
0
            },
465
        );
466
467
0
        volatility_profiles.insert(
468
0
            AssetClass::FixedIncome,
469
0
            VolatilityProfile {
470
0
                annual_volatility: 0.25,
471
0
                max_position_fraction: 0.15,
472
0
                volatility_threshold: 0.03,
473
0
                daily_loss_threshold: 0.025,
474
0
            },
475
        );
476
477
0
        volatility_profiles.insert(
478
0
            AssetClass::Derivatives,
479
0
            VolatilityProfile {
480
0
                annual_volatility: 0.40,
481
0
                max_position_fraction: 0.10,
482
0
                volatility_threshold: 0.05,
483
0
                daily_loss_threshold: 0.04,
484
0
            },
485
        );
486
487
0
        volatility_profiles.insert(
488
0
            AssetClass::Commodities,
489
0
            VolatilityProfile {
490
0
                annual_volatility: 0.30,
491
0
                max_position_fraction: 0.15,
492
0
                volatility_threshold: 0.04,
493
0
                daily_loss_threshold: 0.03,
494
0
            },
495
        );
496
497
0
        let pattern_rules = vec![
498
0
            PatternRule {
499
0
                pattern: r"^(BTC|ETH).*".to_string(),
500
0
                asset_class: AssetClass::Alternatives,
501
0
                priority: 100,
502
0
            },
503
0
            PatternRule {
504
0
                pattern: r".*USD$".to_string(),
505
0
                asset_class: AssetClass::Currencies,
506
0
                priority: 80,
507
0
            },
508
0
            PatternRule {
509
0
                pattern: r".*JPY$".to_string(),
510
0
                asset_class: AssetClass::Currencies,
511
0
                priority: 90,
512
0
            },
513
0
            PatternRule {
514
0
                pattern: r"^[A-Z]{3,6}$".to_string(), // 3-6 letter symbols (likely equities)
515
0
                asset_class: AssetClass::Equities,
516
0
                priority: 50,
517
0
            },
518
        ];
519
520
0
        Self {
521
0
            symbol_mappings,
522
0
            volatility_profiles,
523
0
            pattern_rules,
524
0
        }
525
0
    }
526
}
527
528
impl AssetClassificationConfig {
529
    /// Classify a symbol based on explicit mappings and pattern rules
530
0
    pub fn classify_symbol(&self, symbol: &str) -> AssetClass {
531
0
        let symbol_upper = symbol.to_uppercase();
532
533
        // First check explicit mappings
534
0
        if let Some(asset_class) = self.symbol_mappings.get(&symbol_upper) {
535
0
            return asset_class.clone();
536
0
        }
537
538
        // Then check pattern rules (sorted by priority, highest first)
539
0
        let mut applicable_rules: Vec<_> = self
540
0
            .pattern_rules
541
0
            .iter()
542
0
            .filter(|rule| {
543
0
                if let Ok(regex) = regex::Regex::new(&rule.pattern) {
544
0
                    regex.is_match(&symbol_upper)
545
                } else {
546
0
                    false
547
                }
548
0
            })
549
0
            .collect();
550
551
0
        applicable_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
552
553
0
        if let Some(rule) = applicable_rules.first() {
554
0
            rule.asset_class.clone()
555
        } else {
556
0
            AssetClass::Cash // Default fallback for unknown symbols
557
        }
558
0
    }
559
560
    /// Get volatility profile for a symbol
561
0
    pub fn get_volatility_profile(&self, symbol: &str) -> VolatilityProfile {
562
0
        let asset_class = self.classify_symbol(symbol);
563
0
        self.volatility_profiles
564
0
            .get(&asset_class)
565
0
            .cloned()
566
0
            .unwrap_or(VolatilityProfile {
567
0
                annual_volatility: 0.20,
568
0
                max_position_fraction: 0.05,
569
0
                volatility_threshold: 0.02,
570
0
                daily_loss_threshold: 0.01,
571
0
            })
572
0
    }
573
574
    /// Get daily volatility for a symbol
575
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
576
0
        let profile = self.get_volatility_profile(symbol);
577
0
        profile.annual_volatility / 252.0_f64.sqrt()
578
0
    }
579
580
    /// Get risk configuration tuple (position_fraction, volatility_threshold, daily_loss_threshold)
581
0
    pub fn get_risk_config(&self, symbol: &str) -> (f64, f64, f64) {
582
0
        let profile = self.get_volatility_profile(symbol);
583
0
        (
584
0
            profile.max_position_fraction,
585
0
            profile.volatility_threshold,
586
0
            profile.daily_loss_threshold,
587
0
        )
588
0
    }
589
}
590
591
/// Configuration for backtesting database connections
592
#[derive(Debug, Clone, Serialize, Deserialize)]
593
pub struct BacktestingDatabaseConfig {
594
    /// Database connection URL
595
    pub database_url: String,
596
    /// Maximum number of database connections in the pool
597
    pub max_connections: Option<u32>,
598
    /// Minimum number of database connections in the pool
599
    pub min_connections: Option<u32>,
600
    /// Timeout in milliseconds for acquiring a connection
601
    pub acquire_timeout_ms: Option<u64>,
602
    /// Statement cache capacity
603
    pub statement_cache_capacity: Option<usize>,
604
    /// Enable SQL query logging
605
    pub enable_logging: Option<bool>,
606
}
607
608
/// Configuration for backtesting strategy execution
609
#[derive(Debug, Clone, Serialize, Deserialize)]
610
pub struct BacktestingStrategyConfig {
611
    /// Commission rate for trades (e.g., 0.001 = 0.1%)
612
    pub commission_rate: f64,
613
    /// Slippage rate for trades (e.g., 0.0005 = 0.05%)
614
    pub slippage_rate: f64,
615
    /// Maximum position size as fraction of portfolio
616
    pub max_position_size: Option<f64>,
617
    /// Enable short selling
618
    pub allow_short_selling: Option<bool>,
619
}
620
621
impl Default for BacktestingStrategyConfig {
622
0
    fn default() -> Self {
623
0
        Self {
624
0
            commission_rate: 0.0007,      // 0.07% = 7 bps
625
0
            slippage_rate: 0.0002,        // 0.02% = 2 bps
626
0
            max_position_size: Some(0.2), // 20% max position
627
0
            allow_short_selling: Some(false),
628
0
        }
629
0
    }
630
}
631
632
/// Configuration for backtesting performance analysis
633
#[derive(Debug, Clone, Serialize, Deserialize)]
634
pub struct BacktestingPerformanceConfig {
635
    /// Risk-free rate for Sharpe ratio calculations (annual rate)
636
    pub risk_free_rate: f64,
637
    /// Resolution for equity curve (number of points)
638
    pub equity_curve_resolution: usize,
639
    /// Enable advanced performance metrics
640
    pub enable_advanced_metrics: Option<bool>,
641
}
642
643
impl Default for BacktestingPerformanceConfig {
644
0
    fn default() -> Self {
645
0
        Self {
646
0
            risk_free_rate: 0.04, // 4% annual risk-free rate
647
0
            equity_curve_resolution: 1000,
648
0
            enable_advanced_metrics: Some(true),
649
0
        }
650
0
    }
651
}
652
653
/// TLS/SSL configuration for secure gRPC connections
654
#[derive(Debug, Clone, Serialize, Deserialize)]
655
pub struct TlsConfig {
656
    /// Enable/disable TLS for gRPC connections
657
    pub enabled: bool,
658
    /// Path to server certificate file
659
    pub cert_path: String,
660
    /// Path to server private key file
661
    pub key_path: String,
662
    /// Path to CA certificate for client verification (optional)
663
    pub ca_cert_path: Option<String>,
664
    /// Require client certificate verification
665
    pub require_client_cert: bool,
666
    /// TLS protocol versions to support (e.g., ["TLSv1.2", "TLSv1.3"])
667
    pub protocol_versions: Vec<String>,
668
    /// Cipher suites to use (empty means default)
669
    pub cipher_suites: Vec<String>,
670
}
671
672
impl Default for TlsConfig {
673
0
    fn default() -> Self {
674
        // Wave 75 Fix: Use environment variables with fallback to /tmp instead of /etc
675
0
        let cert_path = std::env::var("TLS_CERT_PATH")
676
0
            .unwrap_or_else(|_| "/tmp/foxhunt/certs/server.crt".to_string());
677
0
        let key_path = std::env::var("TLS_KEY_PATH")
678
0
            .unwrap_or_else(|_| "/tmp/foxhunt/certs/server.key".to_string());
679
0
        let ca_cert_path = std::env::var("TLS_CA_PATH").ok();
680
681
0
        Self {
682
0
            enabled: false,
683
0
            cert_path,
684
0
            key_path,
685
0
            ca_cert_path,
686
0
            require_client_cert: false,
687
0
            protocol_versions: vec!["TLSv1.3".to_string()],
688
0
            cipher_suites: Vec::new(),
689
0
        }
690
0
    }
691
}
692
693
/// Trading system configuration
694
#[derive(Debug, Clone, Serialize, Deserialize)]
695
pub struct TradingConfig {
696
    /// Maximum order size (in base units)
697
    pub max_order_size: f64,
698
    /// Minimum order size (in base units)
699
    pub min_order_size: f64,
700
    /// Maximum price deviation from market (as fraction, e.g., 0.05 = 5%)
701
    pub max_price_deviation: f64,
702
    /// Enable symbol validation
703
    pub enable_symbol_validation: bool,
704
    /// Maximum batch notional value (total value of orders in a batch)
705
    pub max_batch_notional: f64,
706
    /// Maximum position VaR (Value at Risk) limit
707
    pub max_position_var: f64,
708
}
709
710
impl Default for TradingConfig {
711
0
    fn default() -> Self {
712
0
        Self {
713
0
            max_order_size: 1_000_000.0,
714
0
            min_order_size: 0.001,
715
0
            max_price_deviation: 0.05,
716
0
            enable_symbol_validation: false,
717
0
            max_batch_notional: 10_000_000.0, // $10M batch limit
718
0
            max_position_var: 50_000.0,        // $50K VaR limit
719
0
        }
720
0
    }
721
}
722
723
/// Market data ingestion configuration
724
#[derive(Debug, Clone, Serialize, Deserialize)]
725
pub struct MarketDataConfig {
726
    /// Market data server host
727
    pub host: String,
728
    /// WebSocket port for streaming data
729
    pub websocket_port: u16,
730
    /// API key for authentication
731
    pub api_key: String,
732
    /// Use SSL/TLS for connections
733
    pub use_ssl: bool,
734
    /// Connection timeout in seconds
735
    pub timeout_seconds: u64,
736
}
737
738
impl Default for MarketDataConfig {
739
0
    fn default() -> Self {
740
0
        Self {
741
0
            host: "localhost".to_string(),
742
0
            websocket_port: 8080,
743
0
            api_key: String::new(),
744
0
            use_ssl: false,
745
0
            timeout_seconds: 30,
746
0
        }
747
0
    }
748
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html deleted file mode 100644 index 5281daf81..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs
Line
Count
Source
1
//! Symbol classification and configuration management for trading instruments.
2
//!
3
//! This module provides comprehensive symbol classification and configuration
4
//! management for various financial instruments in the Foxhunt HFT trading system.
5
//! It handles asset classification, volatility profiles, trading hours, and
6
//! market-specific parameters for optimal trading execution.
7
8
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc, Weekday};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
use std::time::Duration;
12
use uuid::Uuid;
13
14
/// Asset classification enumeration for different financial instrument types.
15
///
16
/// Provides standardized classification for all tradeable instruments,
17
/// enabling type-specific risk management, execution logic, and regulatory
18
/// compliance across different asset classes.
19
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20
pub enum AssetClassification {
21
    /// Equity securities (stocks, ADRs, REITs)
22
    Equity,
23
    /// Futures contracts (commodities, financials, indices)
24
    Future,
25
    /// Foreign exchange pairs (major, minor, exotic)
26
    Forex,
27
    /// Cryptocurrency and digital assets
28
    Crypto,
29
    /// Physical commodities (metals, energy, agriculture)
30
    Commodity,
31
    /// Fixed income securities (bonds, notes, bills)
32
    FixedIncome,
33
    /// Options contracts (equity, index, commodity options)
34
    Option,
35
    /// Exchange-traded funds and products
36
    Etf,
37
    /// Indices and benchmark instruments
38
    Index,
39
    /// Structured products and derivatives
40
    Derivative,
41
}
42
43
impl AssetClassification {
44
    /// Returns the regulatory classification for compliance purposes.
45
3
    pub fn regulatory_class(&self) -> &'static str {
46
3
        match self {
47
1
            AssetClassification::Equity => "EQUITY",
48
0
            AssetClassification::Future => "FUTURE",
49
1
            AssetClassification::Forex => "FX",
50
1
            AssetClassification::Crypto => "CRYPTO",
51
0
            AssetClassification::Commodity => "COMMODITY",
52
0
            AssetClassification::FixedIncome => "FIXED_INCOME",
53
0
            AssetClassification::Option => "OPTION",
54
0
            AssetClassification::Etf => "ETF",
55
0
            AssetClassification::Index => "INDEX",
56
0
            AssetClassification::Derivative => "DERIVATIVE",
57
        }
58
3
    }
59
60
    /// Returns whether this asset class requires T+1 settlement.
61
0
    pub fn requires_t_plus_one_settlement(&self) -> bool {
62
0
        matches!(self, AssetClassification::Equity | AssetClassification::Etf)
63
0
    }
64
65
    /// Returns whether this asset class supports after-hours trading.
66
0
    pub fn supports_extended_hours(&self) -> bool {
67
0
        matches!(
68
0
            self,
69
            AssetClassification::Equity
70
                | AssetClassification::Etf
71
                | AssetClassification::Forex
72
                | AssetClassification::Crypto
73
        )
74
0
    }
75
}
76
77
/// Volatility profile configuration for risk management and position sizing.
78
///
79
/// Defines volatility characteristics and risk parameters for different
80
/// instruments, enabling dynamic position sizing and risk-adjusted execution.
81
#[derive(Debug, Clone, Serialize, Deserialize)]
82
pub struct VolatilityProfile {
83
    /// Historical average volatility (annualized)
84
    pub average_volatility: f64,
85
    /// Maximum observed volatility (99th percentile)
86
    pub max_volatility: f64,
87
    /// Minimum observed volatility (1st percentile)
88
    pub min_volatility: f64,
89
    /// Beta coefficient relative to market index
90
    pub beta: f64,
91
    /// Average True Range (ATR) for recent period
92
    pub atr: f64,
93
    /// Correlation with market benchmark
94
    pub market_correlation: f64,
95
    /// Volatility regime classification
96
    pub volatility_regime: VolatilityRegime,
97
    /// Last updated timestamp for volatility metrics
98
    pub last_updated: DateTime<Utc>,
99
    /// Number of observations used for calculation
100
    pub sample_size: u32,
101
}
102
103
impl VolatilityProfile {
104
    /// Creates a new volatility profile with default values.
105
3
    pub fn new() -> Self {
106
3
        Self {
107
3
            average_volatility: 0.20,
108
3
            max_volatility: 1.00,
109
3
            min_volatility: 0.05,
110
3
            beta: 1.0,
111
3
            atr: 0.0,
112
3
            market_correlation: 0.0,
113
3
            volatility_regime: VolatilityRegime::Normal,
114
3
            last_updated: Utc::now(),
115
3
            sample_size: 0,
116
3
        }
117
3
    }
118
119
    /// Updates volatility metrics with new data point.
120
1
    pub fn update_metrics(&mut self, new_volatility: f64, new_atr: f64) {
121
        // Update exponential moving average
122
1
        let alpha = 0.1; // Smoothing factor
123
1
        self.average_volatility = alpha * new_volatility + (1.0 - alpha) * self.average_volatility;
124
1
        self.atr = alpha * new_atr + (1.0 - alpha) * self.atr;
125
1
        self.last_updated = Utc::now();
126
1
        self.sample_size += 1;
127
128
        // Update volatility regime
129
1
        self.volatility_regime = self.classify_regime();
130
1
    }
131
132
    /// Classifies current volatility regime based on metrics.
133
1
    fn classify_regime(&self) -> VolatilityRegime {
134
1
        let volatility_ratio = self.average_volatility / 0.20; // Relative to 20% baseline
135
136
1
        if volatility_ratio > 2.0 {
137
0
            VolatilityRegime::High
138
1
        } else if volatility_ratio > 1.5 {
139
0
            VolatilityRegime::Elevated
140
1
        } else if volatility_ratio < 0.5 {
141
0
            VolatilityRegime::Low
142
        } else {
143
1
            VolatilityRegime::Normal
144
        }
145
1
    }
146
147
    /// Returns risk-adjusted position size multiplier.
148
0
    pub fn position_size_multiplier(&self) -> f64 {
149
0
        match self.volatility_regime {
150
0
            VolatilityRegime::Low => 1.5,
151
0
            VolatilityRegime::Normal => 1.0,
152
0
            VolatilityRegime::Elevated => 0.7,
153
0
            VolatilityRegime::High => 0.4,
154
        }
155
0
    }
156
}
157
158
impl Default for VolatilityProfile {
159
0
    fn default() -> Self {
160
0
        Self::new()
161
0
    }
162
}
163
164
/// Volatility regime classification for risk management.
165
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166
pub enum VolatilityRegime {
167
    /// Low volatility environment (< 50% of normal)
168
    Low,
169
    /// Normal volatility environment
170
    Normal,
171
    /// Elevated volatility (50-100% above normal)
172
    Elevated,
173
    /// High volatility environment (> 100% above normal)
174
    High,
175
}
176
177
/// Trading hours configuration for different markets and sessions.
178
///
179
/// Defines market operating hours, pre-market and after-hours sessions,
180
/// and holiday schedules for accurate trade timing and execution.
181
#[derive(Debug, Clone, Serialize, Deserialize)]
182
pub struct TradingHours {
183
    /// Primary market timezone identifier (e.g., "America/New_York")
184
    pub timezone: String,
185
    /// Regular trading session start time
186
    pub market_open: NaiveTime,
187
    /// Regular trading session end time
188
    pub market_close: NaiveTime,
189
    /// Pre-market session start time (optional)
190
    pub pre_market_open: Option<NaiveTime>,
191
    /// After-hours session end time (optional)
192
    pub after_hours_close: Option<NaiveTime>,
193
    /// Trading days of the week
194
    pub trading_days: Vec<Weekday>,
195
    /// Market holidays (dates when market is closed)
196
    pub holidays: Vec<NaiveDate>,
197
    /// Half-day sessions with early close times
198
    pub half_days: HashMap<NaiveDate, NaiveTime>,
199
}
200
201
impl TradingHours {
202
    /// Creates US equity market trading hours configuration.
203
3
    pub fn us_equity() -> Self {
204
3
        Self {
205
3
            timezone: "America/New_York".to_string(),
206
3
            market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
207
3
            market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
208
3
            pre_market_open: Some(NaiveTime::from_hms_opt(4, 0, 0).unwrap()),
209
3
            after_hours_close: Some(NaiveTime::from_hms_opt(20, 0, 0).unwrap()),
210
3
            trading_days: vec![
211
3
                Weekday::Mon,
212
3
                Weekday::Tue,
213
3
                Weekday::Wed,
214
3
                Weekday::Thu,
215
3
                Weekday::Fri,
216
3
            ],
217
3
            holidays: vec![],
218
3
            half_days: HashMap::new(),
219
3
        }
220
3
    }
221
222
    /// Creates 24/7 trading hours for crypto markets.
223
0
    pub fn crypto_24_7() -> Self {
224
0
        Self {
225
0
            timezone: "UTC".to_string(),
226
0
            market_open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
227
0
            market_close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
228
0
            pre_market_open: None,
229
0
            after_hours_close: None,
230
0
            trading_days: vec![
231
0
                Weekday::Mon,
232
0
                Weekday::Tue,
233
0
                Weekday::Wed,
234
0
                Weekday::Thu,
235
0
                Weekday::Fri,
236
0
                Weekday::Sat,
237
0
                Weekday::Sun,
238
0
            ],
239
0
            holidays: vec![],
240
0
            half_days: HashMap::new(),
241
0
        }
242
0
    }
243
244
    /// Creates forex market trading hours (Sunday 5 PM to Friday 5 PM EST).
245
0
    pub fn forex() -> Self {
246
0
        Self {
247
0
            timezone: "America/New_York".to_string(),
248
0
            market_open: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
249
0
            market_close: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
250
0
            pre_market_open: None,
251
0
            after_hours_close: None,
252
0
            trading_days: vec![
253
0
                Weekday::Sun,
254
0
                Weekday::Mon,
255
0
                Weekday::Tue,
256
0
                Weekday::Wed,
257
0
                Weekday::Thu,
258
0
                Weekday::Fri,
259
0
            ],
260
0
            holidays: vec![],
261
0
            half_days: HashMap::new(),
262
0
        }
263
0
    }
264
265
    /// Checks if market is currently open.
266
0
    pub fn is_market_open(&self, current_time: DateTime<Utc>) -> bool {
267
        // Convert to market timezone and check if within trading hours
268
        // This is a simplified implementation - production would use proper timezone handling
269
0
        let current_date = current_time.date_naive();
270
0
        let current_time = current_time.time();
271
0
        let current_weekday = current_date.weekday();
272
273
        // Check if it's a trading day
274
0
        if !self.trading_days.contains(&current_weekday) {
275
0
            return false;
276
0
        }
277
278
        // Check if it's a holiday
279
0
        if self.holidays.contains(&current_date) {
280
0
            return false;
281
0
        }
282
283
        // Check if within trading hours
284
0
        current_time >= self.market_open && current_time <= self.market_close
285
0
    }
286
287
    /// Checks if extended hours trading is active.
288
0
    pub fn is_extended_hours_open(&self, current_time: DateTime<Utc>) -> bool {
289
0
        let current_time = current_time.time();
290
291
        // Check pre-market
292
0
        if let Some(pre_open) = self.pre_market_open {
293
0
            if current_time >= pre_open && current_time < self.market_open {
294
0
                return true;
295
0
            }
296
0
        }
297
298
        // Check after-hours
299
0
        if let Some(after_close) = self.after_hours_close {
300
0
            if current_time > self.market_close && current_time <= after_close {
301
0
                return true;
302
0
            }
303
0
        }
304
305
0
        false
306
0
    }
307
}
308
309
impl Default for TradingHours {
310
0
    fn default() -> Self {
311
0
        Self::us_equity()
312
0
    }
313
}
314
315
/// Comprehensive symbol configuration containing all trading parameters.
316
///
317
/// Central configuration structure for each tradeable symbol, containing
318
/// classification, market parameters, risk settings, and execution rules.
319
#[derive(Debug, Clone, Serialize, Deserialize)]
320
pub struct SymbolConfig {
321
    /// Unique symbol identifier
322
    pub symbol: String,
323
    /// Symbol description or company name
324
    pub description: String,
325
    /// Asset classification
326
    pub classification: AssetClassification,
327
    /// Volatility and risk profile
328
    pub volatility_profile: VolatilityProfile,
329
    /// Market operating hours
330
    pub trading_hours: TradingHours,
331
    /// Minimum price increment (tick size)
332
    pub tick_size: f64,
333
    /// Standard trading unit size
334
    pub lot_size: f64,
335
    /// Minimum order quantity
336
    pub min_order_size: f64,
337
    /// Maximum order quantity
338
    pub max_order_size: f64,
339
    /// Primary exchange or venue
340
    pub primary_exchange: String,
341
    /// Currency denomination
342
    pub currency: String,
343
    /// Sector classification (for equities)
344
    pub sector: Option<String>,
345
    /// Industry classification (for equities)
346
    pub industry: Option<String>,
347
    /// Market capitalization (for equities)
348
    pub market_cap: Option<f64>,
349
    /// Average daily volume
350
    pub avg_daily_volume: f64,
351
    /// Margin requirements
352
    pub margin_requirement: f64,
353
    /// Position limits
354
    pub position_limit: Option<f64>,
355
    /// Risk multiplier for position sizing
356
    pub risk_multiplier: f64,
357
    /// Configuration metadata
358
    pub metadata: SymbolMetadata,
359
}
360
361
impl SymbolConfig {
362
    /// Creates a new symbol configuration with default values.
363
2
    pub fn new(symbol: String, classification: AssetClassification) -> Self {
364
2
        let trading_hours = match classification {
365
0
            AssetClassification::Crypto => TradingHours::crypto_24_7(),
366
0
            AssetClassification::Forex => TradingHours::forex(),
367
2
            _ => TradingHours::us_equity(),
368
        };
369
370
2
        Self {
371
2
            symbol: symbol.clone(),
372
2
            description: format!("{} - Auto-generated", symbol),
373
2
            classification,
374
2
            volatility_profile: VolatilityProfile::new(),
375
2
            trading_hours,
376
2
            tick_size: 0.01,
377
2
            lot_size: 1.0,
378
2
            min_order_size: 1.0,
379
2
            max_order_size: 1_000_000.0,
380
2
            primary_exchange: "".to_string(),
381
2
            currency: "USD".to_string(),
382
2
            sector: None,
383
2
            industry: None,
384
2
            market_cap: None,
385
2
            avg_daily_volume: 0.0,
386
2
            margin_requirement: 0.25,
387
2
            position_limit: None,
388
2
            risk_multiplier: 1.0,
389
2
            metadata: SymbolMetadata::new(),
390
2
        }
391
2
    }
392
393
    /// Validates the symbol configuration for correctness.
394
3
    pub fn validate(&self) -> Result<(), String> {
395
3
        if self.symbol.is_empty() {
396
0
            return Err("Symbol cannot be empty".to_string());
397
3
        }
398
399
3
        if self.tick_size <= 0.0 {
400
1
            return Err("Tick size must be positive".to_string());
401
2
        }
402
403
2
        if self.lot_size <= 0.0 {
404
0
            return Err("Lot size must be positive".to_string());
405
2
        }
406
407
2
        if self.min_order_size <= 0.0 {
408
0
            return Err("Minimum order size must be positive".to_string());
409
2
        }
410
411
2
        if self.max_order_size <= self.min_order_size {
412
0
            return Err("Maximum order size must be greater than minimum".to_string());
413
2
        }
414
415
2
        if self.margin_requirement < 0.0 || self.margin_requirement > 1.0 {
416
0
            return Err("Margin requirement must be between 0 and 1".to_string());
417
2
        }
418
419
2
        Ok(())
420
3
    }
421
422
    /// Calculates the effective position size based on risk parameters.
423
0
    pub fn calculate_position_size(&self, base_size: f64, _account_value: f64) -> f64 {
424
0
        let volatility_multiplier = self.volatility_profile.position_size_multiplier();
425
0
        let risk_adjusted_size = base_size * volatility_multiplier * self.risk_multiplier;
426
427
        // Apply position limits
428
0
        if let Some(limit) = self.position_limit {
429
0
            risk_adjusted_size.min(limit)
430
        } else {
431
0
            risk_adjusted_size
432
        }
433
0
    }
434
435
    /// Returns the appropriate tick size for a given price level.
436
0
    pub fn get_tick_size_for_price(&self, _price: f64) -> f64 {
437
        // Some markets have variable tick sizes based on price
438
        // This is a simplified implementation
439
0
        self.tick_size
440
0
    }
441
442
    /// Rounds price to the nearest valid tick.
443
0
    pub fn round_to_tick(&self, price: f64) -> f64 {
444
0
        let tick = self.get_tick_size_for_price(price);
445
0
        (price / tick).round() * tick
446
0
    }
447
448
    /// Checks if the symbol is currently tradeable.
449
0
    pub fn is_tradeable(&self, current_time: DateTime<Utc>) -> bool {
450
0
        self.trading_hours.is_market_open(current_time) && self.metadata.is_active
451
0
    }
452
453
    /// Checks if extended hours trading is available.
454
0
    pub fn supports_extended_hours(&self) -> bool {
455
0
        self.classification.supports_extended_hours()
456
0
    }
457
}
458
459
/// Symbol configuration metadata for versioning and tracking.
460
#[derive(Debug, Clone, Serialize, Deserialize)]
461
pub struct SymbolMetadata {
462
    /// Unique configuration ID
463
    pub id: Uuid,
464
    /// Configuration version
465
    pub version: u32,
466
    /// Creation timestamp
467
    pub created_at: DateTime<Utc>,
468
    /// Last update timestamp
469
    pub updated_at: DateTime<Utc>,
470
    /// Active status
471
    pub is_active: bool,
472
    /// Data source for configuration
473
    pub data_source: String,
474
    /// Last validation timestamp
475
    pub last_validated: Option<DateTime<Utc>>,
476
    /// Configuration tags for organization
477
    pub tags: Vec<String>,
478
}
479
480
impl SymbolMetadata {
481
    /// Creates new metadata with default values.
482
2
    pub fn new() -> Self {
483
2
        let now = Utc::now();
484
2
        Self {
485
2
            id: Uuid::new_v4(),
486
2
            version: 1,
487
2
            created_at: now,
488
2
            updated_at: now,
489
2
            is_active: true,
490
2
            data_source: "manual".to_string(),
491
2
            last_validated: None,
492
2
            tags: vec![],
493
2
        }
494
2
    }
495
496
    /// Updates the metadata timestamp and version.
497
0
    pub fn update(&mut self) {
498
0
        self.updated_at = Utc::now();
499
0
        self.version += 1;
500
0
    }
501
502
    /// Marks the configuration as validated.
503
0
    pub fn mark_validated(&mut self) {
504
0
        self.last_validated = Some(Utc::now());
505
0
    }
506
}
507
508
impl Default for SymbolMetadata {
509
0
    fn default() -> Self {
510
0
        Self::new()
511
0
    }
512
}
513
514
/// Symbol configuration manager for loading and caching symbol configurations.
515
///
516
/// Provides high-performance access to symbol configurations with caching,
517
/// hot-reload capabilities, and configuration validation.
518
#[derive(Debug)]
519
pub struct SymbolConfigManager {
520
    /// In-memory cache of symbol configurations
521
    symbol_cache: HashMap<String, SymbolConfig>,
522
    /// Last cache update timestamp
523
    last_updated: DateTime<Utc>,
524
    /// Cache timeout duration
525
    cache_timeout: Duration,
526
}
527
528
impl SymbolConfigManager {
529
    /// Creates a new symbol configuration manager.
530
1
    pub fn new() -> Self {
531
1
        Self {
532
1
            symbol_cache: HashMap::new(),
533
1
            last_updated: Utc::now(),
534
1
            cache_timeout: Duration::from_secs(300), // 5 minutes
535
1
        }
536
1
    }
537
538
    /// Loads symbol configuration from cache or source.
539
0
    pub async fn get_symbol_config(
540
0
        &mut self,
541
0
        symbol: &str,
542
0
    ) -> Result<Option<SymbolConfig>, String> {
543
        // Check cache first
544
0
        if let Some(config) = self.symbol_cache.get(symbol) {
545
0
            if !self.is_cache_expired() {
546
0
                return Ok(Some(config.clone()));
547
0
            }
548
0
        }
549
550
        // Load from source (this would integrate with database/external source)
551
0
        self.load_symbol_from_source(symbol).await
552
0
    }
553
554
    /// Loads all symbol configurations into cache.
555
0
    pub async fn load_all_symbols(&mut self) -> Result<usize, String> {
556
        // This would integrate with the database or external configuration source
557
0
        self.refresh_cache().await
558
0
    }
559
560
    /// Adds or updates a symbol configuration.
561
1
    pub fn upsert_symbol_config(&mut self, config: SymbolConfig) -> Result<(), String> {
562
        // Validate configuration
563
1
        config.validate()
?0
;
564
565
        // Update cache
566
1
        self.symbol_cache.insert(config.symbol.clone(), config);
567
1
        self.last_updated = Utc::now();
568
569
1
        Ok(())
570
1
    }
571
572
    /// Removes a symbol configuration.
573
0
    pub fn remove_symbol_config(&mut self, symbol: &str) -> Option<SymbolConfig> {
574
0
        self.symbol_cache.remove(symbol)
575
0
    }
576
577
    /// Returns all cached symbol configurations.
578
1
    pub fn get_all_symbols(&self) -> Vec<&SymbolConfig> {
579
1
        self.symbol_cache.values().collect()
580
1
    }
581
582
    /// Returns symbols filtered by asset classification.
583
0
    pub fn get_symbols_by_classification(
584
0
        &self,
585
0
        classification: &AssetClassification,
586
0
    ) -> Vec<&SymbolConfig> {
587
0
        self.symbol_cache
588
0
            .values()
589
0
            .filter(|config| &config.classification == classification)
590
0
            .collect()
591
0
    }
592
593
    /// Checks if cache has expired.
594
0
    fn is_cache_expired(&self) -> bool {
595
0
        Utc::now()
596
0
            .signed_duration_since(self.last_updated)
597
0
            .to_std()
598
0
            .unwrap_or(Duration::MAX)
599
0
            > self.cache_timeout
600
0
    }
601
602
    /// Loads symbol configuration from external source.
603
0
    async fn load_symbol_from_source(
604
0
        &mut self,
605
0
        _symbol: &str,
606
0
    ) -> Result<Option<SymbolConfig>, String> {
607
        // This would integrate with database or external configuration API
608
        // For now, return None to indicate symbol not found
609
610
        // Example of creating a default config if needed:
611
        // let config = SymbolConfig::new(symbol.to_string(), AssetClassification::Equity);
612
        // self.symbol_cache.insert(symbol.to_string(), config.clone());
613
        // Ok(Some(config))
614
615
0
        Ok(None)
616
0
    }
617
618
    /// Refreshes the entire symbol cache from source.
619
0
    async fn refresh_cache(&mut self) -> Result<usize, String> {
620
        // This would integrate with database to load all active symbols
621
        // For now, return the current cache size
622
0
        Ok(self.symbol_cache.len())
623
0
    }
624
625
    /// Sets cache timeout duration.
626
0
    pub fn set_cache_timeout(&mut self, timeout: Duration) {
627
0
        self.cache_timeout = timeout;
628
0
    }
629
630
    /// Forces cache refresh on next access.
631
0
    pub fn invalidate_cache(&mut self) {
632
0
        self.last_updated = DateTime::<Utc>::MIN_UTC;
633
0
    }
634
635
    /// Returns cache statistics.
636
0
    pub fn cache_stats(&self) -> (usize, DateTime<Utc>, bool) {
637
0
        (
638
0
            self.symbol_cache.len(),
639
0
            self.last_updated,
640
0
            self.is_cache_expired(),
641
0
        )
642
0
    }
643
}
644
645
impl Default for SymbolConfigManager {
646
0
    fn default() -> Self {
647
0
        Self::new()
648
0
    }
649
}
650
651
#[cfg(test)]
652
mod tests {
653
    use super::*;
654
655
    #[test]
656
1
    fn test_asset_classification_regulatory_class() {
657
1
        assert_eq!(AssetClassification::Equity.regulatory_class(), "EQUITY");
658
1
        assert_eq!(AssetClassification::Forex.regulatory_class(), "FX");
659
1
        assert_eq!(AssetClassification::Crypto.regulatory_class(), "CRYPTO");
660
1
    }
661
662
    #[test]
663
1
    fn test_volatility_profile_update() {
664
1
        let mut profile = VolatilityProfile::new();
665
1
        profile.update_metrics(0.40, 2.5);
666
667
        // With exponential smoothing: 0.1 * 0.40 + 0.9 * 0.20 = 0.22
668
1
        assert!(profile.average_volatility > 0.20 && profile.average_volatility < 0.25);
669
        // With exponential smoothing: 0.1 * 2.5 + 0.9 * 0.0 = 0.25
670
1
        assert!((profile.atr - 0.25).abs() < 0.01);
671
1
        assert_eq!(profile.volatility_regime, VolatilityRegime::Normal);
672
1
    }
673
674
    #[test]
675
1
    fn test_symbol_config_validation() {
676
1
        let mut config = SymbolConfig::new("AAPL".to_string(), AssetClassification::Equity);
677
1
        assert!(config.validate().is_ok());
678
679
1
        config.tick_size = -0.01;
680
1
        assert!(config.validate().is_err());
681
1
    }
682
683
    #[test]
684
1
    fn test_trading_hours_us_equity() {
685
1
        let hours = TradingHours::us_equity();
686
1
        assert_eq!(hours.timezone, "America/New_York");
687
1
        assert_eq!(
688
            hours.market_open,
689
1
            NaiveTime::from_hms_opt(9, 30, 0).unwrap()
690
        );
691
1
        assert_eq!(
692
            hours.market_close,
693
1
            NaiveTime::from_hms_opt(16, 0, 0).unwrap()
694
        );
695
1
    }
696
697
    #[test]
698
1
    fn test_symbol_config_manager() {
699
1
        let mut manager = SymbolConfigManager::new();
700
1
        let config = SymbolConfig::new("TEST".to_string(), AssetClassification::Equity);
701
702
1
        assert!(manager.upsert_symbol_config(config).is_ok());
703
1
        assert_eq!(manager.get_all_symbols().len(), 1);
704
1
    }
705
}
\ No newline at end of file diff --git a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html b/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html deleted file mode 100644 index 94dc44ebc..000000000 --- a/coverage_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/vault.rs
Line
Count
Source
1
//! HashiCorp Vault configuration for secure secret management.
2
//!
3
//! This module provides configuration structures for integrating with HashiCorp Vault
4
//! to securely manage secrets, API keys, and sensitive configuration data in the
5
//! Foxhunt trading system. Supports token-based authentication and namespace isolation.
6
7
use serde::{Deserialize, Serialize};
8
use secrecy::{ExposeSecret, SecretString};
9
use std::fmt;
10
11
/// HashiCorp Vault configuration for secure secret storage.
12
///
13
/// Configures connection to HashiCorp Vault for retrieving sensitive
14
/// configuration data such as API keys, database passwords, and other
15
/// secrets. Supports Vault Enterprise features like namespaces.
16
///
17
/// # Security
18
///
19
/// The Vault token is wrapped in `SecretString` to prevent accidental
20
/// exposure in logs, debug output, or memory dumps. The token is automatically
21
/// zeroized when the config is dropped.
22
#[derive(Clone, Serialize, Deserialize)]
23
pub struct VaultConfig {
24
    /// Vault server URL (e.g., "<https://vault.example.com:8200>")
25
    pub url: String,
26
    /// Vault authentication token for API access (securely stored)
27
    #[serde(serialize_with = "serialize_secret", deserialize_with = "deserialize_secret")]
28
    pub token: SecretString,
29
    /// Mount path for the secrets engine (e.g., "secret/")
30
    pub mount_path: String,
31
    /// Vault namespace for multi-tenant deployments (Enterprise feature)
32
    pub namespace: Option<String>,
33
}
34
35
/// Custom serializer for SecretString that prevents token exposure
36
2
fn serialize_secret<S>(_secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
37
2
where
38
2
    S: serde::Serializer,
39
{
40
    // Serialize as redacted placeholder to prevent token exposure
41
2
    serializer.serialize_str("***REDACTED***")
42
2
}
43
44
/// Custom deserializer for SecretString
45
1
fn deserialize_secret<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
46
1
where
47
1
    D: serde::Deserializer<'de>,
48
{
49
1
    let s = String::deserialize(deserializer)
?0
;
50
1
    Ok(SecretString::from(s))
51
1
}
52
53
impl fmt::Debug for VaultConfig {
54
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55
2
        f.debug_struct("VaultConfig")
56
2
            .field("url", &self.url)
57
2
            .field("token", &"***REDACTED***")
58
2
            .field("mount_path", &self.mount_path)
59
2
            .field("namespace", &self.namespace)
60
2
            .finish()
61
2
    }
62
}
63
64
impl Drop for VaultConfig {
65
16
    fn drop(&mut self) {
66
        // Explicitly zeroize the token when VaultConfig is dropped
67
        // This ensures the secret is cleared from memory
68
        // Note: SecretString already implements ZeroizeOnDrop, but we make it explicit
69
        // for documentation purposes
70
16
    }
71
}
72
73
impl VaultConfig {
74
    /// Creates a new VaultConfig with the specified parameters.
75
    ///
76
    /// # Security
77
    ///
78
    /// The token is immediately wrapped in a `SecretString` to prevent exposure.
79
    /// Consider using `from_env()` or loading from secure configuration
80
    /// sources instead of passing plain strings.
81
14
    pub fn new(url: String, token: String, mount_path: String) -> Self {
82
14
        Self {
83
14
            url,
84
14
            token: SecretString::from(token),
85
14
            mount_path,
86
14
            namespace: None,
87
14
        }
88
14
    }
89
90
    /// Sets the namespace for multi-tenant Vault deployments.
91
2
    pub fn with_namespace(mut self, namespace: String) -> Self {
92
2
        self.namespace = Some(namespace);
93
2
        self
94
2
    }
95
96
    /// Gets a reference to the secret token (requires explicit exposure)
97
    ///
98
    /// # Security
99
    ///
100
    /// This method requires the caller to explicitly acknowledge they are
101
    /// exposing the secret. Use only when necessary (e.g., when making
102
    /// API calls to Vault) and ensure the exposed value is not logged
103
    /// or stored in insecure locations.
104
1
    pub fn token(&self) -> &SecretString {
105
1
        &self.token
106
1
    }
107
108
    /// Validates the vault configuration.
109
    ///
110
    /// # Security
111
    ///
112
    /// Validation checks length without exposing the token value.
113
7
    pub fn validate(&self) -> Result<(), String> {
114
7
        if self.url.is_empty() {
115
2
            return Err("Vault URL cannot be empty".to_string());
116
5
        }
117
5
        if self.token.expose_secret().is_empty() {
118
2
            return Err("Vault token cannot be empty".to_string());
119
3
        }
120
3
        if self.mount_path.is_empty() {
121
2
            return Err("Vault mount path cannot be empty".to_string());
122
1
        }
123
1
        Ok(())
124
7
    }
125
}
126
127
#[cfg(test)]
128
mod tests {
129
    use super::*;
130
131
14
    fn create_test_config() -> VaultConfig {
132
14
        VaultConfig::new(
133
14
            "https://vault.example.com:8200".to_string(),
134
14
            "test-token-12345".to_string(),
135
14
            "secret/".to_string(),
136
        )
137
14
    }
138
139
    #[test]
140
1
    fn test_vault_config_creation() {
141
1
        let config = create_test_config();
142
1
        assert_eq!(config.url, "https://vault.example.com:8200");
143
1
        assert_eq!(config.mount_path, "secret/");
144
1
        assert!(config.namespace.is_none());
145
1
    }
146
147
    #[test]
148
1
    fn test_vault_config_with_namespace() {
149
1
        let config = create_test_config().with_namespace("production".to_string());
150
1
        assert_eq!(config.namespace.as_deref(), Some("production"));
151
1
    }
152
153
    #[test]
154
1
    fn test_vault_config_validation_success() {
155
1
        let config = create_test_config();
156
1
        assert!(config.validate().is_ok());
157
1
    }
158
159
    #[test]
160
1
    fn test_vault_config_validation_empty_url() {
161
1
        let mut config = create_test_config();
162
1
        config.url = String::new();
163
1
        assert!(config.validate().is_err());
164
1
        assert_eq!(config.validate().unwrap_err(), "Vault URL cannot be empty");
165
1
    }
166
167
    #[test]
168
1
    fn test_vault_config_validation_empty_token() {
169
1
        let mut config = create_test_config();
170
1
        config.token = SecretString::from(String::new());
171
1
        assert!(config.validate().is_err());
172
1
        assert_eq!(
173
1
            config.validate().unwrap_err(),
174
            "Vault token cannot be empty"
175
        );
176
1
    }
177
178
    #[test]
179
1
    fn test_vault_config_validation_empty_mount_path() {
180
1
        let mut config = create_test_config();
181
1
        config.mount_path = String::new();
182
1
        assert!(config.validate().is_err());
183
1
        assert_eq!(
184
1
            config.validate().unwrap_err(),
185
            "Vault mount path cannot be empty"
186
        );
187
1
    }
188
189
    #[test]
190
1
    fn test_vault_config_serialization() {
191
1
        let config = create_test_config();
192
1
        let serialized = serde_json::to_string(&config).unwrap();
193
        // Token should be redacted in serialization
194
1
        assert!(serialized.contains("***REDACTED***"));
195
1
        assert!(!serialized.contains("test-token-12345"));
196
1
    }
197
198
    #[test]
199
1
    fn test_vault_config_deserialization() {
200
1
        let config = create_test_config();
201
1
        let serialized = serde_json::to_string(&config).unwrap();
202
1
        let deserialized: VaultConfig = serde_json::from_str(&serialized).unwrap();
203
1
        assert_eq!(config.url, deserialized.url);
204
1
        assert_eq!(config.mount_path, deserialized.mount_path);
205
1
    }
206
207
    #[test]
208
1
    fn test_vault_config_clone() {
209
1
        let config1 = create_test_config();
210
1
        let config2 = config1.clone();
211
1
        assert_eq!(config1.url, config2.url);
212
1
    }
213
214
    #[test]
215
1
    fn test_vault_config_debug() {
216
1
        let config = create_test_config();
217
1
        let debug_output = format!("{:?}", config);
218
1
        assert!(debug_output.contains("VaultConfig"));
219
1
        assert!(debug_output.contains("***REDACTED***"));
220
1
        assert!(!debug_output.contains("test-token-12345"));
221
1
    }
222
223
    #[test]
224
1
    fn test_vault_config_namespace_none() {
225
1
        let config = create_test_config();
226
1
        assert!(config.namespace.is_none());
227
1
    }
228
229
    #[test]
230
1
    fn test_vault_config_namespace_some() {
231
1
        let config = create_test_config().with_namespace("dev".to_string());
232
1
        assert!(config.namespace.is_some());
233
1
        assert_eq!(config.namespace.as_deref(), Some("dev"));
234
1
    }
235
236
    #[test]
237
1
    fn test_vault_config_token_not_exposed() {
238
1
        let config = create_test_config();
239
        // Verify token accessor works
240
1
        assert_eq!(config.token().expose_secret(), "test-token-12345");
241
1
    }
242
243
    #[test]
244
1
    fn test_vault_config_token_redacted_in_display() {
245
1
        let config = create_test_config();
246
1
        let debug_str = format!("{:?}", config);
247
1
        assert!(!debug_str.contains("test-token-12345"));
248
1
    }
249
}
\ No newline at end of file diff --git a/coverage_common/html/index.html b/coverage_common/html/index.html deleted file mode 100644 index 905ab89a5..000000000 --- a/coverage_common/html/index.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-06 12:43

Click here for information about interpreting this report.

FilenameFunction CoverageLine CoverageRegion CoverageBranch Coverage
common/src/database.rs
   0.00% (0/15)
   0.00% (0/132)
   0.00% (0/117)
- (0/0)
common/src/error.rs
  29.41% (5/17)
  41.83% (64/153)
  31.51% (69/219)
- (0/0)
common/src/thresholds.rs
 100.00% (4/4)
 100.00% (21/21)
 100.00% (31/31)
- (0/0)
common/src/trading.rs
   0.00% (0/16)
   0.00% (0/87)
   0.00% (0/135)
- (0/0)
common/src/traits.rs
   0.00% (0/2)
   0.00% (0/6)
   0.00% (0/6)
- (0/0)
common/src/types.rs
  55.53% (211/380)
  54.67% (1148/2100)
  53.30% (1574/2953)
- (0/0)
config/src/asset_classification.rs
  76.00% (19/25)
  90.78% (325/358)
  86.34% (278/322)
- (0/0)
config/src/compliance_config.rs
 100.00% (2/2)
 100.00% (40/40)
 100.00% (54/54)
- (0/0)
config/src/data_config.rs
   0.00% (0/13)
   0.00% (0/145)
   0.00% (0/71)
- (0/0)
config/src/data_providers.rs
  75.00% (21/28)
  76.22% (125/164)
  80.73% (155/192)
- (0/0)
config/src/database.rs
  97.30% (36/37)
  98.91% (273/276)
  99.04% (311/314)
- (0/0)
config/src/error.rs
 100.00% (9/9)
  93.48% (43/46)
  92.31% (72/78)
- (0/0)
config/src/lib.rs
   0.00% (0/2)
   0.00% (0/11)
   0.00% (0/15)
- (0/0)
config/src/manager.rs
 100.00% (47/47)
  95.84% (346/361)
  95.69% (644/673)
- (0/0)
config/src/ml_config.rs
   0.00% (0/4)
   0.00% (0/136)
   0.00% (0/55)
- (0/0)
config/src/risk_config.rs
  62.50% (5/8)
  41.80% (102/244)
  56.20% (231/411)
- (0/0)
config/src/runtime.rs
  57.45% (27/47)
  73.02% (314/430)
  55.42% (317/572)
- (0/0)
config/src/schemas.rs
   0.00% (0/5)
   0.00% (0/76)
   0.00% (0/144)
- (0/0)
config/src/storage_config.rs
   0.00% (0/5)
   0.00% (0/26)
   0.00% (0/26)
- (0/0)
config/src/structures.rs
   0.00% (0/27)
   0.00% (0/349)
   0.00% (0/291)
- (0/0)
config/src/symbol_config.rs
  32.65% (16/49)
  44.13% (154/349)
  44.58% (177/397)
- (0/0)
config/src/vault.rs
 100.00% (23/23)
 100.00% (131/131)
  99.53% (214/215)
- (0/0)
Totals
  55.56% (425/765)
  54.71% (3086/5641)
  56.60% (4127/7291)
- (0/0)
Generated by llvm-cov -- llvm version 20.1.7-rust-1.89.0-stable
\ No newline at end of file diff --git a/coverage_common/html/style.css b/coverage_common/html/style.css deleted file mode 100644 index ae4f09f69..000000000 --- a/coverage_common/html/style.css +++ /dev/null @@ -1,194 +0,0 @@ -.red { - background-color: #f004; -} -.cyan { - background-color: cyan; -} -html { - scroll-behavior: smooth; -} -body { - font-family: -apple-system, sans-serif; -} -pre { - margin-top: 0px !important; - margin-bottom: 0px !important; -} -.source-name-title { - padding: 5px 10px; - border-bottom: 1px solid #8888; - background-color: #0002; - line-height: 35px; -} -.centered { - display: table; - margin-left: left; - margin-right: auto; - border: 1px solid #8888; - border-radius: 3px; -} -.expansion-view { - margin-left: 0px; - margin-top: 5px; - margin-right: 5px; - margin-bottom: 5px; - border: 1px solid #8888; - border-radius: 3px; -} -table { - border-collapse: collapse; -} -.light-row { - border: 1px solid #8888; - border-left: none; - border-right: none; -} -.light-row-bold { - border: 1px solid #8888; - border-left: none; - border-right: none; - font-weight: bold; -} -.column-entry { - text-align: left; -} -.column-entry-bold { - font-weight: bold; - text-align: left; -} -.column-entry-yellow { - text-align: left; - background-color: #ff06; -} -.column-entry-red { - text-align: left; - background-color: #f004; -} -.column-entry-gray { - text-align: left; - background-color: #fff4; -} -.column-entry-green { - text-align: left; - background-color: #0f04; -} -.line-number { - text-align: right; -} -.covered-line { - text-align: right; - color: #06d; -} -.uncovered-line { - text-align: right; - color: #d00; -} -.uncovered-line.selected { - color: #f00; - font-weight: bold; -} -.region.red.selected { - background-color: #f008; - font-weight: bold; -} -.branch.red.selected { - background-color: #f008; - font-weight: bold; -} -.tooltip { - position: relative; - display: inline; - background-color: #bef; - text-decoration: none; -} -.tooltip span.tooltip-content { - position: absolute; - width: 100px; - margin-left: -50px; - color: #FFFFFF; - background: #000000; - height: 30px; - line-height: 30px; - text-align: center; - visibility: hidden; - border-radius: 6px; -} -.tooltip span.tooltip-content:after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - margin-left: -8px; - width: 0; height: 0; - border-top: 8px solid #000000; - border-right: 8px solid transparent; - border-left: 8px solid transparent; -} -:hover.tooltip span.tooltip-content { - visibility: visible; - opacity: 0.8; - bottom: 30px; - left: 50%; - z-index: 999; -} -th, td { - vertical-align: top; - padding: 2px 8px; - border-collapse: collapse; - border-right: 1px solid #8888; - border-left: 1px solid #8888; - text-align: left; -} -td pre { - display: inline-block; - text-decoration: inherit; -} -td:first-child { - border-left: none; -} -td:last-child { - border-right: none; -} -tr:hover { - background-color: #eee; -} -tr:last-child { - border-bottom: none; -} -tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { - background-color: #8884; -} -a { - color: inherit; -} -.control { - position: fixed; - top: 0em; - right: 0em; - padding: 1em; - background: #FFF8; -} -@media (prefers-color-scheme: dark) { - body { - background-color: #222; - color: whitesmoke; - } - tr:hover { - background-color: #111; - } - .covered-line { - color: #39f; - } - .uncovered-line { - color: #f55; - } - .tooltip { - background-color: #068; - } - .control { - background: #2228; - } - tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { - background-color: #8884; - } -} diff --git a/coverage_report_common/html/control.js b/coverage_report_common/html/control.js deleted file mode 100644 index 5897b005c..000000000 --- a/coverage_report_common/html/control.js +++ /dev/null @@ -1,99 +0,0 @@ - -function next_uncovered(selector, reverse, scroll_selector) { - function visit_element(element) { - element.classList.add("seen"); - element.classList.add("selected"); - - if (!scroll_selector) { - scroll_selector = "tr:has(.selected) td.line-number" - } - - const scroll_to = document.querySelector(scroll_selector); - if (scroll_to) { - scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"}); - } - } - - function select_one() { - if (!reverse) { - const previously_selected = document.querySelector(".selected"); - - if (previously_selected) { - previously_selected.classList.remove("selected"); - } - - return document.querySelector(selector + ":not(.seen)"); - } else { - const previously_selected = document.querySelector(".selected"); - - if (previously_selected) { - previously_selected.classList.remove("selected"); - previously_selected.classList.remove("seen"); - } - - const nodes = document.querySelectorAll(selector + ".seen"); - if (nodes) { - const last = nodes[nodes.length - 1]; // last - return last; - } else { - return undefined; - } - } - } - - function reset_all() { - if (!reverse) { - const all_seen = document.querySelectorAll(selector + ".seen"); - - if (all_seen) { - all_seen.forEach(e => e.classList.remove("seen")); - } - } else { - const all_seen = document.querySelectorAll(selector + ":not(.seen)"); - - if (all_seen) { - all_seen.forEach(e => e.classList.add("seen")); - } - } - - } - - const uncovered = select_one(); - - if (uncovered) { - visit_element(uncovered); - } else { - reset_all(); - - const uncovered = select_one(); - - if (uncovered) { - visit_element(uncovered); - } - } -} - -function next_line(reverse) { - next_uncovered("td.uncovered-line", reverse) -} - -function next_region(reverse) { - next_uncovered("span.red.region", reverse); -} - -function next_branch(reverse) { - next_uncovered("span.red.branch", reverse); -} - -document.addEventListener("keypress", function(event) { - const reverse = event.shiftKey; - if (event.code == "KeyL") { - next_line(reverse); - } - if (event.code == "KeyB") { - next_branch(reverse); - } - if (event.code == "KeyR") { - next_region(reverse); - } -}); diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html deleted file mode 100644 index 8b4b81f92..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/database.rs
Line
Count
Source
1
//! Database connection utilities and configurations
2
//!
3
//! This module provides shared database connection management utilities
4
//! that can be used across all Foxhunt services.
5
6
use serde::{Deserialize, Serialize};
7
use sqlx::{Pool, Postgres};
8
use std::time::Duration;
9
use thiserror::Error;
10
11
// Import centralized database configuration
12
pub use config::database::DatabaseConfig;
13
use config::structures::BacktestingDatabaseConfig;
14
15
/// Database-specific errors
16
#[derive(Debug, Error)]
17
#[allow(clippy::module_name_repetitions)]
18
pub enum DatabaseError {
19
    /// Connection failed - wrapper around SQLx connection errors
20
    #[error("Connection failed: {0}")]
21
    Connection(#[from] sqlx::Error),
22
    /// Query exceeded maximum allowed execution time
23
    #[error("Query timeout: operation took {actual_ms}ms, max allowed {max_ms}ms")]
24
    QueryTimeout {
25
        /// Actual execution time in milliseconds
26
        actual_ms: u64,
27
        /// Maximum allowed execution time in milliseconds
28
        max_ms: u64,
29
    },
30
    /// Connection pool has no available connections
31
    #[error("Pool exhausted: no connections available")]
32
    PoolExhausted,
33
    /// Database configuration is invalid or missing required parameters
34
    #[error("Configuration error: {0}")]
35
    Configuration(String),
36
    /// Performance constraint violation detected
37
    #[error("Performance violation: {0}")]
38
    Performance(String),
39
}
40
41
/// Database connection configuration (local extended version)
42
#[derive(Debug, Clone, Deserialize, Serialize)]
43
pub struct LocalDatabaseConfig {
44
    /// Database connection URL
45
    pub url: String,
46
    /// Pool configuration
47
    pub pool: PoolConfig,
48
    /// Performance settings
49
    pub performance: PerformanceConfig,
50
}
51
52
/// Connection pool configuration
53
#[derive(Debug, Clone, Deserialize, Serialize)]
54
pub struct PoolConfig {
55
    /// Maximum number of connections in the pool
56
    pub max_connections: u32,
57
    /// Minimum number of connections to maintain
58
    pub min_connections: u32,
59
    /// Connection timeout in milliseconds
60
    pub connect_timeout_ms: u64,
61
    /// Connection acquire timeout in milliseconds
62
    pub acquire_timeout_ms: u64,
63
    /// Maximum connection lifetime in seconds
64
    pub max_lifetime_seconds: u64,
65
    /// Idle timeout in seconds
66
    pub idle_timeout_seconds: u64,
67
}
68
69
/// Performance configuration for HFT operations
70
#[derive(Debug, Clone, Deserialize, Serialize)]
71
pub struct PerformanceConfig {
72
    /// Query timeout in microseconds for HFT operations
73
    pub query_timeout_micros: u64,
74
    /// Enable connection prewarming
75
    pub enable_prewarming: bool,
76
    /// Enable statement preparation
77
    pub enable_prepared_statements: bool,
78
    /// Enable query logging for slow queries
79
    pub enable_slow_query_logging: bool,
80
    /// Slow query threshold in microseconds
81
    pub slow_query_threshold_micros: u64,
82
}
83
84
impl Default for LocalDatabaseConfig {
85
0
    fn default() -> Self {
86
0
        Self {
87
0
            url: "postgresql://foxhunt:password@localhost:5432/foxhunt".to_owned(),
88
0
            pool: PoolConfig::default(),
89
0
            performance: PerformanceConfig::default(),
90
0
        }
91
0
    }
92
}
93
94
impl Default for PoolConfig {
95
0
    fn default() -> Self {
96
0
        Self {
97
0
            max_connections: 50,
98
0
            min_connections: 10,
99
0
            connect_timeout_ms: 100,
100
0
            acquire_timeout_ms: 50,
101
0
            max_lifetime_seconds: 3600,
102
0
            idle_timeout_seconds: 300,
103
0
        }
104
0
    }
105
}
106
107
impl Default for PerformanceConfig {
108
0
    fn default() -> Self {
109
0
        Self {
110
0
            query_timeout_micros: 800, // <1ms for HFT operations
111
0
            enable_prewarming: true,
112
0
            enable_prepared_statements: true,
113
0
            enable_slow_query_logging: true,
114
0
            slow_query_threshold_micros: 1000, // Log queries >1ms
115
0
        }
116
0
    }
117
}
118
119
/// Convert from centralized config to common crate config with HFT optimizations
120
#[allow(clippy::integer_division)]
121
impl From<DatabaseConfig> for LocalDatabaseConfig {
122
0
    fn from(config: DatabaseConfig) -> Self {
123
0
        Self {
124
0
            url: config.url,
125
0
            pool: PoolConfig {
126
0
                max_connections: config.max_connections,
127
0
                // 20% of max, min 2. Uses integer division intentionally for simplicity.
128
0
                min_connections: (config.max_connections / 5).max(2),
129
0
                connect_timeout_ms: u64::try_from(config.connect_timeout.as_millis().min(100))
130
0
                    .unwrap_or(100), // Convert to ms, cap at 100ms for HFT
131
0
                acquire_timeout_ms: 50,     // Fast acquire for HFT
132
0
                max_lifetime_seconds: 3600, // 1 hour default
133
0
                idle_timeout_seconds: 300,  // 5 minutes default
134
0
            },
135
0
            performance: PerformanceConfig {
136
0
                query_timeout_micros: u64::try_from(config.query_timeout.as_micros().min(800))
137
0
                    .unwrap_or(800), // Convert to microseconds, cap at 800μs for HFT
138
0
                enable_prewarming: true,
139
0
                enable_prepared_statements: true,
140
0
                enable_slow_query_logging: config.enable_query_logging,
141
0
                slow_query_threshold_micros: 1000, // 1ms threshold
142
0
            },
143
0
        }
144
0
    }
145
}
146
147
/// Convert from backtesting config to common crate config with backtesting optimizations
148
#[allow(clippy::integer_division)]
149
impl From<BacktestingDatabaseConfig> for LocalDatabaseConfig {
150
0
    fn from(config: BacktestingDatabaseConfig) -> Self {
151
0
        let max_conn = config.max_connections.unwrap_or(10);
152
0
        Self {
153
0
            url: config.database_url,
154
0
            pool: PoolConfig {
155
0
                max_connections: max_conn,
156
0
                // 25% of max, min 2. Uses integer division intentionally for simplicity.
157
0
                min_connections: (max_conn / 4).max(2),
158
0
                connect_timeout_ms: config.acquire_timeout_ms.unwrap_or(1000), // Use acquire timeout as connection timeout
159
0
                acquire_timeout_ms: 100,    // Less strict for backtesting
160
0
                max_lifetime_seconds: 3600, // 1 hour default
161
0
                idle_timeout_seconds: 600,  // 10 minutes for backtesting
162
0
            },
163
0
            performance: PerformanceConfig {
164
0
                query_timeout_micros: 10000, // 10ms default for backtesting queries
165
0
                enable_prewarming: true,
166
0
                enable_prepared_statements: true,
167
0
                enable_slow_query_logging: config.enable_logging.unwrap_or(false),
168
0
                slow_query_threshold_micros: 5000, // 5ms threshold for backtesting
169
0
            },
170
0
        }
171
0
    }
172
}
173
174
/// Database connection pool wrapper
175
#[derive(Debug)]
176
#[allow(clippy::module_name_repetitions)]
177
pub struct DatabasePool {
178
    pool: Pool<Postgres>,
179
    config: LocalDatabaseConfig,
180
}
181
182
impl DatabasePool {
183
    /// Create a new database connection pool
184
    ///
185
    /// # Errors
186
    ///
187
    /// Returns `DatabaseError` if:
188
    /// - Connection URL is invalid
189
    /// - Database connection fails
190
0
    pub async fn new(config: LocalDatabaseConfig) -> Result<Self, DatabaseError> {
191
        use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
192
193
        // Parse connection options
194
0
        let mut connect_options: PgConnectOptions = config
195
0
            .url
196
0
            .parse()
197
0
            .map_err(|e| DatabaseError::Configuration(format!("Invalid URL: {}", e)))?;
198
199
        // Configure connection-level optimizations
200
0
        connect_options = connect_options
201
0
            .application_name("foxhunt-service")
202
0
            .statement_cache_capacity(1000);
203
204
        // Create connection pool with optimized settings
205
0
        let pool = PgPoolOptions::new()
206
0
            .max_connections(config.pool.max_connections)
207
0
            .min_connections(config.pool.min_connections)
208
0
            .acquire_timeout(Duration::from_millis(config.pool.acquire_timeout_ms))
209
0
            .max_lifetime(Duration::from_secs(config.pool.max_lifetime_seconds))
210
0
            .idle_timeout(Duration::from_secs(config.pool.idle_timeout_seconds))
211
0
            .test_before_acquire(true)
212
0
            .connect_with(connect_options)
213
0
            .await
214
0
            .map_err(DatabaseError::Connection)?;
215
216
        // Pre-warm connections if enabled
217
0
        if config.performance.enable_prewarming {
218
0
            for _ in 0..config.pool.min_connections {
219
0
                let _conn = pool.acquire().await.map_err(DatabaseError::Connection)?;
220
0
                sqlx::query("SELECT 1")
221
0
                    .fetch_one(&pool)
222
0
                    .await
223
0
                    .map_err(DatabaseError::Connection)?;
224
            }
225
0
        }
226
227
0
        Ok(Self { pool, config })
228
0
    }
229
230
    /// Get the underlying connection pool
231
0
    pub const fn pool(&self) -> &Pool<Postgres> {
232
0
        &self.pool
233
0
    }
234
235
    /// Get current configuration
236
0
    pub const fn config(&self) -> &LocalDatabaseConfig {
237
0
        &self.config
238
0
    }
239
240
    /// Health check for the database connection
241
    ///
242
    /// # Errors
243
    ///
244
    /// Returns `DatabaseError` if:
245
    /// - Database connection fails
246
    /// - Query times out
247
0
    pub async fn health_check(&self) -> Result<(), DatabaseError> {
248
0
        let result = tokio::time::timeout(
249
0
            Duration::from_millis(100),
250
0
            sqlx::query("SELECT 1").fetch_one(&self.pool),
251
0
        )
252
0
        .await;
253
254
0
        match result {
255
0
            Ok(Ok(_)) => Ok(()),
256
0
            Ok(Err(e)) => Err(DatabaseError::Connection(e)),
257
0
            Err(_) => Err(DatabaseError::QueryTimeout {
258
0
                actual_ms: 100,
259
0
                max_ms: 100,
260
0
            }),
261
        }
262
0
    }
263
264
    /// Get connection pool statistics
265
    #[allow(clippy::integer_division)]
266
0
    pub fn pool_stats(&self) -> PoolStats {
267
0
        PoolStats {
268
0
            size: self.pool.size(),
269
0
            idle: u32::try_from(self.pool.num_idle()).unwrap_or(0),
270
0
            active: self.pool.size().saturating_sub(u32::try_from(self.pool.num_idle()).unwrap_or(0)),
271
0
            max_size: self.config.pool.max_connections,
272
0
        }
273
0
    }
274
}
275
276
/// Connection pool statistics
277
#[derive(Debug, Clone, Serialize, Deserialize)]
278
pub struct PoolStats {
279
    /// Current pool size
280
    pub size: u32,
281
    /// Number of idle connections
282
    pub idle: u32,
283
    /// Number of active connections
284
    pub active: u32,
285
    /// Maximum pool size
286
    pub max_size: u32,
287
}
288
289
impl PoolStats {
290
    /// Calculate pool utilization percentage
291
    #[allow(clippy::float_arithmetic)]
292
0
    pub fn utilization_percentage(&self) -> f64 {
293
0
        (f64::from(self.active) / f64::from(self.max_size)) * 100.0
294
0
    }
295
296
    /// Check if pool is healthy (not over-utilized)
297
0
    pub fn is_healthy(&self) -> bool {
298
0
        self.utilization_percentage() < 80.0
299
0
    }
300
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html deleted file mode 100644 index 8f1bb869f..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/error.rs
Line
Count
Source
1
//! Common error types and utilities
2
//!
3
//! This module provides shared error types and utilities used across
4
//! all Foxhunt services.
5
6
use serde::{Deserialize, Serialize};
7
use std::fmt;
8
use std::time::Duration;
9
use thiserror::Error;
10
11
/// Common error type for all Foxhunt services
12
#[derive(Debug, Error)]
13
pub enum CommonError {
14
    /// Database operation failed - wraps database-specific errors
15
    #[error("Database error: {0}")]
16
    Database(#[from] crate::database::DatabaseError),
17
    /// Configuration is invalid or missing required parameters
18
    #[error("Configuration error: {0}")]
19
    Configuration(String),
20
    /// Network communication error occurred
21
    #[error("Network error: {0}")]
22
    Network(String),
23
    /// Service-specific error with categorization for metrics
24
    #[error("Service error: {category} - {message}")]
25
    Service {
26
        /// Error category for classification
27
        category: ErrorCategory,
28
        /// Descriptive error message
29
        message: String,
30
    },
31
    /// Input validation failed
32
    #[error("Validation error: {0}")]
33
    Validation(String),
34
    /// Operation exceeded maximum allowed execution time
35
    #[error("Timeout error: operation took {actual_ms}ms, max allowed {max_ms}ms")]
36
    Timeout {
37
        /// Actual execution time in milliseconds
38
        actual_ms: u64,
39
        /// Maximum allowed execution time in milliseconds
40
        max_ms: u64,
41
    },
42
}
43
44
/// Error categories for classification and metrics
45
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46
#[allow(clippy::module_name_repetitions)]
47
pub enum ErrorCategory {
48
    /// Market data related errors
49
    MarketData,
50
    /// Trading and order management errors
51
    Trading,
52
    /// Network and communication errors
53
    Network,
54
    /// System and infrastructure errors
55
    System,
56
    /// Configuration errors
57
    Configuration,
58
    /// Validation errors
59
    Validation,
60
    /// Critical errors requiring immediate attention
61
    Critical,
62
    /// Connection errors (data providers)
63
    Connection,
64
    /// Authentication errors
65
    Authentication,
66
    /// Rate limiting errors
67
    RateLimit,
68
    /// Data parsing errors
69
    Parse,
70
    /// Subscription errors
71
    Subscription,
72
    /// Financial safety and calculation errors
73
    FinancialSafety,
74
    /// Risk management and circuit breakers
75
    RiskManagement,
76
    /// Database and persistence layer
77
    Database,
78
    /// Broker connectivity and execution
79
    Broker,
80
    /// Machine learning and AI errors
81
    MachineLearning,
82
    /// Security and authentication errors
83
    Security,
84
    /// Business logic errors
85
    BusinessLogic,
86
    /// Resource errors (not found, conflicts)
87
    Resource,
88
    /// Development and testing errors
89
    Development,
90
    /// Risk management errors
91
    Risk,
92
    /// Machine learning errors (alias for `MachineLearning`)
93
    ML,
94
    /// Unknown/other errors
95
    Other,
96
}
97
98
impl fmt::Display for ErrorCategory {
99
49
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100
49
        match self {
101
2
            Self::MarketData => write!(f, "MARKET_DATA"),
102
3
            Self::Trading => write!(f, "TRADING"),
103
2
            Self::Network => write!(f, "NETWORK"),
104
2
            Self::System => write!(f, "SYSTEM"),
105
2
            Self::Configuration => write!(f, "CONFIGURATION"),
106
2
            Self::Validation => write!(f, "VALIDATION"),
107
2
            Self::Critical => write!(f, "CRITICAL"),
108
2
            Self::Connection => write!(f, "CONNECTION"),
109
2
            Self::Authentication => write!(f, "AUTHENTICATION"),
110
2
            Self::RateLimit => write!(f, "RATE_LIMIT"),
111
2
            Self::Parse => write!(f, "PARSE"),
112
2
            Self::Subscription => write!(f, "SUBSCRIPTION"),
113
2
            Self::FinancialSafety => write!(f, "FINANCIAL_SAFETY"),
114
2
            Self::RiskManagement => write!(f, "RISK_MANAGEMENT"),
115
2
            Self::Database => write!(f, "DATABASE"),
116
2
            Self::Broker => write!(f, "BROKER"),
117
2
            Self::MachineLearning => write!(f, "MACHINE_LEARNING"),
118
2
            Self::Security => write!(f, "SECURITY"),
119
2
            Self::BusinessLogic => write!(f, "BUSINESS_LOGIC"),
120
2
            Self::Resource => write!(f, "RESOURCE"),
121
2
            Self::Development => write!(f, "DEVELOPMENT"),
122
2
            Self::Risk => write!(f, "RISK"),
123
2
            Self::ML => write!(f, "ML"),
124
2
            Self::Other => write!(f, "OTHER"),
125
        }
126
49
    }
127
}
128
129
/// Error severity levels for prioritization and alerting
130
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131
#[allow(clippy::module_name_repetitions)]
132
pub enum ErrorSeverity {
133
    /// Debug level - for development and troubleshooting
134
    Debug,
135
    /// Info level - informational messages
136
    Info,
137
    /// Warning level - potentially problematic situations
138
    Warn,
139
    /// Error level - error conditions that should be addressed
140
    Error,
141
    /// Critical level - serious error conditions requiring immediate attention
142
    Critical,
143
}
144
145
impl fmt::Display for ErrorSeverity {
146
10
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147
10
        match self {
148
2
            Self::Debug => write!(f, "DEBUG"),
149
2
            Self::Info => write!(f, "INFO"),
150
2
            Self::Warn => write!(f, "WARN"),
151
2
            Self::Error => write!(f, "ERROR"),
152
2
            Self::Critical => write!(f, "CRITICAL"),
153
        }
154
10
    }
155
}
156
157
/// Retry strategies for error recovery
158
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159
pub enum RetryStrategy {
160
    /// Do not retry - error is permanent
161
    NoRetry,
162
    /// Retry immediately without delay
163
    Immediate,
164
    /// Linear backoff with fixed intervals
165
    Linear {
166
        /// Base delay in milliseconds between retries
167
        base_delay_ms: u64,
168
    },
169
    /// Exponential backoff with jitter
170
    Exponential {
171
        /// Base delay in milliseconds for exponential backoff
172
        base_delay_ms: u64,
173
        /// Maximum delay cap in milliseconds
174
        max_delay_ms: u64,
175
    },
176
    /// Wait for circuit breaker to close
177
    CircuitBreaker,
178
}
179
180
impl RetryStrategy {
181
    /// Calculate delay for retry attempt
182
    #[must_use]
183
23
    pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
184
23
        match self {
185
3
            Self::NoRetry => None,
186
2
            Self::Immediate => Some(Duration::from_millis(0)),
187
5
            Self::Linear { base_delay_ms } => {
188
5
                Some(Duration::from_millis(base_delay_ms.saturating_mul(u64::from(attempt))))
189
            },
190
            Self::Exponential {
191
11
                base_delay_ms,
192
11
                max_delay_ms,
193
            } => {
194
11
                let delay_ms = base_delay_ms.saturating_mul(2_u64.saturating_pow(attempt.min(10)));
195
11
                let capped_delay = delay_ms.min(*max_delay_ms);
196
197
                // Add simple jitter (±10%)
198
                #[allow(clippy::integer_division)]
199
11
                let jitter_ms = capped_delay / 10;
200
                #[allow(clippy::integer_division)]
201
11
                let final_delay = capped_delay.saturating_sub(jitter_ms / 2);
202
203
11
                Some(Duration::from_millis(final_delay))
204
            },
205
2
            Self::CircuitBreaker => Some(Duration::from_secs(30)),
206
        }
207
23
    }
208
209
    /// Get maximum recommended retry attempts
210
    #[must_use]
211
5
    pub const fn max_attempts(&self) -> Option<u32> {
212
5
        match self {
213
1
            Self::NoRetry => Some(0),
214
1
            Self::Immediate => Some(3),
215
1
            Self::Linear { .. } => Some(5),
216
1
            Self::Exponential { .. } => Some(7),
217
1
            Self::CircuitBreaker => Some(1),
218
        }
219
5
    }
220
}
221
222
/// Convenience functions for creating common errors
223
impl CommonError {
224
    /// Create a configuration error
225
13
    pub fn config<S: Into<String>>(message: S) -> Self {
226
13
        Self::Configuration(message.into())
227
13
    }
228
229
    /// Create a network error
230
10
    pub fn network<S: Into<String>>(message: S) -> Self {
231
10
        Self::Network(message.into())
232
10
    }
233
234
    /// Create a service error with category
235
65
    pub fn service<S: Into<String>>(category: ErrorCategory, message: S) -> Self {
236
65
        Self::Service {
237
65
            category,
238
65
            message: message.into(),
239
65
        }
240
65
    }
241
242
    /// Create a validation error
243
7
    pub fn validation<S: Into<String>>(message: S) -> Self {
244
7
        Self::Validation(message.into())
245
7
    }
246
247
    /// Create a timeout error
248
8
    pub const fn timeout(actual_ms: u64, max_ms: u64) -> Self {
249
8
        Self::Timeout { actual_ms, max_ms }
250
8
    }
251
252
    /// Create a machine learning specific service error
253
3
    pub fn ml<S: Into<String>, M: Into<String>>(model_name: S, message: M) -> Self {
254
3
        Self::Service {
255
3
            category: ErrorCategory::MachineLearning,
256
3
            message: format!("{}: {}", model_name.into(), message.into()),
257
3
        }
258
3
    }
259
260
    /// Create a serialization error
261
1
    pub fn serialization<S: Into<String>>(message: S) -> Self {
262
1
        Self::Service {
263
1
            category: ErrorCategory::Parse,
264
1
            message: format!("Serialization error: {}", message.into()),
265
1
        }
266
1
    }
267
268
    /// Create an internal error
269
3
    pub fn internal<S: Into<String>>(message: S) -> Self {
270
3
        Self::Service {
271
3
            category: ErrorCategory::System,
272
3
            message: format!("Internal error: {}", message.into()),
273
3
        }
274
3
    }
275
276
    /// Create a resource exhausted error
277
1
    pub fn resource_exhausted<S: Into<String>>(resource: S) -> Self {
278
1
        Self::Service {
279
1
            category: ErrorCategory::Resource,
280
1
            message: format!("Resource exhausted: {}", resource.into()),
281
1
        }
282
1
    }
283
284
    /// Get the error category for classification and metrics
285
39
    pub const fn category(&self) -> ErrorCategory {
286
39
        match self {
287
0
            Self::Database(_) => ErrorCategory::Database,
288
3
            Self::Configuration(_) => ErrorCategory::Configuration,
289
3
            Self::Network(_) => ErrorCategory::Network,
290
29
            Self::Service { category, .. } => *category,
291
2
            Self::Validation(_) => ErrorCategory::Validation,
292
2
            Self::Timeout { .. } => ErrorCategory::System,
293
        }
294
39
    }
295
296
    /// Get error severity level
297
40
    pub const fn severity(&self) -> ErrorSeverity {
298
40
        match self {
299
1
            Self::Database(_) => ErrorSeverity::Critical,
300
3
            Self::Configuration(_) => ErrorSeverity::Critical,
301
3
            Self::Network(_) => ErrorSeverity::Error,
302
27
            Self::Service { category, .. } => match category {
303
                ErrorCategory::Critical
304
                | ErrorCategory::FinancialSafety
305
6
                | ErrorCategory::Authentication => ErrorSeverity::Critical,
306
                ErrorCategory::Trading
307
                | ErrorCategory::RiskManagement
308
4
                | ErrorCategory::Database => ErrorSeverity::Error,
309
17
                _ => ErrorSeverity::Warn,
310
            },
311
3
            Self::Validation(_) => ErrorSeverity::Warn,
312
3
            Self::Timeout { .. } => ErrorSeverity::Error,
313
        }
314
40
    }
315
316
    /// Check if the error is retryable
317
28
    pub const fn is_retryable(&self) -> bool {
318
28
        match self {
319
1
            Self::Database(_) => true,       // Database operations can be retried
320
5
            Self::Configuration(_) => false, // Configuration errors are permanent
321
5
            Self::Network(_) => true,        // Network errors are often transient
322
11
            Self::Service { category, .. } => !
matches!7
(
323
11
                category,
324
                ErrorCategory::Authentication
325
                    | ErrorCategory::Configuration
326
                    | ErrorCategory::Validation
327
            ),
328
3
            Self::Validation(_) => false, // Validation errors are permanent
329
3
            Self::Timeout { .. } => true, // Timeouts can be retried
330
        }
331
28
    }
332
333
    /// Get retry strategy for this error
334
13
    pub const fn retry_strategy(&self) -> RetryStrategy {
335
13
        if !self.is_retryable() {
336
4
            return RetryStrategy::NoRetry;
337
9
        }
338
339
9
        match self {
340
1
            Self::Database(_) => RetryStrategy::Exponential {
341
1
                base_delay_ms: 1000,
342
1
                max_delay_ms: 10000,
343
1
            },
344
2
            Self::Network(_) => RetryStrategy::Linear { base_delay_ms: 500 },
345
5
            Self::Service { category, .. } => match category {
346
                ErrorCategory::Network | ErrorCategory::Connection => {
347
2
                    RetryStrategy::Linear { base_delay_ms: 500 }
348
                },
349
1
                ErrorCategory::RateLimit => RetryStrategy::Exponential {
350
1
                    base_delay_ms: 5000,
351
1
                    max_delay_ms: 60000,
352
1
                },
353
2
                _ => RetryStrategy::Immediate,
354
            },
355
1
            Self::Timeout { .. } => RetryStrategy::Linear {
356
1
                base_delay_ms: 1000,
357
1
            },
358
0
            _ => RetryStrategy::NoRetry,
359
        }
360
13
    }
361
}
362
363
/// Result type for common operations
364
pub type CommonResult<T> = Result<T, CommonError>;
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/market_data.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/market_data.rs.html deleted file mode 100644 index 42a86a40a..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/market_data.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/market_data.rs
Line
Count
Source
1
//! Market data types for common use
2
3
use crate::types::{OrderSide, Price, Quantity, Symbol};
4
use chrono::{DateTime, Utc};
5
use serde::{Deserialize, Serialize};
6
7
/// Market data event types
8
#[derive(Debug, Clone, Serialize, Deserialize)]
9
#[allow(clippy::module_name_repetitions)]
10
pub enum MarketDataEvent {
11
    /// Trade execution event
12
    Trade(TradeEvent),
13
    /// Quote update (bid/ask) event
14
    Quote(QuoteEvent),
15
    /// Bar/candlestick data event
16
    Bar(BarEvent),
17
    /// Order book update event
18
    OrderBook(OrderBookEvent),
19
    /// News and market information event
20
    News(NewsEvent),
21
}
22
23
/// Trade event
24
#[derive(Debug, Clone, Serialize, Deserialize)]
25
pub struct TradeEvent {
26
    /// Trading symbol
27
    pub symbol: Symbol,
28
    /// Trade execution price
29
    pub price: Price,
30
    /// Trade quantity
31
    pub quantity: Quantity,
32
    /// Trade side (buy or sell)
33
    pub side: OrderSide,
34
    /// Trade execution timestamp
35
    pub timestamp: DateTime<Utc>,
36
    /// Unique trade identifier
37
    pub trade_id: String,
38
}
39
40
/// Quote event
41
#[derive(Debug, Clone, Serialize, Deserialize)]
42
pub struct QuoteEvent {
43
    /// Trading symbol
44
    pub symbol: Symbol,
45
    /// Best bid price
46
    pub bid_price: Price,
47
    /// Best bid quantity
48
    pub bid_quantity: Quantity,
49
    /// Best ask price
50
    pub ask_price: Price,
51
    /// Best ask quantity
52
    pub ask_quantity: Quantity,
53
    /// Quote timestamp
54
    pub timestamp: DateTime<Utc>,
55
}
56
57
/// Bar event (OHLCV)
58
#[derive(Debug, Clone, Serialize, Deserialize)]
59
pub struct BarEvent {
60
    /// Trading symbol
61
    pub symbol: Symbol,
62
    /// Opening price
63
    pub open: Price,
64
    /// Highest price
65
    pub high: Price,
66
    /// Lowest price
67
    pub low: Price,
68
    /// Closing price
69
    pub close: Price,
70
    /// Trading volume
71
    pub volume: Quantity,
72
    /// Bar timestamp
73
    pub timestamp: DateTime<Utc>,
74
    /// Bar time interval
75
    pub interval: BarInterval,
76
}
77
78
/// Bar interval
79
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
80
pub enum BarInterval {
81
    /// 1-second interval
82
    Second1,
83
    /// 1-minute interval
84
    Minute1,
85
    /// 5-minute interval
86
    Minute5,
87
    /// 15-minute interval
88
    Minute15,
89
    /// 1-hour interval
90
    Hour1,
91
    /// 1-day interval
92
    Day1,
93
}
94
95
/// Order book event
96
#[derive(Debug, Clone, Serialize, Deserialize)]
97
pub struct OrderBookEvent {
98
    /// Trading symbol
99
    pub symbol: Symbol,
100
    /// Bid levels (price, quantity)
101
    pub bids: Vec<(Price, Quantity)>,
102
    /// Ask levels (price, quantity)
103
    pub asks: Vec<(Price, Quantity)>,
104
    /// Order book timestamp
105
    pub timestamp: DateTime<Utc>,
106
}
107
108
/// News event
109
#[derive(Debug, Clone, Serialize, Deserialize)]
110
pub struct NewsEvent {
111
    /// Related trading symbol (if applicable)
112
    pub symbol: Option<Symbol>,
113
    /// News headline
114
    pub headline: String,
115
    /// News content/body
116
    pub content: String,
117
    /// News publication timestamp
118
    pub timestamp: DateTime<Utc>,
119
    /// News source identifier
120
    pub source: String,
121
}
122
123
impl MarketDataEvent {
124
    /// Get the timestamp for any market data event
125
0
    pub const fn timestamp(&self) -> Option<DateTime<Utc>> {
126
0
        match self {
127
0
            MarketDataEvent::Quote(q) => Some(q.timestamp),
128
0
            MarketDataEvent::Trade(t) => Some(t.timestamp),
129
0
            MarketDataEvent::Bar(b) => Some(b.timestamp),
130
0
            MarketDataEvent::OrderBook(o) => Some(o.timestamp),
131
0
            MarketDataEvent::News(n) => Some(n.timestamp),
132
        }
133
0
    }
134
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/ml_strategy.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/ml_strategy.rs.html deleted file mode 100644 index 193e42969..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/ml_strategy.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/ml_strategy.rs
Line
Count
Source
1
//! Shared ML Strategy for Foxhunt Trading System
2
//!
3
//! This module provides a unified ML strategy implementation that is used by both
4
//! trading service and backtesting service to ensure consistent ML predictions
5
//! across all services. This eliminates code duplication and ensures ONE SINGLE SYSTEM.
6
//!
7
//! # Architecture
8
//!
9
//! ```text
10
//! SharedMLStrategy
11
//!     ├─ MLModelAdapter (abstraction over ml crate models)
12
//!     ├─ FeatureExtractor (consistent feature engineering)
13
//!     ├─ EnsembleCoordinator (weighted voting)
14
//!     └─ ModelPerformanceTracker (metrics)
15
//! ```
16
17
use anyhow::Result;
18
use chrono::{DateTime, Datelike, Utc, Timelike};
19
use serde::{Deserialize, Serialize};
20
use std::collections::HashMap;
21
use std::sync::Arc;
22
use tokio::sync::RwLock;
23
24
/// ML prediction result
25
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct MLPrediction {
27
    /// Model identifier
28
    pub model_id: String,
29
    /// Prediction value (0.0-1.0)
30
    pub prediction_value: f64,
31
    /// Confidence score (0.0-1.0)
32
    pub confidence: f64,
33
    /// Features used for prediction
34
    pub features: Vec<f64>,
35
    /// Prediction timestamp
36
    pub timestamp: DateTime<Utc>,
37
    /// Inference latency in microseconds
38
    pub inference_latency_us: u64,
39
}
40
41
/// ML model performance metrics
42
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43
pub struct MLModelPerformance {
44
    /// Model identifier
45
    pub model_id: String,
46
    /// Total predictions made
47
    pub total_predictions: u64,
48
    /// Correct predictions
49
    pub correct_predictions: u64,
50
    /// Average inference latency
51
    pub avg_latency_us: f64,
52
    /// Average confidence score
53
    pub avg_confidence: f64,
54
    /// Model accuracy percentage
55
    pub accuracy_percentage: f64,
56
    /// Returns generated
57
    pub returns: Vec<f64>,
58
    /// Sharpe ratio
59
    pub sharpe_ratio: f64,
60
    /// Maximum drawdown
61
    pub max_drawdown: f64,
62
}
63
64
/// Feature extraction for ML models
65
#[derive(Debug, Clone)]
66
pub struct MLFeatureExtractor {
67
    /// Lookback window for features
68
    pub lookback_periods: usize,
69
    /// Price history buffer
70
    price_history: Vec<f64>,
71
    /// Volume history buffer
72
    volume_history: Vec<f64>,
73
}
74
75
impl MLFeatureExtractor {
76
    /// Create new feature extractor
77
13
    pub fn new(lookback_periods: usize) -> Self {
78
13
        Self {
79
13
            lookback_periods,
80
13
            price_history: Vec::with_capacity(lookback_periods + 1),
81
13
            volume_history: Vec::with_capacity(lookback_periods + 1),
82
13
        }
83
13
    }
84
85
    /// Extract features from market data
86
27
    pub fn extract_features(&mut self, price: f64, volume: f64, timestamp: DateTime<Utc>) -> Vec<f64> {
87
        // Update price and volume history
88
27
        self.price_history.push(price);
89
27
        self.volume_history.push(volume);
90
91
        // Keep only the required lookback periods
92
27
        if self.price_history.len() > self.lookback_periods {
93
0
            self.price_history.remove(0);
94
27
        }
95
27
        if self.volume_history.len() > self.lookback_periods {
96
0
            self.volume_history.remove(0);
97
27
        }
98
99
        // Extract technical features
100
27
        let mut features = Vec::new();
101
102
27
        if self.price_history.len() >= 2 {
103
            // Price momentum (returns)
104
20
            let current_price = self.price_history.last().copied().unwrap_or(0.0);
105
20
            let prev_price = self.price_history.get(self.price_history.len() - 2).copied().unwrap_or(current_price);
106
20
            let price_return = if prev_price != 0.0 {
107
20
                (current_price - prev_price) / prev_price
108
            } else {
109
0
                0.0
110
            };
111
20
            features.push(price_return);
112
113
            // Short-term moving average
114
20
            if self.price_history.len() >= 5 {
115
12
                let short_ma: f64 = self.price_history.iter().rev().take(5).sum::<f64>() / 5.0;
116
12
                let ma_ratio = if short_ma != 0.0 { current_price / short_ma - 1.0 } else { 
0.00
};
117
12
                features.push(ma_ratio);
118
8
            } else {
119
8
                features.push(0.0);
120
8
            }
121
122
            // Price volatility (rolling standard deviation)
123
20
            if self.price_history.len() >= 10 {
124
2
                let recent_returns: Vec<f64> = self.price_history
125
2
                    .windows(2)
126
2
                    .rev()
127
2
                    .take(9)
128
18
                    .
map2
(|w| (w[1] - w[0]) / w[0])
129
2
                    .collect();
130
131
2
                let mean_return = recent_returns.iter().sum::<f64>() / recent_returns.len() as f64;
132
2
                let variance = recent_returns.iter()
133
18
                    .
map2
(|&r| (r - mean_return).powi(2))
134
2
                    .sum::<f64>() / recent_returns.len() as f64;
135
2
                let volatility = variance.sqrt();
136
2
                features.push(volatility);
137
18
            } else {
138
18
                features.push(0.0);
139
18
            }
140
7
        } else {
141
7
            features.extend_from_slice(&[0.0, 0.0, 0.0]);
142
7
        }
143
144
        // Volume features
145
27
        if self.volume_history.len() >= 2 {
146
20
            let current_volume = self.volume_history.last().copied().unwrap_or(0.0);
147
20
            let prev_volume = self.volume_history.get(self.volume_history.len() - 2).copied().unwrap_or(current_volume);
148
20
            let volume_ratio = if prev_volume != 0.0 {
149
20
                current_volume / prev_volume - 1.0
150
            } else {
151
0
                0.0
152
            };
153
20
            features.push(volume_ratio);
154
155
            // Volume moving average
156
20
            if self.volume_history.len() >= 5 {
157
12
                let volume_ma = self.volume_history.iter().rev().take(5).sum::<f64>() / 5.0;
158
12
                let volume_ma_ratio = if volume_ma != 0.0 { current_volume / volume_ma - 1.0 } else { 
0.00
};
159
12
                features.push(volume_ma_ratio);
160
8
            } else {
161
8
                features.push(0.0);
162
8
            }
163
7
        } else {
164
7
            features.extend_from_slice(&[0.0, 0.0]);
165
7
        }
166
167
        // Add time-based features
168
27
        let hour = timestamp.hour() as f64 / 24.0; // Normalized hour
169
27
        let day_of_week = timestamp.weekday().num_days_from_monday() as f64 / 6.0; // Normalized day
170
27
        features.push(hour);
171
27
        features.push(day_of_week);
172
173
        // Normalize all features to [-1, 1] range using tanh
174
189
        
features.iter()27
.
map27
(|&f| f.tanh()).
collect27
()
175
27
    }
176
}
177
178
/// Trait for ML model adapters
179
pub trait MLModelAdapter: Send + Sync {
180
    /// Get model prediction
181
    fn predict(&self, features: &[f64]) -> Result<MLPrediction>;
182
183
    /// Get model identifier
184
    fn model_id(&self) -> &str;
185
186
    /// Validate prediction against actual outcome
187
    fn validate_prediction(&mut self, prediction: &MLPrediction, actual_outcome: bool);
188
}
189
190
/// Simple DQN model adapter (for backtesting/simulation)
191
#[derive(Debug)]
192
pub struct SimpleDQNAdapter {
193
    model_id: String,
194
    weights: Vec<f64>,
195
    predictions_made: u64,
196
    correct_predictions: u64,
197
}
198
199
impl SimpleDQNAdapter {
200
    /// Create new DQN adapter
201
13
    pub fn new(model_id: String) -> Self {
202
        // Initialize with simulated weights
203
13
        let weights = vec![0.1, -0.05, 0.2, 0.15, -0.1, 0.08, 0.03];
204
205
13
        Self {
206
13
            model_id,
207
13
            weights,
208
13
            predictions_made: 0,
209
13
            correct_predictions: 0,
210
13
        }
211
13
    }
212
}
213
214
impl MLModelAdapter for SimpleDQNAdapter {
215
27
    fn predict(&self, features: &[f64]) -> Result<MLPrediction> {
216
27
        if features.len() != self.weights.len() {
217
0
            return Err(anyhow::anyhow!("Feature dimension mismatch: expected {}, got {}",
218
0
                self.weights.len(), features.len()));
219
27
        }
220
221
        // Simple linear combination with sigmoid activation
222
27
        let linear_output: f64 = features.iter()
223
27
            .zip(self.weights.iter())
224
189
            .
map27
(|(f, w)| f * w)
225
27
            .sum();
226
227
27
        let prediction_value = 1.0 / (1.0 + (-linear_output).exp()); // Sigmoid activation
228
229
        // Calculate confidence based on distance from 0.5
230
27
        let confidence = 0.5 + (prediction_value - 0.5).abs() * 0.8;
231
232
27
        Ok(MLPrediction {
233
27
            model_id: self.model_id.clone(),
234
27
            prediction_value,
235
27
            confidence,
236
27
            features: features.to_vec(),
237
27
            timestamp: Utc::now(),
238
27
            inference_latency_us: 50, // Simulated latency
239
27
        })
240
27
    }
241
242
0
    fn model_id(&self) -> &str {
243
0
        &self.model_id
244
0
    }
245
246
10
    fn validate_prediction(&mut self, prediction: &MLPrediction, actual_outcome: bool) {
247
10
        self.predictions_made += 1;
248
249
        // Simple validation: if prediction > 0.5 and outcome is positive, it's correct
250
10
        let predicted_positive = prediction.prediction_value > 0.5;
251
10
        if predicted_positive == actual_outcome {
252
5
            self.correct_predictions += 1;
253
5
        }
254
10
    }
255
}
256
257
/// Shared ML strategy implementation (ONE SINGLE SYSTEM)
258
pub struct SharedMLStrategy {
259
    /// Available ML models
260
    models: Arc<RwLock<HashMap<String, Box<dyn MLModelAdapter>>>>,
261
    /// Feature extractor
262
    feature_extractor: Arc<RwLock<MLFeatureExtractor>>,
263
    /// Model performance tracking
264
    model_performance: Arc<RwLock<HashMap<String, MLModelPerformance>>>,
265
    /// Minimum confidence threshold
266
    min_confidence_threshold: f64,
267
}
268
269
impl std::fmt::Debug for SharedMLStrategy {
270
0
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271
0
        f.debug_struct("SharedMLStrategy")
272
0
            .field("min_confidence_threshold", &self.min_confidence_threshold)
273
0
            .field("model_count", &"<async_lock>")
274
0
            .finish()
275
0
    }
276
}
277
278
impl SharedMLStrategy {
279
    /// Create new shared ML strategy
280
13
    pub fn new(lookback_periods: usize, min_confidence_threshold: f64) -> Self {
281
13
        let mut models: HashMap<String, Box<dyn MLModelAdapter>> = HashMap::new();
282
283
        // Add default models
284
13
        models.insert("dqn_v1".to_string(), Box::new(SimpleDQNAdapter::new("dqn_v1".to_string())));
285
286
13
        Self {
287
13
            models: Arc::new(RwLock::new(models)),
288
13
            feature_extractor: Arc::new(RwLock::new(MLFeatureExtractor::new(lookback_periods))),
289
13
            model_performance: Arc::new(RwLock::new(HashMap::new())),
290
13
            min_confidence_threshold,
291
13
        }
292
13
    }
293
294
    /// Get ensemble prediction from all models
295
27
    pub async fn get_ensemble_prediction(
296
27
        &self,
297
27
        price: f64,
298
27
        volume: f64,
299
27
        timestamp: DateTime<Utc>,
300
27
    ) -> Result<Vec<MLPrediction>> {
301
        // Extract features
302
27
        let features = {
303
27
            let mut extractor = self.feature_extractor.write().await;
304
27
            extractor.extract_features(price, volume, timestamp)
305
        };
306
307
27
        let mut predictions = Vec::new();
308
309
        // Get predictions from all models
310
27
        let models = self.models.read().await;
311
27
        for (model_id, model) in models.iter() {
312
27
            match model.predict(&features) {
313
27
                Ok(prediction) => {
314
27
                    if prediction.confidence >= self.min_confidence_threshold {
315
26
                        predictions.push(prediction);
316
26
                    
}1
317
                }
318
0
                Err(e) => {
319
0
                    tracing::warn!("Model {} failed to predict: {}", model_id, e);
320
                }
321
            }
322
        }
323
324
27
        Ok(predictions)
325
27
    }
326
327
    /// Calculate weighted ensemble vote
328
5
    pub fn calculate_ensemble_vote(&self, predictions: &[MLPrediction]) -> Option<(f64, f64)> {
329
5
        if predictions.is_empty() {
330
1
            return None;
331
4
        }
332
333
4
        let total_confidence: f64 = predictions.iter().map(|p| p.confidence).sum();
334
4
        if total_confidence == 0.0 {
335
0
            return None;
336
4
        }
337
338
        // Weighted average by confidence
339
4
        let weighted_prediction: f64 = predictions.iter()
340
7
            .
map4
(|p| p.prediction_value * p.confidence)
341
4
            .sum::<f64>() / total_confidence;
342
343
4
        let average_confidence: f64 = predictions.iter().map(|p| p.confidence).sum::<f64>() / predictions.len() as f64;
344
345
4
        Some((weighted_prediction, average_confidence))
346
5
    }
347
348
    /// Validate predictions against actual market outcomes
349
13
    pub async fn validate_predictions(&self, predictions: &[MLPrediction], actual_return: f64) {
350
13
        let actual_outcome = actual_return > 0.0; // Positive return = good outcome
351
352
13
        let mut models = self.models.write().await;
353
13
        let mut performance = self.model_performance.write().await;
354
355
26
        for 
prediction13
in predictions {
356
13
            if let Some(
model10
) = models.get_mut(&prediction.model_id) {
357
10
                model.validate_prediction(prediction, actual_outcome);
358
10
            
}3
359
360
            // Update performance tracking
361
13
            let perf = performance.entry(prediction.model_id.clone())
362
13
                .or_insert_with(|| MLModelPerformance {
363
3
                    model_id: prediction.model_id.clone(),
364
3
                    ..Default::default()
365
3
                });
366
367
13
            perf.total_predictions += 1;
368
369
13
            let predicted_positive = prediction.prediction_value > 0.5;
370
13
            if predicted_positive == actual_outcome {
371
7
                perf.correct_predictions += 1;
372
7
            
}6
373
374
13
            perf.accuracy_percentage = if perf.total_predictions > 0 {
375
13
                (perf.correct_predictions as f64 / perf.total_predictions as f64) * 100.0
376
            } else {
377
0
                0.0
378
            };
379
380
            // Update average confidence
381
13
            let total_samples = perf.total_predictions as f64;
382
13
            perf.avg_confidence = (perf.avg_confidence * (total_samples - 1.0) + prediction.confidence) / total_samples;
383
384
            // Update average latency
385
13
            perf.avg_latency_us = (perf.avg_latency_us * (total_samples - 1.0) + prediction.inference_latency_us as f64) / total_samples;
386
        }
387
13
    }
388
389
    /// Get performance summary for all models
390
4
    pub async fn get_performance_summary(&self) -> HashMap<String, MLModelPerformance> {
391
4
        self.model_performance.read().await.clone()
392
4
    }
393
394
    /// Add a model to the strategy
395
0
    pub async fn add_model(&self, model_id: String, model: Box<dyn MLModelAdapter>) {
396
0
        let mut models = self.models.write().await;
397
0
        models.insert(model_id, model);
398
0
    }
399
400
    /// Get minimum confidence threshold
401
1
    pub fn min_confidence_threshold(&self) -> f64 {
402
1
        self.min_confidence_threshold
403
1
    }
404
}
405
406
#[cfg(test)]
407
mod tests {
408
    use super::*;
409
410
    #[tokio::test]
411
1
    async fn test_shared_ml_strategy_creation() {
412
1
        let strategy = SharedMLStrategy::new(20, 0.6);
413
1
        assert_eq!(strategy.min_confidence_threshold(), 0.6);
414
1
    }
415
416
    #[tokio::test]
417
1
    async fn test_ensemble_prediction() {
418
1
        let strategy = SharedMLStrategy::new(20, 0.0);
419
420
1
        let predictions = strategy.get_ensemble_prediction(
421
1
            100.0,
422
1
            1000.0,
423
1
            Utc::now(),
424
1
        ).await.unwrap_or_default();
425
426
        // Should have at least one model prediction
427
1
        assert!(!predictions.is_empty());
428
1
    }
429
430
    #[tokio::test]
431
1
    async fn test_ensemble_vote() {
432
1
        let strategy = SharedMLStrategy::new(20, 0.0);
433
434
1
        let predictions = vec![
435
1
            MLPrediction {
436
1
                model_id: "model1".to_string(),
437
1
                prediction_value: 0.8,
438
1
                confidence: 0.9,
439
1
                features: vec![],
440
1
                timestamp: Utc::now(),
441
1
                inference_latency_us: 50,
442
1
            },
443
1
            MLPrediction {
444
1
                model_id: "model2".to_string(),
445
1
                prediction_value: 0.6,
446
1
                confidence: 0.7,
447
1
                features: vec![],
448
1
                timestamp: Utc::now(),
449
1
                inference_latency_us: 60,
450
1
            },
451
        ];
452
453
1
        let (vote, confidence) = strategy.calculate_ensemble_vote(&predictions).unwrap_or_default();
454
455
        // Weighted average should be between 0.6 and 0.8
456
1
        assert!(vote >= 0.6 && vote <= 0.8);
457
1
        assert!(confidence >= 0.7 && confidence <= 0.9);
458
1
    }
459
460
    #[tokio::test]
461
1
    async fn test_performance_tracking() {
462
1
        let strategy = SharedMLStrategy::new(20, 0.0);
463
464
1
        let prediction = MLPrediction {
465
1
            model_id: "test_model".to_string(),
466
1
            prediction_value: 0.7,
467
1
            confidence: 0.8,
468
1
            features: vec![],
469
1
            timestamp: Utc::now(),
470
1
            inference_latency_us: 50,
471
1
        };
472
473
        // Validate with positive outcome
474
1
        strategy.validate_predictions(&[prediction.clone()], 0.05).await;
475
476
1
        let performance = strategy.get_performance_summary().await;
477
1
        let model_perf = performance.get("test_model").cloned();
478
479
1
        assert!(model_perf.is_some());
480
1
        let perf = model_perf.unwrap_or_default();
481
1
        assert_eq!(perf.total_predictions, 1);
482
1
        assert_eq!(perf.correct_predictions, 1);
483
1
        assert_eq!(perf.accuracy_percentage, 100.0);
484
1
    }
485
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html deleted file mode 100644 index ad2ccf37a..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/thresholds.rs
Line
Count
Source
1
//! Centralized threshold constants for the Foxhunt HFT system
2
//!
3
//! This module consolidates all hardcoded threshold values that were
4
//! previously scattered throughout the codebase. Constants here are
5
//! compile-time values for performance-critical operations.
6
//!
7
//! For runtime-configurable values, see the `config` crate's runtime module.
8
9
use std::time::Duration;
10
11
/// Risk management thresholds
12
pub mod risk {
13
    
14
15
    /// Breach severity warning threshold (percentage of limit)
16
    ///
17
    /// Used when position is at 80-90% of limit
18
    pub const BREACH_WARNING_PCT: u8 = 80;
19
20
    /// Breach severity soft threshold (percentage of limit)
21
    ///
22
    /// Used when position is at 90-100% of limit
23
    pub const BREACH_SOFT_PCT: u8 = 90;
24
25
    /// Breach severity hard threshold (percentage of limit)
26
    ///
27
    /// Used when position is at 100-120% of limit
28
    pub const BREACH_HARD_PCT: u8 = 100;
29
30
    /// Breach severity critical threshold (percentage of limit)
31
    ///
32
    /// Used when position exceeds 120% of limit
33
    pub const BREACH_CRITICAL_PCT: u8 = 120;
34
35
    /// Minimum capital adequacy ratio (Basel III standard)
36
    pub const MIN_CAPITAL_ADEQUACY_RATIO: f64 = 0.08;
37
38
    /// Minimum leverage ratio (Basel III standard)
39
    pub const MIN_LEVERAGE_RATIO: f64 = 0.03;
40
41
    /// Default `VaR` confidence level (95%)
42
    pub const DEFAULT_VAR_CONFIDENCE: f64 = 0.95;
43
44
    /// High `VaR` confidence level (99%)
45
    pub const HIGH_VAR_CONFIDENCE: f64 = 0.99;
46
47
    /// Maximum drawdown warning threshold (percentage)
48
    pub const MAX_DRAWDOWN_WARNING_PCT: u8 = 15;
49
50
    /// Maximum drawdown critical threshold (percentage)
51
    pub const MAX_DRAWDOWN_CRITICAL_PCT: u8 = 25;
52
}
53
54
/// `VaR` calculation constants
55
pub mod var {
56
    /// Z-score for 90% confidence level
57
    pub const Z_SCORE_P90: f64 = 1.282;
58
59
    /// Z-score for 95% confidence level
60
    pub const Z_SCORE_P95: f64 = 1.645;
61
62
    /// Z-score for 97.5% confidence level
63
    pub const Z_SCORE_P97_5: f64 = 1.96;
64
65
    /// Z-score for 99% confidence level
66
    pub const Z_SCORE_P99: f64 = 2.326;
67
68
    /// Z-score for 99.9% confidence level
69
    pub const Z_SCORE_P99_9: f64 = 3.09;
70
71
    /// Default lookback period for historical `VaR` (trading days)
72
    pub const DEFAULT_LOOKBACK_DAYS: usize = 252;
73
74
    /// Minimum data quality score for `VaR` calculation
75
    pub const MIN_DATA_QUALITY_SCORE: f64 = 0.6;
76
}
77
78
/// Performance and timing constants
79
pub mod performance {
80
    
81
82
    /// Maximum latency for HFT critical path operations (nanoseconds)
83
    pub const MAX_CRITICAL_PATH_LATENCY_NS: u64 = 14;
84
85
    /// Maximum acceptable latency for risk checks (microseconds)
86
    pub const MAX_RISK_CHECK_LATENCY_US: u64 = 50;
87
88
    /// Maximum latency for ML inference (microseconds)
89
    pub const MAX_ML_INFERENCE_LATENCY_US: u64 = 100;
90
91
    /// Default batch processing size
92
    pub const DEFAULT_BATCH_SIZE: usize = 100;
93
94
    /// Ring buffer size for lock-free operations
95
    pub const RING_BUFFER_SIZE: usize = 4096;
96
97
    /// Small batch size for SIMD operations
98
    pub const SIMD_BATCH_SIZE: usize = 8;
99
100
    /// Maximum small batch size
101
    pub const MAX_SMALL_BATCH_SIZE: usize = 10;
102
103
    /// Default worker thread count (adjusted based on CPU cores at runtime)
104
    pub const DEFAULT_WORKER_THREADS: usize = 4;
105
106
    /// Default queue capacity for async operations
107
    pub const DEFAULT_QUEUE_CAPACITY: usize = 10000;
108
}
109
110
/// Cache TTL defaults (can be overridden by runtime config)
111
pub mod cache {
112
    use super::Duration;
113
114
    /// Default TTL for position cache entries (1 minute)
115
    pub const POSITION_CACHE_TTL: Duration = Duration::from_secs(60);
116
117
    /// Default TTL for `VaR` calculation cache (1 hour)
118
    pub const VAR_CACHE_TTL: Duration = Duration::from_secs(3600);
119
120
    /// Default TTL for compliance check cache (24 hours)
121
    pub const COMPLIANCE_CACHE_TTL: Duration = Duration::from_secs(86400);
122
123
    /// Default TTL for market data cache (5 minutes)
124
    pub const MARKET_DATA_CACHE_TTL: Duration = Duration::from_secs(300);
125
126
    /// Default TTL for model predictions cache (1 minute)
127
    pub const MODEL_PREDICTION_CACHE_TTL: Duration = Duration::from_secs(60);
128
129
    /// Redis key TTL for position limits (5 minutes)
130
    pub const REDIS_POSITION_LIMIT_TTL_SECS: i32 = 300;
131
132
    /// Redis key TTL for compliance checks (24 hours)
133
    pub const REDIS_COMPLIANCE_TTL_SECS: i32 = 86400;
134
135
    /// Redis key TTL for `VaR` calculations (1 hour)
136
    pub const REDIS_VAR_TTL_SECS: i32 = 3600;
137
}
138
139
/// Database operation defaults
140
pub mod database {
141
    use super::Duration;
142
143
    /// Default query timeout for standard operations
144
    pub const QUERY_TIMEOUT: Duration = Duration::from_millis(1000);
145
146
    /// Default connection timeout
147
    pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100);
148
149
    /// Default pool acquire timeout
150
    pub const ACQUIRE_TIMEOUT: Duration = Duration::from_millis(50);
151
152
    /// Default connection lifetime (1 hour)
153
    pub const CONNECTION_LIFETIME: Duration = Duration::from_secs(3600);
154
155
    /// Default idle timeout (5 minutes)
156
    pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
157
158
    /// Default pool size
159
    pub const DEFAULT_POOL_SIZE: u32 = 20;
160
161
    /// Maximum pool size
162
    pub const MAX_POOL_SIZE: u32 = 100;
163
164
    /// Maximum query result limit
165
    pub const MAX_QUERY_LIMIT: i64 = 1000;
166
}
167
168
/// Network and gRPC defaults
169
pub mod network {
170
    use super::Duration;
171
172
    /// Default connect timeout for gRPC clients
173
    pub const GRPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
174
175
    /// Default request timeout for gRPC
176
    pub const GRPC_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
177
178
    /// Default keep-alive interval
179
    pub const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
180
181
    /// Keep-alive timeout
182
    pub const KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5);
183
184
    /// Maximum concurrent connections
185
    pub const MAX_CONCURRENT_CONNECTIONS: u32 = 100;
186
187
    /// HTTP/2 initial stream window size
188
    pub const INITIAL_STREAM_WINDOW_SIZE: u32 = 65535;
189
190
    /// HTTP/2 initial connection window size
191
    pub const INITIAL_CONNECTION_WINDOW_SIZE: u32 = 1048576;
192
}
193
194
/// Retry and recovery defaults
195
pub mod retry {
196
    use super::Duration;
197
198
    /// Initial delay for exponential backoff
199
    pub const INITIAL_RETRY_DELAY: Duration = Duration::from_millis(100);
200
201
    /// Maximum delay for exponential backoff
202
    pub const MAX_RETRY_DELAY: Duration = Duration::from_secs(30);
203
204
    /// Maximum retry attempts for critical operations
205
    pub const MAX_RETRY_ATTEMPTS: u32 = 3;
206
207
    /// Backoff multiplier for exponential backoff
208
    pub const BACKOFF_MULTIPLIER: f32 = 1.5;
209
210
    /// Maximum total duration for retry attempts
211
    pub const MAX_TOTAL_RETRY_DURATION: Duration = Duration::from_secs(60);
212
}
213
214
/// Health check and monitoring intervals
215
pub mod monitoring {
216
    use super::Duration;
217
218
    /// Default health check interval
219
    pub const HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(30);
220
221
    /// Default metrics collection interval
222
    pub const METRICS_COLLECTION_INTERVAL: Duration = Duration::from_secs(10);
223
224
    /// Default log flush interval
225
    pub const LOG_FLUSH_INTERVAL: Duration = Duration::from_secs(5);
226
227
    /// Circuit breaker check interval
228
    pub const CIRCUIT_BREAKER_CHECK_INTERVAL: Duration = Duration::from_millis(100);
229
230
    /// Kill switch session timeout (5 minutes)
231
    pub const KILL_SWITCH_SESSION_TIMEOUT: Duration = Duration::from_secs(300);
232
}
233
234
/// Event processing defaults
235
pub mod events {
236
    use super::Duration;
237
238
    /// Event batch timeout
239
    pub const BATCH_TIMEOUT: Duration = Duration::from_millis(100);
240
241
    /// Event batch size
242
    pub const BATCH_SIZE: usize = 100;
243
244
    /// Event retry delay
245
    pub const RETRY_DELAY: Duration = Duration::from_millis(50);
246
247
    /// Maximum event backlog before applying backpressure
248
    pub const MAX_EVENT_BACKLOG: usize = 10000;
249
250
    /// Maximum span buffer size for tracing
251
    pub const MAX_SPAN_BUFFER_SIZE: usize = 100_000;
252
253
    /// Span export batch size
254
    pub const SPAN_EXPORT_BATCH_SIZE: usize = 1000;
255
}
256
257
/// ML model constants
258
pub mod ml {
259
    use super::Duration;
260
261
    /// Maximum GPU batch size
262
    pub const MAX_GPU_BATCH_SIZE: usize = 8192;
263
264
    /// Maximum CPU batch size
265
    pub const MAX_CPU_BATCH_SIZE: usize = 1024;
266
267
    /// Default model cache cleanup interval (1 hour)
268
    pub const MODEL_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(3600);
269
270
    /// Default model health check interval (30 seconds)
271
    pub const MODEL_HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(30);
272
273
    /// Model deployment stage timeout (5 minutes)
274
    pub const DEPLOYMENT_STAGE_TIMEOUT: Duration = Duration::from_secs(300);
275
276
    /// Model deployment total timeout (30 minutes)
277
    pub const DEPLOYMENT_TOTAL_TIMEOUT: Duration = Duration::from_secs(1800);
278
279
    /// Model validation scan timeout (10 minutes)
280
    pub const VALIDATION_SCAN_TIMEOUT: Duration = Duration::from_secs(600);
281
282
    /// Canary deployment duration (5 minutes)
283
    pub const CANARY_DURATION: Duration = Duration::from_secs(300);
284
285
    /// Model rollback timeout (1 minute)
286
    pub const ROLLBACK_TIMEOUT: Duration = Duration::from_secs(60);
287
288
    /// Drift detection check interval (5 minutes)
289
    pub const DRIFT_CHECK_INTERVAL: Duration = Duration::from_secs(300);
290
291
    /// Drift detection warning threshold
292
    pub const DRIFT_WARNING_THRESHOLD: f64 = 0.05;
293
294
    /// Maximum recommendation age for Kelly sizing (1 minute)
295
    pub const MAX_KELLY_RECOMMENDATION_AGE: Duration = Duration::from_secs(60);
296
297
    /// Kelly sizing cache TTL (5 minutes)
298
    pub const KELLY_CACHE_TTL: Duration = Duration::from_secs(300);
299
}
300
301
/// Safety system defaults
302
pub mod safety {
303
    use super::Duration;
304
305
    /// Safety check timeout for production (5ms)
306
    pub const PRODUCTION_SAFETY_CHECK_TIMEOUT: Duration = Duration::from_millis(5);
307
308
    /// Safety check timeout for development (50ms)
309
    pub const DEVELOPMENT_SAFETY_CHECK_TIMEOUT: Duration = Duration::from_millis(50);
310
311
    /// Auto-recovery delay for production (30 minutes)
312
    pub const PRODUCTION_AUTO_RECOVERY_DELAY: Duration = Duration::from_secs(1800);
313
314
    /// Auto-recovery delay for development (1 minute)
315
    pub const DEVELOPMENT_AUTO_RECOVERY_DELAY: Duration = Duration::from_secs(60);
316
317
    /// Loss check interval for production (5 seconds)
318
    pub const PRODUCTION_LOSS_CHECK_INTERVAL: Duration = Duration::from_secs(5);
319
320
    /// Loss check interval for development (30 seconds)
321
    pub const DEVELOPMENT_LOSS_CHECK_INTERVAL: Duration = Duration::from_secs(30);
322
323
    /// Position check interval for production (2 seconds)
324
    pub const PRODUCTION_POSITION_CHECK_INTERVAL: Duration = Duration::from_secs(2);
325
326
    /// Position check interval for development (15 seconds)
327
    pub const DEVELOPMENT_POSITION_CHECK_INTERVAL: Duration = Duration::from_secs(15);
328
329
    /// Memory check interval
330
    pub const MEMORY_CHECK_INTERVAL: Duration = Duration::from_secs(1);
331
332
    /// Circuit breaker trip cooldown (30 seconds)
333
    pub const CIRCUIT_BREAKER_COOLDOWN: Duration = Duration::from_secs(30);
334
}
335
336
/// Time conversion constants
337
pub mod time {
338
    /// Nanoseconds per microsecond
339
    pub const NANOS_PER_MICRO: u64 = 1_000;
340
341
    /// Nanoseconds per millisecond
342
    pub const NANOS_PER_MILLI: u64 = 1_000_000;
343
344
    /// Nanoseconds per second
345
    pub const NANOS_PER_SECOND: u64 = 1_000_000_000;
346
347
    /// Microseconds per second
348
    pub const MICROS_PER_SECOND: u64 = 1_000_000;
349
350
    /// Milliseconds per second
351
    pub const MILLIS_PER_SECOND: u64 = 1_000;
352
353
    /// Seconds per minute
354
    pub const SECONDS_PER_MINUTE: u64 = 60;
355
356
    /// Seconds per hour
357
    pub const SECONDS_PER_HOUR: u64 = 3600;
358
359
    /// Seconds per day
360
    pub const SECONDS_PER_DAY: u64 = 86400;
361
362
    /// Trading days per year
363
    pub const TRADING_DAYS_PER_YEAR: usize = 252;
364
}
365
366
/// Financial constants
367
pub mod financial {
368
    /// Basis points per unit
369
    pub const BASIS_POINTS_PER_UNIT: u32 = 10_000;
370
371
    /// Cents per dollar
372
    pub const CENTS_PER_DOLLAR: u32 = 100;
373
374
    /// Default profit target in basis points (1%)
375
    pub const DEFAULT_PROFIT_TARGET_BPS: u32 = 100;
376
377
    /// Default stop loss in basis points (0.5%)
378
    pub const DEFAULT_STOP_LOSS_BPS: u32 = 50;
379
380
    /// Minimum return threshold in basis points
381
    pub const MIN_RETURN_THRESHOLD_BPS: i32 = 5;
382
383
    /// Price scaling factor (6 decimal places)
384
    pub const PRICE_SCALE: i64 = 1_000_000;
385
386
    /// Quantity scaling factor (6 decimal places)
387
    pub const QUANTITY_SCALE: i64 = 1_000_000;
388
389
    /// Money scaling factor (6 decimal places)
390
    pub const MONEY_SCALE: i64 = 1_000_000;
391
392
    /// Unified scaling factor for all financial operations
393
    pub const UNIFIED_SCALE_FACTOR: i64 = 1_000_000;
394
395
    /// ML precision factor (8 decimal places)
396
    pub const PRECISION_FACTOR: i64 = 100_000_000;
397
398
    /// VPIN precision factor (4 decimal places)
399
    pub const VPIN_PRECISION_FACTOR: i64 = 10_000;
400
}
401
402
/// Validation limits
403
pub mod limits {
404
    /// Maximum symbol length
405
    pub const MAX_SYMBOL_LENGTH: usize = 12;
406
407
    /// Maximum account ID length
408
    pub const MAX_ACCOUNT_ID_LENGTH: usize = 32;
409
410
    /// Maximum description length
411
    pub const MAX_DESCRIPTION_LENGTH: usize = 256;
412
413
    /// Maximum metadata key length
414
    pub const MAX_METADATA_KEY_LENGTH: usize = 64;
415
416
    /// Maximum metadata value length
417
    pub const MAX_METADATA_VALUE_LENGTH: usize = 512;
418
419
    /// Maximum metadata entries
420
    pub const MAX_METADATA_ENTRIES: usize = 100;
421
422
    /// Maximum price value
423
    pub const MAX_PRICE: f64 = 1_000_000.0;
424
425
    /// Minimum price value
426
    pub const MIN_PRICE: f64 = 0.000_001;
427
428
    /// Maximum quantity value
429
    pub const MAX_QUANTITY: f64 = 1_000_000_000.0;
430
431
    /// Minimum quantity value
432
    pub const MIN_QUANTITY: f64 = 0.000_001;
433
434
    /// Maximum leverage
435
    pub const MAX_LEVERAGE: f64 = 1000.0;
436
437
    /// Minimum leverage
438
    pub const MIN_LEVERAGE: f64 = 0.1;
439
440
    /// Maximum allocation size (1GB)
441
    pub const MAX_ALLOCATION_SIZE: usize = 1024 * 1024 * 1024;
442
443
    /// Maximum duration in milliseconds (24 hours)
444
    pub const MAX_DURATION_MILLIS: u64 = 24 * 60 * 60 * 1000;
445
}
446
447
/// Hardware alignment constants
448
pub mod hardware {
449
    /// CPU cache line size
450
    pub const CACHE_LINE_SIZE: usize = 64;
451
452
    /// SIMD alignment for AVX2
453
    pub const SIMD_ALIGNMENT: usize = 32;
454
455
    /// Page size (4KB)
456
    pub const PAGE_SIZE: usize = 4096;
457
}
458
459
#[cfg(test)]
460
mod tests {
461
    use super::*;
462
463
    #[test]
464
    #[allow(clippy::assertions_on_constants)]
465
1
    fn test_breach_thresholds_ordered() {
466
1
        assert!(risk::BREACH_WARNING_PCT < risk::BREACH_SOFT_PCT);
467
1
        assert!(risk::BREACH_SOFT_PCT < risk::BREACH_HARD_PCT);
468
1
        assert!(risk::BREACH_HARD_PCT < risk::BREACH_CRITICAL_PCT);
469
1
    }
470
471
    #[test]
472
    #[allow(clippy::assertions_on_constants)]
473
1
    fn test_var_z_scores_ordered() {
474
1
        assert!(var::Z_SCORE_P90 < var::Z_SCORE_P95);
475
1
        assert!(var::Z_SCORE_P95 < var::Z_SCORE_P97_5);
476
1
        assert!(var::Z_SCORE_P97_5 < var::Z_SCORE_P99);
477
1
        assert!(var::Z_SCORE_P99 < var::Z_SCORE_P99_9);
478
1
    }
479
480
    #[test]
481
1
    fn test_time_conversions() {
482
1
        assert_eq!(time::NANOS_PER_MICRO * 1000, time::NANOS_PER_MILLI);
483
1
        assert_eq!(time::NANOS_PER_MILLI * 1000, time::NANOS_PER_SECOND);
484
1
        assert_eq!(time::MICROS_PER_SECOND * 1000, time::NANOS_PER_SECOND);
485
1
    }
486
487
    #[test]
488
1
    fn test_financial_scales_consistent() {
489
1
        assert_eq!(financial::PRICE_SCALE, financial::UNIFIED_SCALE_FACTOR);
490
1
        assert_eq!(financial::QUANTITY_SCALE, financial::UNIFIED_SCALE_FACTOR);
491
1
        assert_eq!(financial::MONEY_SCALE, financial::UNIFIED_SCALE_FACTOR);
492
1
    }
493
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html deleted file mode 100644 index 0c4209dc9..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/trading.rs
Line
Count
Source
1
//! Trading-specific types and enums
2
//!
3
//! This module contains the canonical definitions for all trading-related
4
//! types used across the Foxhunt HFT system. This is the single source
5
//! of truth for all trading types.
6
7
use chrono::{DateTime, Utc};
8
use rust_decimal::Decimal;
9
use serde::{Deserialize, Serialize};
10
use std::fmt;
11
12
// ELIMINATED: Re-exports removed to force explicit imports
13
// REMOVED: TimeInForce duplicate - use canonical definition from common::types
14
15
// Currency moved to canonical source: common::types::Currency
16
17
/// Tick type for market data
18
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19
#[cfg_attr(feature = "database", derive(sqlx::Type))]
20
#[cfg_attr(
21
    feature = "database",
22
    sqlx(type_name = "tick_type", rename_all = "snake_case")
23
)]
24
pub enum TickType {
25
    /// Trade tick
26
    Trade,
27
    /// Bid price update
28
    Bid,
29
    /// Ask price update
30
    Ask,
31
    /// Quote update (bid and ask)
32
    Quote,
33
}
34
35
impl fmt::Display for TickType {
36
    /// Format the tick type for display
37
4
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38
4
        match self {
39
1
            Self::Trade => write!(f, "TRADE"),
40
1
            Self::Bid => write!(f, "BID"),
41
1
            Self::Ask => write!(f, "ASK"),
42
1
            Self::Quote => write!(f, "QUOTE"),
43
        }
44
4
    }
45
}
46
47
/// Order book action type
48
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49
pub enum BookAction {
50
    /// Update price level
51
    Update,
52
    /// Delete price level
53
    Delete,
54
    /// Clear entire book
55
    Clear,
56
}
57
58
impl fmt::Display for BookAction {
59
    /// Format the book action for display
60
3
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61
3
        match self {
62
1
            Self::Update => write!(f, "UPDATE"),
63
1
            Self::Delete => write!(f, "DELETE"),
64
1
            Self::Clear => write!(f, "CLEAR"),
65
        }
66
3
    }
67
}
68
69
/// Market regime classification
70
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71
pub enum MarketRegime {
72
    /// Normal market conditions
73
    Normal,
74
    /// Crisis/stress market conditions
75
    Crisis,
76
    /// Trending market (strong directional movement)
77
    Trending,
78
    /// Sideways/ranging market (low volatility)
79
    Sideways,
80
    /// Bull market (sustained upward trend)
81
    Bull,
82
    /// Bear market (sustained downward trend)
83
    Bear,
84
}
85
86
impl fmt::Display for MarketRegime {
87
    /// Format the market regime for display
88
6
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89
6
        match self {
90
1
            Self::Normal => write!(f, "NORMAL"),
91
1
            Self::Crisis => write!(f, "CRISIS"),
92
1
            Self::Trending => write!(f, "TRENDING"),
93
1
            Self::Sideways => write!(f, "SIDEWAYS"),
94
1
            Self::Bull => write!(f, "BULL"),
95
1
            Self::Bear => write!(f, "BEAR"),
96
        }
97
6
    }
98
}
99
100
/// Core Quantity type using fixed-point arithmetic for precise calculations
101
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
102
pub struct Quantity {
103
    /// Internal representation using 6 decimal places (scale factor of `1_000_000`)
104
    value: u64,
105
}
106
107
impl Quantity {
108
    /// Scale factor for fixed-point arithmetic (6 decimal places)
109
    pub const SCALE: u64 = 1_000_000;
110
111
    /// Zero quantity
112
    pub const ZERO: Self = Self { value: 0 };
113
114
    /// Create a new quantity from a floating-point value
115
    ///
116
    /// # Errors
117
    /// Returns error if the operation fails
118
    ///
119
    /// # Errors
120
    /// Returns error if the value is negative or not finite
121
    #[allow(clippy::float_arithmetic)]
122
10
    pub fn new(value: f64) -> Result<Self, &'static str> {
123
10
        if value < 0.0_f64 {
124
1
            return Err("Quantity cannot be negative");
125
9
        }
126
9
        if !value.is_finite() {
127
2
            return Err("Quantity must be finite");
128
7
        }
129
130
        #[allow(clippy::as_conversions)]
131
7
        let scaled = (value * (Self::SCALE as f64)).round() as u64;
132
        
133
7
        Ok(Self { value: scaled })
134
10
    }
135
136
    /// Create from raw internal value
137
0
    pub const fn from_raw(value: u64) -> Self {
138
0
        Self { value }
139
0
    }
140
141
    /// Get raw internal value
142
0
    pub const fn raw(&self) -> u64 {
143
0
        self.value
144
0
    }
145
146
    /// Convert to floating-point value
147
    #[allow(clippy::float_arithmetic, clippy::as_conversions)]
148
6
    pub fn to_f64(&self) -> f64 {
149
6
        (self.value as f64) / (Self::SCALE as f64)
150
6
    }
151
152
    /// Convert to decimal
153
1
    pub fn to_decimal(&self) -> Decimal {
154
1
        Decimal::new(i64::try_from(self.value).unwrap_or(0), 6)
155
1
    }
156
157
    /// Add two quantities
158
1
    pub const fn add(&self, other: Self) -> Self {
159
1
        Self {
160
1
            value: self.value.saturating_add(other.value),
161
1
        }
162
1
    }
163
164
    /// Subtract two quantities
165
1
    pub const fn subtract(&self, other: Self) -> Self {
166
1
        Self {
167
1
            value: self.value.saturating_sub(other.value),
168
1
        }
169
1
    }
170
}
171
172
impl fmt::Display for Quantity {
173
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174
1
        write!(f, "{:.6}", self.to_f64())
175
1
    }
176
}
177
178
impl std::ops::Add for Quantity {
179
    type Output = Self;
180
181
1
    fn add(self, other: Self) -> Self::Output {
182
1
        Self {
183
1
            value: self.value.saturating_add(other.value),
184
1
        }
185
1
    }
186
}
187
188
impl std::ops::Sub for Quantity {
189
    type Output = Self;
190
191
1
    fn sub(self, other: Self) -> Self::Output {
192
1
        Self {
193
1
            value: self.value.saturating_sub(other.value),
194
1
        }
195
1
    }
196
}
197
198
/// Order event for tracking order lifecycle
199
#[derive(Debug, Clone, Serialize, Deserialize)]
200
pub struct OrderEvent {
201
    /// Unique order identifier
202
    pub order_id: String,
203
    /// Trading symbol
204
    pub symbol: String,
205
    /// Order type (Market, Limit, etc.)
206
    pub order_type: OrderType,
207
    /// Order side (Buy/Sell)
208
    pub side: OrderSide,
209
    /// Order quantity
210
    pub quantity: Quantity,
211
    /// Order price (None for market orders)
212
    pub price: Option<Decimal>,
213
    /// Event timestamp
214
    pub timestamp: DateTime<Utc>,
215
    /// Strategy identifier
216
    pub strategy_id: String,
217
    /// Type of order event
218
    pub event_type: OrderEventType,
219
    /// Previous quantity for modifications
220
    pub previous_quantity: Option<Quantity>,
221
    /// Previous price for modifications
222
    pub previous_price: Option<Decimal>,
223
    /// Reason for cancellation or modification
224
    pub reason: Option<String>,
225
}
226
227
/// Types of order events
228
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229
pub enum OrderEventType {
230
    /// Order was placed
231
    Placed,
232
    /// Order was modified
233
    Modified,
234
    /// Order was cancelled
235
    Cancelled,
236
    /// Order was rejected
237
    Rejected,
238
    /// Order expired
239
    Expired,
240
}
241
242
impl fmt::Display for OrderEventType {
243
5
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244
5
        match self {
245
1
            Self::Placed => write!(f, "PLACED"),
246
1
            Self::Modified => write!(f, "MODIFIED"),
247
1
            Self::Cancelled => write!(f, "CANCELLED"),
248
1
            Self::Rejected => write!(f, "REJECTED"),
249
1
            Self::Expired => write!(f, "EXPIRED"),
250
        }
251
5
    }
252
}
253
254
/// Order type enumeration
255
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256
pub enum OrderType {
257
    /// Market order - execute immediately at best available price
258
    Market,
259
    /// Limit order - execute only at specified price or better
260
    Limit,
261
    /// Stop order - becomes market order when stop price is reached
262
    Stop,
263
    /// Stop-limit order - becomes limit order when stop price is reached
264
    StopLimit,
265
}
266
267
impl fmt::Display for OrderType {
268
4
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269
4
        match self {
270
1
            Self::Market => write!(f, "MARKET"),
271
1
            Self::Limit => write!(f, "LIMIT"),
272
1
            Self::Stop => write!(f, "STOP"),
273
1
            Self::StopLimit => write!(f, "STOP_LIMIT"),
274
        }
275
4
    }
276
}
277
278
/// Order side enumeration
279
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
280
pub enum OrderSide {
281
    /// Buy order
282
    Buy,
283
    /// Sell order
284
    Sell,
285
}
286
287
impl fmt::Display for OrderSide {
288
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289
2
        match self {
290
1
            Self::Buy => write!(f, "BUY"),
291
1
            Self::Sell => write!(f, "SELL"),
292
        }
293
2
    }
294
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html deleted file mode 100644 index 669810e1b..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/traits.rs
Line
Count
Source
1
//! Common traits used across services
2
//!
3
//! This module provides shared traits that define common interfaces
4
//! for services in the Foxhunt HFT trading system.
5
6
use crate::error::CommonResult;
7
use crate::types::{ServiceStatus, Timestamp};
8
use async_trait::async_trait;
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
12
/// Trait for configurable components
13
#[async_trait]
14
pub trait Configurable {
15
    /// Configuration type for this component
16
    type Config: Clone + Send + Sync;
17
18
    /// Apply configuration changes
19
    async fn configure(&mut self, config: Self::Config) -> CommonResult<()>;
20
21
    /// Get current configuration
22
    fn get_config(&self) -> &Self::Config;
23
24
    /// Validate configuration before applying
25
    ///
26
    /// # Errors
27
    /// Returns error if the operation fails
28
    fn validate_config(config: &Self::Config) -> CommonResult<()>;
29
}
30
31
/// Trait for health check capabilities
32
#[async_trait]
33
pub trait HealthCheck {
34
    /// Perform a health check
35
    async fn health_check(&self) -> CommonResult<HealthStatus>;
36
37
    /// Get detailed health information
38
    async fn detailed_health(&self) -> CommonResult<DetailedHealth>;
39
}
40
41
/// Health status for components
42
#[derive(Debug, Clone, Serialize, Deserialize)]
43
pub struct HealthStatus {
44
    /// Overall health status
45
    pub status: ServiceStatus,
46
    /// Timestamp of the health check
47
    pub timestamp: Timestamp,
48
    /// Optional message
49
    pub message: Option<String>,
50
}
51
52
/// Detailed health information
53
#[derive(Debug, Clone, Serialize, Deserialize)]
54
pub struct DetailedHealth {
55
    /// Basic health status
56
    pub status: HealthStatus,
57
    /// Component-specific metrics
58
    pub metrics: HashMap<String, f64>,
59
    /// Sub-component health statuses
60
    pub components: HashMap<String, HealthStatus>,
61
}
62
63
/// Trait for metrics collection
64
pub trait Metrics {
65
    /// Metrics type for this component
66
    type Metrics: Clone + Send + Sync + Serialize;
67
68
    /// Get current metrics
69
    fn get_metrics(&self) -> Self::Metrics;
70
71
    /// Reset metrics counters
72
    fn reset_metrics(&mut self);
73
}
74
75
/// Trait for service lifecycle management
76
#[async_trait]
77
pub trait Service: Send + Sync {
78
    /// Start the service
79
    async fn start(&mut self) -> CommonResult<()>;
80
81
    /// Stop the service gracefully
82
    async fn stop(&mut self) -> CommonResult<()>;
83
84
    /// Get current service status
85
    fn status(&self) -> ServiceStatus;
86
87
    /// Get service name
88
    fn name(&self) -> &str;
89
90
    /// Get service version
91
    fn version(&self) -> &str;
92
}
93
94
/// Trait for components that can be reloaded
95
#[async_trait]
96
pub trait Reloadable {
97
    /// Reload the component (hot reload)
98
    async fn reload(&mut self) -> CommonResult<()>;
99
100
    /// Check if reload is supported
101
0
    fn supports_reload(&self) -> bool {
102
0
        true
103
0
    }
104
}
105
106
/// Trait for components with graceful shutdown
107
#[async_trait]
108
pub trait GracefulShutdown {
109
    /// Initiate graceful shutdown
110
    async fn shutdown(&mut self) -> CommonResult<()>;
111
112
    /// Force shutdown (emergency stop)
113
    async fn force_shutdown(&mut self) -> CommonResult<()>;
114
115
    /// Get shutdown timeout duration in seconds
116
0
    fn shutdown_timeout_seconds(&self) -> u64 {
117
0
        30 // Default 30 seconds
118
0
    }
119
}
120
121
/// Trait for components that support circuit breaking
122
pub trait CircuitBreaker {
123
    /// Check if circuit is open
124
    fn is_circuit_open(&self) -> bool;
125
126
    /// Get failure count
127
    fn failure_count(&self) -> u64;
128
129
    /// Reset circuit breaker
130
    fn reset_circuit(&mut self);
131
}
132
133
/// Trait for rate-limited operations
134
pub trait RateLimited {
135
    /// Check if operation is allowed under rate limits
136
    fn is_allowed(&self) -> bool;
137
138
    /// Get current rate limit status
139
    fn rate_limit_status(&self) -> RateLimitStatus;
140
}
141
142
/// Rate limit status information
143
#[derive(Debug, Clone, Serialize, Deserialize)]
144
pub struct RateLimitStatus {
145
    /// Current request count in the window
146
    pub current_count: u64,
147
    /// Maximum requests allowed in the window
148
    pub max_requests: u64,
149
    /// Time window in seconds
150
    pub window_seconds: u64,
151
    /// Seconds until window resets
152
    pub reset_in_seconds: u64,
153
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html deleted file mode 100644 index 999697362..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/types.rs
Line
Count
Source
1
//! Common data types used across services
2
//!
3
//! This module provides shared data types that are used throughout
4
//! the Foxhunt HFT trading system. This includes both infrastructure types
5
//! and core trading types migrated from foxhunt-common-types.
6
7
use crate::error::ErrorCategory;
8
use chrono::{DateTime, Utc};
9
// ELIMINATED: Re-exports removed to force explicit imports
10
// NO RE-EXPORTS: Import rust_decimal::Decimal directly in each crate that needs it
11
use rust_decimal::Decimal; // Internal use only - other crates must import directly
12
use serde::{Deserialize, Serialize};
13
use serde_json::Value;
14
use std::collections::HashMap;
15
use std::sync::{Arc, Mutex, RwLock};
16
17
use crate::error::{CommonError, ErrorCategory as CommonErrorCategory};
18
use std::convert::TryFrom;
19
use std::fmt;
20
use std::iter::Sum;
21
use std::num::ParseIntError;
22
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
23
use std::str::FromStr;
24
use uuid::Uuid;
25
26
// =============================================================================
27
// Type Aliases for Complex Types
28
// =============================================================================
29
30
/// Common error type for async operations
31
pub type AsyncResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
32
33
/// Thread-safe hash map for shared state
34
pub type SharedHashMap<K, V> = Arc<RwLock<HashMap<K, V>>>;
35
36
/// Thread-safe hash map with Mutex for shared state
37
pub type MutexHashMap<K, V> = Arc<Mutex<HashMap<K, V>>>;
38
39
/// Thread-safe container for any value
40
pub type SharedValue<T> = Arc<RwLock<T>>;
41
42
/// Thread-safe container with Mutex for any value
43
pub type MutexValue<T> = Arc<Mutex<T>>;
44
45
// Trading-specific type aliases
46
/// Map of positions by symbol
47
pub type PositionMap<T> = SharedHashMap<String, T>;
48
49
/// Map of orders by order ID
50
pub type OrderMap<T> = SharedHashMap<String, T>;
51
52
/// Map of accounts by account ID
53
pub type AccountMap<T> = SharedHashMap<String, T>;
54
55
/// Map of instruments by instrument ID
56
pub type InstrumentMap<T> = SharedHashMap<String, T>;
57
58
/// Map of market data by symbol
59
pub type MarketDataMap<T> = SharedHashMap<String, T>;
60
61
/// Cache entry with timestamp
62
pub type CacheEntry<T> = (T, DateTime<Utc>);
63
64
/// Cache map with timestamped entries
65
pub type CacheMap<K, V> = SharedHashMap<K, CacheEntry<V>>;
66
67
/// Risk factor loadings by instrument
68
pub type RiskFactorMap = SharedHashMap<String, HashMap<String, Decimal>>;
69
70
/// Performance metrics history
71
pub type PerformanceHistory<T> = SharedHashMap<String, std::collections::VecDeque<T>>;
72
73
/// Model registry for ML models
74
pub type ModelRegistry<T> = SharedHashMap<String, T>;
75
76
/// Generic configuration cache
77
pub type ConfigCache<K, V> = SharedHashMap<K, V>;
78
79
// =============================================================================
80
// Event Types - Moved from trading_engine to enforce pure client architecture
81
// =============================================================================
82
83
/// Order events for the complete order lifecycle
84
#[derive(Debug, Clone, Serialize, Deserialize)]
85
pub struct OrderEvent {
86
    /// Unique identifier for the order
87
    pub order_id: OrderId,
88
    /// Trading symbol for the order
89
    pub symbol: Symbol,
90
    /// Type of order (market, limit, stop, etc.)
91
    pub order_type: OrderType,
92
    /// Order side (buy or sell)
93
    pub side: OrderSide,
94
    /// Order quantity
95
    pub quantity: Quantity,
96
    /// Order price (None for market orders)
97
    pub price: Option<Price>,
98
    /// Timestamp when the event occurred
99
    pub timestamp: DateTime<Utc>,
100
    /// Strategy or client identifier
101
    pub strategy_id: String,
102
    /// Order event type (placed, modified, cancelled)
103
    pub event_type: OrderEventType,
104
    /// Previous quantity for modifications
105
    pub previous_quantity: Option<Quantity>,
106
    /// Previous price for modifications
107
    pub previous_price: Option<Price>,
108
    /// Reason for cancellation or modification
109
    pub reason: Option<String>,
110
}
111
112
/// Types of order events
113
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114
pub enum OrderEventType {
115
    /// Order was placed
116
    Placed,
117
    /// Order was modified
118
    Modified,
119
    /// Order was cancelled
120
    Cancelled,
121
    /// Order was rejected
122
    Rejected,
123
}
124
125
// =============================================================================
126
// Core Data Types
127
// =============================================================================
128
129
/// Unique identifier for services
130
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
131
pub struct ServiceId(pub String);
132
133
impl ServiceId {
134
    /// Create a new service ID
135
2
    pub fn new<S: Into<String>>(id: S) -> Self {
136
2
        Self(id.into())
137
2
    }
138
139
    /// Get the inner string value
140
    ///
141
    /// Get the execution ID as a string slice
142
    ///
143
    /// Get execution ID as string slice
144
2
    pub fn as_str(&self) -> &str {
145
2
        &self.0
146
2
    }
147
}
148
149
impl fmt::Display for ServiceId {
150
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151
1
        write!(f, "{}", self.0)
152
1
    }
153
}
154
155
impl From<&str> for ServiceId {
156
0
    fn from(s: &str) -> Self {
157
0
        Self(s.to_owned())
158
0
    }
159
}
160
161
impl From<String> for ServiceId {
162
1
    fn from(s: String) -> Self {
163
1
        Self(s)
164
1
    }
165
}
166
167
/// Service status enumeration
168
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169
pub enum ServiceStatus {
170
    /// Service is starting up
171
    Starting,
172
    /// Service is running normally
173
    Running,
174
    /// Service is degraded but functional
175
    Degraded,
176
    /// Service is stopping
177
    Stopping,
178
    /// Service is stopped
179
    Stopped,
180
    /// Service has encountered an error
181
    Error,
182
    /// Service is in maintenance mode
183
    Maintenance,
184
}
185
186
impl fmt::Display for ServiceStatus {
187
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188
0
        match self {
189
0
            Self::Starting => write!(f, "STARTING"),
190
0
            Self::Running => write!(f, "RUNNING"),
191
0
            Self::Degraded => write!(f, "DEGRADED"),
192
0
            Self::Stopping => write!(f, "STOPPING"),
193
0
            Self::Stopped => write!(f, "STOPPED"),
194
0
            Self::Error => write!(f, "ERROR"),
195
0
            Self::Maintenance => write!(f, "MAINTENANCE"),
196
        }
197
0
    }
198
}
199
200
impl ServiceStatus {
201
    /// Check if the service is healthy
202
4
    pub const fn is_healthy(&self) -> bool {
203
4
        
matches!2
(self, Self::Running | Self::Starting)
204
4
    }
205
206
    /// Check if the service is available for requests
207
4
    pub const fn is_available(&self) -> bool {
208
4
        
matches!2
(self, Self::Running | Self::Degraded)
209
4
    }
210
}
211
212
/// Configuration version for tracking changes
213
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214
pub struct ConfigVersion {
215
    /// Version number
216
    pub version: u64,
217
    /// Timestamp when version was created
218
    pub timestamp: DateTime<Utc>,
219
    /// Optional description of changes
220
    pub description: Option<String>,
221
}
222
223
impl ConfigVersion {
224
    /// Create a new config version
225
1
    pub fn new(version: u64) -> Self {
226
1
        Self {
227
1
            version,
228
1
            timestamp: Utc::now(),
229
1
            description: None,
230
1
        }
231
1
    }
232
233
    /// Create a new config version with description
234
1
    pub fn with_description<S: Into<String>>(version: u64, description: S) -> Self {
235
1
        Self {
236
1
            version,
237
1
            timestamp: Utc::now(),
238
1
            description: Some(description.into()),
239
1
        }
240
1
    }
241
}
242
243
// TECHNICAL DEBT ELIMINATED - Use DateTime<Utc> directly instead of Timestamp alias
244
245
/// Timestamp type alias for consistency across the system
246
pub type Timestamp = DateTime<Utc>;
247
248
/// Request ID for tracing and correlation
249
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
250
pub struct RequestId(pub Uuid);
251
252
impl Default for RequestId {
253
    /// Create a default request ID with a new UUID
254
0
    fn default() -> Self {
255
0
        Self::new()
256
0
    }
257
}
258
259
impl RequestId {
260
    /// Generate a new random request ID
261
2
    pub fn new() -> Self {
262
2
        Self(Uuid::new_v4())
263
2
    }
264
265
    /// Create from UUID
266
0
    pub const fn from_uuid(uuid: Uuid) -> Self {
267
0
        Self(uuid)
268
0
    }
269
270
    /// Get the inner UUID
271
0
    pub const fn as_uuid(&self) -> Uuid {
272
0
        self.0
273
0
    }
274
}
275
276
// Default implementation is now in the derive macro above
277
278
// =============================================================================
279
// MARKET DATA EVENT TYPES (Consolidated from data and trading_engine crates)
280
// =============================================================================
281
282
/// Market data event types - CANONICAL DEFINITION
283
#[derive(Debug, Clone, Serialize, Deserialize)]
284
pub enum MarketDataEvent {
285
    /// Quote update (bid/ask)
286
    Quote(QuoteEvent),
287
    /// Trade execution
288
    Trade(TradeEvent),
289
    /// Aggregate trade data
290
    Aggregate(Aggregate),
291
    /// Bar/candle data
292
    Bar(BarEvent),
293
    /// Level 2 market data update
294
    Level2(Level2Update),
295
    /// Market status update
296
    Status(MarketStatus),
297
    /// Connection status updates
298
    ConnectionStatus(ConnectionEvent),
299
    /// Error events with details
300
    Error(ErrorEvent),
301
    /// Order book update
302
    OrderBook(OrderBookEvent),
303
    /// Level 2 order book snapshot
304
    OrderBookL2Snapshot(OrderBookSnapshot),
305
    /// Level 2 order book incremental update
306
    OrderBookL2Update(OrderBookUpdate),
307
}
308
309
/// Quote event structure - CANONICAL DEFINITION
310
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
311
pub struct QuoteEvent {
312
    /// Symbol
313
    pub symbol: String,
314
    /// Bid price
315
    pub bid: Option<Decimal>,
316
    /// Ask price
317
    pub ask: Option<Decimal>,
318
    /// Bid size
319
    pub bid_size: Option<Decimal>,
320
    /// Ask size
321
    pub ask_size: Option<Decimal>,
322
    /// Exchange
323
    pub exchange: Option<String>,
324
    /// Bid exchange
325
    pub bid_exchange: Option<String>,
326
    /// Ask exchange
327
    pub ask_exchange: Option<String>,
328
    /// Quote conditions
329
    pub conditions: Vec<String>,
330
    /// Timestamp
331
    pub timestamp: DateTime<Utc>,
332
    /// Sequence number
333
    pub sequence: u64,
334
}
335
336
impl QuoteEvent {
337
    /// Create a new quote event
338
    #[must_use]
339
6
    pub const fn new(symbol: String, timestamp: DateTime<Utc>) -> Self {
340
6
        Self {
341
6
            symbol,
342
6
            bid: None,
343
6
            ask: None,
344
6
            bid_size: None,
345
6
            ask_size: None,
346
6
            exchange: None,
347
6
            bid_exchange: None,
348
6
            ask_exchange: None,
349
6
            conditions: Vec::new(),
350
6
            timestamp,
351
6
            sequence: 0,
352
6
        }
353
6
    }
354
355
    /// Set bid price and size
356
4
    pub const fn with_bid(mut self, price: Decimal, size: Decimal) -> Self {
357
4
        self.bid = Some(price);
358
4
        self.bid_size = Some(size);
359
4
        self
360
4
    }
361
362
    /// Set ask price and size
363
4
    pub const fn with_ask(mut self, price: Decimal, size: Decimal) -> Self {
364
4
        self.ask = Some(price);
365
4
        self.ask_size = Some(size);
366
4
        self
367
4
    }
368
369
    /// Set exchange
370
1
    pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
371
1
        self.exchange = Some(exchange.into());
372
1
        self
373
1
    }
374
375
    /// Set bid exchange
376
0
    pub fn with_bid_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
377
0
        self.bid_exchange = Some(exchange.into());
378
0
        self
379
0
    }
380
381
    /// Set ask exchange
382
0
    pub fn with_ask_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
383
0
        self.ask_exchange = Some(exchange.into());
384
0
        self
385
0
    }
386
387
    /// Add quote condition
388
0
    pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
389
0
        self.conditions.push(condition.into());
390
0
        self
391
0
    }
392
393
    /// Set sequence number
394
1
    pub const fn with_sequence(mut self, sequence: u64) -> Self {
395
1
        self.sequence = sequence;
396
1
        self
397
1
    }
398
399
    /// Get mid price
400
1
    pub fn mid_price(&self) -> Option<Decimal> {
401
1
        match (self.bid, self.ask) {
402
1
            (Some(bid), Some(ask)) => {
403
1
                let sum = bid.checked_add(ask)
?0
;
404
1
                let two = Decimal::from(2);
405
1
                sum.checked_div(two)
406
            }
407
0
            _ => None,
408
        }
409
1
    }
410
411
    /// Get spread
412
1
    pub fn spread(&self) -> Option<Decimal> {
413
1
        match (self.bid, self.ask) {
414
1
            (Some(bid), Some(ask)) => ask.checked_sub(bid),
415
0
            _ => None,
416
        }
417
1
    }
418
}
419
420
/// Trade event structure - CANONICAL DEFINITION
421
#[derive(Debug, Clone, Serialize, Deserialize)]
422
pub struct TradeEvent {
423
    /// Symbol
424
    pub symbol: String,
425
    /// Trade price
426
    pub price: Decimal,
427
    /// Trade size
428
    pub size: Decimal,
429
    /// Trade ID
430
    pub trade_id: Option<String>,
431
    /// Exchange
432
    pub exchange: Option<String>,
433
    /// Trade conditions
434
    pub conditions: Vec<String>,
435
    /// Timestamp
436
    pub timestamp: DateTime<Utc>,
437
    /// Sequence number
438
    pub sequence: u64,
439
}
440
441
impl TradeEvent {
442
    /// Create a new trade event
443
    #[must_use]
444
4
    pub const fn new(symbol: String, price: Decimal, size: Decimal, timestamp: DateTime<Utc>) -> Self {
445
4
        Self {
446
4
            symbol,
447
4
            price,
448
4
            size,
449
4
            trade_id: None,
450
4
            exchange: None,
451
4
            conditions: Vec::new(),
452
4
            timestamp,
453
4
            sequence: 0,
454
4
        }
455
4
    }
456
457
    /// Set trade ID
458
0
    pub fn with_trade_id<S: Into<String>>(mut self, trade_id: S) -> Self {
459
0
        self.trade_id = Some(trade_id.into());
460
0
        self
461
0
    }
462
463
    /// Set exchange
464
0
    pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
465
0
        self.exchange = Some(exchange.into());
466
0
        self
467
0
    }
468
469
    /// Add trade condition
470
0
    pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
471
0
        self.conditions.push(condition.into());
472
0
        self
473
0
    }
474
475
    /// Set sequence number
476
0
    pub const fn with_sequence(mut self, sequence: u64) -> Self {
477
0
        self.sequence = sequence;
478
0
        self
479
0
    }
480
481
    /// Get notional value
482
1
    pub fn notional_value(&self) -> Decimal {
483
1
        self.price.checked_mul(self.size).unwrap_or(Decimal::ZERO)
484
1
    }
485
}
486
487
/// Aggregate trade data
488
#[derive(Debug, Clone, Serialize, Deserialize)]
489
pub struct Aggregate {
490
    /// Symbol
491
    pub symbol: String,
492
    /// Open price
493
    pub open: Decimal,
494
    /// High price
495
    pub high: Decimal,
496
    /// Low price
497
    pub low: Decimal,
498
    /// Close price
499
    pub close: Decimal,
500
    /// Volume
501
    pub volume: Decimal,
502
    /// Volume weighted average price
503
    pub vwap: Option<Decimal>,
504
    /// Start timestamp
505
    pub start_timestamp: DateTime<Utc>,
506
    /// End timestamp
507
    pub end_timestamp: DateTime<Utc>,
508
}
509
510
/// Bar/candle event structure
511
#[derive(Debug, Clone, Serialize, Deserialize)]
512
pub struct BarEvent {
513
    /// Symbol
514
    pub symbol: String,
515
    /// Open price
516
    pub open: Decimal,
517
    /// High price
518
    pub high: Decimal,
519
    /// Low price
520
    pub low: Decimal,
521
    /// Close price
522
    pub close: Decimal,
523
    /// Volume
524
    pub volume: Decimal,
525
    /// Volume weighted average price
526
    pub vwap: Option<Decimal>,
527
    /// Start timestamp
528
    pub start_timestamp: DateTime<Utc>,
529
    /// End timestamp
530
    pub end_timestamp: DateTime<Utc>,
531
    /// Timeframe (e.g., "1m", "5m", "1h")
532
    pub timeframe: String,
533
}
534
535
/// Level 2 market data update
536
#[derive(Debug, Clone, Serialize, Deserialize)]
537
pub struct Level2Update {
538
    /// Symbol
539
    pub symbol: String,
540
    /// Bid levels
541
    pub bids: Vec<PriceLevel>,
542
    /// Ask levels
543
    pub asks: Vec<PriceLevel>,
544
    /// Timestamp
545
    pub timestamp: DateTime<Utc>,
546
}
547
548
/// Price level for order book
549
#[derive(Debug, Clone, Serialize, Deserialize)]
550
pub struct PriceLevel {
551
    /// Price
552
    pub price: Decimal,
553
    /// Size at this price level
554
    pub size: Decimal,
555
}
556
557
/// Order book snapshot from providers
558
#[derive(Debug, Clone, Serialize, Deserialize)]
559
pub struct OrderBookSnapshot {
560
    /// Symbol
561
    pub symbol: String,
562
    /// Bid levels (price, size) sorted by price descending
563
    pub bids: Vec<PriceLevel>,
564
    /// Ask levels (price, size) sorted by price ascending
565
    pub asks: Vec<PriceLevel>,
566
    /// Exchange
567
    pub exchange: String,
568
    /// Timestamp of snapshot
569
    pub timestamp: DateTime<Utc>,
570
    /// Sequence number
571
    pub sequence: u64,
572
}
573
574
/// Incremental order book update from providers
575
#[derive(Debug, Clone, Serialize, Deserialize)]
576
pub struct OrderBookUpdate {
577
    /// Symbol
578
    pub symbol: String,
579
    /// Changes to bid levels
580
    pub bid_changes: Vec<PriceLevelChange>,
581
    /// Changes to ask levels
582
    pub ask_changes: Vec<PriceLevelChange>,
583
    /// Exchange
584
    pub exchange: String,
585
    /// Timestamp of update
586
    pub timestamp: DateTime<Utc>,
587
    /// Sequence number
588
    pub sequence: u64,
589
}
590
591
/// Change to a price level
592
#[derive(Debug, Clone, Serialize, Deserialize)]
593
pub struct PriceLevelChange {
594
    /// Price level being modified
595
    pub price: Decimal,
596
    /// New size (0 = remove level)
597
    pub size: Decimal,
598
    /// Type of change
599
    pub change_type: PriceLevelChangeType,
600
    /// Side (bid or ask)
601
    pub side: OrderBookSide,
602
}
603
604
/// Type of price level change
605
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
606
pub enum PriceLevelChangeType {
607
    /// Add new price level
608
    Add,
609
    /// Update existing price level
610
    Update,
611
    /// Remove price level
612
    Delete,
613
}
614
615
/// Order book side
616
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
617
pub enum OrderBookSide {
618
    /// Bid side
619
    Bid,
620
    /// Ask side
621
    Ask,
622
}
623
624
/// Market status information
625
#[derive(Debug, Clone, Serialize, Deserialize)]
626
pub struct MarketStatus {
627
    /// Market
628
    pub market: String,
629
    /// Status (open, closed, `early_hours`, etc.)
630
    pub status: String,
631
    /// Timestamp
632
    pub timestamp: DateTime<Utc>,
633
}
634
635
/// Connection event for status updates
636
#[derive(Debug, Clone, Serialize, Deserialize)]
637
pub struct ConnectionEvent {
638
    /// Provider name
639
    pub provider: String,
640
    /// Connection status
641
    pub status: ConnectionStatus,
642
    /// Optional message
643
    pub message: Option<String>,
644
    /// Timestamp
645
    pub timestamp: DateTime<Utc>,
646
}
647
648
/// Connection status enumeration
649
///
650
/// Connection status for data providers and brokers
651
#[derive(Debug, Clone, Serialize, Deserialize)]
652
#[cfg_attr(feature = "database", derive(sqlx::Type))]
653
#[cfg_attr(
654
    feature = "database",
655
    sqlx(type_name = "connection_status", rename_all = "snake_case")
656
)]
657
pub enum ConnectionStatus {
658
    /// Successfully connected and operational
659
    Connected,
660
    /// Disconnected from the service
661
    Disconnected,
662
    /// Currently attempting to reconnect
663
    Reconnecting,
664
}
665
666
/// Error event structure
667
#[derive(Debug, Clone, Serialize, Deserialize)]
668
pub struct ErrorEvent {
669
    /// Provider name
670
    pub provider: String,
671
    /// Error message
672
    pub message: String,
673
    /// Error category
674
    pub category: ErrorCategory,
675
    /// Timestamp
676
    pub timestamp: DateTime<Utc>,
677
}
678
679
// ErrorCategory is imported from crate::error as CommonErrorCategory
680
681
/// Order book event
682
#[derive(Debug, Clone, Serialize, Deserialize)]
683
pub struct OrderBookEvent {
684
    /// Symbol
685
    pub symbol: String,
686
    /// Timestamp
687
    pub timestamp: DateTime<Utc>,
688
    /// Bid levels
689
    pub bids: Vec<(Price, Quantity)>,
690
    /// Ask levels
691
    pub asks: Vec<(Price, Quantity)>,
692
}
693
694
/// Data types for subscription
695
#[derive(Debug, Clone, Serialize, Deserialize)]
696
pub enum DataType {
697
    /// Real-time quotes
698
    Quotes,
699
    /// Real-time trades
700
    Trades,
701
    /// Aggregate/minute bars
702
    Aggregates,
703
    /// Level 2 order book
704
    Level2,
705
    /// Market status
706
    Status,
707
    /// Historical bars/aggregates
708
    Bars,
709
    /// Order book data
710
    OrderBook,
711
    /// Volume data
712
    Volume,
713
}
714
715
/// Market data subscription request
716
#[derive(Debug, Clone, Serialize, Deserialize)]
717
pub struct Subscription {
718
    /// Symbols to subscribe to
719
    pub symbols: Vec<String>,
720
    /// Data types to subscribe to
721
    pub data_types: Vec<DataType>,
722
    /// Exchange filter (optional)
723
    pub exchanges: Vec<String>,
724
}
725
726
impl MarketDataEvent {
727
    /// Get the symbol for any market data event
728
1
    pub fn symbol(&self) -> &str {
729
1
        match self {
730
1
            MarketDataEvent::Quote(q) => &q.symbol,
731
0
            MarketDataEvent::Trade(t) => &t.symbol,
732
0
            MarketDataEvent::Aggregate(a) => &a.symbol,
733
0
            MarketDataEvent::Bar(b) => &b.symbol,
734
0
            MarketDataEvent::Level2(l) => &l.symbol,
735
0
            MarketDataEvent::Status(s) => &s.market,
736
0
            MarketDataEvent::ConnectionStatus(_) => "",
737
0
            MarketDataEvent::Error(_) => "",
738
0
            MarketDataEvent::OrderBook(o) => &o.symbol,
739
0
            MarketDataEvent::OrderBookL2Snapshot(s) => &s.symbol,
740
0
            MarketDataEvent::OrderBookL2Update(u) => &u.symbol,
741
        }
742
1
    }
743
744
    /// Get the timestamp for any market data event
745
1
    pub const fn timestamp(&self) -> Option<DateTime<Utc>> {
746
1
        match self {
747
0
            MarketDataEvent::Quote(q) => Some(q.timestamp),
748
1
            MarketDataEvent::Trade(t) => Some(t.timestamp),
749
0
            MarketDataEvent::Aggregate(a) => Some(a.end_timestamp),
750
0
            MarketDataEvent::Bar(b) => Some(b.end_timestamp),
751
0
            MarketDataEvent::Level2(l) => Some(l.timestamp),
752
0
            MarketDataEvent::Status(s) => Some(s.timestamp),
753
0
            MarketDataEvent::ConnectionStatus(c) => Some(c.timestamp),
754
0
            MarketDataEvent::Error(e) => Some(e.timestamp),
755
0
            MarketDataEvent::OrderBook(o) => Some(o.timestamp),
756
0
            MarketDataEvent::OrderBookL2Snapshot(s) => Some(s.timestamp),
757
0
            MarketDataEvent::OrderBookL2Update(u) => Some(u.timestamp),
758
        }
759
1
    }
760
}
761
impl fmt::Display for RequestId {
762
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
763
0
        write!(f, "{}", self.0)
764
0
    }
765
}
766
767
/// Connection information for services
768
#[derive(Debug, Clone, Serialize, Deserialize)]
769
pub struct ConnectionInfo {
770
    /// Host address
771
    pub host: String,
772
    /// Port number
773
    pub port: u16,
774
    /// Whether TLS is enabled
775
    pub tls: bool,
776
    /// Connection timeout in milliseconds
777
    pub timeout_ms: u64,
778
}
779
780
impl ConnectionInfo {
781
    /// Create new connection info
782
1
    pub fn new<S: Into<String>>(host: S, port: u16) -> Self {
783
1
        Self {
784
1
            host: host.into(),
785
1
            port,
786
1
            tls: false,
787
1
            timeout_ms: 5000,
788
1
        }
789
1
    }
790
791
    /// Enable TLS
792
1
    pub const fn with_tls(mut self) -> Self {
793
1
        self.tls = true;
794
1
        self
795
1
    }
796
797
    /// Set timeout
798
0
    pub const fn with_timeout(mut self, timeout_ms: u64) -> Self {
799
0
        self.timeout_ms = timeout_ms;
800
0
        self
801
0
    }
802
803
    /// Get connection URL
804
2
    pub fn url(&self) -> String {
805
2
        let scheme = if self.tls { 
"https"1
} else {
"http"1
};
806
2
        format!("{}://{}:{}", scheme, self.host, self.port)
807
2
    }
808
}
809
810
/// Resource limits for services
811
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
812
pub struct ResourceLimits {
813
    /// Maximum memory usage in bytes
814
    pub max_memory_bytes: Option<u64>,
815
    /// Maximum CPU usage as percentage (0-100)
816
    pub max_cpu_percent: Option<f64>,
817
    /// Maximum number of open file descriptors
818
    pub max_file_descriptors: Option<u32>,
819
    /// Maximum number of network connections
820
    pub max_connections: Option<u32>,
821
}
822
823
// =============================================================================
824
// TRADING TYPES (Migrated from foxhunt-common-types)
825
// =============================================================================
826
827
/// Common error types for trading operations
828
///
829
/// This error type implements Send + Sync for use in async contexts
830
#[derive(thiserror::Error, Debug)]
831
pub enum CommonTypeError {
832
    /// Invalid price value
833
    #[error("Invalid price: {value} - {reason}")]
834
    InvalidPrice {
835
        /// The invalid price value as string
836
        value: String,
837
        /// Reason why the price is invalid
838
        reason: String,
839
    },
840
841
    /// Invalid quantity value
842
    #[error("Invalid quantity: {value} - {reason}")]
843
    InvalidQuantity {
844
        /// The invalid quantity value as string
845
        value: String,
846
        /// Reason why the quantity is invalid
847
        reason: String,
848
    },
849
850
    /// Invalid identifier
851
    #[error("Invalid {field}: {reason}")]
852
    InvalidIdentifier {
853
        /// The field name that contains the invalid identifier
854
        field: String,
855
        /// Reason why the identifier is invalid
856
        reason: String,
857
    },
858
859
    /// Validation error
860
    #[error("Validation error for {field}: {reason}")]
861
    ValidationError {
862
        /// The field name that failed validation
863
        field: String,
864
        /// Reason why the validation failed
865
        reason: String,
866
    },
867
868
    /// Conversion error
869
    #[error("Conversion error: {message}")]
870
    ConversionError {
871
        /// Detailed error message describing the conversion failure
872
        message: String,
873
    },
874
875
    /// I/O error
876
    #[error("I/O error: {0}")]
877
    IoError(#[from] std::io::Error),
878
879
    /// JSON serialization/deserialization error
880
    #[error("JSON error: {0}")]
881
    JsonError(#[from] serde_json::Error),
882
883
    /// Float parsing error
884
    #[error("Float parsing error: {0}")]
885
    ParseFloatError(#[from] std::num::ParseFloatError),
886
887
    /// Integer parsing error
888
    #[error("Integer parsing error: {0}")]
889
    ParseIntError(#[from] std::num::ParseIntError),
890
}
891
892
// Manual trait implementations for CommonTypeError
893
// (Cannot derive Clone, PartialEq, Eq, Serialize due to std::io::Error and serde_json::Error)
894
895
impl Clone for CommonTypeError {
896
    /// Clone the error, converting IO and JSON errors to conversion errors
897
2
    fn clone(&self) -> Self {
898
2
        match self {
899
1
            Self::InvalidPrice { value, reason } => Self::InvalidPrice {
900
1
                value: value.clone(),
901
1
                reason: reason.clone(),
902
1
            },
903
0
            Self::InvalidQuantity { value, reason } => Self::InvalidQuantity {
904
0
                value: value.clone(),
905
0
                reason: reason.clone(),
906
0
            },
907
0
            Self::InvalidIdentifier { field, reason } => Self::InvalidIdentifier {
908
0
                field: field.clone(),
909
0
                reason: reason.clone(),
910
0
            },
911
0
            Self::ValidationError { field, reason } => Self::ValidationError {
912
0
                field: field.clone(),
913
0
                reason: reason.clone(),
914
0
            },
915
0
            Self::ConversionError { message } => Self::ConversionError {
916
0
                message: message.clone(),
917
0
            },
918
            // Cannot clone std::io::Error or serde_json::Error, so create new instances
919
1
            Self::IoError(e) => Self::ConversionError {
920
1
                message: format!("I/O error: {}", e),
921
1
            },
922
0
            Self::JsonError(e) => Self::ConversionError {
923
0
                message: format!("JSON error: {}", e),
924
0
            },
925
0
            Self::ParseFloatError(e) => Self::ParseFloatError(e.clone()),
926
0
            Self::ParseIntError(e) => Self::ParseIntError(e.clone()),
927
        }
928
2
    }
929
}
930
impl PartialEq for CommonTypeError {
931
    /// Compare two errors for equality
932
3
    fn eq(&self, other: &Self) -> bool {
933
3
        match (self, other) {
934
            (
935
                Self::InvalidPrice {
936
3
                    value: v1,
937
3
                    reason: r1,
938
                },
939
                Self::InvalidPrice {
940
3
                    value: v2,
941
3
                    reason: r2,
942
                },
943
3
            ) => v1 == v2 && 
r1 == r22
,
944
            (
945
                Self::InvalidQuantity {
946
0
                    value: v1,
947
0
                    reason: r1,
948
                },
949
                Self::InvalidQuantity {
950
0
                    value: v2,
951
0
                    reason: r2,
952
                },
953
0
            ) => v1 == v2 && r1 == r2,
954
            (
955
                Self::InvalidIdentifier {
956
0
                    field: f1,
957
0
                    reason: r1,
958
                },
959
                Self::InvalidIdentifier {
960
0
                    field: f2,
961
0
                    reason: r2,
962
                },
963
0
            ) => f1 == f2 && r1 == r2,
964
            (
965
                Self::ValidationError {
966
0
                    field: f1,
967
0
                    reason: r1,
968
                },
969
                Self::ValidationError {
970
0
                    field: f2,
971
0
                    reason: r2,
972
                },
973
0
            ) => f1 == f2 && r1 == r2,
974
0
            (Self::ConversionError { message: m1 }, Self::ConversionError { message: m2 }) => {
975
0
                m1 == m2
976
            },
977
0
            (Self::ParseFloatError(e1), Self::ParseFloatError(e2)) => e1 == e2,
978
0
            (Self::ParseIntError(e1), Self::ParseIntError(e2)) => e1 == e2,
979
            // std::io::Error and serde_json::Error don't implement PartialEq, so they're never equal
980
0
            (Self::IoError(_), Self::IoError(_)) => false,
981
0
            (Self::JsonError(_), Self::JsonError(_)) => false,
982
0
            _ => false,
983
        }
984
3
    }
985
}
986
987
impl Eq for CommonTypeError {}
988
989
// Note: Display is automatically implemented by thiserror::Error derive
990
// based on the #[error("...")] attributes on each variant
991
impl Serialize for CommonTypeError {
992
1
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
993
1
    where
994
1
        S: serde::Serializer,
995
    {
996
        use serde::ser::SerializeStruct;
997
1
        match self {
998
0
            Self::InvalidPrice { value, reason } => {
999
0
                let mut state = serializer.serialize_struct("InvalidPrice", 2)?;
1000
0
                state.serialize_field("value", value)?;
1001
0
                state.serialize_field("reason", reason)?;
1002
0
                state.end()
1003
            },
1004
0
            Self::InvalidQuantity { value, reason } => {
1005
0
                let mut state = serializer.serialize_struct("InvalidQuantity", 2)?;
1006
0
                state.serialize_field("value", value)?;
1007
0
                state.serialize_field("reason", reason)?;
1008
0
                state.end()
1009
            },
1010
0
            Self::InvalidIdentifier { field, reason } => {
1011
0
                let mut state = serializer.serialize_struct("InvalidIdentifier", 2)?;
1012
0
                state.serialize_field("field", field)?;
1013
0
                state.serialize_field("reason", reason)?;
1014
0
                state.end()
1015
            },
1016
1
            Self::ValidationError { field, reason } => {
1017
1
                let mut state = serializer.serialize_struct("ValidationError", 2)
?0
;
1018
1
                state.serialize_field("field", field)
?0
;
1019
1
                state.serialize_field("reason", reason)
?0
;
1020
1
                state.end()
1021
            },
1022
0
            Self::ConversionError { message } => {
1023
0
                let mut state = serializer.serialize_struct("ConversionError", 1)?;
1024
0
                state.serialize_field("message", message)?;
1025
0
                state.end()
1026
            },
1027
0
            Self::IoError(e) => {
1028
0
                let mut state = serializer.serialize_struct("IoError", 1)?;
1029
0
                state.serialize_field("message", &format!("I/O error: {}", e))?;
1030
0
                state.end()
1031
            },
1032
0
            Self::JsonError(e) => {
1033
0
                let mut state = serializer.serialize_struct("JsonError", 1)?;
1034
0
                state.serialize_field("message", &format!("JSON error: {}", e))?;
1035
0
                state.end()
1036
            },
1037
0
            Self::ParseFloatError(e) => {
1038
0
                let mut state = serializer.serialize_struct("ParseFloatError", 1)?;
1039
0
                state.serialize_field("message", &format!("Float parsing error: {}", e))?;
1040
0
                state.end()
1041
            },
1042
0
            Self::ParseIntError(e) => {
1043
0
                let mut state = serializer.serialize_struct("ParseIntError", 1)?;
1044
0
                state.serialize_field("message", &format!("Integer parsing error: {}", e))?;
1045
0
                state.end()
1046
            },
1047
        }
1048
1
    }
1049
}
1050
1051
impl<'de> Deserialize<'de> for CommonTypeError {
1052
1
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1053
1
    where
1054
1
        D: serde::Deserializer<'de>,
1055
    {
1056
        // For deserialization, we'll convert everything to ConversionError since
1057
        // we can't reconstruct std::io::Error or serde_json::Error from serialized form
1058
        use serde::de::{MapAccess, Visitor};
1059
        use std::fmt;
1060
1061
        struct CommonTypeErrorVisitor;
1062
1063
        impl<'de> Visitor<'de> for CommonTypeErrorVisitor {
1064
            type Value = CommonTypeError;
1065
1066
0
            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1067
0
                formatter.write_str("a CommonTypeError")
1068
0
            }
1069
1070
1
            fn visit_map<V>(self, mut map: V) -> Result<CommonTypeError, V::Error>
1071
1
            where
1072
1
                V: MapAccess<'de>,
1073
            {
1074
                // For simplicity, deserialize everything as ConversionError
1075
1
                let mut message = String::new();
1076
3
                while let Some(
key2
) = map.next_key::<String>()
?0
{
1077
2
                    let value: serde_json::Value = map.next_value()
?0
;
1078
2
                    if key == "message" {
1079
0
                        if let Some(msg) = value.as_str() {
1080
0
                            message = msg.to_owned();
1081
0
                        }
1082
2
                    } else {
1083
2
                        message = format!("Deserialized error: {}: {}", key, value);
1084
2
                    }
1085
                }
1086
1
                if message.is_empty() {
1087
0
                    message = "Unknown deserialized error".to_owned();
1088
1
                }
1089
1
                Ok(CommonTypeError::ConversionError { message })
1090
1
            }
1091
        }
1092
1093
1
        deserializer.deserialize_struct(
1094
            "CommonTypeError",
1095
1
            &["value", "reason", "field", "message"],
1096
1
            CommonTypeErrorVisitor,
1097
        )
1098
1
    }
1099
}
1100
1101
// =============================================================================
1102
// ORDER TYPES (Moved from trading_engine)
1103
// =============================================================================
1104
1105
/// Order type specifying execution behavior - CANONICAL DEFINITION
1106
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1107
#[non_exhaustive]
1108
pub enum OrderType {
1109
    /// Market order - executes immediately at current market price
1110
    Market,
1111
    /// Limit order - executes only at specified price or better
1112
    Limit,
1113
    /// Stop order - becomes market order when stop price is reached
1114
    Stop,
1115
    /// Stop-limit order - becomes limit order when stop price is reached
1116
    StopLimit,
1117
    /// Iceberg order - large order split into smaller visible portions
1118
    Iceberg,
1119
    /// Trailing stop order - stop price adjusts with favorable price movement
1120
    TrailingStop,
1121
    /// Hidden order - not displayed in order book
1122
    Hidden,
1123
}
1124
1125
impl fmt::Display for OrderType {
1126
19
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1127
19
        match self {
1128
4
            Self::Market => write!(f, "MARKET"),
1129
4
            Self::Limit => write!(f, "LIMIT"),
1130
4
            Self::Stop => write!(f, "STOP"),
1131
4
            Self::StopLimit => write!(f, "STOP_LIMIT"),
1132
1
            Self::Iceberg => write!(f, "ICEBERG"),
1133
1
            Self::TrailingStop => write!(f, "TRAILING_STOP"),
1134
1
            Self::Hidden => write!(f, "HIDDEN"),
1135
        }
1136
19
    }
1137
}
1138
1139
impl Default for OrderType {
1140
    /// Returns the default order type (Market)
1141
2
    fn default() -> Self {
1142
2
        Self::Market
1143
2
    }
1144
}
1145
1146
impl TryFrom<i32> for OrderType {
1147
    type Error = String;
1148
1149
9
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1150
9
        match value {
1151
2
            0 => Ok(OrderType::Market),
1152
2
            1 => Ok(OrderType::Limit),
1153
2
            2 => Ok(OrderType::Stop),
1154
1
            3 => Ok(OrderType::StopLimit),
1155
0
            4 => Ok(OrderType::Iceberg),
1156
0
            5 => Ok(OrderType::TrailingStop),
1157
0
            6 => Ok(OrderType::Hidden),
1158
2
            _ => Err(format!("Invalid OrderType: {}", value)),
1159
        }
1160
9
    }
1161
}
1162
1163
/// Supported broker types - CANONICAL DEFINITION
1164
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1165
pub enum BrokerType {
1166
    /// Interactive Brokers TWS/API
1167
    InteractiveBrokers,
1168
    /// IC Markets FIX API
1169
    ICMarkets,
1170
    /// Paper trading simulation
1171
    PaperTrading,
1172
    /// Demo/Test broker
1173
    Demo,
1174
}
1175
1176
impl Default for BrokerType {
1177
    /// Returns the default broker type (`InteractiveBrokers`)
1178
1
    fn default() -> Self {
1179
1
        Self::InteractiveBrokers
1180
1
    }
1181
}
1182
1183
/// Order status throughout its lifecycle - CANONICAL DEFINITION
1184
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1185
#[non_exhaustive]
1186
pub enum OrderStatus {
1187
    /// Order has been created but not yet submitted to broker
1188
    Created,
1189
    /// Order has been submitted to broker for execution
1190
    Submitted,
1191
    /// Order has been partially executed with remaining quantity
1192
    PartiallyFilled,
1193
    /// Order has been completely executed
1194
    Filled,
1195
    /// Order was rejected by broker or exchange
1196
    Rejected,
1197
    /// Order was cancelled by user or system
1198
    Cancelled,
1199
    /// New order accepted by broker
1200
    New,
1201
    /// Order expired due to time restrictions
1202
    Expired,
1203
    /// Order is pending broker acceptance
1204
    Pending,
1205
    /// Order is actively working in the market
1206
    Working,
1207
    /// Order status is unknown or not yet determined
1208
    Unknown,
1209
    /// Order is temporarily suspended
1210
    Suspended,
1211
    /// Order cancellation is pending
1212
    PendingCancel,
1213
    /// Order modification is pending
1214
    PendingReplace,
1215
}
1216
impl fmt::Display for OrderStatus {
1217
16
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1218
16
        match self {
1219
3
            Self::Created => write!(f, "CREATED"),
1220
1
            Self::Submitted => write!(f, "SUBMITTED"),
1221
2
            Self::PartiallyFilled => write!(f, "PARTIALLY_FILLED"),
1222
3
            Self::Filled => write!(f, "FILLED"),
1223
2
            Self::Rejected => write!(f, "REJECTED"),
1224
3
            Self::Cancelled => write!(f, "CANCELLED"),
1225
0
            Self::New => write!(f, "NEW"),
1226
1
            Self::Expired => write!(f, "EXPIRED"),
1227
1
            Self::Pending => write!(f, "PENDING"),
1228
0
            Self::Working => write!(f, "WORKING"),
1229
0
            Self::Unknown => write!(f, "UNKNOWN"),
1230
0
            Self::Suspended => write!(f, "SUSPENDED"),
1231
0
            Self::PendingCancel => write!(f, "PENDING_CANCEL"),
1232
0
            Self::PendingReplace => write!(f, "PENDING_REPLACE"),
1233
        }
1234
16
    }
1235
}
1236
1237
impl Default for OrderStatus {
1238
    /// Returns the default order status (Created)
1239
0
    fn default() -> Self {
1240
0
        Self::Created
1241
0
    }
1242
}
1243
1244
impl TryFrom<i32> for OrderStatus {
1245
    type Error = String;
1246
1247
8
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1248
8
        match value {
1249
2
            0 => Ok(OrderStatus::Created),
1250
0
            1 => Ok(OrderStatus::Submitted),
1251
0
            2 => Ok(OrderStatus::PartiallyFilled),
1252
2
            3 => Ok(OrderStatus::Filled),
1253
0
            4 => Ok(OrderStatus::Rejected),
1254
2
            5 => Ok(OrderStatus::Cancelled),
1255
0
            6 => Ok(OrderStatus::New),
1256
0
            7 => Ok(OrderStatus::Expired),
1257
0
            8 => Ok(OrderStatus::Pending),
1258
0
            9 => Ok(OrderStatus::Working),
1259
0
            10 => Ok(OrderStatus::Unknown),
1260
0
            11 => Ok(OrderStatus::Suspended),
1261
0
            12 => Ok(OrderStatus::PendingCancel),
1262
0
            13 => Ok(OrderStatus::PendingReplace),
1263
2
            _ => Err(format!("Invalid OrderStatus: {}", value)),
1264
        }
1265
8
    }
1266
}
1267
1268
/// Order side - whether the order is a buy or sell - CANONICAL DEFINITION
1269
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1270
pub enum OrderSide {
1271
    /// Buy order - purchasing securities
1272
    Buy,
1273
    /// Sell order - selling securities
1274
    Sell,
1275
}
1276
1277
impl fmt::Display for OrderSide {
1278
6
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1279
6
        match self {
1280
3
            Self::Buy => write!(f, "BUY"),
1281
3
            Self::Sell => write!(f, "SELL"),
1282
        }
1283
6
    }
1284
}
1285
1286
impl Default for OrderSide {
1287
    /// Returns the default order side (Buy)
1288
1
    fn default() -> Self {
1289
1
        Self::Buy
1290
1
    }
1291
}
1292
1293
impl TryFrom<i32> for OrderSide {
1294
    type Error = String;
1295
1296
6
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1297
6
        match value {
1298
2
            0 => Ok(OrderSide::Buy),
1299
2
            1 => Ok(OrderSide::Sell),
1300
2
            _ => Err(format!("Invalid OrderSide: {}", value)),
1301
        }
1302
6
    }
1303
}
1304
1305
// REMOVED: Side alias - use OrderSide directly
1306
1307
/// Currency enumeration - CANONICAL DEFINITION
1308
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1309
#[cfg_attr(feature = "database", derive(sqlx::Type))]
1310
pub enum Currency {
1311
    /// US Dollar
1312
    USD,
1313
    /// Euro
1314
    EUR,
1315
    /// British Pound Sterling
1316
    GBP,
1317
    /// Japanese Yen
1318
    JPY,
1319
    /// Swiss Franc
1320
    CHF,
1321
    /// Canadian Dollar
1322
    CAD,
1323
    /// Australian Dollar
1324
    AUD,
1325
    /// New Zealand Dollar
1326
    NZD,
1327
    /// Bitcoin
1328
    BTC,
1329
    /// Ethereum
1330
    ETH,
1331
}
1332
1333
impl fmt::Display for Currency {
1334
17
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1335
17
        match self {
1336
5
            Self::USD => write!(f, "USD"),
1337
3
            Self::EUR => write!(f, "EUR"),
1338
2
            Self::GBP => write!(f, "GBP"),
1339
2
            Self::JPY => write!(f, "JPY"),
1340
0
            Self::CHF => write!(f, "CHF"),
1341
0
            Self::CAD => write!(f, "CAD"),
1342
0
            Self::AUD => write!(f, "AUD"),
1343
0
            Self::NZD => write!(f, "NZD"),
1344
3
            Self::BTC => write!(f, "BTC"),
1345
2
            Self::ETH => write!(f, "ETH"),
1346
        }
1347
17
    }
1348
}
1349
1350
impl Default for Currency {
1351
    /// Returns the default currency (USD)
1352
2
    fn default() -> Self {
1353
2
        Self::USD
1354
2
    }
1355
}
1356
1357
/// Time in force enumeration - CANONICAL DEFINITION
1358
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1359
pub enum TimeInForce {
1360
    /// Order is valid for the current trading day only
1361
    Day,
1362
    /// Order remains active until explicitly cancelled
1363
    GoodTillCancel,
1364
    /// Order must be executed immediately or cancelled
1365
    ImmediateOrCancel,
1366
    /// Order must be executed completely or cancelled
1367
    FillOrKill,
1368
}
1369
1370
impl fmt::Display for TimeInForce {
1371
12
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1372
12
        match self {
1373
3
            Self::Day => write!(f, "DAY"),
1374
3
            Self::GoodTillCancel => write!(f, "GTC"),
1375
3
            Self::ImmediateOrCancel => write!(f, "IOC"),
1376
3
            Self::FillOrKill => write!(f, "FOK"),
1377
        }
1378
12
    }
1379
}
1380
1381
impl Default for TimeInForce {
1382
    /// Returns the default time in force (Day)
1383
15
    fn default() -> Self {
1384
15
        Self::Day
1385
15
    }
1386
}
1387
1388
// =============================================================================
1389
// CORE ID TYPES (MIGRATED FROM TRADING_ENGINE)
1390
// =============================================================================
1391
1392
// Duplicate TradeId removed - using definition from line 1008
1393
1394
/// Event identifier for tracking system events
1395
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1396
pub struct EventId(String);
1397
1398
impl EventId {
1399
    /// Create a new random event ID
1400
4
    pub fn new() -> Self {
1401
        use uuid::Uuid;
1402
4
        Self(Uuid::new_v4().to_string())
1403
4
    }
1404
1405
    /// Create an event ID from a string, generating new if empty
1406
4
    pub fn from_string<S: Into<String>>(id: S) -> Self {
1407
4
        let id = id.into();
1408
4
        if id.is_empty() {
1409
2
            Self::new() // Generate new ID if empty
1410
        } else {
1411
2
            Self(id)
1412
        }
1413
4
    }
1414
1415
    /// Get the string value of the event ID
1416
1
    pub fn value(&self) -> &str {
1417
1
        &self.0
1418
1
    }
1419
}
1420
1421
impl fmt::Display for EventId {
1422
    /// Format the event ID for display
1423
3
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1424
3
        write!(f, "{}", self.0)
1425
3
    }
1426
}
1427
1428
impl From<String> for EventId {
1429
    /// Create an `EventId` from a String
1430
0
    fn from(s: String) -> Self {
1431
0
        Self(s)
1432
0
    }
1433
}
1434
1435
impl Default for EventId {
1436
    /// Create a default `EventId` with a new UUID
1437
0
    fn default() -> Self {
1438
0
        Self::new()
1439
0
    }
1440
}
1441
1442
// =============================================================================
1443
// Decimal Extension Trait
1444
// =============================================================================
1445
1446
/// Extension trait for `rust_decimal::Decimal` with convenience methods
1447
pub trait DecimalExt {
1448
    /// Create Decimal from f64, compatible with legacy `from_f64` usage
1449
    fn from_f64(value: f64) -> Option<Self>
1450
    where
1451
        Self: Sized;
1452
    
1453
    /// Calculate square root of Decimal
1454
    fn sqrt(&self) -> Option<Self>
1455
    where
1456
        Self: Sized;
1457
}
1458
1459
impl DecimalExt for Decimal {
1460
6
    fn from_f64(value: f64) -> Option<Self> {
1461
6
        Decimal::from_f64_retain(value)
1462
6
    }
1463
    
1464
4
    fn sqrt(&self) -> Option<Self> {
1465
4
        if self.is_sign_negative() {
1466
1
            return None;
1467
3
        }
1468
3
        let value_f64: f64 = self.to_string().parse().ok()
?0
;
1469
3
        let sqrt_f64 = value_f64.sqrt();
1470
3
        Decimal::from_f64_retain(sqrt_f64)
1471
4
    }
1472
}
1473
1474
/// Fill identifier with validation
1475
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1476
pub struct FillId(String);
1477
1478
impl FillId {
1479
    /// Create a new fill ID with validation
1480
    ///
1481
    /// # Errors
1482
    /// Returns error if the operation fails
1483
3
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1484
3
        let id = id.into();
1485
3
        if id.is_empty() {
1486
1
            return Err(CommonTypeError::ValidationError {
1487
1
                field: "fill_id".to_owned(),
1488
1
                reason: "Fill ID cannot be empty".to_owned(),
1489
1
            });
1490
2
        }
1491
2
        Ok(Self(id))
1492
3
    }
1493
1494
    /// Get the fill ID as a string slice
1495
0
    pub fn as_str(&self) -> &str {
1496
0
        &self.0
1497
0
    }
1498
    /// Convert the fill ID into an owned string
1499
    ///
1500
    /// Convert the execution ID into an owned string
1501
    ///
1502
    /// Convert execution ID into owned string
1503
0
    pub fn into_string(self) -> String {
1504
0
        self.0
1505
0
    }
1506
}
1507
1508
impl fmt::Display for FillId {
1509
    /// Format the fill ID for display
1510
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1511
2
        write!(f, "{}", self.0)
1512
2
    }
1513
}
1514
1515
/// Aggregate identifier with validation
1516
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1517
pub struct AggregateId(String);
1518
1519
impl AggregateId {
1520
    /// Create a new aggregate ID with validation
1521
    ///
1522
    /// # Errors
1523
    /// Returns error if the operation fails
1524
2
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1525
2
        let id = id.into();
1526
2
        if id.is_empty() {
1527
1
            return Err(CommonTypeError::ValidationError {
1528
1
                field: "aggregate_id".to_owned(),
1529
1
                reason: "Aggregate ID cannot be empty".to_owned(),
1530
1
            });
1531
1
        }
1532
1
        Ok(Self(id))
1533
2
    }
1534
1535
    /// Get the aggregate ID as a string slice
1536
0
    pub fn as_str(&self) -> &str {
1537
0
        &self.0
1538
0
    }
1539
    /// Convert the aggregate ID into an owned string
1540
0
    pub fn into_string(self) -> String {
1541
0
        self.0
1542
0
    }
1543
}
1544
1545
impl fmt::Display for AggregateId {
1546
    /// Format the aggregate ID for display
1547
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1548
1
        write!(f, "{}", self.0)
1549
1
    }
1550
}
1551
1552
/// Asset identifier with validation
1553
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1554
pub struct AssetId(String);
1555
1556
impl AssetId {
1557
    /// Create a new asset ID with validation
1558
    ///
1559
    /// # Errors
1560
    /// Returns error if the operation fails
1561
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1562
0
        let id = id.into();
1563
0
        if id.is_empty() {
1564
0
            return Err(CommonTypeError::ValidationError {
1565
0
                field: "asset_id".to_owned(),
1566
0
                reason: "Asset ID cannot be empty".to_owned(),
1567
0
            });
1568
0
        }
1569
0
        Ok(Self(id))
1570
0
    }
1571
1572
    /// Get the asset ID as a string slice
1573
0
    pub fn as_str(&self) -> &str {
1574
0
        &self.0
1575
0
    }
1576
    /// Convert the asset ID into an owned string
1577
0
    pub fn into_string(self) -> String {
1578
0
        self.0
1579
0
    }
1580
}
1581
1582
impl fmt::Display for AssetId {
1583
    /// Format the asset ID for display
1584
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1585
0
        write!(f, "{}", self.0)
1586
0
    }
1587
}
1588
1589
/// Client identifier with validation
1590
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1591
pub struct ClientId(String);
1592
1593
impl ClientId {
1594
    /// Create a new client ID with validation
1595
    ///
1596
    /// # Errors
1597
    /// Returns error if the operation fails
1598
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1599
0
        let id = id.into();
1600
0
        if id.is_empty() {
1601
0
            return Err(CommonTypeError::ValidationError {
1602
0
                field: "client_id".to_owned(),
1603
0
                reason: "Client ID cannot be empty".to_owned(),
1604
0
            });
1605
0
        }
1606
0
        Ok(Self(id))
1607
0
    }
1608
1609
    /// Get the client ID as a string slice
1610
0
    pub fn as_str(&self) -> &str {
1611
0
        &self.0
1612
0
    }
1613
    /// Convert the client ID into an owned string
1614
0
    pub fn into_string(self) -> String {
1615
0
        self.0
1616
0
    }
1617
}
1618
1619
impl fmt::Display for ClientId {
1620
    /// Format the client ID for display
1621
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1622
0
        write!(f, "{}", self.0)
1623
0
    }
1624
}
1625
1626
// =============================================================================
1627
// CORE TRADING TYPES - MIGRATED FROM TRADING_ENGINE
1628
// =============================================================================
1629
1630
/// Canonical Order struct - UNIFIED DEFINITION based on Agent 1's comprehensive analysis
1631
///
1632
/// This represents the single source of truth for Order across all services
1633
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1634
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
1635
pub struct Order {
1636
    // Core Identity
1637
    /// Unique order identifier
1638
    pub id: OrderId,
1639
    /// Client-provided order identifier
1640
    pub client_order_id: Option<String>,
1641
    /// Broker-assigned order identifier
1642
    pub broker_order_id: Option<String>,
1643
    /// Account identifier for the order
1644
    pub account_id: Option<String>,
1645
1646
    // Trading Details
1647
    /// Trading symbol for the order
1648
    pub symbol: Symbol,
1649
    /// Order side (buy or sell)
1650
    pub side: OrderSide,
1651
    /// Type of order (market, limit, etc.)
1652
    pub order_type: OrderType,
1653
    /// Current status of the order
1654
    pub status: OrderStatus,
1655
    /// Time in force policy
1656
    pub time_in_force: TimeInForce,
1657
1658
    // Quantities & Pricing
1659
    /// Total order quantity
1660
    pub quantity: Quantity,
1661
    /// Limit price for the order
1662
    pub price: Option<Price>,
1663
    /// Stop price for stop orders
1664
    pub stop_price: Option<Price>,
1665
    /// Quantity that has been filled
1666
    pub filled_quantity: Quantity,
1667
    /// Remaining quantity to be filled
1668
    pub remaining_quantity: Quantity,
1669
    /// Average execution price
1670
    pub average_price: Option<Price>,
1671
    /// Alias for `average_price` for database compatibility
1672
    pub avg_fill_price: Option<Price>,
1673
    /// Alias for `average_price` for API compatibility
1674
    pub average_fill_price: Option<Price>,
1675
    /// Exchange-assigned order identifier
1676
    pub exchange_order_id: Option<String>,
1677
1678
    // Strategy Fields (from Agent 1)
1679
    /// Parent order ID for iceberg/algo orders
1680
    pub parent_id: Option<String>,
1681
    /// Execution algorithm name
1682
    pub execution_algorithm: Option<String>,
1683
    /// Execution algorithm parameters stored as JSON
1684
    pub execution_params: Value,
1685
1686
    // Risk Management (from Agent 1)
1687
    /// Stop loss price for risk management
1688
    pub stop_loss: Option<Price>,
1689
    /// Take profit price for profit taking
1690
    pub take_profit: Option<Price>,
1691
1692
    // Timestamps
1693
    /// Order creation timestamp
1694
    pub created_at: HftTimestamp,
1695
    /// Last update timestamp
1696
    pub updated_at: Option<HftTimestamp>,
1697
    /// Order expiration timestamp
1698
    pub expires_at: Option<HftTimestamp>,
1699
1700
    // Extensibility
1701
    /// Additional order metadata stored as JSON
1702
    pub metadata: Value,
1703
}
1704
1705
impl Order {
1706
    /// Create a new order with canonical fields
1707
14
    pub fn new(
1708
14
        symbol: Symbol,
1709
14
        side: OrderSide,
1710
14
        quantity: Quantity,
1711
14
        price: Option<Price>,
1712
14
        order_type: OrderType,
1713
14
    ) -> Self {
1714
14
        let now = HftTimestamp::now_or_zero();
1715
14
        Self {
1716
14
            // Core Identity
1717
14
            id: OrderId::new(),
1718
14
            client_order_id: None,
1719
14
            broker_order_id: None,
1720
14
            account_id: None,
1721
14
1722
14
            // Trading Details
1723
14
            symbol,
1724
14
            side,
1725
14
            order_type,
1726
14
            status: OrderStatus::Created,
1727
14
            time_in_force: TimeInForce::default(),
1728
14
1729
14
            // Quantities & Pricing
1730
14
            quantity,
1731
14
            price,
1732
14
            stop_price: None,
1733
14
            filled_quantity: Quantity::ZERO,
1734
14
            remaining_quantity: quantity,
1735
14
            average_price: None,
1736
14
            avg_fill_price: None, // Database compatibility alias
1737
14
            average_fill_price: None, // API compatibility alias
1738
14
            exchange_order_id: None,
1739
14
1740
14
            // Strategy Fields
1741
14
            parent_id: None,
1742
14
            execution_algorithm: None,
1743
14
            execution_params: serde_json::json!({}),
1744
14
1745
14
            // Risk Management
1746
14
            stop_loss: None,
1747
14
            take_profit: None,
1748
14
1749
14
            // Timestamps
1750
14
            created_at: now,
1751
14
            updated_at: None,
1752
14
            expires_at: None,
1753
14
1754
14
            // Extensibility
1755
14
            metadata: serde_json::json!({}),
1756
14
        }
1757
14
    }
1758
1759
    /// Check if the order is fully filled
1760
12
    pub fn is_filled(&self) -> bool {
1761
12
        self.filled_quantity == self.quantity
1762
12
    }
1763
1764
    /// Check if the order is partially filled
1765
3
    pub fn is_partially_filled(&self) -> bool {
1766
3
        self.filled_quantity > Quantity::ZERO && 
self.filled_quantity < self.quantity2
1767
3
    }
1768
1769
    /// Calculate fill percentage
1770
    #[allow(clippy::float_arithmetic)]
1771
5
    pub fn fill_percentage(&self) -> f64 {
1772
5
        if self.quantity.is_zero() {
1773
1
            0.0
1774
        } else {
1775
4
            (self.filled_quantity.to_f64() / self.quantity.to_f64()) * 100.0
1776
        }
1777
5
    }
1778
1779
    /// Set client order ID for tracking
1780
1
    pub fn with_client_order_id(mut self, client_order_id: String) -> Self {
1781
1
        self.client_order_id = Some(client_order_id);
1782
1
        self
1783
1
    }
1784
1785
    /// Set account ID
1786
1
    pub fn with_account_id(mut self, account_id: String) -> Self {
1787
1
        self.account_id = Some(account_id);
1788
1
        self
1789
1
    }
1790
1791
    /// Set time in force
1792
1
    pub const fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self {
1793
1
        self.time_in_force = time_in_force;
1794
1
        self
1795
1
    }
1796
1797
    /// Set stop price
1798
0
    pub const fn with_stop_price(mut self, stop_price: Price) -> Self {
1799
0
        self.stop_price = Some(stop_price);
1800
0
        self
1801
0
    }
1802
1803
    /// Set execution algorithm
1804
0
    pub fn with_execution_algorithm(mut self, algorithm: String) -> Self {
1805
0
        self.execution_algorithm = Some(algorithm);
1806
0
        self
1807
0
    }
1808
1809
    /// Add execution parameter
1810
0
    pub fn with_execution_param(mut self, key: String, value: f64) -> Self {
1811
0
        if let Some(obj) = self.execution_params.as_object_mut() {
1812
0
            obj.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
1813
0
        } else {
1814
0
            let mut map = serde_json::Map::new();
1815
0
            map.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
1816
0
            self.execution_params = Value::Object(map);
1817
0
        }
1818
0
        self
1819
0
    }
1820
1821
    /// Set stop loss
1822
0
    pub const fn with_stop_loss(mut self, stop_loss: Price) -> Self {
1823
0
        self.stop_loss = Some(stop_loss);
1824
0
        self
1825
0
    }
1826
1827
    /// Set take profit
1828
0
    pub const fn with_take_profit(mut self, take_profit: Price) -> Self {
1829
0
        self.take_profit = Some(take_profit);
1830
0
        self
1831
0
    }
1832
1833
    /// Add metadata
1834
0
    pub fn with_metadata(mut self, key: String, value: String) -> Self {
1835
0
        if let Some(obj) = self.metadata.as_object_mut() {
1836
0
            obj.insert(key, Value::String(value));
1837
0
        } else {
1838
0
            let mut map = serde_json::Map::new();
1839
0
            map.insert(key, Value::String(value));
1840
0
            self.metadata = Value::Object(map);
1841
0
        }
1842
0
        self
1843
0
    }
1844
1845
    /// Update order status and timestamp
1846
10
    pub fn update_status(&mut self, status: OrderStatus) {
1847
10
        self.status = status;
1848
10
        self.updated_at = Some(HftTimestamp::now_or_zero());
1849
10
    }
1850
1851
    /// Fill order with given quantity and price
1852
    ///
1853
    /// # Errors
1854
    /// Returns error if the operation fails
1855
    #[allow(clippy::float_arithmetic)]
1856
11
    pub fn fill(
1857
11
        &mut self,
1858
11
        fill_quantity: Quantity,
1859
11
        fill_price: Price,
1860
11
    ) -> Result<(), CommonTypeError> {
1861
        // Use Add trait instead of checked_add (Quantity doesn't have checked_add method)
1862
11
        let new_filled_value = self.filled_quantity.to_f64() + fill_quantity.to_f64();
1863
11
        if !new_filled_value.is_finite() || new_filled_value < 0.0 {
1864
0
            return Err(CommonTypeError::ValidationError {
1865
0
                field: "fill_quantity".to_owned(),
1866
0
                reason: "Fill quantity overflow".to_owned(),
1867
0
            });
1868
11
        }
1869
11
        let new_filled = Quantity::from_f64(new_filled_value).map_err(|e| CommonTypeError::ValidationError {
1870
0
            field: "fill_quantity".to_owned(),
1871
0
            reason: format!("Fill quantity overflow: {}", e),
1872
0
        })?;
1873
11
        if new_filled > self.quantity {
1874
1
            return Err(CommonTypeError::ValidationError {
1875
1
                field: "fill_quantity".to_owned(),
1876
1
                reason: "Fill quantity exceeds remaining quantity".to_owned(),
1877
1
            });
1878
10
        }
1879
1880
        // Update filled quantity
1881
10
        let previous_filled = self.filled_quantity;
1882
10
        let new_filled_value = self.filled_quantity.value.checked_add(fill_quantity.value).ok_or_else(|| CommonTypeError::ValidationError {
1883
0
            field: "filled_quantity".to_owned(),
1884
0
            reason: "Filled quantity overflow".to_owned(),
1885
0
        })?;
1886
10
        self.filled_quantity = Quantity { value: new_filled_value };
1887
        
1888
10
        let new_remaining_value = self.quantity.value.checked_sub(self.filled_quantity.value).ok_or_else(|| CommonTypeError::ValidationError {
1889
0
            field: "remaining_quantity".to_owned(),
1890
0
            reason: "Remaining quantity underflow".to_owned(),
1891
0
        })?;
1892
10
        self.remaining_quantity = Quantity { value: new_remaining_value };
1893
1894
        // Update average price
1895
10
        if let Some(
avg_price5
) = self.average_price {
1896
5
            let total_value = avg_price.to_f64() * previous_filled.to_f64()
1897
5
                + fill_price.to_f64() * fill_quantity.to_f64();
1898
5
            let new_avg = Some(
1899
5
                Price::from_f64(total_value / self.filled_quantity.to_f64()).unwrap_or(fill_price),
1900
5
            );
1901
5
            self.average_price = new_avg;
1902
5
            self.avg_fill_price = new_avg; // Keep in sync
1903
5
        } else {
1904
5
            self.average_price = Some(fill_price);
1905
5
            self.avg_fill_price = Some(fill_price); // Keep in sync
1906
5
        }
1907
1908
        // Update status
1909
10
        if self.is_filled() {
1910
4
            self.update_status(OrderStatus::Filled);
1911
6
        } else {
1912
6
            self.update_status(OrderStatus::PartiallyFilled);
1913
6
        }
1914
1915
10
        Ok(())
1916
11
    }
1917
1918
    /// Create a limit order - convenience constructor
1919
12
    pub fn limit(symbol: Symbol, side: OrderSide, quantity: Quantity, price: Price) -> Self {
1920
12
        Self::new(symbol, side, quantity, Some(price), OrderType::Limit)
1921
12
    }
1922
1923
    /// Create a market order - convenience constructor
1924
1
    pub fn market(symbol: Symbol, side: OrderSide, quantity: Quantity) -> Self {
1925
1
        Self::new(symbol, side, quantity, None, OrderType::Market)
1926
1
    }
1927
1928
    /// Get symbol hash for performance-critical operations
1929
1
    pub fn symbol_hash(&self) -> i64 {
1930
        use std::collections::hash_map::DefaultHasher;
1931
        use std::hash::{Hash, Hasher};
1932
1933
1
        let mut hasher = DefaultHasher::new();
1934
1
        self.symbol.as_str().hash(&mut hasher);
1935
1
        i64::try_from(hasher.finish()).unwrap_or(0)
1936
1
    }
1937
1938
    /// Get order timestamp
1939
0
    pub const fn timestamp(&self) -> HftTimestamp {
1940
0
        self.created_at
1941
0
    }
1942
}
1943
1944
impl Default for Order {
1945
0
    fn default() -> Self {
1946
0
        Self {
1947
0
            id: OrderId::new(),
1948
0
            client_order_id: None,
1949
0
            broker_order_id: None,
1950
0
            account_id: None,
1951
0
1952
0
            symbol: Symbol::from("DEFAULT"),
1953
0
            side: OrderSide::Buy,
1954
0
            order_type: OrderType::Market,
1955
0
            status: OrderStatus::Created,
1956
0
            time_in_force: TimeInForce::Day,
1957
0
1958
0
            quantity: Quantity::ONE,
1959
0
            price: None,
1960
0
            stop_price: None,
1961
0
            filled_quantity: Quantity::ZERO,
1962
0
            remaining_quantity: Quantity::ONE,
1963
0
            average_price: None,
1964
0
            avg_fill_price: None,
1965
0
            average_fill_price: None,
1966
0
            exchange_order_id: None,
1967
0
            
1968
0
            parent_id: None,
1969
0
            execution_algorithm: None,
1970
0
            execution_params: serde_json::json!({}),
1971
0
1972
0
            stop_loss: None,
1973
0
            take_profit: None,
1974
0
1975
0
            created_at: HftTimestamp::now().unwrap_or(HftTimestamp { nanos: 0 }),
1976
0
            updated_at: None,
1977
0
            expires_at: None,
1978
0
1979
0
            metadata: serde_json::json!({}),
1980
0
        }
1981
0
    }
1982
}
1983
1984
/// Represents a trading position - CANONICAL DEFINITION
1985
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1986
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
1987
pub struct Position {
1988
    /// Unique position identifier
1989
    pub id: Uuid,
1990
1991
    /// Trading symbol
1992
    pub symbol: String,
1993
1994
    /// Position quantity (positive for long, negative for short)
1995
    pub quantity: Decimal,
1996
1997
    /// Average entry price
1998
    pub avg_price: Decimal,
1999
2000
    /// Average cost per share
2001
    pub avg_cost: Decimal,
2002
2003
    /// Cost basis for tax calculations
2004
    pub basis: Decimal,
2005
2006
    /// Average entry price
2007
    pub average_price: Decimal,
2008
2009
    /// Market value of position
2010
    pub market_value: Decimal,
2011
2012
    /// Unrealized P&L
2013
    pub unrealized_pnl: Decimal,
2014
2015
    /// Realized P&L
2016
    pub realized_pnl: Decimal,
2017
2018
    /// Position creation timestamp
2019
    pub created_at: DateTime<Utc>,
2020
2021
    /// Last update timestamp
2022
    pub updated_at: DateTime<Utc>,
2023
2024
    /// Last updated timestamp
2025
    pub last_updated: DateTime<Utc>,
2026
2027
    /// Current market price (for P&L calculation)
2028
    pub current_price: Option<Decimal>,
2029
2030
    /// Position size in base currency
2031
    pub notional_value: Decimal,
2032
2033
    /// Margin requirement
2034
    pub margin_requirement: Decimal,
2035
}
2036
2037
impl Position {
2038
    /// Create a new position
2039
7
    pub fn new(symbol: String, quantity: Decimal, avg_price: Decimal) -> Self {
2040
7
        let now = Utc::now();
2041
7
        let notional_value = quantity.abs().checked_mul(avg_price).unwrap_or(Decimal::ZERO);
2042
2043
7
        Self {
2044
7
            id: Uuid::new_v4(),
2045
7
            symbol,
2046
7
            quantity,
2047
7
            avg_price,
2048
7
            avg_cost: avg_price, // Keep avg_cost synchronized with avg_price
2049
7
            basis: quantity.checked_mul(avg_price).unwrap_or(Decimal::ZERO), // Cost basis calculation
2050
7
            average_price: avg_price, // Same as avg_price for compatibility
2051
7
            market_value: notional_value, // Initialize market value to notional value
2052
7
            unrealized_pnl: Decimal::ZERO,
2053
7
            realized_pnl: Decimal::ZERO,
2054
7
            created_at: now,
2055
7
            updated_at: now,
2056
7
            last_updated: now, // Same as updated_at for compatibility
2057
7
            current_price: None,
2058
7
            notional_value,
2059
7
            margin_requirement: notional_value.checked_mul(
2060
7
                Decimal::from_str_exact("0.02").unwrap_or(Decimal::ZERO)
2061
7
            ).unwrap_or(Decimal::ZERO), // 2% margin
2062
7
        }
2063
7
    }
2064
2065
    /// Check if position is long
2066
2
    pub fn is_long(&self) -> bool {
2067
2
        self.quantity > Decimal::ZERO
2068
2
    }
2069
2070
    /// Check if position is short
2071
2
    pub fn is_short(&self) -> bool {
2072
2
        self.quantity < Decimal::ZERO
2073
2
    }
2074
2075
    /// Calculate unrealized P&L based on current price
2076
3
    pub fn calculate_unrealized_pnl(&mut self, current_price: Decimal) {
2077
3
        self.current_price = Some(current_price);
2078
3
        self.market_value = self.quantity.abs().checked_mul(current_price).unwrap_or(Decimal::ZERO);
2079
        // For both long and short: quantity * (current_price - avg_price)
2080
3
        let price_diff = current_price.checked_sub(self.avg_price).unwrap_or(Decimal::ZERO);
2081
3
        self.unrealized_pnl = self.quantity.checked_mul(price_diff).unwrap_or(Decimal::ZERO);
2082
3
        let now = Utc::now();
2083
3
        self.updated_at = now;
2084
3
        self.last_updated = now; // Keep alias synchronized
2085
3
    }
2086
2087
    /// Get total P&L (realized + unrealized)
2088
1
    pub fn total_pnl(&self) -> Decimal {
2089
1
        self.realized_pnl.checked_add(self.unrealized_pnl).unwrap_or(Decimal::ZERO)
2090
1
    }
2091
2092
    /// Calculate return on investment percentage
2093
2
    pub fn roi_percentage(&self) -> Decimal {
2094
2
        if self.notional_value.is_zero() {
2095
1
            Decimal::ZERO
2096
        } else {
2097
1
            let pnl_ratio = self.total_pnl().checked_div(self.notional_value).unwrap_or(Decimal::ZERO);
2098
1
            pnl_ratio.checked_mul(Decimal::from(100)).unwrap_or(Decimal::ZERO)
2099
        }
2100
2
    }
2101
}
2102
2103
/// Represents a trade execution - CANONICAL DEFINITION
2104
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2105
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
2106
pub struct Execution {
2107
    /// Unique execution identifier
2108
    pub id: Uuid,
2109
2110
    /// Related order ID
2111
    pub order_id: Uuid,
2112
2113
    /// Trading symbol
2114
    pub symbol: String,
2115
2116
    /// Executed quantity
2117
    pub quantity: Decimal,
2118
2119
    /// Execution price
2120
    pub price: Decimal,
2121
2122
    /// Execution side
2123
    pub side: OrderSide,
2124
2125
    /// Trading fees
2126
    pub fees: Decimal,
2127
2128
    /// Fee currency
2129
    pub fee_currency: String,
2130
2131
    /// Execution timestamp
2132
    pub executed_at: DateTime<Utc>,
2133
2134
    /// Execution timestamp
2135
    pub timestamp: DateTime<Utc>,
2136
2137
    /// Symbol hash for performance
2138
    pub symbol_hash: i64,
2139
2140
    /// Broker execution ID
2141
    pub broker_execution_id: Option<String>,
2142
2143
    /// Counterparty information
2144
    pub counterparty: Option<String>,
2145
2146
    /// Trade venue
2147
    pub venue: Option<String>,
2148
2149
    /// Gross trade value
2150
    pub gross_value: Decimal,
2151
2152
    /// Net trade value (after fees)
2153
    pub net_value: Decimal,
2154
}
2155
2156
impl Execution {
2157
    /// Create a new execution
2158
5
    pub fn new(
2159
5
        order_id: Uuid,
2160
5
        symbol: String,
2161
5
        quantity: Decimal,
2162
5
        price: Decimal,
2163
5
        side: OrderSide,
2164
5
        fees: Decimal,
2165
5
    ) -> Self {
2166
5
        let gross_value = quantity.checked_mul(price).unwrap_or(Decimal::ZERO);
2167
5
        let net_value = if side == OrderSide::Buy {
2168
4
            gross_value.checked_add(fees).unwrap_or(gross_value)
2169
        } else {
2170
1
            gross_value.checked_sub(fees).unwrap_or(gross_value)
2171
        };
2172
5
        let now = Utc::now();
2173
5
        let symbol_hash = Self::hash_symbol(&symbol);
2174
2175
5
        Self {
2176
5
            id: Uuid::new_v4(),
2177
5
            order_id,
2178
5
            symbol,
2179
5
            quantity,
2180
5
            price,
2181
5
            side,
2182
5
            fees,
2183
5
            fee_currency: "USD".to_owned(), // Default to USD
2184
5
            executed_at: now,
2185
5
            timestamp: now, // Same as executed_at for compatibility
2186
5
            symbol_hash,
2187
5
            broker_execution_id: None,
2188
5
            counterparty: None,
2189
5
            venue: None,
2190
5
            gross_value,
2191
5
            net_value,
2192
5
        }
2193
5
    }
2194
2195
    /// Calculate effective price including fees
2196
2
    pub fn effective_price(&self) -> Decimal {
2197
2
        if self.quantity.is_zero() {
2198
1
            self.price
2199
        } else {
2200
1
            self.net_value.checked_div(self.quantity).unwrap_or(self.price)
2201
        }
2202
2
    }
2203
2204
    /// Hash symbol for performance
2205
5
    fn hash_symbol(symbol: &str) -> i64 {
2206
        use std::collections::hash_map::DefaultHasher;
2207
        use std::hash::{Hash, Hasher};
2208
2209
5
        let mut hasher = DefaultHasher::new();
2210
5
        symbol.hash(&mut hasher);
2211
5
        i64::try_from(hasher.finish()).unwrap_or(0)
2212
5
    }
2213
}
2214
2215
/// Core Price type using fixed-point arithmetic for precision
2216
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2217
pub struct Price {
2218
    value: u64,
2219
}
2220
2221
impl Price {
2222
    /// Zero price constant
2223
    pub const ZERO: Self = Self { value: 0 };
2224
    /// One unit price constant (1.0)
2225
    pub const ONE: Self = Self { value: 100_000_000 };
2226
    /// One cent constant (0.01)
2227
    pub const CENT: Self = Self { value: 1_000_000 };
2228
    /// Maximum price value
2229
    pub const MAX: Self = Self { value: u64::MAX };
2230
2231
    /// Create a Price from a floating-point value
2232
    ///
2233
    /// # Errors
2234
    /// Returns error if the operation fails
2235
    #[allow(clippy::as_conversions)]
2236
    #[allow(clippy::float_arithmetic)]
2237
2.10k
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
2238
2.10k
        if value < 0.0_f64 || 
!value.is_finite()2.09k
{
2239
12
            return Err(CommonTypeError::InvalidPrice {
2240
12
                value: value.to_string(),
2241
12
                reason: "Price validation failed".to_owned(),
2242
12
            });
2243
2.09k
        }
2244
2.09k
        Ok(Self {
2245
2.09k
            #[allow(clippy::as_conversions)]
2246
2.09k
            #[allow(clippy::float_arithmetic)]
2247
2.09k
            value: (value * 100_000_000.0).round() as u64,
2248
2.09k
        })
2249
2.10k
    }
2250
2251
    /// Convert to floating-point representation
2252
    #[must_use]
2253
    #[allow(clippy::float_arithmetic)]
2254
    /// Convert the quantity to a floating point value
2255
    #[allow(clippy::as_conversions)]
2256
77
    pub fn to_f64(&self) -> f64 {
2257
        #[allow(clippy::as_conversions)]
2258
77
        { self.value as f64 / 100_000_000.0 }
2259
77
    }
2260
2261
    /// Get floating-point representation (alias for `to_f64`)
2262
    ///
2263
    /// Convert quantity to f64 representation
2264
    ///
2265
    /// Convert quantity to f64 representation
2266
    ///
2267
    /// Convert quantity to f64 representation
2268
    #[must_use]
2269
0
    pub fn as_f64(&self) -> f64 {
2270
0
        self.to_f64()
2271
0
    }
2272
2273
    /// Create a zero price
2274
    ///
2275
    /// Create a zero quantity
2276
    ///
2277
    /// Create zero quantity
2278
    #[must_use]
2279
0
    pub const fn zero() -> Self {
2280
0
        Self::ZERO
2281
0
    }
2282
2283
    /// Convert to Decimal type for precise calculations
2284
    ///
2285
    /// # Errors
2286
    /// Returns error if the operation fails
2287
2
    pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
2288
2
        <Decimal as DecimalExt>::from_f64(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidPrice {
2289
0
            value: "0.0".to_owned(),
2290
0
            reason: "Price to Decimal conversion failed".to_owned(),
2291
0
        })
2292
2
    }
2293
2294
    /// Create a Price from a Decimal value
2295
    #[must_use]
2296
2
    pub fn from_decimal(decimal: Decimal) -> Self {
2297
2
        Self::from(decimal)
2298
2
    }
2299
2300
    /// Create a new Price (alias for `from_f64`)
2301
    ///
2302
    /// Create a new quantity from a floating point value
2303
    ///
2304
    /// Create new quantity from f64 value
2305
    ///
2306
    /// # Errors
2307
    /// Returns error if the operation fails
2308
0
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
2309
0
        Self::from_f64(value)
2310
0
    }
2311
2312
    /// Get the raw internal value representation
2313
    ///
2314
    /// Get the raw internal value
2315
    ///
2316
    /// Get the raw internal value representation
2317
    #[must_use]
2318
1
    pub const fn raw_value(&self) -> u64 {
2319
1
        self.value
2320
1
    }
2321
2322
    /// Get the price as a u64 value (same as `raw_value`)
2323
    ///
2324
    /// Convert to u64 representation
2325
    ///
2326
    /// Convert quantity to u64 representation
2327
    #[must_use]
2328
0
    pub const fn as_u64(&self) -> u64 {
2329
0
        self.value
2330
0
    }
2331
2332
    /// Create a Price from a raw u64 value
2333
    ///
2334
    /// Create a quantity from raw internal value
2335
    ///
2336
    /// Create quantity from raw u64 value
2337
    #[must_use]
2338
0
    pub const fn from_raw(value: u64) -> Self {
2339
0
        Self { value }
2340
0
    }
2341
2342
    /// Convert price to cents (divides by 1M for 8 decimal places)
2343
    #[must_use]
2344
    #[allow(clippy::integer_division)]
2345
2
    pub const fn to_cents(&self) -> u64 {
2346
2
        self.value / 1_000_000
2347
2
    }
2348
2349
    /// Create a Price from cents value
2350
    #[must_use]
2351
2
    pub const fn from_cents(cents: u64) -> Self {
2352
2
        Self {
2353
2
            value: cents.saturating_mul(1_000_000),
2354
2
        }
2355
2
    }
2356
2357
    /// Check if the price is zero
2358
    ///
2359
    /// Check if the quantity is zero
2360
    ///
2361
    /// Check if quantity is zero
2362
    #[must_use]
2363
2
    pub const fn is_zero(&self) -> bool {
2364
2
        self.value == 0
2365
2
    }
2366
2367
    /// Check if the price is non-zero (has some value)
2368
    ///
2369
    /// Check if the quantity is non-zero (has some value)
2370
    ///
2371
    /// Check if quantity has a non-zero value
2372
    #[must_use]
2373
0
    pub const fn is_some(&self) -> bool {
2374
0
        !self.is_zero()
2375
0
    }
2376
2377
    /// Check if the price is zero (has no value)
2378
    ///
2379
    /// Check if the quantity is zero (has no value)
2380
    ///
2381
    /// Check if quantity is zero (none)
2382
    #[must_use]
2383
0
    pub const fn is_none(&self) -> bool {
2384
0
        self.is_zero()
2385
0
    }
2386
2387
    /// Get a reference to this price
2388
    ///
2389
    /// Get a reference to self
2390
    ///
2391
    /// Get a reference to self
2392
    #[must_use]
2393
0
    pub const fn as_ref(&self) -> &Self {
2394
0
        self
2395
0
    }
2396
2397
    /// Get the absolute value of the price (prices are always positive)
2398
    ///
2399
    /// Get the absolute value (quantities are always positive)
2400
    ///
2401
    /// Get absolute value (always positive for Quantity)
2402
    #[must_use]
2403
0
    pub const fn abs(&self) -> Self {
2404
0
        *self
2405
0
    }
2406
2407
    /// Multiply this price by another price
2408
    ///
2409
    /// # Errors
2410
    /// Returns error if the operation fails
2411
    #[allow(clippy::arithmetic_side_effects)]
2412
3
    pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
2413
        #[allow(clippy::arithmetic_side_effects)]
2414
3
        let result = *self * other;
2415
3
        result
2416
3
    }
2417
2418
    /// Subtract another price from this price
2419
    ///
2420
    /// Subtract another quantity from this quantity
2421
    ///
2422
    /// Subtract another quantity from this quantity
2423
    ///
2424
    /// Subtract another quantity from this quantity
2425
    ///
2426
    /// Subtract another quantity from this quantity
2427
    #[must_use]
2428
    #[allow(clippy::arithmetic_side_effects)]
2429
0
    pub fn subtract(&self, other: Self) -> Self {
2430
0
        *self - other
2431
0
    }
2432
2433
    /// Divide this price by a floating point divisor
2434
    ///
2435
    /// # Errors
2436
    /// Returns error if the operation fails
2437
2
    pub fn divide(&self, divisor: f64) -> Result<Self, CommonTypeError> {
2438
2
        if divisor == 0.0 {
2439
1
            return Err(CommonTypeError::InvalidPrice {
2440
1
                value: format!("{:?}", self),
2441
1
                reason: "Division by zero".to_owned(),
2442
1
            });
2443
1
        }
2444
        #[allow(clippy::cast_precision_loss)]
2445
        #[allow(clippy::arithmetic_side_effects)]
2446
1
        let result = (self.value as f64 / divisor) as u64;
2447
1
        Ok(Self { value: result })
2448
2
    }
2449
}
2450
2451
impl fmt::Display for Price {
2452
    /// Format the price for display with 8 decimal places
2453
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2454
2
        write!(f, "{:.8}", self.to_f64())
2455
2
    }
2456
}
2457
2458
impl Default for Price {
2459
    /// Returns the default price (zero)
2460
0
    fn default() -> Self {
2461
0
        Self::ZERO
2462
0
    }
2463
}
2464
2465
impl FromStr for Price {
2466
    type Err = CommonTypeError;
2467
2468
7
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2469
7
        let 
parsed_value4
= s
2470
7
            .parse::<f64>()
2471
7
            .map_err(|e| CommonTypeError::InvalidPrice {
2472
3
                value: s.to_owned(),
2473
3
                reason: format!("Cannot parse '{}' as price: {}", s, e),
2474
3
            })?;
2475
4
        Self::from_f64(parsed_value)
2476
7
    }
2477
}
2478
2479
impl Add for Price {
2480
    type Output = Self;
2481
1.00k
    fn add(self, rhs: Self) -> Self::Output {
2482
1.00k
        Self {
2483
1.00k
            value: self.value.saturating_add(rhs.value),
2484
1.00k
        }
2485
1.00k
    }
2486
}
2487
2488
impl Sub for Price {
2489
    type Output = Self;
2490
1.00k
    fn sub(self, rhs: Self) -> Self::Output {
2491
1.00k
        Self {
2492
1.00k
            value: self.value.saturating_sub(rhs.value),
2493
1.00k
        }
2494
1.00k
    }
2495
}
2496
2497
impl Mul<f64> for Price {
2498
    #[allow(clippy::float_arithmetic)]
2499
    type Output = Result<Self, CommonTypeError>;
2500
2
    fn mul(self, rhs: f64) -> Self::Output {
2501
2
        Self::from_f64(self.to_f64() * rhs)
2502
2
    }
2503
}
2504
2505
#[allow(clippy::float_arithmetic)]
2506
impl Div<f64> for Price {
2507
    type Output = Result<Self, CommonTypeError>;
2508
4
    fn div(self, rhs: f64) -> Self::Output {
2509
4
        if rhs == 0.0_f64 {
2510
2
            return Err(CommonTypeError::ConversionError {
2511
2
                message: "Cannot divide price by zero".to_owned(),
2512
2
            });
2513
2
        }
2514
2
        Self::from_f64(self.to_f64() / rhs)
2515
4
    }
2516
}
2517
2518
impl From<Decimal> for Price {
2519
2
    fn from(decimal: Decimal) -> Self {
2520
2
        let f64_val: f64 = TryInto::<f64>::try_into(decimal).unwrap_or_else(|_| 
{0
2521
0
            tracing::warn!("Failed to convert Decimal to f64, using 0.0 as fallback");
2522
0
            0.0_f64
2523
0
        });
2524
2
        Self::from_f64(f64_val).unwrap_or_else(|_| 
{0
2525
0
            tracing::warn!(
2526
0
                "Failed to create Price from f64 value {}, using ZERO",
2527
                f64_val
2528
            );
2529
0
            Self::ZERO
2530
0
        })
2531
2
    }
2532
}
2533
2534
impl From<Price> for Decimal {
2535
0
    fn from(price: Price) -> Self {
2536
0
        price.to_decimal().unwrap_or(Decimal::ZERO)
2537
0
    }
2538
}
2539
2540
// TryFrom<Quantity> for Decimal removed due to conflicting blanket implementation
2541
// Use qty.to_decimal() directly instead
2542
impl From<Quantity> for Decimal {
2543
0
    fn from(qty: Quantity) -> Self {
2544
0
        qty.to_decimal().unwrap_or(Decimal::ZERO)
2545
0
    }
2546
}
2547
2548
// TryFrom<Quantity> for Decimal removed due to conflict with From implementation
2549
// Use the From implementation instead which handles errors by returning ZERO
2550
2551
// =============================================================================
2552
// Decimal Extension Trait
2553
// =============================================================================
2554
2555
impl Mul<Self> for Price {
2556
    #[allow(clippy::float_arithmetic)]
2557
    #[allow(clippy::arithmetic_side_effects)]
2558
    type Output = Result<Self, CommonTypeError>;
2559
3
    fn mul(self, rhs: Self) -> Self::Output {
2560
3
        Self::from_f64(self.to_f64() * rhs.to_f64())
2561
3
    }
2562
}
2563
2564
impl TryFrom<String> for Price {
2565
    type Error = CommonTypeError;
2566
0
    fn try_from(s: String) -> Result<Self, Self::Error> {
2567
0
        Self::from_str(&s)
2568
0
    }
2569
}
2570
2571
impl TryFrom<&str> for Price {
2572
    type Error = CommonTypeError;
2573
0
    fn try_from(s: &str) -> Result<Self, Self::Error> {
2574
0
        Self::from_str(s)
2575
0
    }
2576
}
2577
2578
impl PartialEq<f64> for Price {
2579
    #[allow(clippy::float_arithmetic)]
2580
5
    fn eq(&self, other: &f64) -> bool {
2581
5
        (self.to_f64() - other).abs() < f64::EPSILON
2582
5
    }
2583
}
2584
2585
impl PartialEq<Price> for f64 {
2586
    #[allow(clippy::float_arithmetic)]
2587
2
    fn eq(&self, other: &Price) -> bool {
2588
2
        (self - other.to_f64()).abs() < f64::EPSILON
2589
2
    }
2590
}
2591
2592
impl AddAssign for Price {
2593
1
    fn add_assign(&mut self, rhs: Self) {
2594
1
        self.value = self.value.saturating_add(rhs.value);
2595
1
    }
2596
}
2597
2598
impl SubAssign for Price {
2599
0
    fn sub_assign(&mut self, rhs: Self) {
2600
0
        self.value = self.value.saturating_sub(rhs.value);
2601
0
    }
2602
}
2603
2604
impl MulAssign<f64> for Price {
2605
0
    fn mul_assign(&mut self, rhs: f64) {
2606
0
        if let Ok(result) = self.mul(rhs) {
2607
0
            *self = result;
2608
0
        }
2609
        // If multiplication fails, self remains unchanged
2610
0
    }
2611
}
2612
2613
impl DivAssign<f64> for Price {
2614
0
    fn div_assign(&mut self, rhs: f64) {
2615
0
        if let Ok(result) = self.div(rhs) {
2616
0
            *self = result;
2617
0
        }
2618
        // If division fails, self remains unchanged
2619
0
    }
2620
}
2621
2622
impl PartialOrd<f64> for Price {
2623
2
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
2624
2
        self.to_f64().partial_cmp(other)
2625
2
    }
2626
}
2627
2628
impl PartialOrd<Price> for f64 {
2629
0
    fn partial_cmp(&self, other: &Price) -> Option<std::cmp::Ordering> {
2630
0
        self.partial_cmp(&other.to_f64())
2631
0
    }
2632
}
2633
2634
/// Core Quantity type using fixed-point arithmetic
2635
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2636
pub struct Quantity {
2637
    value: u64,
2638
}
2639
2640
impl Quantity {
2641
    /// Zero quantity constant
2642
    pub const ZERO: Self = Self { value: 0 };
2643
    /// One unit quantity constant
2644
    pub const ONE: Self = Self { value: 100_000_000 };
2645
    /// Maximum possible quantity
2646
    pub const MAX: Self = Self { value: u64::MAX };
2647
2648
    /// Create a Quantity from a floating point value
2649
    #[allow(clippy::as_conversions)]
2650
    ///
2651
    /// # Errors
2652
    /// Returns error if the operation fails
2653
    #[allow(clippy::float_arithmetic)]
2654
96
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
2655
96
        if value < 0.0_f64 || 
!value.is_finite()93
{
2656
7
            return Err(CommonTypeError::InvalidQuantity {
2657
7
                value: value.to_string(),
2658
7
                reason: "Quantity validation failed".to_owned(),
2659
7
            });
2660
89
        }
2661
89
        Ok(Self {
2662
89
            value: (value * 100_000_000.0).round() as u64,
2663
89
        })
2664
96
    }
2665
2666
    /// Convert quantity to floating point representation
2667
    #[must_use]
2668
    #[allow(clippy::as_conversions)]
2669
    #[allow(clippy::float_arithmetic)]
2670
87
    pub fn to_f64(&self) -> f64 {
2671
87
        self.value as f64 / 100_000_000.0
2672
87
    }
2673
2674
    /// Convert the quantity to a Decimal value
2675
    ///
2676
    /// # Errors
2677
    /// Returns error if the operation fails
2678
3
    pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
2679
3
        Decimal::from_f64_retain(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidQuantity {
2680
0
            value: "0.0".to_owned(),
2681
0
            reason: "Quantity to Decimal conversion failed".to_owned(),
2682
0
        })
2683
3
    }
2684
2685
    /// Get the internal value representation
2686
    #[must_use]
2687
1
    pub const fn value(&self) -> u64 {
2688
1
        self.value
2689
1
    }
2690
2691
    /// Get the raw internal value representation
2692
    #[must_use]
2693
2
    pub const fn raw_value(&self) -> u64 {
2694
2
        self.value
2695
2
    }
2696
2697
    /// Convert quantity to u64 representation
2698
    #[must_use]
2699
1
    pub const fn as_u64(&self) -> u64 {
2700
1
        self.value
2701
1
    }
2702
2703
    /// Create quantity from raw u64 value
2704
    #[must_use]
2705
1
    pub const fn from_raw(value: u64) -> Self {
2706
1
        Self { value }
2707
1
    }
2708
2709
    /// Create new quantity from f64 value
2710
    ///
2711
    /// # Errors
2712
    /// Returns error if the operation fails
2713
    #[allow(clippy::as_conversions)]
2714
1
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
2715
1
        Self::from_f64(value)
2716
1
    }
2717
2718
    /// Create zero quantity
2719
    #[must_use]
2720
0
    pub const fn zero() -> Self {
2721
    #[allow(clippy::as_conversions)]
2722
0
        Self::ZERO
2723
0
    }
2724
2725
    /// Create a quantity from an i64 value
2726
    ///
2727
    /// # Errors
2728
    /// Returns error if the operation fails
2729
    #[allow(clippy::as_conversions)]
2730
0
    pub fn from_i64(value: i64) -> Result<Self, CommonTypeError> {
2731
0
        Self::from_f64(value as f64)
2732
0
    }
2733
2734
    /// Create a quantity from a u64 value
2735
    ///
2736
    /// # Errors
2737
    #[allow(clippy::as_conversions)]
2738
    /// Returns error if the operation fails
2739
0
    pub fn from_u64(value: u64) -> Result<Self, CommonTypeError> {
2740
0
        Self::from_f64(value as f64)
2741
0
    }
2742
2743
    /// Create a quantity from a Decimal value
2744
    ///
2745
    /// # Errors
2746
    /// Returns error if the operation fails
2747
2
    pub fn from_decimal(decimal: Decimal) -> Result<Self, CommonTypeError> {
2748
        use std::convert::TryFrom;
2749
2
        Self::try_from(decimal).map_err(|e| CommonTypeError::InvalidQuantity {
2750
0
            value: decimal.to_string(),
2751
0
            reason: format!("Failed to convert Decimal to Quantity: {}", e),
2752
0
        })
2753
2
    }
2754
2755
    /// Check if quantity is zero
2756
    #[must_use]
2757
13
    pub const fn is_zero(&self) -> bool {
2758
13
        self.value == 0
2759
13
    }
2760
2761
    /// Check if quantity has a non-zero value
2762
    #[must_use]
2763
0
    pub const fn is_some(&self) -> bool {
2764
0
        !self.is_zero()
2765
0
    }
2766
2767
    /// Check if quantity is zero (none)
2768
    #[must_use]
2769
0
    pub const fn is_none(&self) -> bool {
2770
0
        self.is_zero()
2771
0
    }
2772
2773
    /// Get a reference to self
2774
    #[must_use]
2775
0
    pub const fn as_ref(&self) -> &Self {
2776
0
        self
2777
0
    }
2778
2779
    /// Get absolute value (always positive for Quantity)
2780
    #[must_use]
2781
0
    pub const fn abs(&self) -> Self {
2782
0
        *self
2783
0
    }
2784
2785
    /// Get the sign of the quantity (1.0 for positive, 0.0 for zero)
2786
    #[must_use]
2787
0
    pub const fn signum(&self) -> f64 {
2788
0
        if self.value > 0 {
2789
0
            1.0
2790
        } else {
2791
0
            0.0
2792
        }
2793
0
    }
2794
2795
    /// Check if quantity is positive
2796
    #[must_use]
2797
5
    pub const fn is_positive(&self) -> bool {
2798
5
        self.value > 0
2799
5
    }
2800
2801
    /// Check if quantity is negative (always false for Quantity)
2802
    #[must_use]
2803
2
    pub const fn is_negative(&self) -> bool {
2804
2
        false
2805
2
    }
2806
2807
    /// Convert quantity to f64 representation
2808
    #[must_use]
2809
0
    pub fn as_f64(&self) -> f64 {
2810
0
        self.to_f64()
2811
0
    }
2812
2813
    /// Create quantity from number of shares
2814
    #[must_use]
2815
2
    pub const fn from_shares(shares: u64) -> Self {
2816
2
        Self {
2817
2
            value: shares.saturating_mul(100_000_000),
2818
2
        }
2819
2
    }
2820
2821
    /// Convert quantity to number of shares
2822
    #[must_use]
2823
    #[allow(clippy::integer_division)]
2824
2
    pub const fn to_shares(&self) -> u64 {
2825
2
        self.value / 100_000_000
2826
2
    }
2827
2828
    /// Multiply this quantity by another quantity
2829
    ///
2830
    /// # Errors
2831
    /// Returns error if the operation fails
2832
    #[allow(clippy::float_arithmetic)]
2833
    #[allow(clippy::arithmetic_side_effects)]
2834
2
    pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
2835
2
        Self::from_f64(self.to_f64() * other.to_f64())
2836
2
    }
2837
2838
    /// Subtract another quantity from this quantity
2839
    #[must_use]
2840
0
    pub fn subtract(&self, other: Self) -> Self {
2841
        Self {
2842
0
            value: self.value.checked_sub(other.value).unwrap_or_else(|| {
2843
0
                tracing::warn!(
2844
0
                    "Quantity subtraction underflow: {} - {}, returning 0",
2845
                    self.value, other.value
2846
                );
2847
0
                0
2848
0
            }),
2849
        }
2850
0
    }
2851
2852
    /// Checked addition with overflow protection
2853
    #[must_use]
2854
0
    pub const fn checked_add(&self, other: Self) -> Option<Self> {
2855
0
        match self.value.checked_add(other.value) {
2856
0
            Some(value) => Some(Self { value }),
2857
0
            None => None,
2858
        }
2859
0
    }
2860
2861
    /// Checked subtraction with underflow protection
2862
    #[must_use]
2863
0
    pub const fn checked_sub(&self, other: Self) -> Option<Self> {
2864
0
        match self.value.checked_sub(other.value) {
2865
0
            Some(value) => Some(Self { value }),
2866
0
            None => None,
2867
        }
2868
0
    }
2869
}
2870
2871
impl Default for Quantity {
2872
0
    fn default() -> Self {
2873
0
        Self::ZERO
2874
0
    }
2875
}
2876
2877
impl FromStr for Quantity {
2878
    type Err = CommonTypeError;
2879
2880
1
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2881
1
        let parsed_value = s
2882
1
            .parse::<f64>()
2883
1
            .map_err(|e| CommonTypeError::InvalidQuantity {
2884
0
                value: s.to_owned(),
2885
0
                reason: format!("Cannot parse '{}' as quantity: {}", s, e),
2886
0
            })?;
2887
1
        Self::from_f64(parsed_value)
2888
1
    }
2889
}
2890
2891
impl fmt::Display for Quantity {
2892
    /// Format the quantity for display with 8 decimal places
2893
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2894
0
        write!(f, "{:.8}", self.to_f64())
2895
0
    }
2896
}
2897
2898
impl TryFrom<i32> for Quantity {
2899
    type Error = CommonTypeError;
2900
1
    fn try_from(value: i32) -> Result<Self, Self::Error> {
2901
1
        Self::new(f64::from(value))
2902
1
    }
2903
}
2904
2905
impl TryFrom<u64> for Quantity {
2906
    type Error = CommonTypeError;
2907
0
    fn try_from(value: u64) -> Result<Self, Self::Error> {
2908
        #[allow(clippy::as_conversions)]
2909
0
        let f64_value = value as f64;
2910
0
        Self::new(f64_value)
2911
0
    }
2912
}
2913
2914
impl TryFrom<f64> for Quantity {
2915
    type Error = CommonTypeError;
2916
0
    fn try_from(value: f64) -> Result<Self, Self::Error> {
2917
0
        Self::new(value)
2918
0
    }
2919
}
2920
2921
impl TryFrom<Decimal> for Quantity {
2922
    type Error = CommonTypeError;
2923
3
    fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
2924
3
        let f64_val: f64 =
2925
3
            TryInto::<f64>::try_into(decimal).map_err(|e| CommonTypeError::ConversionError {
2926
0
                message: format!("Failed to convert Decimal to f64: {}", e),
2927
0
            })?;
2928
3
        Self::from_f64(f64_val)
2929
3
    }
2930
}
2931
2932
impl TryFrom<String> for Quantity {
2933
    type Error = CommonTypeError;
2934
0
    fn try_from(s: String) -> Result<Self, Self::Error> {
2935
0
        Self::from_str(&s)
2936
0
    }
2937
}
2938
2939
impl TryFrom<&str> for Quantity {
2940
    type Error = CommonTypeError;
2941
1
    fn try_from(s: &str) -> Result<Self, Self::Error> {
2942
1
        Self::from_str(s)
2943
1
    }
2944
}
2945
2946
impl PartialEq<f64> for Quantity {
2947
    #[allow(clippy::float_arithmetic)]
2948
0
    fn eq(&self, other: &f64) -> bool {
2949
0
        (self.to_f64() - other).abs() < f64::EPSILON
2950
0
    }
2951
}
2952
2953
impl PartialEq<Quantity> for f64 {
2954
    #[allow(clippy::float_arithmetic)]
2955
0
    fn eq(&self, other: &Quantity) -> bool {
2956
0
        (self - other.to_f64()).abs() < f64::EPSILON
2957
0
    }
2958
}
2959
2960
impl PartialOrd<f64> for Quantity {
2961
0
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
2962
0
        self.to_f64().partial_cmp(other)
2963
0
    }
2964
}
2965
2966
impl PartialOrd<Quantity> for f64 {
2967
0
    fn partial_cmp(&self, other: &Quantity) -> Option<std::cmp::Ordering> {
2968
0
        self.partial_cmp(&other.to_f64())
2969
0
    }
2970
}
2971
2972
impl Add for Quantity {
2973
    type Output = Self;
2974
4
    fn add(self, rhs: Self) -> Self::Output {
2975
4
        Self {
2976
4
            value: self.value.saturating_add(rhs.value),
2977
4
        }
2978
4
    }
2979
}
2980
2981
impl Sub for Quantity {
2982
    type Output = Self;
2983
6
    fn sub(self, rhs: Self) -> Self::Output {
2984
6
        Self {
2985
6
            value: self.value.saturating_sub(rhs.value),
2986
6
        }
2987
6
    }
2988
}
2989
2990
impl Mul<f64> for Quantity {
2991
    #[allow(clippy::float_arithmetic)]
2992
    type Output = Result<Self, CommonTypeError>;
2993
1
    fn mul(self, rhs: f64) -> Self::Output {
2994
1
        Self::from_f64(self.to_f64() * rhs)
2995
1
    }
2996
}
2997
2998
impl Div<f64> for Quantity {
2999
    #[allow(clippy::float_arithmetic)]
3000
    type Output = Result<Self, CommonTypeError>;
3001
2
    fn div(self, rhs: f64) -> Self::Output {
3002
2
        if rhs == 0.0_f64 {
3003
1
            return Err(CommonTypeError::ConversionError {
3004
1
                message: "Cannot divide quantity by zero".to_owned(),
3005
1
            });
3006
1
        }
3007
1
        Self::from_f64(self.to_f64() / rhs)
3008
2
    }
3009
}
3010
3011
impl Sum for Quantity {
3012
2
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
3013
6
        
iter2
.
fold2
(Self::ZERO, |acc, x| Self { value: acc.value.saturating_add(x.value) })
3014
2
    }
3015
}
3016
3017
impl<'quantity> Sum<&'quantity Self> for Quantity {
3018
0
    fn sum<I: Iterator<Item = &'quantity Self>>(iter: I) -> Self {
3019
0
        iter.fold(Self::ZERO, |acc, x| Self { value: acc.value.saturating_add(x.value) })
3020
0
    }
3021
}
3022
3023
// =============================================================================
3024
// SQLX IMPLEMENTATIONS FOR FINANCIAL TYPES
3025
// =============================================================================
3026
3027
#[cfg(feature = "database")]
3028
mod sqlx_impls {
3029
    use super::{HftTimestamp, MarketRegime, OrderSide, OrderStatus, OrderType, Price, Quantity};
3030
    use rust_decimal::Decimal as RustDecimal;
3031
    use sqlx::{
3032
        decode::Decode,
3033
        encode::{Encode, IsNull},
3034
        error::BoxDynError,
3035
        postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres},
3036
        Type,
3037
    };
3038
3039
    // SQLx implementations for Price
3040
    impl Type<Postgres> for Price {
3041
0
        fn type_info() -> PgTypeInfo {
3042
0
            PgTypeInfo::with_name("NUMERIC")
3043
0
        }
3044
    }
3045
3046
    impl<'query> Encode<'query, Postgres> for Price {
3047
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3048
            // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
3049
0
            let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8);
3050
0
            decimal_value.encode_by_ref(buf)
3051
0
        }
3052
    }
3053
3054
    impl<'row> Decode<'row, Postgres> for Price {
3055
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3056
            // Decode from NUMERIC to rust_decimal::Decimal
3057
0
            let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
3058
3059
            // Validate scale matches our fixed-point precision (8 decimal places)
3060
0
            if decimal_value.scale() != 8 {
3061
0
                return Err(format!(
3062
0
                    "Invalid scale for Price: expected 8, got {}",
3063
0
                    decimal_value.scale()
3064
0
                )
3065
0
                .into());
3066
0
            }
3067
3068
            // Extract mantissa and convert to our u64 representation
3069
0
            let mantissa = decimal_value.mantissa();
3070
0
            let inner_val = u64::try_from(mantissa)
3071
0
                .map_err(|e| format!("Failed to convert negative or overflowing NUMERIC to Price: {}", e))?;
3072
3073
0
            Ok(Price::from_raw(inner_val))
3074
0
        }
3075
    }
3076
    // SQLx implementations for Quantity
3077
    impl Type<Postgres> for Quantity {
3078
0
        fn type_info() -> PgTypeInfo {
3079
0
            PgTypeInfo::with_name("NUMERIC")
3080
0
        }
3081
    }
3082
3083
    impl<'query> Encode<'query, Postgres> for Quantity {
3084
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3085
            // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
3086
0
            let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8);
3087
0
            decimal_value.encode_by_ref(buf)
3088
0
        }
3089
    }
3090
3091
    impl<'row> Decode<'row, Postgres> for Quantity {
3092
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3093
            // Decode from NUMERIC to rust_decimal::Decimal
3094
0
            let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
3095
3096
            // Validate scale matches our fixed-point precision (8 decimal places)
3097
0
            if decimal_value.scale() != 8 {
3098
0
                return Err(format!(
3099
0
                    "Invalid scale for Quantity: expected 8, got {}",
3100
0
                    decimal_value.scale()
3101
0
                )
3102
0
                .into());
3103
0
            }
3104
3105
            // Extract mantissa and convert to our u64 representation
3106
0
            let mantissa = decimal_value.mantissa();
3107
0
            let inner_val = u64::try_from(mantissa)
3108
0
                .map_err(|e| format!("Failed to convert negative or overflowing NUMERIC to Quantity: {}", e))?;
3109
3110
0
            Ok(Quantity::from_raw(inner_val))
3111
0
        }
3112
    }
3113
3114
    // SQLx implementations for TimeInForce
3115
    impl Type<Postgres> for super::TimeInForce {
3116
0
        fn type_info() -> PgTypeInfo {
3117
0
            PgTypeInfo::with_name("TEXT")
3118
0
        }
3119
    }
3120
3121
    impl<'query> Encode<'query, Postgres> for super::TimeInForce {
3122
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3123
            // Use the Display trait to convert enum to string representation
3124
0
            <&str as Encode<Postgres>>::encode(self.to_string().as_str(), buf)
3125
0
        }
3126
    }
3127
3128
    impl<'row> Decode<'row, Postgres> for super::TimeInForce {
3129
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3130
            // Decode from TEXT to string, then parse to enum
3131
0
            let s = <&str as Decode<Postgres>>::decode(value)?;
3132
0
            match s {
3133
0
                "DAY" => Ok(super::TimeInForce::Day),
3134
0
                "GTC" => Ok(super::TimeInForce::GoodTillCancel),
3135
0
                "IOC" => Ok(super::TimeInForce::ImmediateOrCancel),
3136
0
                "FOK" => Ok(super::TimeInForce::FillOrKill),
3137
0
                _ => Err(format!("Invalid TimeInForce value: {}", s).into()),
3138
            }
3139
0
        }
3140
    }
3141
3142
    // SQLx implementations for OrderStatus
3143
    impl Type<Postgres> for OrderStatus {
3144
0
        fn type_info() -> PgTypeInfo {
3145
0
            PgTypeInfo::with_name("TEXT")
3146
0
        }
3147
    }
3148
3149
    impl<'query> Encode<'query, Postgres> for OrderStatus {
3150
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3151
0
            let value = match self {
3152
0
                OrderStatus::Created => "CREATED",
3153
0
                OrderStatus::Submitted => "SUBMITTED",
3154
0
                OrderStatus::PartiallyFilled => "PARTIALLY_FILLED",
3155
0
                OrderStatus::Filled => "FILLED",
3156
0
                OrderStatus::Rejected => "REJECTED",
3157
0
                OrderStatus::Cancelled => "CANCELLED",
3158
0
                OrderStatus::New => "NEW",
3159
0
                OrderStatus::Expired => "EXPIRED",
3160
0
                OrderStatus::Pending => "PENDING",
3161
0
                OrderStatus::Working => "WORKING",
3162
0
                OrderStatus::Unknown => "UNKNOWN",
3163
0
                OrderStatus::Suspended => "SUSPENDED",
3164
0
                OrderStatus::PendingCancel => "PENDING_CANCEL",
3165
0
                OrderStatus::PendingReplace => "PENDING_REPLACE",
3166
            };
3167
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3168
0
        }
3169
    }
3170
3171
    impl<'row> Decode<'row, Postgres> for OrderStatus {
3172
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3173
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3174
0
            match s.as_str() {
3175
0
                "CREATED" => Ok(OrderStatus::Created),
3176
0
                "SUBMITTED" => Ok(OrderStatus::Submitted),
3177
0
                "PARTIALLY_FILLED" => Ok(OrderStatus::PartiallyFilled),
3178
0
                "FILLED" => Ok(OrderStatus::Filled),
3179
0
                "REJECTED" => Ok(OrderStatus::Rejected),
3180
0
                "CANCELLED" => Ok(OrderStatus::Cancelled),
3181
0
                "NEW" => Ok(OrderStatus::New),
3182
0
                "EXPIRED" => Ok(OrderStatus::Expired),
3183
0
                "PENDING" => Ok(OrderStatus::Pending),
3184
0
                "WORKING" => Ok(OrderStatus::Working),
3185
0
                "UNKNOWN" => Ok(OrderStatus::Unknown),
3186
0
                "SUSPENDED" => Ok(OrderStatus::Suspended),
3187
0
                "PENDING_CANCEL" => Ok(OrderStatus::PendingCancel),
3188
0
                "PENDING_REPLACE" => Ok(OrderStatus::PendingReplace),
3189
0
                _ => Err(format!("Invalid OrderStatus value: {}", s).into()),
3190
            }
3191
0
        }
3192
    }
3193
3194
    // SQLx implementations for OrderSide
3195
    impl Type<Postgres> for OrderSide {
3196
0
        fn type_info() -> PgTypeInfo {
3197
0
            PgTypeInfo::with_name("TEXT")
3198
0
        }
3199
    }
3200
3201
    impl<'query> Encode<'query, Postgres> for OrderSide {
3202
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3203
0
            let value = match self {
3204
0
                OrderSide::Buy => "BUY",
3205
0
                OrderSide::Sell => "SELL",
3206
            };
3207
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3208
0
        }
3209
    }
3210
3211
    impl<'row> Decode<'row, Postgres> for OrderSide {
3212
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3213
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3214
0
            match s.as_str() {
3215
0
                "BUY" => Ok(OrderSide::Buy),
3216
0
                "SELL" => Ok(OrderSide::Sell),
3217
0
                _ => Err(format!("Invalid OrderSide value: {}", s).into()),
3218
            }
3219
0
        }
3220
    }
3221
3222
    // SQLx implementations for OrderType
3223
    impl Type<Postgres> for OrderType {
3224
0
        fn type_info() -> PgTypeInfo {
3225
0
            PgTypeInfo::with_name("TEXT")
3226
0
        }
3227
    }
3228
3229
    impl<'query> Encode<'query, Postgres> for OrderType {
3230
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3231
0
            let value = match self {
3232
0
                OrderType::Market => "MARKET",
3233
0
                OrderType::Limit => "LIMIT",
3234
0
                OrderType::Stop => "STOP",
3235
0
                OrderType::StopLimit => "STOP_LIMIT",
3236
0
                OrderType::Iceberg => "ICEBERG",
3237
0
                OrderType::TrailingStop => "TRAILING_STOP",
3238
0
                OrderType::Hidden => "HIDDEN",
3239
            };
3240
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3241
0
        }
3242
    }
3243
3244
    impl<'row> Decode<'row, Postgres> for OrderType {
3245
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3246
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3247
0
            match s.as_str() {
3248
0
                "MARKET" => Ok(OrderType::Market),
3249
0
                "LIMIT" => Ok(OrderType::Limit),
3250
0
                "STOP" => Ok(OrderType::Stop),
3251
0
                "STOP_LIMIT" => Ok(OrderType::StopLimit),
3252
0
                "ICEBERG" => Ok(OrderType::Iceberg),
3253
0
                "TRAILING_STOP" => Ok(OrderType::TrailingStop),
3254
0
                "HIDDEN" => Ok(OrderType::Hidden),
3255
0
                _ => Err(format!("Invalid OrderType value: {}", s).into()),
3256
            }
3257
0
        }
3258
    }
3259
3260
    // SQLx implementations for MarketRegime
3261
    impl Type<Postgres> for MarketRegime {
3262
0
        fn type_info() -> PgTypeInfo {
3263
0
            PgTypeInfo::with_name("TEXT")
3264
0
        }
3265
    }
3266
3267
    impl<'query> Encode<'query, Postgres> for MarketRegime {
3268
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3269
0
            let value = match self {
3270
0
                MarketRegime::Normal => "NORMAL",
3271
0
                MarketRegime::Crisis => "CRISIS",
3272
0
                MarketRegime::Trending => "TRENDING",
3273
0
                MarketRegime::Sideways => "SIDEWAYS",
3274
0
                MarketRegime::Bull => "BULL",
3275
0
                MarketRegime::Bear => "BEAR",
3276
0
                MarketRegime::HighVolatility => "HIGH_VOLATILITY",
3277
0
                MarketRegime::LowVolatility => "LOW_VOLATILITY",
3278
0
                MarketRegime::Volatile => "VOLATILE",
3279
0
                MarketRegime::Calm => "CALM",
3280
0
                MarketRegime::Unknown => "UNKNOWN",
3281
0
                MarketRegime::Recovery => "RECOVERY",
3282
0
                MarketRegime::Bubble => "BUBBLE",
3283
0
                MarketRegime::Correction => "CORRECTION",
3284
0
                MarketRegime::Custom(id) => {
3285
0
                    return <String as Encode<Postgres>>::encode_by_ref(
3286
0
                        &format!("CUSTOM_{}", id),
3287
0
                        buf,
3288
                    )
3289
                },
3290
            };
3291
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3292
0
        }
3293
    }
3294
3295
    impl<'row> Decode<'row, Postgres> for MarketRegime {
3296
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3297
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3298
0
            match s.as_str() {
3299
0
                "NORMAL" => Ok(MarketRegime::Normal),
3300
0
                "CRISIS" => Ok(MarketRegime::Crisis),
3301
0
                "TRENDING" => Ok(MarketRegime::Trending),
3302
0
                "SIDEWAYS" => Ok(MarketRegime::Sideways),
3303
0
                "BULL" => Ok(MarketRegime::Bull),
3304
0
                "BEAR" => Ok(MarketRegime::Bear),
3305
0
                "HIGH_VOLATILITY" => Ok(MarketRegime::HighVolatility),
3306
0
                "LOW_VOLATILITY" => Ok(MarketRegime::LowVolatility),
3307
0
                "VOLATILE" => Ok(MarketRegime::Volatile),
3308
0
                "CALM" => Ok(MarketRegime::Calm),
3309
0
                "UNKNOWN" => Ok(MarketRegime::Unknown),
3310
0
                "RECOVERY" => Ok(MarketRegime::Recovery),
3311
0
                "BUBBLE" => Ok(MarketRegime::Bubble),
3312
0
                "CORRECTION" => Ok(MarketRegime::Correction),
3313
                _ => {
3314
                    // Handle Custom(id) format
3315
0
                    if let Some(id_str) = s.strip_prefix("CUSTOM_") {
3316
0
                        if let Ok(id) = id_str.parse::<usize>() {
3317
0
                            Ok(MarketRegime::Custom(id))
3318
                        } else {
3319
0
                            Err(format!("Invalid MarketRegime Custom ID: {}", id_str).into())
3320
                        }
3321
                    } else {
3322
0
                        Err(format!("Invalid MarketRegime value: {}", s).into())
3323
                    }
3324
                },
3325
            }
3326
0
        }
3327
    }
3328
3329
    // SQLx implementations for HftTimestamp
3330
    // Maps to PostgreSQL BIGINT (stores nanoseconds since Unix epoch)
3331
    // Note: Limited to i64::MAX nanoseconds (year 2262) due to PostgreSQL BIGINT constraints
3332
    impl<'query> Encode<'query, Postgres> for HftTimestamp {
3333
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3334
            // Cast u64 to i64 for PostgreSQL BIGINT compatibility
3335
0
            <i64 as Encode<Postgres>>::encode(i64::try_from(self.nanos()).unwrap_or(0), buf)
3336
0
        }
3337
    }
3338
3339
    impl<'row> Decode<'row, Postgres> for HftTimestamp {
3340
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3341
0
            let val = <i64 as Decode<Postgres>>::decode(value)?;
3342
            // Cast i64 back to u64 for internal representation
3343
0
            Ok(HftTimestamp::from_nanos(u64::try_from(val).unwrap_or(0)))
3344
0
        }
3345
    }
3346
3347
    impl Type<Postgres> for HftTimestamp {
3348
0
        fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
3349
0
            <i64 as Type<Postgres>>::type_info()
3350
0
        }
3351
3352
0
        fn compatible(ty: &<Postgres as sqlx::Database>::TypeInfo) -> bool {
3353
0
            <i64 as Type<Postgres>>::compatible(ty)
3354
0
        }
3355
    }
3356
3357
    // SQLx implementations for OrderId (uses BIGINT for u64)
3358
    impl Type<Postgres> for super::OrderId {
3359
0
        fn type_info() -> PgTypeInfo {
3360
0
            PgTypeInfo::with_name("BIGINT")
3361
0
        }
3362
    }
3363
3364
    impl<'query> Encode<'query, Postgres> for super::OrderId {
3365
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3366
0
            <i64 as Encode<Postgres>>::encode_by_ref(&(i64::try_from(self.value()).unwrap_or(0)), buf)
3367
0
        }
3368
    }
3369
3370
    impl<'row> Decode<'row, Postgres> for super::OrderId {
3371
0
        fn decode(value: PgValueRef<'row>) -> Result<Self, BoxDynError> {
3372
0
            let id = <i64 as Decode<Postgres>>::decode(value)?;
3373
0
            Ok(super::OrderId::from_u64(u64::try_from(id).unwrap_or(0)))
3374
0
        }
3375
    }
3376
}
3377
3378
/// Volume type - alias for Quantity with the same fixed-point arithmetic
3379
///
3380
/// `SQLx` traits are automatically inherited from Quantity
3381
pub type Volume = Quantity;
3382
3383
// ORDER TYPES ALREADY DEFINED ABOVE - No need to re-export from trading_engine
3384
// =============================================================================
3385
// CORE ID TYPES (MOVED FROM TRADING_ENGINE)
3386
// =============================================================================
3387
3388
/// Order identifier with ultra-fast atomic generation
3389
///
3390
/// Replaces slow UUID generation (1ms+) with atomic increment (~5ns)
3391
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3392
pub struct OrderId(u64);
3393
3394
impl Default for OrderId {
3395
0
    fn default() -> Self {
3396
0
        Self::new()
3397
0
    }
3398
}
3399
3400
impl OrderId {
3401
    /// Generate next `OrderId` using atomic counter - <50ns performance
3402
1.01k
    pub fn new() -> Self {
3403
        use std::sync::atomic::{AtomicU64, Ordering};
3404
        static COUNTER: AtomicU64 = AtomicU64::new(1);
3405
1.01k
        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
3406
1.01k
    }
3407
3408
    /// Create `OrderId` from u64 value
3409
    #[must_use]
3410
2
    pub const fn from_u64(value: u64) -> Self {
3411
2
        Self(value)
3412
2
    }
3413
3414
    /// Get u64 value
3415
    #[must_use]
3416
8
    pub const fn value(&self) -> u64 {
3417
8
        self.0
3418
8
    }
3419
3420
    /// Get u64 value for performance-critical code (alias for value)
3421
    #[must_use]
3422
1
    pub const fn as_u64(&self) -> u64 {
3423
1
        self.0
3424
1
    }
3425
3426
    /// Get as string for compatibility
3427
    #[must_use]
3428
0
    pub fn as_str(&self) -> String {
3429
0
        self.0.to_string()
3430
0
    }
3431
}
3432
3433
impl fmt::Display for OrderId {
3434
    /// Format the order ID for display
3435
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3436
1
        write!(f, "{}", self.0)
3437
1
    }
3438
}
3439
3440
impl From<u64> for OrderId {
3441
    /// Create an `OrderId` from a u64 value
3442
0
    fn from(value: u64) -> Self {
3443
0
        Self(value)
3444
0
    }
3445
}
3446
3447
impl From<OrderId> for u64 {
3448
    /// Convert an `OrderId` to u64
3449
0
    fn from(order_id: OrderId) -> Self {
3450
0
        order_id.0
3451
0
    }
3452
}
3453
3454
impl FromStr for OrderId {
3455
    type Err = ParseIntError;
3456
3457
2
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3458
2
        s.parse::<u64>().map(OrderId)
3459
2
    }
3460
}
3461
3462
impl From<String> for OrderId {
3463
    /// Create an `OrderId` from a String, generating new ID if parsing fails
3464
2
    fn from(s: String) -> Self {
3465
2
        s.parse().unwrap_or_else(|_| 
Self::new1
())
3466
2
    }
3467
}
3468
3469
impl From<&str> for OrderId {
3470
    /// Create an `OrderId` from a &str, generating new ID if parsing fails
3471
0
    fn from(s: &str) -> Self {
3472
0
        s.parse().unwrap_or_else(|_| Self::new())
3473
0
    }
3474
}
3475
3476
/// Execution identifier with validation
3477
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3478
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3479
pub struct ExecutionId(String);
3480
3481
impl ExecutionId {
3482
    /// Create a new execution ID with validation
3483
    ///
3484
    /// # Errors
3485
    /// Returns error if the operation fails
3486
3
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3487
3
        let id = id.into();
3488
3
        if id.trim().is_empty() {
3489
2
            return Err(CommonTypeError::ValidationError {
3490
2
                field: "execution_id".to_owned(),
3491
2
                reason: "Execution ID cannot be empty".to_owned(),
3492
2
            });
3493
1
        }
3494
1
        Ok(Self(id))
3495
3
    }
3496
3497
    /// Generate a new random execution ID
3498
1
    pub fn generate() -> Self {
3499
1
        Self(uuid::Uuid::new_v4().to_string())
3500
1
    }
3501
3502
    /// Get execution ID as string slice
3503
3
    pub fn as_str(&self) -> &str {
3504
3
        &self.0
3505
3
    }
3506
3507
    /// Convert execution ID into owned string
3508
0
    pub fn into_string(self) -> String {
3509
0
        self.0
3510
0
    }
3511
}
3512
3513
impl fmt::Display for ExecutionId {
3514
    /// Format the execution ID for display
3515
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3516
0
        write!(f, "{}", self.0)
3517
0
    }
3518
}
3519
3520
impl FromStr for ExecutionId {
3521
    type Err = CommonTypeError;
3522
3523
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3524
0
        Self::new(s)
3525
0
    }
3526
}
3527
3528
/// Trade identifier with validation
3529
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3530
pub struct TradeId(String);
3531
3532
impl TradeId {
3533
    /// Create a new trade ID with validation
3534
    ///
3535
    /// # Errors
3536
    /// Returns error if the operation fails
3537
2
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3538
2
        let id = id.into();
3539
2
        if id.is_empty() {
3540
1
            return Err(CommonTypeError::ValidationError {
3541
1
                field: "trade_id".to_owned(),
3542
1
                reason: "Trade ID cannot be empty".to_owned(),
3543
1
            });
3544
1
        }
3545
1
        Ok(Self(id))
3546
2
    }
3547
3548
    /// Get the trade ID as a string slice
3549
1
    pub fn as_str(&self) -> &str {
3550
1
        &self.0
3551
1
    }
3552
    /// Convert the trade ID into an owned string
3553
0
    pub fn into_string(self) -> String {
3554
0
        self.0
3555
0
    }
3556
}
3557
3558
impl fmt::Display for TradeId {
3559
    /// Format the trade ID for display
3560
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3561
0
        write!(f, "{}", self.0)
3562
0
    }
3563
}
3564
3565
/// Trading symbol with validation
3566
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3567
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3568
pub struct Symbol {
3569
    value: String,
3570
}
3571
3572
impl Symbol {
3573
    /// Create a new symbol from a string
3574
    #[must_use]
3575
20
    pub const fn new(s: String) -> Self {
3576
20
        Self { value: s }
3577
20
    }
3578
3579
    /// Create a new Symbol with validation
3580
    ///
3581
    /// # Errors
3582
    /// Returns error if the operation fails
3583
6
    pub fn new_validated(s: String) -> Result<Self, CommonTypeError> {
3584
6
        if s.trim().is_empty() {
3585
4
            return Err(CommonTypeError::ValidationError {
3586
4
                field: "symbol".to_owned(),
3587
4
                reason: "Symbol cannot be empty".to_owned(),
3588
4
            });
3589
2
        }
3590
2
        Ok(Self { value: s })
3591
6
    }
3592
3593
    /// Create a Symbol from &str with validation
3594
    ///
3595
    /// # Errors
3596
    /// Returns error if the operation fails
3597
0
    pub fn from_str_validated(s: &str) -> Result<Self, CommonTypeError> {
3598
0
        Self::new_validated(s.to_owned())
3599
0
    }
3600
3601
    /// Get the symbol as a string slice
3602
    #[must_use]
3603
7
    pub fn as_str(&self) -> &str {
3604
7
        &self.value
3605
7
    }
3606
    /// Get the symbol value as a string slice
3607
    #[must_use]
3608
0
    pub fn value(&self) -> &str {
3609
0
        &self.value
3610
0
    }
3611
    /// Get the symbol as bytes
3612
    #[must_use]
3613
0
    pub fn as_bytes(&self) -> &[u8] {
3614
0
        self.value.as_bytes()
3615
0
    }
3616
    /// Check if the symbol is empty
3617
    #[must_use]
3618
2
    pub fn is_empty(&self) -> bool {
3619
2
        self.value.is_empty()
3620
2
    }
3621
    /// Convert the symbol to uppercase
3622
    #[must_use]
3623
2
    pub fn to_uppercase(&self) -> String {
3624
2
        self.value.to_uppercase()
3625
2
    }
3626
    /// Replace occurrences in the symbol
3627
    #[must_use]
3628
2
    pub fn replace(&self, from: &str, to: &str) -> String {
3629
2
        self.value.replace(from, to)
3630
2
    }
3631
3632
    /// Helper for risk management - creates a 'NONE' symbol
3633
    #[must_use]
3634
1
    pub fn none() -> Self {
3635
1
        "NONE".parse().unwrap()
3636
1
    }
3637
3638
    /// Check if the symbol contains a pattern
3639
    #[must_use]
3640
4
    pub fn contains(&self, pattern: &str) -> bool {
3641
4
        self.value.contains(pattern)
3642
4
    }
3643
}
3644
3645
impl FromStr for Symbol {
3646
    type Err = std::convert::Infallible;
3647
3648
6
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3649
6
        Ok(Self {
3650
6
            value: s.to_owned(),
3651
6
        })
3652
6
    }
3653
}
3654
3655
// Additional implementation to support conversion from &Symbol to &str
3656
impl AsRef<str> for Symbol {
3657
    /// Convert symbol to string reference
3658
0
    fn as_ref(&self) -> &str {
3659
0
        &self.value
3660
0
    }
3661
}
3662
3663
impl fmt::Display for Symbol {
3664
    /// Format the symbol for display
3665
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3666
0
        write!(f, "{}", self.value)
3667
0
    }
3668
}
3669
3670
impl From<String> for Symbol {
3671
    /// Create a Symbol from a String
3672
0
    fn from(s: String) -> Self {
3673
0
        Self::new(s)
3674
0
    }
3675
}
3676
impl From<&str> for Symbol {
3677
    /// Create a Symbol from a &str
3678
19
    fn from(s: &str) -> Self {
3679
19
        Self::new(s.to_owned())
3680
19
    }
3681
}
3682
3683
// TryFrom implementations removed due to conflicting blanket implementations
3684
// Use Symbol::new_validated() or Symbol::from_validated() directly instead
3685
3686
impl Default for Symbol {
3687
    /// Returns the default symbol (empty string)
3688
0
    fn default() -> Self {
3689
0
        Self::new(String::new())
3690
0
    }
3691
}
3692
3693
impl PartialEq<str> for Symbol {
3694
0
    fn eq(&self, other: &str) -> bool {
3695
0
        self.value == other
3696
0
    }
3697
}
3698
3699
impl PartialEq<&str> for Symbol {
3700
2
    fn eq(&self, other: &&str) -> bool {
3701
2
        self.value == *other
3702
2
    }
3703
}
3704
3705
impl PartialEq<String> for Symbol {
3706
1
    fn eq(&self, other: &String) -> bool {
3707
1
        &self.value == other
3708
1
    }
3709
}
3710
3711
impl PartialEq<Symbol> for &str {
3712
2
    fn eq(&self, other: &Symbol) -> bool {
3713
2
        *self == other.value
3714
2
    }
3715
}
3716
3717
impl PartialEq<Symbol> for String {
3718
1
    fn eq(&self, other: &Symbol) -> bool {
3719
1
        self == &other.value
3720
1
    }
3721
}
3722
3723
// TimeInForce moved to canonical source: common::types::TimeInForce
3724
3725
// Currency moved to canonical source: common::types::Currency
3726
3727
// Price moved to canonical source: common::types::Price
3728
3729
// Quantity moved to canonical source: common::types::Quantity
3730
// Volume moved to canonical source: common::types::Quantity (as Volume alias)
3731
3732
/// Money amount with currency
3733
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3734
pub struct Money {
3735
    /// The monetary amount
3736
    pub amount: Decimal,
3737
    /// The currency of the amount
3738
    pub currency: Currency,
3739
}
3740
3741
impl Money {
3742
    /// Create new money amount
3743
3
    pub const fn new(amount: Decimal, currency: Currency) -> Self {
3744
3
        Self { amount, currency }
3745
3
    }
3746
}
3747
3748
impl fmt::Display for Money {
3749
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3750
2
        write!(f, "{} {}", self.amount, self.currency)
3751
2
    }
3752
}
3753
3754
// OrderId moved to canonical source: common::types::OrderId
3755
3756
// TradeId moved to canonical source: common::types::TradeId
3757
3758
// Symbol moved to canonical source: common::types::Symbol
3759
3760
/// Type-safe account identifier
3761
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3762
pub struct AccountId(String);
3763
3764
impl AccountId {
3765
    /// Create a new account ID with validation
3766
    ///
3767
    /// # Errors
3768
    /// Returns error if the operation fails
3769
3
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3770
3
        let id = id.into();
3771
3
        if id.trim().is_empty() {
3772
2
            return Err(CommonTypeError::InvalidIdentifier {
3773
2
                field: "account_id".to_owned(),
3774
2
                reason: "Account ID cannot be empty".to_owned(),
3775
2
            });
3776
1
        }
3777
1
        Ok(Self(id))
3778
3
    }
3779
3780
    /// Get the ID as a string slice
3781
0
    pub fn as_str(&self) -> &str {
3782
0
        &self.0
3783
0
    }
3784
3785
    /// Convert to owned String
3786
0
    pub fn into_string(self) -> String {
3787
0
        self.0
3788
0
    }
3789
}
3790
3791
impl fmt::Display for AccountId {
3792
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3793
0
        write!(f, "{}", self.0)
3794
0
    }
3795
}
3796
3797
/// High-precision timestamp for HFT applications - CANONICAL DEFINITION
3798
///
3799
/// Robust implementation with error handling for financial safety
3800
#[derive(
3801
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
3802
)]
3803
pub struct HftTimestamp {
3804
    nanos: u64,
3805
}
3806
3807
impl HftTimestamp {
3808
    /// Get current timestamp with error handling for financial safety
3809
    ///
3810
    /// # Errors
3811
    /// Returns error if the operation fails
3812
26
    pub fn now() -> Result<Self, CommonError> {
3813
        use std::time::{SystemTime, UNIX_EPOCH};
3814
26
        let nanos = SystemTime::now()
3815
26
            .duration_since(UNIX_EPOCH)
3816
26
            .map_err(|e| CommonError::Service {
3817
0
                category: CommonErrorCategory::System,
3818
0
                message: format!("System time before UNIX epoch: {e}"),
3819
0
            })?
3820
26
            .as_nanos().try_into().unwrap_or(0_u64);
3821
26
        Ok(Self { nanos })
3822
26
    }
3823
3824
    /// Get current timestamp with error handling for financial safety (`CommonTypeError` version)
3825
    ///
3826
    /// # Errors
3827
    /// Returns error if the operation fails
3828
1
    pub fn now_common() -> Result<Self, CommonTypeError> {
3829
        use std::time::{SystemTime, UNIX_EPOCH};
3830
1
        let nanos = SystemTime::now()
3831
1
            .duration_since(UNIX_EPOCH)
3832
1
            .map_err(|e| CommonTypeError::ConversionError {
3833
0
                message: format!("System time before UNIX epoch: {e}"),
3834
0
            })?
3835
1
            .as_nanos().try_into().unwrap_or(0_u64);
3836
1
        Ok(Self { nanos })
3837
1
    }
3838
3839
    /// Get current timestamp or zero if system time is invalid
3840
    #[must_use]
3841
25
    pub fn now_or_zero() -> Self {
3842
25
        Self::now().unwrap_or(Self { nanos: 0 })
3843
25
    }
3844
3845
    /// Get nanoseconds since epoch
3846
    #[must_use]
3847
4
    pub const fn nanos(self) -> u64 {
3848
4
        self.nanos
3849
4
    }
3850
3851
    #[allow(clippy::as_conversions)]
3852
    /// Create from nanoseconds since epoch
3853
    #[must_use]
3854
2
    pub const fn from_nanos(nanos: u64) -> Self {
3855
2
        Self { nanos }
3856
2
    }
3857
3858
    /// Create from signed nanoseconds (cast to unsigned)
3859
    #[must_use]
3860
    #[allow(clippy::as_conversions)]
3861
0
    pub const fn from_nanos_i64(nanos: i64) -> Self {
3862
0
        Self {
3863
0
            nanos: nanos as u64,
3864
0
        }
3865
0
    }
3866
3867
    /// Get nanoseconds since epoch
3868
0
    pub const fn as_nanos(&self) -> u64 {
3869
0
        self.nanos
3870
0
    }
3871
3872
    /// Convert to `DateTime<Utc>`
3873
    #[allow(clippy::integer_division)]
3874
1
    pub fn to_datetime(&self) -> DateTime<Utc> {
3875
1
        let secs = self.nanos / 1_000_000_000;
3876
1
        let nsecs = u32::try_from(self.nanos % 1_000_000_000).unwrap_or(0);
3877
1
        DateTime::from_timestamp(i64::try_from(secs).unwrap_or(0), nsecs).unwrap_or_default()
3878
1
    }
3879
}
3880
3881
impl fmt::Display for HftTimestamp {
3882
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3883
0
        write!(f, "{}", self.to_datetime())
3884
0
    }
3885
}
3886
3887
/// Generic timestamp for general use cases
3888
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3889
pub struct GenericTimestamp {
3890
    nanos: u64,
3891
}
3892
3893
impl GenericTimestamp {
3894
    /// Create from nanoseconds since epoch
3895
    #[must_use]
3896
0
    pub const fn from_nanos(nanos: u64) -> Self {
3897
0
        Self { nanos }
3898
0
    }
3899
3900
    /// Get nanoseconds since epoch
3901
    #[must_use]
3902
0
    pub const fn nanos(&self) -> u64 {
3903
0
        self.nanos
3904
0
    }
3905
}
3906
3907
// =============================================================================
3908
// MARKET TYPES (MIGRATED FROM TRADING_ENGINE)
3909
// =============================================================================
3910
3911
/// Market regime enumeration for position sizing scaling and risk management
3912
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3913
pub enum MarketRegime {
3914
    /// Normal market conditions
3915
    Normal,
3916
    /// Crisis/stress market conditions
3917
    Crisis,
3918
    /// Trending market (strong directional movement)
3919
    Trending,
3920
    /// Sideways/ranging market (low volatility)
3921
    Sideways,
3922
    /// Bull market (sustained upward trend)
3923
    Bull,
3924
    /// Bear market (sustained downward trend)
3925
    Bear,
3926
    /// High volatility market conditions
3927
    HighVolatility,
3928
    /// Low volatility market conditions
3929
    LowVolatility,
3930
    /// Volatile market conditions (alias for `HighVolatility`)
3931
    Volatile,
3932
    /// Calm market conditions (alias for `LowVolatility`)
3933
    Calm,
3934
    /// Unknown/unclassified regime
3935
    Unknown,
3936
    /// Recovery regime - transitioning from crisis
3937
    Recovery,
3938
    /// Bubble regime - unsustainable upward movement
3939
    Bubble,
3940
    /// Correction regime - temporary downward adjustment
3941
    Correction,
3942
    /// Custom regime with numeric identifier
3943
    Custom(usize),
3944
}
3945
3946
impl Default for MarketRegime {
3947
0
    fn default() -> Self {
3948
0
        Self::Normal
3949
0
    }
3950
}
3951
3952
impl fmt::Display for MarketRegime {
3953
6
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3954
6
        match self {
3955
1
            Self::Normal => write!(f, "Normal"),
3956
1
            Self::Crisis => write!(f, "Crisis"),
3957
0
            Self::Trending => write!(f, "Trending"),
3958
0
            Self::Sideways => write!(f, "Sideways"),
3959
1
            Self::Bull => write!(f, "Bull"),
3960
1
            Self::Bear => write!(f, "Bear"),
3961
1
            Self::HighVolatility => write!(f, "HighVolatility"),
3962
0
            Self::LowVolatility => write!(f, "LowVolatility"),
3963
0
            Self::Volatile => write!(f, "Volatile"),
3964
0
            Self::Calm => write!(f, "Calm"),
3965
0
            Self::Unknown => write!(f, "Unknown"),
3966
0
            Self::Recovery => write!(f, "Recovery"),
3967
0
            Self::Bubble => write!(f, "Bubble"),
3968
0
            Self::Correction => write!(f, "Correction"),
3969
1
            Self::Custom(id) => write!(f, "Custom({id})"),
3970
        }
3971
6
    }
3972
}
3973
3974
/// Tick type enumeration for market data
3975
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3976
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3977
#[cfg_attr(
3978
    feature = "database",
3979
    sqlx(type_name = "tick_type", rename_all = "snake_case")
3980
)]
3981
pub enum TickType {
3982
    /// Trade execution tick
3983
    Trade,
3984
    /// Bid price update tick
3985
    Bid,
3986
    /// Ask price update tick
3987
    Ask,
3988
    /// Quote (bid/ask) update tick
3989
    Quote,
3990
}
3991
3992
/// Exchange enumeration for trading venues
3993
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3994
pub enum Exchange {
3995
    /// New York Stock Exchange
3996
    NYSE,
3997
    /// NASDAQ
3998
    NASDAQ,
3999
    /// Chicago Mercantile Exchange
4000
    CME,
4001
    /// Intercontinental Exchange
4002
    ICE,
4003
    /// London Stock Exchange
4004
    LSE,
4005
    /// Tokyo Stock Exchange
4006
    TSE,
4007
    /// Hong Kong Stock Exchange
4008
    HKEX,
4009
    /// Shanghai Stock Exchange
4010
    SSE,
4011
    /// Shenzhen Stock Exchange
4012
    SZSE,
4013
    /// Euronext
4014
    EURONEXT,
4015
    /// Deutsche Börse
4016
    XETRA,
4017
    /// Chicago Board of Trade
4018
    CBOT,
4019
    /// Chicago Board Options Exchange
4020
    CBOE,
4021
    /// BATS Global Markets
4022
    BATS,
4023
    /// IEX Exchange
4024
    IEX,
4025
    /// Interactive Brokers
4026
    IBKR,
4027
    /// IC Markets
4028
    ICMARKETS,
4029
    /// Forex.com
4030
    FOREX,
4031
    /// Binance
4032
    BINANCE,
4033
    /// Coinbase
4034
    COINBASE,
4035
    /// Kraken
4036
    KRAKEN,
4037
    /// Unknown or unrecognized exchange
4038
    UNKNOWN,
4039
}
4040
4041
impl Default for Exchange {
4042
0
    fn default() -> Self {
4043
0
        Self::UNKNOWN
4044
0
    }
4045
}
4046
4047
impl fmt::Display for Exchange {
4048
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4049
0
        match self {
4050
0
            Self::NYSE => write!(f, "NYSE"),
4051
0
            Self::NASDAQ => write!(f, "NASDAQ"),
4052
0
            Self::CME => write!(f, "CME"),
4053
0
            Self::ICE => write!(f, "ICE"),
4054
0
            Self::LSE => write!(f, "LSE"),
4055
0
            Self::TSE => write!(f, "TSE"),
4056
0
            Self::HKEX => write!(f, "HKEX"),
4057
0
            Self::SSE => write!(f, "SSE"),
4058
0
            Self::SZSE => write!(f, "SZSE"),
4059
0
            Self::EURONEXT => write!(f, "EURONEXT"),
4060
0
            Self::XETRA => write!(f, "XETRA"),
4061
0
            Self::CBOT => write!(f, "CBOT"),
4062
0
            Self::CBOE => write!(f, "CBOE"),
4063
0
            Self::BATS => write!(f, "BATS"),
4064
0
            Self::IEX => write!(f, "IEX"),
4065
0
            Self::IBKR => write!(f, "IBKR"),
4066
0
            Self::ICMARKETS => write!(f, "ICMARKETS"),
4067
0
            Self::FOREX => write!(f, "FOREX"),
4068
0
            Self::BINANCE => write!(f, "BINANCE"),
4069
0
            Self::COINBASE => write!(f, "COINBASE"),
4070
0
            Self::KRAKEN => write!(f, "KRAKEN"),
4071
0
            Self::UNKNOWN => write!(f, "UNKNOWN"),
4072
        }
4073
0
    }
4074
}
4075
4076
impl FromStr for Exchange {
4077
    type Err = CommonTypeError;
4078
4079
4
    fn from_str(s: &str) -> Result<Self, Self::Err> {
4080
4
        match s.to_uppercase().as_str() {
4081
4
            "NYSE" => 
Ok(Self::NYSE)1
,
4082
3
            "NASDAQ" => 
Ok(Self::NASDAQ)2
,
4083
1
            "CME" => 
Ok(Self::CME)0
,
4084
1
            "ICE" => 
Ok(Self::ICE)0
,
4085
1
            "LSE" => 
Ok(Self::LSE)0
,
4086
1
            "TSE" => 
Ok(Self::TSE)0
,
4087
1
            "HKEX" => 
Ok(Self::HKEX)0
,
4088
1
            "SSE" => 
Ok(Self::SSE)0
,
4089
1
            "SZSE" => 
Ok(Self::SZSE)0
,
4090
1
            "EURONEXT" => 
Ok(Self::EURONEXT)0
,
4091
1
            "XETRA" => 
Ok(Self::XETRA)0
,
4092
1
            "CBOT" => 
Ok(Self::CBOT)0
,
4093
1
            "CBOE" => 
Ok(Self::CBOE)0
,
4094
1
            "BATS" => 
Ok(Self::BATS)0
,
4095
1
            "IEX" => 
Ok(Self::IEX)0
,
4096
1
            "IBKR" => 
Ok(Self::IBKR)0
,
4097
1
            "ICMARKETS" => 
Ok(Self::ICMARKETS)0
,
4098
1
            "FOREX" => 
Ok(Self::FOREX)0
,
4099
1
            "BINANCE" => 
Ok(Self::BINANCE)0
,
4100
1
            "COINBASE" => 
Ok(Self::COINBASE)0
,
4101
1
            "KRAKEN" => 
Ok(Self::KRAKEN)0
,
4102
1
            "UNKNOWN" => 
Ok(Self::UNKNOWN)0
,
4103
1
            _ => Ok(Self::UNKNOWN), // Default to UNKNOWN for unrecognized exchanges
4104
        }
4105
4
    }
4106
}
4107
4108
/// Market tick data structure - CANONICAL SINGLE SOURCE OF TRUTH
4109
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4110
pub struct MarketTick {
4111
    /// Trading symbol
4112
    pub symbol: Symbol,
4113
    /// Tick price
4114
    pub price: Price,
4115
    /// Tick size/quantity
4116
    pub size: Quantity,
4117
    /// Tick timestamp
4118
    pub timestamp: HftTimestamp,
4119
    /// Type of tick (trade, bid, ask, quote)
4120
    pub tick_type: TickType,
4121
    /// Exchange where the tick occurred
4122
    pub exchange: Exchange,
4123
    /// Sequence number for ordering
4124
    pub sequence_number: u64,
4125
}
4126
4127
impl MarketTick {
4128
    /// Create a new market tick with current timestamp
4129
    ///
4130
    /// # Errors
4131
    /// Returns error if the operation fails
4132
0
    pub fn new(
4133
0
        symbol: Symbol,
4134
0
        price: Price,
4135
0
        size: Quantity,
4136
0
        tick_type: TickType,
4137
0
        exchange: Exchange,
4138
0
        sequence_number: u64,
4139
0
    ) -> Result<Self, CommonError> {
4140
        Ok(Self {
4141
0
            symbol,
4142
0
            price,
4143
0
            size,
4144
0
            timestamp: HftTimestamp::now()?,
4145
0
            tick_type,
4146
0
            exchange,
4147
0
            sequence_number,
4148
        })
4149
0
    }
4150
4151
    /// Create a new market tick with specified timestamp (for backtesting)
4152
    #[must_use]
4153
0
    pub const fn with_timestamp(
4154
0
        symbol: Symbol,
4155
0
        price: Price,
4156
0
        size: Quantity,
4157
0
        timestamp: HftTimestamp,
4158
0
        tick_type: TickType,
4159
0
        exchange: Exchange,
4160
0
        sequence_number: u64,
4161
0
    ) -> Self {
4162
0
        Self {
4163
0
            symbol,
4164
0
            price,
4165
0
            size,
4166
0
            timestamp,
4167
0
            tick_type,
4168
0
            exchange,
4169
0
            sequence_number,
4170
0
        }
4171
0
    }
4172
}
4173
4174
/// Trading signal for algorithmic trading
4175
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4176
pub struct TradingSignal {
4177
    /// Signal ID
4178
    pub signal_id: Uuid,
4179
    /// Symbol this signal applies to
4180
    pub symbol: Symbol,
4181
    /// Signal strength (-1.0 to 1.0)
4182
    pub strength: f64,
4183
    /// Signal direction
4184
    pub direction: OrderSide,
4185
    /// Confidence level (0.0 to 1.0)
4186
    pub confidence: f64,
4187
    /// Signal generation timestamp
4188
    pub timestamp: HftTimestamp,
4189
    /// Signal source/strategy
4190
    pub source: String,
4191
    /// Additional metadata
4192
    pub metadata: std::collections::HashMap<String, String>,
4193
}
4194
4195
impl TradingSignal {
4196
    /// Create a new trading signal
4197
    ///
4198
    /// # Errors
4199
    /// Returns error if the operation fails
4200
3
    pub fn new(
4201
3
        symbol: Symbol,
4202
3
        strength: f64,
4203
3
        direction: OrderSide,
4204
3
        confidence: f64,
4205
3
        source: String,
4206
3
    ) -> Result<Self, CommonTypeError> {
4207
3
        if !(0.0_f64..=1.0_f64).contains(&confidence) {
4208
1
            return Err(CommonTypeError::ValidationError {
4209
1
                field: "confidence".to_owned(),
4210
1
                reason: "Confidence must be between 0.0 and 1.0".to_owned(),
4211
1
            });
4212
2
        }
4213
2
        if !(-1.0_f64..=1.0_f64).contains(&strength) {
4214
1
            return Err(CommonTypeError::ValidationError {
4215
1
                field: "strength".to_owned(),
4216
1
                reason: "Strength must be between -1.0 and 1.0".to_owned(),
4217
1
            });
4218
1
        }
4219
4220
        Ok(Self {
4221
1
            signal_id: Uuid::new_v4(),
4222
1
            symbol,
4223
1
            strength,
4224
1
            direction,
4225
1
            confidence,
4226
1
            timestamp: HftTimestamp::now_common()
?0
,
4227
1
            source,
4228
1
            metadata: std::collections::HashMap::new(),
4229
        })
4230
3
    }
4231
4232
    /// Add metadata to the signal
4233
    #[must_use]
4234
0
    pub fn with_metadata(mut self, key: String, value: String) -> Self {
4235
0
        self.metadata.insert(key, value);
4236
0
        self
4237
0
    }
4238
}
4239
4240
// =============================================================================
4241
// HIGH-PERFORMANCE TYPES FOR COPY/CLONE OPTIMIZATION
4242
// =============================================================================
4243
4244
/// Lightweight Order reference for high-performance contexts requiring Copy trait
4245
///
4246
/// This struct contains only the essential order data needed for performance-critical
4247
/// operations like `SmallBatchRing` processing, while maintaining Copy semantics.
4248
///
4249
/// For full order details, use the complete Order struct.
4250
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4251
pub struct OrderRef {
4252
    /// Order ID (u64 for performance)
4253
    pub id: u64,
4254
    /// Symbol hash for fast lookups
4255
    pub symbol_hash: i64,
4256
    /// Order side (Buy/Sell)
4257
    pub side: OrderSide,
4258
    /// Order type
4259
    pub order_type: OrderType,
4260
    /// Quantity (fixed-point u64)
4261
    pub quantity: u64,
4262
    /// Price (fixed-point u64, 0 for market orders)
4263
    pub price: u64,
4264
    /// Timestamp (nanoseconds since epoch)
4265
    pub timestamp: u64,
4266
}
4267
4268
impl OrderRef {
4269
    /// Create `OrderRef` from a full Order struct
4270
    #[must_use]
4271
1
    pub fn from_order(order: &Order) -> Self {
4272
        Self {
4273
1
            id: order.id.value(),
4274
1
            symbol_hash: order.symbol_hash(),
4275
1
            side: order.side,
4276
1
            order_type: order.order_type,
4277
1
            quantity: order.quantity.raw_value(),
4278
1
            price: order.price.map_or(0, |p| p.raw_value()),
4279
1
            timestamp: order.created_at.nanos(),
4280
        }
4281
1
    }
4282
4283
    /// Create a limit order reference
4284
    #[must_use]
4285
0
    pub fn limit(symbol_hash: i64, side: OrderSide, quantity: u64, price: u64) -> Self {
4286
0
        Self {
4287
0
            id: OrderId::new().value(),
4288
0
            symbol_hash,
4289
0
            side,
4290
0
            order_type: OrderType::Limit,
4291
0
            quantity,
4292
0
            price,
4293
0
            timestamp: HftTimestamp::now_or_zero().nanos(),
4294
0
        }
4295
0
    }
4296
4297
    /// Create a market order reference  
4298
    #[must_use]
4299
0
    pub fn market(symbol_hash: i64, side: OrderSide, quantity: u64) -> Self {
4300
0
        Self {
4301
0
            id: OrderId::new().value(),
4302
0
            symbol_hash,
4303
0
            side,
4304
0
            order_type: OrderType::Market,
4305
0
            quantity,
4306
0
            price: 0,
4307
0
            timestamp: HftTimestamp::now_or_zero().nanos(),
4308
0
        }
4309
0
    }
4310
4311
    /// Get quantity as Quantity type
4312
    #[must_use]
4313
0
    pub const fn get_quantity(&self) -> Quantity {
4314
0
        Quantity::from_raw(self.quantity)
4315
0
    }
4316
4317
    /// Get price as Price type (None for market orders)
4318
    #[must_use]
4319
0
    pub const fn get_price(&self) -> Option<Price> {
4320
0
        if self.price == 0 {
4321
0
            None
4322
        } else {
4323
0
            Some(Price::from_raw(self.price))
4324
        }
4325
0
    }
4326
4327
    /// Check if this is a buy order
4328
    #[must_use]
4329
0
    pub fn is_buy(&self) -> bool {
4330
0
        self.side == OrderSide::Buy
4331
0
    }
4332
4333
    /// Check if this is a sell order
4334
    #[must_use]
4335
0
    pub fn is_sell(&self) -> bool {
4336
0
        self.side == OrderSide::Sell
4337
0
    }
4338
4339
    /// Check if this is a market order
4340
    #[must_use]
4341
0
    pub fn is_market_order(&self) -> bool {
4342
0
        self.order_type == OrderType::Market || self.price == 0
4343
0
    }
4344
4345
    /// Check if this is a limit order
4346
    #[must_use]
4347
0
    pub fn is_limit_order(&self) -> bool {
4348
0
        self.order_type == OrderType::Limit && self.price > 0
4349
0
    }
4350
}
4351
4352
impl Default for OrderRef {
4353
0
    fn default() -> Self {
4354
0
        Self {
4355
0
            id: 0,
4356
0
            symbol_hash: 0,
4357
0
            side: OrderSide::Buy,
4358
0
            order_type: OrderType::Market,
4359
0
            quantity: 0,
4360
0
            price: 0,
4361
0
            timestamp: 0,
4362
0
        }
4363
0
    }
4364
}
4365
4366
// =============================================================================
4367
// COMPREHENSIVE TESTS
4368
// =============================================================================
4369
4370
#[cfg(test)]
4371
mod tests {
4372
    use super::*;
4373
    use std::str::FromStr;
4374
4375
    // =============================================================================
4376
    // Price Tests
4377
    // =============================================================================
4378
4379
    #[test]
4380
1
    fn test_price_from_f64_valid() {
4381
1
        let price = Price::from_f64(100.50).unwrap();
4382
1
        assert_eq!(price.to_f64(), 100.50);
4383
1
    }
4384
4385
    #[test]
4386
1
    fn test_price_from_f64_negative() {
4387
1
        let result = Price::from_f64(-10.0);
4388
1
        assert!(result.is_err());
4389
1
    }
4390
4391
    #[test]
4392
1
    fn test_price_from_f64_nan() {
4393
1
        let result = Price::from_f64(f64::NAN);
4394
1
        assert!(result.is_err());
4395
1
    }
4396
4397
    #[test]
4398
1
    fn test_price_from_f64_infinity() {
4399
1
        let result = Price::from_f64(f64::INFINITY);
4400
1
        assert!(result.is_err());
4401
1
    }
4402
4403
    #[test]
4404
1
    fn test_price_constants() {
4405
1
        assert_eq!(Price::ZERO.to_f64(), 0.0);
4406
1
        assert_eq!(Price::ONE.to_f64(), 1.0);
4407
1
        assert_eq!(Price::CENT.to_f64(), 0.01);
4408
1
    }
4409
4410
    #[test]
4411
1
    fn test_price_addition() {
4412
1
        let p1 = Price::from_f64(10.0).unwrap();
4413
1
        let p2 = Price::from_f64(5.5).unwrap();
4414
1
        let result = p1 + p2;
4415
1
        assert!((result.to_f64() - 15.5).abs() < 0.00001);
4416
1
    }
4417
4418
    #[test]
4419
1
    fn test_price_subtraction() {
4420
1
        let p1 = Price::from_f64(10.0).unwrap();
4421
1
        let p2 = Price::from_f64(5.5).unwrap();
4422
1
        let result = p1 - p2;
4423
1
        assert!((result.to_f64() - 4.5).abs() < 0.00001);
4424
1
    }
4425
4426
    #[test]
4427
1
    fn test_price_multiplication() {
4428
1
        let price = Price::from_f64(10.0).unwrap();
4429
1
        let result = (price * 2.5).unwrap();
4430
1
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4431
1
    }
4432
4433
    #[test]
4434
1
    fn test_price_division() {
4435
1
        let price = Price::from_f64(10.0).unwrap();
4436
1
        let result = (price / 2.0).unwrap();
4437
1
        assert!((result.to_f64() - 5.0).abs() < 0.00001);
4438
1
    }
4439
4440
    #[test]
4441
1
    fn test_price_division_by_zero() {
4442
1
        let price = Price::from_f64(10.0).unwrap();
4443
1
        let result = price / 0.0;
4444
1
        assert!(result.is_err());
4445
1
    }
4446
4447
    #[test]
4448
1
    fn test_price_from_cents() {
4449
1
        let price = Price::from_cents(150);
4450
1
        assert!((price.to_f64() - 1.50).abs() < 0.00001);
4451
1
    }
4452
4453
    #[test]
4454
1
    fn test_price_to_cents() {
4455
1
        let price = Price::from_f64(1.50).unwrap();
4456
1
        assert_eq!(price.to_cents(), 150);
4457
1
    }
4458
4459
    #[test]
4460
1
    fn test_price_is_zero() {
4461
1
        assert!(Price::ZERO.is_zero());
4462
1
        assert!(!Price::from_f64(1.0).unwrap().is_zero());
4463
1
    }
4464
4465
    #[test]
4466
1
    fn test_price_from_str() {
4467
1
        let price = Price::from_str("123.45").unwrap();
4468
1
        assert!((price.to_f64() - 123.45).abs() < 0.00001);
4469
1
    }
4470
4471
    #[test]
4472
1
    fn test_price_from_str_invalid() {
4473
1
        let result = Price::from_str("invalid");
4474
1
        assert!(result.is_err());
4475
1
    }
4476
4477
    #[test]
4478
1
    fn test_price_display() {
4479
1
        let price = Price::from_f64(123.456789).unwrap();
4480
1
        let display = format!("{}", price);
4481
1
        assert!(display.starts_with("123.45678"));
4482
1
    }
4483
4484
    #[test]
4485
1
    fn test_price_partial_eq_f64() {
4486
1
        let price = Price::from_f64(10.0).unwrap();
4487
1
        assert_eq!(price, 10.0);
4488
1
        assert_eq!(10.0, price);
4489
1
    }
4490
4491
    #[test]
4492
1
    fn test_price_multiply_price() {
4493
1
        let p1 = Price::from_f64(10.0).unwrap();
4494
1
        let p2 = Price::from_f64(2.5).unwrap();
4495
1
        let result = p1.multiply(p2).unwrap();
4496
1
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4497
1
    }
4498
4499
    // =============================================================================
4500
    // Quantity Tests
4501
    // =============================================================================
4502
4503
    #[test]
4504
1
    fn test_quantity_from_f64_valid() {
4505
1
        let qty = Quantity::from_f64(100.5).unwrap();
4506
1
        assert_eq!(qty.to_f64(), 100.5);
4507
1
    }
4508
4509
    #[test]
4510
1
    fn test_quantity_from_f64_negative() {
4511
1
        let result = Quantity::from_f64(-10.0);
4512
1
        assert!(result.is_err());
4513
1
    }
4514
4515
    #[test]
4516
1
    fn test_quantity_from_f64_nan() {
4517
1
        let result = Quantity::from_f64(f64::NAN);
4518
1
        assert!(result.is_err());
4519
1
    }
4520
4521
    #[test]
4522
1
    fn test_quantity_constants() {
4523
1
        assert_eq!(Quantity::ZERO.to_f64(), 0.0);
4524
1
        assert_eq!(Quantity::ONE.to_f64(), 1.0);
4525
1
    }
4526
4527
    #[test]
4528
1
    fn test_quantity_addition() {
4529
1
        let q1 = Quantity::from_f64(10.0).unwrap();
4530
1
        let q2 = Quantity::from_f64(5.5).unwrap();
4531
1
        let result = q1 + q2;
4532
1
        assert!((result.to_f64() - 15.5).abs() < 0.00001);
4533
1
    }
4534
4535
    #[test]
4536
1
    fn test_quantity_subtraction() {
4537
1
        let q1 = Quantity::from_f64(10.0).unwrap();
4538
1
        let q2 = Quantity::from_f64(5.5).unwrap();
4539
1
        let result = q1 - q2;
4540
1
        assert!((result.to_f64() - 4.5).abs() < 0.00001);
4541
1
    }
4542
4543
    #[test]
4544
1
    fn test_quantity_multiplication() {
4545
1
        let qty = Quantity::from_f64(10.0).unwrap();
4546
1
        let result = (qty * 2.5).unwrap();
4547
1
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4548
1
    }
4549
4550
    #[test]
4551
1
    fn test_quantity_division() {
4552
1
        let qty = Quantity::from_f64(10.0).unwrap();
4553
1
        let result = (qty / 2.0).unwrap();
4554
1
        assert!((result.to_f64() - 5.0).abs() < 0.00001);
4555
1
    }
4556
4557
    #[test]
4558
1
    fn test_quantity_division_by_zero() {
4559
1
        let qty = Quantity::from_f64(10.0).unwrap();
4560
1
        let result = qty / 0.0;
4561
1
        assert!(result.is_err());
4562
1
    }
4563
4564
    #[test]
4565
1
    fn test_quantity_is_zero() {
4566
1
        assert!(Quantity::ZERO.is_zero());
4567
1
        assert!(!Quantity::from_f64(1.0).unwrap().is_zero());
4568
1
    }
4569
4570
    #[test]
4571
1
    fn test_quantity_is_positive() {
4572
1
        assert!(Quantity::from_f64(1.0).unwrap().is_positive());
4573
1
        assert!(!Quantity::ZERO.is_positive());
4574
1
    }
4575
4576
    #[test]
4577
1
    fn test_quantity_is_negative() {
4578
        // Quantity is always non-negative
4579
1
        assert!(!Quantity::from_f64(1.0).unwrap().is_negative());
4580
1
        assert!(!Quantity::ZERO.is_negative());
4581
1
    }
4582
4583
    #[test]
4584
1
    fn test_quantity_from_shares() {
4585
1
        let qty = Quantity::from_shares(100);
4586
1
        assert_eq!(qty.to_shares(), 100);
4587
1
    }
4588
4589
    #[test]
4590
1
    fn test_quantity_sum() {
4591
1
        let quantities = vec![
4592
1
            Quantity::from_f64(1.0).unwrap(),
4593
1
            Quantity::from_f64(2.0).unwrap(),
4594
1
            Quantity::from_f64(3.0).unwrap(),
4595
        ];
4596
1
        let sum: Quantity = quantities.into_iter().sum();
4597
1
        assert!((sum.to_f64() - 6.0).abs() < 0.00001);
4598
1
    }
4599
4600
    #[test]
4601
1
    fn test_quantity_try_from_i32() {
4602
1
        let qty = Quantity::try_from(100i32).unwrap();
4603
1
        assert_eq!(qty.to_f64(), 100.0);
4604
1
    }
4605
4606
    #[test]
4607
1
    fn test_quantity_try_from_string() {
4608
1
        let qty = Quantity::try_from("123.45").unwrap();
4609
1
        assert!((qty.to_f64() - 123.45).abs() < 0.00001);
4610
1
    }
4611
4612
    // =============================================================================
4613
    // Money Tests
4614
    // =============================================================================
4615
4616
    #[test]
4617
1
    fn test_money_new() {
4618
1
        let amount = Decimal::from_f64_retain(100.50).unwrap();
4619
1
        let money = Money::new(amount, Currency::USD);
4620
1
        assert_eq!(money.currency, Currency::USD);
4621
1
        assert_eq!(money.amount, amount);
4622
1
    }
4623
4624
    #[test]
4625
1
    fn test_money_display() {
4626
1
        let amount = Decimal::from_f64_retain(100.50).unwrap();
4627
1
        let money = Money::new(amount, Currency::USD);
4628
1
        let display = format!("{}", money);
4629
1
        assert!(display.contains("100.5"));
4630
1
        assert!(display.contains("USD"));
4631
1
    }
4632
4633
    // =============================================================================
4634
    // Symbol Tests
4635
    // =============================================================================
4636
4637
    #[test]
4638
1
    fn test_symbol_new() {
4639
1
        let symbol = Symbol::new("AAPL".to_owned());
4640
1
        assert_eq!(symbol.as_str(), "AAPL");
4641
1
    }
4642
4643
    #[test]
4644
1
    fn test_symbol_new_validated_valid() {
4645
1
        let symbol = Symbol::new_validated("AAPL".to_owned()).unwrap();
4646
1
        assert_eq!(symbol.as_str(), "AAPL");
4647
1
    }
4648
4649
    #[test]
4650
1
    fn test_symbol_new_validated_empty() {
4651
1
        let result = Symbol::new_validated("".to_owned());
4652
1
        assert!(result.is_err());
4653
1
    }
4654
4655
    #[test]
4656
1
    fn test_symbol_new_validated_whitespace() {
4657
1
        let result = Symbol::new_validated("   ".to_owned());
4658
1
        assert!(result.is_err());
4659
1
    }
4660
4661
    #[test]
4662
1
    fn test_symbol_from_str() {
4663
1
        let symbol = Symbol::from_str("AAPL").unwrap();
4664
1
        assert_eq!(symbol.as_str(), "AAPL");
4665
1
    }
4666
4667
    #[test]
4668
1
    fn test_symbol_to_uppercase() {
4669
1
        let symbol = Symbol::from_str("aapl").unwrap();
4670
1
        assert_eq!(symbol.to_uppercase(), "AAPL");
4671
1
    }
4672
4673
    #[test]
4674
1
    fn test_symbol_replace() {
4675
1
        let symbol = Symbol::from_str("AAPL.US").unwrap();
4676
1
        assert_eq!(symbol.replace(".US", ""), "AAPL");
4677
1
    }
4678
4679
    #[test]
4680
1
    fn test_symbol_contains() {
4681
1
        let symbol = Symbol::from_str("AAPL.US").unwrap();
4682
1
        assert!(symbol.contains("AAPL"));
4683
1
        assert!(!symbol.contains("MSFT"));
4684
1
    }
4685
4686
    #[test]
4687
1
    fn test_symbol_partial_eq_str() {
4688
1
        let symbol = Symbol::from_str("AAPL").unwrap();
4689
1
        assert_eq!("AAPL", symbol);
4690
1
        assert_eq!(symbol.as_str(), "AAPL");
4691
1
    }
4692
4693
    #[test]
4694
1
    fn test_symbol_none() {
4695
1
        let symbol = Symbol::none();
4696
1
        assert_eq!(symbol.as_str(), "NONE");
4697
1
    }
4698
4699
    // =============================================================================
4700
    // TimeInForce Tests
4701
    // =============================================================================
4702
4703
    #[test]
4704
1
    fn test_time_in_force_display() {
4705
1
        assert_eq!(format!("{}", TimeInForce::Day), "DAY");
4706
1
        assert_eq!(format!("{}", TimeInForce::GoodTillCancel), "GTC");
4707
1
        assert_eq!(format!("{}", TimeInForce::ImmediateOrCancel), "IOC");
4708
1
        assert_eq!(format!("{}", TimeInForce::FillOrKill), "FOK");
4709
1
    }
4710
4711
    #[test]
4712
1
    fn test_time_in_force_default() {
4713
1
        assert_eq!(TimeInForce::default(), TimeInForce::Day);
4714
1
    }
4715
4716
    // =============================================================================
4717
    // OrderType Tests
4718
    // =============================================================================
4719
4720
    #[test]
4721
1
    fn test_order_type_display() {
4722
1
        assert_eq!(format!("{}", OrderType::Market), "MARKET");
4723
1
        assert_eq!(format!("{}", OrderType::Limit), "LIMIT");
4724
1
        assert_eq!(format!("{}", OrderType::Stop), "STOP");
4725
1
        assert_eq!(format!("{}", OrderType::StopLimit), "STOP_LIMIT");
4726
1
    }
4727
4728
    #[test]
4729
1
    fn test_order_type_try_from_i32_valid() {
4730
1
        assert_eq!(OrderType::try_from(0).unwrap(), OrderType::Market);
4731
1
        assert_eq!(OrderType::try_from(1).unwrap(), OrderType::Limit);
4732
1
        assert_eq!(OrderType::try_from(2).unwrap(), OrderType::Stop);
4733
1
    }
4734
4735
    #[test]
4736
1
    fn test_order_type_try_from_i32_invalid() {
4737
1
        let result = OrderType::try_from(99);
4738
1
        assert!(result.is_err());
4739
1
    }
4740
4741
    #[test]
4742
1
    fn test_order_type_default() {
4743
1
        assert_eq!(OrderType::default(), OrderType::Market);
4744
1
    }
4745
4746
    // =============================================================================
4747
    // OrderStatus Tests
4748
    // =============================================================================
4749
4750
    #[test]
4751
1
    fn test_order_status_display() {
4752
1
        assert_eq!(format!("{}", OrderStatus::Created), "CREATED");
4753
1
        assert_eq!(format!("{}", OrderStatus::Filled), "FILLED");
4754
1
        assert_eq!(format!("{}", OrderStatus::Cancelled), "CANCELLED");
4755
1
    }
4756
4757
    #[test]
4758
1
    fn test_order_status_try_from_i32_valid() {
4759
1
        assert_eq!(OrderStatus::try_from(0).unwrap(), OrderStatus::Created);
4760
1
        assert_eq!(OrderStatus::try_from(3).unwrap(), OrderStatus::Filled);
4761
1
        assert_eq!(OrderStatus::try_from(5).unwrap(), OrderStatus::Cancelled);
4762
1
    }
4763
4764
    #[test]
4765
1
    fn test_order_status_try_from_i32_invalid() {
4766
1
        let result = OrderStatus::try_from(99);
4767
1
        assert!(result.is_err());
4768
1
    }
4769
4770
    // =============================================================================
4771
    // OrderSide Tests
4772
    // =============================================================================
4773
4774
    #[test]
4775
1
    fn test_order_side_display() {
4776
1
        assert_eq!(format!("{}", OrderSide::Buy), "BUY");
4777
1
        assert_eq!(format!("{}", OrderSide::Sell), "SELL");
4778
1
    }
4779
4780
    #[test]
4781
1
    fn test_order_side_try_from_i32_valid() {
4782
1
        assert_eq!(OrderSide::try_from(0).unwrap(), OrderSide::Buy);
4783
1
        assert_eq!(OrderSide::try_from(1).unwrap(), OrderSide::Sell);
4784
1
    }
4785
4786
    #[test]
4787
1
    fn test_order_side_try_from_i32_invalid() {
4788
1
        let result = OrderSide::try_from(99);
4789
1
        assert!(result.is_err());
4790
1
    }
4791
4792
    #[test]
4793
1
    fn test_order_side_default() {
4794
1
        assert_eq!(OrderSide::default(), OrderSide::Buy);
4795
1
    }
4796
4797
    // =============================================================================
4798
    // Currency Tests
4799
    // =============================================================================
4800
4801
    #[test]
4802
1
    fn test_currency_display() {
4803
1
        assert_eq!(format!("{}", Currency::USD), "USD");
4804
1
        assert_eq!(format!("{}", Currency::EUR), "EUR");
4805
1
        assert_eq!(format!("{}", Currency::BTC), "BTC");
4806
1
    }
4807
4808
    #[test]
4809
1
    fn test_currency_default() {
4810
1
        assert_eq!(Currency::default(), Currency::USD);
4811
1
    }
4812
4813
    // =============================================================================
4814
    // Error Type Tests
4815
    // =============================================================================
4816
4817
    #[test]
4818
1
    fn test_common_type_error_invalid_price() {
4819
1
        let error = CommonTypeError::InvalidPrice {
4820
1
            value: "abc".to_owned(),
4821
1
            reason: "not a number".to_owned(),
4822
1
        };
4823
1
        let display = format!("{}", error);
4824
1
        assert!(display.contains("abc"));
4825
1
    }
4826
4827
    #[test]
4828
1
    fn test_common_type_error_invalid_quantity() {
4829
1
        let error = CommonTypeError::InvalidQuantity {
4830
1
            value: "xyz".to_owned(),
4831
1
            reason: "not a number".to_owned(),
4832
1
        };
4833
1
        let display = format!("{}", error);
4834
1
        assert!(display.contains("xyz"));
4835
1
    }
4836
4837
    #[test]
4838
1
    fn test_common_type_error_validation() {
4839
1
        let error = CommonTypeError::ValidationError {
4840
1
            field: "symbol".to_owned(),
4841
1
            reason: "cannot be empty".to_owned(),
4842
1
        };
4843
1
        let display = format!("{}", error);
4844
1
        assert!(display.contains("symbol"));
4845
1
    }
4846
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html deleted file mode 100644 index 5b26ed743..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs
Line
Count
Source
1
//! Comprehensive Asset Classification Configuration System
2
//!
3
//! This module provides production-ready asset classification capabilities with:
4
//! - Sophisticated asset class hierarchies
5
//! - Dynamic trading parameter configuration
6
//! - Pattern-based symbol matching with regex support
7
//! - Database-backed configuration with hot-reload
8
//! - Volatility profiling and risk management integration
9
10
use chrono::{DateTime, Datelike, NaiveTime, Utc};
11
use log;
12
use regex::Regex;
13
use rust_decimal::Decimal;
14
use serde::{Deserialize, Serialize};
15
use std::collections::HashMap;
16
use uuid::Uuid;
17
18
/// Comprehensive asset classification enum with detailed sub-categories
19
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20
pub enum AssetClass {
21
    /// Equity instruments with sector-specific characteristics
22
    Equity {
23
        sector: EquitySector,
24
        market_cap: MarketCapTier,
25
        region: GeographicRegion,
26
    },
27
    /// Futures contracts with underlying asset classification
28
    Future {
29
        underlying: FutureType,
30
        expiry_type: ExpiryType,
31
        exchange: String,
32
    },
33
    /// Foreign exchange pairs with specific characteristics
34
    Forex {
35
        base: String,
36
        quote: String,
37
        pair_type: ForexPairType,
38
    },
39
    /// Cryptocurrency assets with network and type classification
40
    Crypto {
41
        network: String,
42
        crypto_type: CryptoType,
43
        market_cap_rank: Option<u32>,
44
    },
45
    /// Commodity instruments with category classification
46
    Commodity {
47
        category: CommodityType,
48
        storage_type: StorageType,
49
    },
50
    /// Fixed income securities
51
    FixedIncome {
52
        instrument_type: FixedIncomeType,
53
        credit_rating: CreditRating,
54
        maturity: MaturityBucket,
55
    },
56
    /// Derivatives and structured products
57
    Derivative {
58
        underlying_class: Box<AssetClass>,
59
        derivative_type: DerivativeType,
60
    },
61
    /// Unknown or unclassified assets (conservative defaults)
62
    Unknown,
63
}
64
65
/// Equity sector classifications aligned with industry standards
66
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
67
pub enum EquitySector {
68
    Technology,
69
    Healthcare,
70
    Financial,
71
    ConsumerDiscretionary,
72
    ConsumerStaples,
73
    Industrial,
74
    Energy,
75
    Materials,
76
    Utilities,
77
    RealEstate,
78
    CommunicationServices,
79
}
80
81
/// Market capitalization tiers for equity classification
82
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
83
pub enum MarketCapTier {
84
    LargeCap, // > $10B
85
    MidCap,   // $2B - $10B
86
    SmallCap, // $300M - $2B
87
    MicroCap, // < $300M
88
}
89
90
/// Geographic regions for asset classification
91
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
92
pub enum GeographicRegion {
93
    NorthAmerica,
94
    Europe,
95
    Asia,
96
    EmergingMarkets,
97
    Global,
98
}
99
100
/// Future contract underlying asset types
101
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
102
pub enum FutureType {
103
    Equity,
104
    Currency,
105
    Commodity,
106
    Interest,
107
    Volatility,
108
}
109
110
/// Futures expiry categorization
111
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
112
pub enum ExpiryType {
113
    Weekly,
114
    Monthly,
115
    Quarterly,
116
    Annual,
117
}
118
119
/// Forex pair type classification
120
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
121
pub enum ForexPairType {
122
    Major,   // EUR/USD, GBP/USD, USD/JPY, etc.
123
    Minor,   // Cross-currency pairs without USD
124
    Exotic,  // Emerging market currencies
125
    JPYPair, // Special handling for JPY pairs
126
}
127
128
/// Cryptocurrency type classification
129
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
130
pub enum CryptoType {
131
    Bitcoin,
132
    Ethereum,
133
    Stablecoin,
134
    AltcoinMajor, // Top 20 market cap
135
    AltcoinMinor, // Beyond top 20
136
    DeFi,
137
    GameFi,
138
    Meme,
139
}
140
141
/// Commodity categories
142
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
143
pub enum CommodityType {
144
    PreciousMetals,
145
    Energy,
146
    Agricultural,
147
    IndustrialMetals,
148
    Livestock,
149
}
150
151
/// Storage characteristics for commodities
152
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
153
pub enum StorageType {
154
    Physical,
155
    Financial,
156
}
157
158
/// Fixed income instrument types
159
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
160
pub enum FixedIncomeType {
161
    Government,
162
    Corporate,
163
    Municipal,
164
    InflationProtected,
165
}
166
167
/// Credit rating classifications
168
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
169
pub enum CreditRating {
170
    AAA,
171
    AA,
172
    A,
173
    BBB,
174
    BB,
175
    B,
176
    CCC,
177
    Unrated,
178
}
179
180
/// Maturity buckets for fixed income
181
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
182
pub enum MaturityBucket {
183
    ShortTerm,  // < 2 years
184
    MediumTerm, // 2-10 years
185
    LongTerm,   // > 10 years
186
}
187
188
/// Derivative instrument types
189
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
190
pub enum DerivativeType {
191
    Option,
192
    Swap,
193
    Forward,
194
    Structured,
195
}
196
197
/// Comprehensive volatility profile with regime-aware parameters
198
#[derive(Debug, Clone, Serialize, Deserialize)]
199
pub struct VolatilityProfile {
200
    /// Base annual volatility (standard market conditions)
201
    pub base_annual_volatility: f64,
202
    /// Stress volatility multiplier for high-stress periods
203
    pub stress_volatility_multiplier: f64,
204
    /// Intraday volatility pattern (hourly multipliers)
205
    pub intraday_pattern: Vec<f64>,
206
    /// Volatility clustering parameter (GARCH-like)
207
    pub volatility_persistence: f64,
208
    /// Jump risk probability and magnitude
209
    pub jump_risk: JumpRiskProfile,
210
}
211
212
/// Jump risk characteristics
213
#[derive(Debug, Clone, Serialize, Deserialize)]
214
pub struct JumpRiskProfile {
215
    /// Probability of large price jumps per day
216
    pub jump_probability: f64,
217
    /// Average magnitude of jumps (as fraction of price)
218
    pub jump_magnitude: f64,
219
    /// Maximum expected jump size
220
    pub max_jump_size: f64,
221
}
222
223
/// Dynamic trading parameters that adapt to market conditions
224
#[derive(Debug, Clone, Serialize, Deserialize)]
225
pub struct TradingParameters {
226
    /// Position sizing constraints
227
    pub position_limits: PositionLimits,
228
    /// Risk management thresholds
229
    pub risk_thresholds: RiskThresholds,
230
    /// Execution parameters
231
    pub execution_config: ExecutionConfig,
232
    /// Market making parameters (if applicable)
233
    pub market_making: Option<MarketMakingConfig>,
234
}
235
236
/// Position sizing and exposure limits
237
#[derive(Debug, Clone, Serialize, Deserialize)]
238
pub struct PositionLimits {
239
    /// Maximum position size as fraction of portfolio NAV
240
    pub max_position_fraction: f64,
241
    /// Maximum leverage allowed for this asset
242
    pub max_leverage: f64,
243
    /// Concentration limit (max % of total positions in this asset class)
244
    pub concentration_limit: f64,
245
    /// Minimum position size (to avoid micro-positions)
246
    pub min_position_size: Decimal,
247
}
248
249
/// Risk management thresholds and limits
250
#[derive(Debug, Clone, Serialize, Deserialize)]
251
pub struct RiskThresholds {
252
    /// VaR limit as fraction of portfolio
253
    pub var_limit: f64,
254
    /// Daily loss limit
255
    pub daily_loss_limit: f64,
256
    /// Stop-loss threshold
257
    pub stop_loss_threshold: f64,
258
    /// Volatility circuit breaker threshold
259
    pub volatility_circuit_breaker: f64,
260
    /// Maximum drawdown before position reduction
261
    pub max_drawdown_threshold: f64,
262
}
263
264
/// Execution configuration parameters
265
#[derive(Debug, Clone, Serialize, Deserialize)]
266
pub struct ExecutionConfig {
267
    /// Preferred order types for this asset
268
    pub preferred_order_types: Vec<OrderType>,
269
    /// Tick size for price increments
270
    pub tick_size: Decimal,
271
    /// Minimum order size
272
    pub min_order_size: Decimal,
273
    /// Maximum order size before breaking up
274
    pub max_order_size: Decimal,
275
    /// Execution time constraints
276
    pub time_in_force_default: TimeInForce,
277
    /// Slippage tolerance
278
    pub slippage_tolerance: f64,
279
}
280
281
/// Market making specific configuration
282
#[derive(Debug, Clone, Serialize, Deserialize)]
283
pub struct MarketMakingConfig {
284
    /// Bid-ask spread targets
285
    pub target_spread: f64,
286
    /// Inventory limits
287
    pub max_inventory: Decimal,
288
    /// Quote size
289
    pub quote_size: Decimal,
290
    /// Refresh frequency
291
    pub refresh_frequency: std::time::Duration,
292
}
293
294
/// Order type enumeration
295
#[derive(Debug, Clone, Serialize, Deserialize)]
296
pub enum OrderType {
297
    Market,
298
    Limit,
299
    Stop,
300
    StopLimit,
301
    Hidden,
302
    Iceberg,
303
}
304
305
/// Time in force options
306
#[derive(Debug, Clone, Serialize, Deserialize)]
307
pub enum TimeInForce {
308
    Day,
309
    GoodTillCancel,
310
    ImmediateOrCancel,
311
    FillOrKill,
312
    GTD, // Good Till Date
313
}
314
315
/// Symbol pattern matching configuration with compiled regex
316
#[derive(Debug, Clone, Serialize, Deserialize)]
317
pub struct AssetConfig {
318
    /// UUID for database storage
319
    pub id: Uuid,
320
    /// Human-readable name for this configuration
321
    pub name: String,
322
    /// Regex pattern for symbol matching
323
    pub symbol_pattern: String,
324
    /// Compiled regex (not serialized, rebuilt on load)
325
    #[serde(skip)]
326
    pub compiled_pattern: Option<Regex>,
327
    /// Asset class classification
328
    pub asset_class: AssetClass,
329
    /// Volatility profile
330
    pub volatility_profile: VolatilityProfile,
331
    /// Trading parameters
332
    pub trading_parameters: TradingParameters,
333
    /// Priority for pattern matching (higher = checked first)
334
    pub priority: u32,
335
    /// Whether this configuration is active
336
    pub is_active: bool,
337
    /// Creation timestamp
338
    pub created_at: DateTime<Utc>,
339
    /// Last update timestamp
340
    pub updated_at: DateTime<Utc>,
341
    /// Trading hours (if applicable)
342
    pub trading_hours: Option<TradingHours>,
343
    /// Settlement details
344
    pub settlement_config: SettlementConfig,
345
}
346
347
/// Trading hours configuration
348
#[derive(Debug, Clone, Serialize, Deserialize)]
349
pub struct TradingHours {
350
    /// Regular trading session start
351
    pub market_open: NaiveTime,
352
    /// Regular trading session end
353
    pub market_close: NaiveTime,
354
    /// Pre-market session (if available)
355
    pub pre_market: Option<(NaiveTime, NaiveTime)>,
356
    /// After-hours session (if available)
357
    pub after_hours: Option<(NaiveTime, NaiveTime)>,
358
    /// Timezone for these hours
359
    pub timezone: String,
360
    /// Days of week when trading is active (0=Sunday, 6=Saturday)
361
    pub trading_days: Vec<u8>,
362
}
363
364
/// Settlement configuration
365
#[derive(Debug, Clone, Serialize, Deserialize)]
366
pub struct SettlementConfig {
367
    /// Settlement period (T+n days)
368
    pub settlement_days: u32,
369
    /// Settlement currency
370
    pub settlement_currency: String,
371
    /// Whether physical delivery is possible
372
    pub physical_settlement: bool,
373
}
374
375
/// Asset classification manager with caching and hot-reload capabilities
376
#[allow(clippy::module_name_repetitions)]
377
pub struct AssetClassificationManager {
378
    /// Asset configurations indexed by priority
379
    configs: Vec<AssetConfig>,
380
    /// Explicit symbol mappings for fast lookup
381
    symbol_cache: HashMap<String, AssetClass>,
382
    /// Last configuration reload timestamp
383
    last_reload: DateTime<Utc>,
384
    /// Configuration reload interval
385
    reload_interval: std::time::Duration,
386
}
387
388
impl AssetClassificationManager {
389
    /// Create a new asset classification manager
390
0
    pub fn new() -> Self {
391
0
        Self {
392
0
            configs: Vec::new(),
393
0
            symbol_cache: HashMap::new(),
394
0
            last_reload: Utc::now(),
395
0
            reload_interval: std::time::Duration::from_secs(300), // 5 minutes
396
0
        }
397
0
    }
398
399
    /// Load configurations from database
400
    ///
401
    /// # Errors
402
    ///
403
    /// Returns error if:
404
    /// - Regex pattern compilation fails
405
    /// - Configuration validation fails
406
    /// - Database access fails
407
    ///
408
    /// # Errors
409
    /// Returns error if the operation fails
410
0
    pub async fn load_configurations(
411
0
        &mut self,
412
0
        configs: Vec<AssetConfig>,
413
0
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
414
0
        self.configs = configs;
415
        // Sort by priority (highest first)
416
0
        self.configs.sort_by(|a, b| b.priority.cmp(&a.priority));
417
418
        // Compile regex patterns
419
0
        for config in &mut self.configs {
420
0
            match Regex::new(&config.symbol_pattern) {
421
0
                Ok(regex) => config.compiled_pattern = Some(regex),
422
0
                Err(e) => {
423
0
                    log::warn!(
424
0
                        "Failed to compile regex pattern '{}': {}",
425
                        config.symbol_pattern,
426
                        e
427
                    );
428
0
                    config.is_active = false;
429
                }
430
            }
431
        }
432
433
0
        self.last_reload = Utc::now();
434
0
        log::info!(
435
0
            "Loaded {} asset classification configurations",
436
0
            self.configs.len()
437
        );
438
0
        Ok(())
439
0
    }
440
441
    /// Classify a symbol using the configured rules
442
0
    pub fn classify_symbol(&self, symbol: &str) -> AssetClass {
443
0
        let symbol_upper = symbol.to_uppercase();
444
445
        // Check cache first
446
0
        if let Some(asset_class) = self.symbol_cache.get(&symbol_upper) {
447
0
            return asset_class.clone();
448
0
        }
449
450
        // Check pattern rules in priority order
451
0
        for config in &self.configs {
452
0
            if !config.is_active {
453
0
                continue;
454
0
            }
455
456
0
            if let Some(ref regex) = config.compiled_pattern {
457
0
                if regex.is_match(&symbol_upper) {
458
0
                    return config.asset_class.clone();
459
0
                }
460
0
            }
461
        }
462
463
0
        AssetClass::Unknown
464
0
    }
465
466
    /// Get complete asset configuration for a symbol
467
0
    pub fn get_asset_config(&self, symbol: &str) -> Option<&AssetConfig> {
468
0
        let symbol_upper = symbol.to_uppercase();
469
470
0
        for config in &self.configs {
471
0
            if !config.is_active {
472
0
                continue;
473
0
            }
474
475
0
            if let Some(ref regex) = config.compiled_pattern {
476
0
                if regex.is_match(&symbol_upper) {
477
0
                    return Some(config);
478
0
                }
479
0
            }
480
        }
481
482
0
        None
483
0
    }
484
485
    /// Get volatility profile for a symbol
486
0
    pub fn get_volatility_profile(&self, symbol: &str) -> Option<&VolatilityProfile> {
487
0
        self.get_asset_config(symbol)
488
0
            .map(|config| &config.volatility_profile)
489
0
    }
490
491
    /// Get trading parameters for a symbol
492
0
    pub fn get_trading_parameters(&self, symbol: &str) -> Option<&TradingParameters> {
493
0
        self.get_asset_config(symbol)
494
0
            .map(|config| &config.trading_parameters)
495
0
    }
496
497
    /// Get daily volatility estimate for a symbol
498
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
499
0
        if let Some(profile) = self.get_volatility_profile(symbol) {
500
            #[allow(clippy::float_arithmetic)]
501
0
            let result = profile.base_annual_volatility / 252.0_f64.sqrt();
502
0
            result
503
        } else {
504
            #[allow(clippy::float_arithmetic)]
505
0
            let result = 0.5 / 252.0_f64.sqrt();
506
0
            result // Default high volatility
507
        }
508
0
    }
509
510
    /// Get position sizing recommendation
511
0
    pub fn get_position_size_recommendation(
512
0
        &self,
513
0
        symbol: &str,
514
0
        portfolio_nav: Decimal,
515
0
    ) -> Option<Decimal> {
516
0
        if let Some(config) = self.get_asset_config(symbol) {
517
0
            let max_fraction = config
518
0
                .trading_parameters
519
0
                .position_limits
520
0
                .max_position_fraction;
521
0
            if let Some(decimal_fraction) = Decimal::from_f64_retain(max_fraction) {
522
0
                portfolio_nav.checked_mul(decimal_fraction).or(Some(Decimal::ZERO))
523
            } else {
524
0
                Some(Decimal::ZERO)
525
            }
526
        } else {
527
0
            None
528
        }
529
0
    }
530
531
    /// Check if symbol is within trading hours
532
0
    pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime<Utc>) -> bool {
533
0
        if let Some(config) = self.get_asset_config(symbol) {
534
0
            if let Some(ref trading_hours) = config.trading_hours {
535
                // Simplified check - in production would need proper timezone handling
536
0
                let weekday = timestamp.weekday().num_days_from_sunday().try_into().unwrap_or(0u8);
537
0
                trading_hours.trading_days.contains(&weekday)
538
            } else {
539
0
                true // No trading hours restriction
540
            }
541
        } else {
542
0
            true // Default to always active for unknown symbols
543
        }
544
0
    }
545
546
    /// Add explicit symbol mapping to cache
547
0
    pub fn cache_symbol_mapping(&mut self, symbol: String, asset_class: AssetClass) {
548
0
        self.symbol_cache.insert(symbol.to_uppercase(), asset_class);
549
0
    }
550
551
    /// Clear symbol cache
552
0
    pub fn clear_cache(&mut self) {
553
0
        self.symbol_cache.clear();
554
0
    }
555
556
    /// Check if configuration needs reload
557
0
    pub fn needs_reload(&self) -> bool {
558
0
        Utc::now().signed_duration_since(self.last_reload)
559
0
            > chrono::Duration::from_std(self.reload_interval).unwrap_or_default()
560
0
    }
561
562
    /// Get all active configurations
563
0
    pub fn get_active_configurations(&self) -> Vec<&AssetConfig> {
564
0
        self.configs
565
0
            .iter()
566
0
            .filter(|config| config.is_active)
567
0
            .collect()
568
0
    }
569
570
    /// Get configurations by asset class
571
0
    pub fn get_configurations_by_class(&self, asset_class: &AssetClass) -> Vec<&AssetConfig> {
572
0
        self.configs
573
0
            .iter()
574
0
            .filter(|config| config.is_active && &config.asset_class == asset_class)
575
0
            .collect()
576
0
    }
577
}
578
579
impl Default for AssetClassificationManager {
580
0
    fn default() -> Self {
581
0
        Self::new()
582
0
    }
583
}
584
585
/// Create default asset configurations for common instruments
586
0
pub fn create_default_configurations() -> Vec<AssetConfig> {
587
0
    let mut configs = Vec::new();
588
0
    let now = Utc::now();
589
590
    // Blue chip US equities
591
0
    configs.push(AssetConfig {
592
0
        id: Uuid::new_v4(),
593
0
        name: "Blue Chip US Equities".to_owned(),
594
0
        symbol_pattern: "^(AAPL|MSFT|GOOGL|AMZN|META|TSLA|NVDA|JPM|JNJ|V|PG|UNH|HD|BAC|DIS|MA|NFLX|CRM|ADBE|PYPL|INTC|CMCSA|PFE|T|VZ|MRK|WMT|KO|NKE|CVX|XOM)$".to_owned(),
595
0
        compiled_pattern: None,
596
0
        asset_class: AssetClass::Equity {
597
0
            sector: EquitySector::Technology,
598
0
            market_cap: MarketCapTier::LargeCap,
599
0
            region: GeographicRegion::NorthAmerica,
600
0
        },
601
0
        volatility_profile: VolatilityProfile {
602
0
            base_annual_volatility: 0.25,
603
0
            stress_volatility_multiplier: 2.0,
604
0
            intraday_pattern: vec![1.0_f64; 24], // Flat pattern for simplicity
605
0
            volatility_persistence: 0.85,
606
0
            jump_risk: JumpRiskProfile {
607
0
                jump_probability: 0.02,
608
0
                jump_magnitude: 0.05,
609
0
                max_jump_size: 0.15,
610
0
            },
611
0
        },
612
0
        trading_parameters: TradingParameters {
613
0
            position_limits: PositionLimits {
614
0
                max_position_fraction: 0.20,
615
0
                max_leverage: 2.0,
616
0
                concentration_limit: 0.30,
617
0
                min_position_size: Decimal::from(100_i64),
618
0
            },
619
0
            risk_thresholds: RiskThresholds {
620
0
                var_limit: 0.05,
621
0
                daily_loss_limit: 0.03,
622
0
                stop_loss_threshold: 0.10,
623
0
                volatility_circuit_breaker: 0.05,
624
0
                max_drawdown_threshold: 0.15,
625
0
            },
626
0
            execution_config: ExecutionConfig {
627
0
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
628
0
                tick_size: "0.01".parse().unwrap(),
629
0
                min_order_size: Decimal::from(1_i64),
630
0
                max_order_size: Decimal::from(10000_i64),
631
0
                time_in_force_default: TimeInForce::Day,
632
0
                slippage_tolerance: 0.001,
633
0
            },
634
0
            market_making: None,
635
0
        },
636
0
        priority: 100,
637
0
        is_active: true,
638
0
        created_at: now,
639
0
        updated_at: now,
640
0
        trading_hours: Some(TradingHours {
641
0
            market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
642
0
            market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
643
0
            pre_market: Some((NaiveTime::from_hms_opt(4, 0, 0).unwrap(), NaiveTime::from_hms_opt(9, 30, 0).unwrap())),
644
0
            after_hours: Some((NaiveTime::from_hms_opt(16, 0, 0).unwrap(), NaiveTime::from_hms_opt(20, 0, 0).unwrap())),
645
0
            timezone: "America/New_York".to_owned(),
646
0
            trading_days: vec![1, 2, 3, 4, 5], // Monday-Friday
647
0
        }),
648
0
        settlement_config: SettlementConfig {
649
0
            settlement_days: 2,
650
0
            settlement_currency: "USD".to_owned(),
651
0
            physical_settlement: false,
652
0
        },
653
0
    });
654
655
    // Major cryptocurrency pairs
656
0
    configs.push(AssetConfig {
657
0
        id: Uuid::new_v4(),
658
0
        name: "Major Cryptocurrencies".to_owned(),
659
0
        symbol_pattern: "^(BTC|ETH|BTCUSD|ETHUSD|BTCUSDT|ETHUSDT).*$".to_owned(),
660
0
        compiled_pattern: None,
661
0
        asset_class: AssetClass::Crypto {
662
0
            network: "Bitcoin".to_owned(),
663
0
            crypto_type: CryptoType::Bitcoin,
664
0
            market_cap_rank: Some(1),
665
0
        },
666
0
        volatility_profile: VolatilityProfile {
667
0
            base_annual_volatility: 0.80,
668
0
            stress_volatility_multiplier: 3.0,
669
0
            intraday_pattern: vec![1.0_f64; 24],
670
0
            volatility_persistence: 0.90,
671
0
            jump_risk: JumpRiskProfile {
672
0
                jump_probability: 0.05,
673
0
                jump_magnitude: 0.10,
674
0
                max_jump_size: 0.30,
675
0
            },
676
0
        },
677
0
        trading_parameters: TradingParameters {
678
0
            position_limits: PositionLimits {
679
0
                max_position_fraction: 0.10,
680
0
                max_leverage: 1.5,
681
0
                concentration_limit: 0.15,
682
0
                min_position_size: "0.001".parse().unwrap(),
683
0
            },
684
0
            risk_thresholds: RiskThresholds {
685
0
                var_limit: 0.10,
686
0
                daily_loss_limit: 0.05,
687
0
                stop_loss_threshold: 0.15,
688
0
                volatility_circuit_breaker: 0.15,
689
0
                max_drawdown_threshold: 0.25,
690
0
            },
691
0
            execution_config: ExecutionConfig {
692
0
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
693
0
                tick_size: "0.01".parse().unwrap(),
694
0
                min_order_size: "0.001".parse().unwrap(),
695
0
                max_order_size: Decimal::from(100_i64),
696
0
                time_in_force_default: TimeInForce::GoodTillCancel,
697
0
                slippage_tolerance: 0.005,
698
0
            },
699
0
            market_making: None,
700
0
        },
701
0
        priority: 90,
702
0
        is_active: true,
703
0
        created_at: now,
704
0
        updated_at: now,
705
0
        trading_hours: None, // 24/7 trading
706
0
        settlement_config: SettlementConfig {
707
0
            settlement_days: 0,
708
0
            settlement_currency: "USD".to_owned(),
709
0
            physical_settlement: true,
710
0
        },
711
0
    });
712
713
    // Major forex pairs
714
0
    configs.push(AssetConfig {
715
0
        id: Uuid::new_v4(),
716
0
        name: "Major Forex Pairs".to_owned(),
717
0
        symbol_pattern: "^(EUR|GBP|USD|JPY|AUD|CAD|CHF|NZD)(USD|EUR|GBP|JPY)$".to_owned(),
718
0
        compiled_pattern: None,
719
0
        asset_class: AssetClass::Forex {
720
0
            base: "EUR".to_owned(),
721
0
            quote: "USD".to_owned(),
722
0
            pair_type: ForexPairType::Major,
723
0
        },
724
0
        volatility_profile: VolatilityProfile {
725
0
            base_annual_volatility: 0.12,
726
0
            stress_volatility_multiplier: 2.5,
727
0
            intraday_pattern: vec![1.0_f64; 24],
728
0
            volatility_persistence: 0.80,
729
0
            jump_risk: JumpRiskProfile {
730
0
                jump_probability: 0.01,
731
0
                jump_magnitude: 0.02,
732
0
                max_jump_size: 0.08,
733
0
            },
734
0
        },
735
0
        trading_parameters: TradingParameters {
736
0
            position_limits: PositionLimits {
737
0
                max_position_fraction: 0.30,
738
0
                max_leverage: 10.0,
739
0
                concentration_limit: 0.40,
740
0
                min_position_size: Decimal::from(1000_i64),
741
0
            },
742
0
            risk_thresholds: RiskThresholds {
743
0
                var_limit: 0.03,
744
0
                daily_loss_limit: 0.02,
745
0
                stop_loss_threshold: 0.05,
746
0
                volatility_circuit_breaker: 0.03,
747
0
                max_drawdown_threshold: 0.10,
748
0
            },
749
0
            execution_config: ExecutionConfig {
750
0
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
751
0
                tick_size: "0.00001".parse().unwrap(),
752
0
                min_order_size: Decimal::from(1000_i64),
753
0
                max_order_size: Decimal::from(10000000_i64),
754
0
                time_in_force_default: TimeInForce::GoodTillCancel,
755
0
                slippage_tolerance: 0.0002_f64,
756
0
            },
757
0
            market_making: Some(MarketMakingConfig {
758
0
                target_spread: 0.0001_f64,
759
0
                max_inventory: Decimal::from(100000_i64),
760
0
                quote_size: Decimal::from(10000_i64),
761
0
                refresh_frequency: std::time::Duration::from_millis(100),
762
0
            }),
763
0
        },
764
0
        priority: 80,
765
0
        is_active: true,
766
0
        created_at: now,
767
0
        updated_at: now,
768
0
        trading_hours: None, // 24/5 trading
769
0
        settlement_config: SettlementConfig {
770
0
            settlement_days: 2,
771
0
            settlement_currency: "USD".to_owned(),
772
0
            physical_settlement: false,
773
0
        },
774
0
    });
775
776
0
    configs
777
0
}
778
779
#[cfg(test)]
780
mod tests {
781
    use super::*;
782
783
    #[tokio::test]
784
    async fn test_symbol_classification() {
785
        let mut manager = AssetClassificationManager::new();
786
        let configs = create_default_configurations();
787
        manager.load_configurations(configs).await.unwrap();
788
789
        // Test blue chip classification
790
        match manager.classify_symbol("AAPL") {
791
            AssetClass::Equity {
792
                sector: EquitySector::Technology,
793
                ..
794
            } => (),
795
            _ => panic!("AAPL should be classified as Technology equity"),
796
        }
797
798
        // Test crypto classification
799
        match manager.classify_symbol("BTCUSD") {
800
            AssetClass::Crypto {
801
                crypto_type: CryptoType::Bitcoin,
802
                ..
803
            } => (),
804
            _ => panic!("BTCUSD should be classified as Bitcoin crypto"),
805
        }
806
807
        // Test unknown symbol
808
        assert_eq!(manager.classify_symbol("UNKNOWN"), AssetClass::Unknown);
809
    }
810
811
    #[tokio::test]
812
    async fn test_volatility_profile() {
813
        let mut manager = AssetClassificationManager::new();
814
        let configs = create_default_configurations();
815
        manager.load_configurations(configs).await.unwrap();
816
817
        let profile = manager.get_volatility_profile("AAPL").unwrap();
818
        assert_eq!(profile.base_annual_volatility, 0.25);
819
820
        let daily_vol = manager.get_daily_volatility("AAPL");
821
        assert!((daily_vol - (0.25 / 252.0_f64.sqrt())).abs() < 1e-10);
822
    }
823
824
    #[tokio::test]
825
    async fn test_trading_parameters() {
826
        let mut manager = AssetClassificationManager::new();
827
        let configs = create_default_configurations();
828
        manager.load_configurations(configs).await.unwrap();
829
830
        let params = manager.get_trading_parameters("AAPL").unwrap();
831
        assert_eq!(params.position_limits.max_position_fraction, 0.20);
832
        assert_eq!(params.position_limits.max_leverage, 2.0);
833
    }
834
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html deleted file mode 100644 index 7fc7ea8d4..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/data_config.rs
Line
Count
Source
1
//! Data configuration
2
3
use num_cpus;
4
use serde::{Deserialize, Serialize};
5
6
#[derive(Debug, Clone, Serialize, Deserialize)]
7
pub struct DataConfig {
8
    pub provider: String,
9
    pub symbols: Vec<String>,
10
    pub batch_size: usize,
11
    pub buffer_size: usize,
12
}
13
14
#[derive(Debug, Clone, Serialize, Deserialize)]
15
pub struct DataMicrostructureConfig {
16
    pub enable_bid_ask_spread: bool,
17
    pub enable_order_flow: bool,
18
    pub tick_size: f64,
19
    pub lot_size: f64,
20
    pub bid_ask_spread: bool,
21
    pub volume_imbalance: bool,
22
    pub price_impact: bool,
23
    pub kyle_lambda: bool,
24
    pub amihud_ratio: bool,
25
}
26
27
impl Default for DataMicrostructureConfig {
28
0
    fn default() -> Self {
29
0
        Self {
30
0
            enable_bid_ask_spread: true,
31
0
            enable_order_flow: true,
32
0
            tick_size: 0.01,
33
0
            lot_size: 100.0,
34
0
            bid_ask_spread: true,
35
0
            volume_imbalance: true,
36
0
            price_impact: false,
37
0
            kyle_lambda: false,
38
0
            amihud_ratio: false,
39
0
        }
40
0
    }
41
}
42
43
#[derive(Debug, Clone, Serialize, Deserialize)]
44
pub struct DataTLOBConfig {
45
    pub depth_levels: usize,
46
    pub enable_imbalance: bool,
47
    pub enable_pressure: bool,
48
    pub window_size: usize,
49
}
50
51
#[derive(Debug, Clone, Serialize, Deserialize)]
52
pub struct DataTechnicalIndicatorsConfig {
53
    pub enable_moving_averages: bool,
54
    pub enable_momentum: bool,
55
    pub enable_volatility: bool,
56
    pub window_sizes: Vec<usize>,
57
    pub ma_periods: Vec<usize>,
58
    pub rsi_periods: Vec<usize>,
59
    pub bollinger_periods: Vec<usize>,
60
    pub macd: DataMACDConfig,
61
}
62
63
impl Default for DataTechnicalIndicatorsConfig {
64
0
    fn default() -> Self {
65
0
        Self {
66
0
            enable_moving_averages: true,
67
0
            enable_momentum: true,
68
0
            enable_volatility: true,
69
0
            window_sizes: vec![10, 20, 50],
70
0
            ma_periods: vec![10, 20, 50, 200],
71
0
            rsi_periods: vec![14],
72
0
            bollinger_periods: vec![20],
73
0
            macd: DataMACDConfig::default(),
74
0
        }
75
0
    }
76
}
77
78
#[derive(Debug, Clone, Serialize, Deserialize)]
79
pub struct TrainingBenzingaConfig {
80
    pub api_key: String,
81
    pub api_key_env: String,
82
    pub symbols: Vec<String>,
83
    pub data_types: Vec<String>,
84
    pub timeout: u64,
85
    pub rate_limit: usize,
86
    pub batch_size: usize,
87
    pub enable_caching: bool,
88
}
89
90
impl Default for TrainingBenzingaConfig {
91
0
    fn default() -> Self {
92
0
        Self {
93
0
            api_key: String::new(),
94
0
            api_key_env: "BENZINGA_API_KEY".to_owned(),
95
0
            symbols: vec!["SPY".to_owned(), "AAPL".to_owned()],
96
0
            data_types: vec![
97
0
                "news".to_owned(),
98
0
                "sentiment".to_owned(),
99
0
                "ratings".to_owned(),
100
0
                "options".to_owned(),
101
0
            ],
102
0
            timeout: 30,
103
0
            rate_limit: 60,
104
0
            batch_size: 1000,
105
0
            enable_caching: true,
106
0
        }
107
0
    }
108
}
109
110
#[derive(Debug, Clone, Serialize, Deserialize)]
111
pub enum DataCompressionAlgorithm {
112
    GZIP,
113
    ZSTD,
114
    LZ4,
115
    Snappy,
116
    None,
117
}
118
119
#[derive(Debug, Clone, Serialize, Deserialize)]
120
pub struct DataCompressionConfig {
121
    pub algorithm: DataCompressionAlgorithm,
122
    pub enabled: bool,
123
    pub level: Option<i32>,
124
}
125
126
impl Default for DataCompressionConfig {
127
0
    fn default() -> Self {
128
0
        Self {
129
0
            algorithm: DataCompressionAlgorithm::ZSTD,
130
0
            enabled: true,
131
0
            level: Some(3),
132
0
        }
133
0
    }
134
}
135
#[derive(Debug, Clone, Serialize, Deserialize)]
136
pub struct DataVersioningConfig {
137
    pub enabled: bool,
138
    pub version_format: String,
139
    pub keep_versions: usize,
140
}
141
142
impl Default for DataVersioningConfig {
143
0
    fn default() -> Self {
144
0
        Self {
145
0
            enabled: false,
146
0
            version_format: "v%Y%m%d_%H%M%S".to_owned(),
147
0
            keep_versions: 5,
148
0
        }
149
0
    }
150
}
151
152
#[derive(Debug, Clone, Serialize, Deserialize)]
153
pub struct DataRetentionConfig {
154
    pub auto_cleanup: bool,
155
    pub retention_days: u32,
156
}
157
158
impl Default for DataRetentionConfig {
159
0
    fn default() -> Self {
160
0
        Self {
161
0
            auto_cleanup: false,
162
0
            retention_days: 30,
163
0
        }
164
0
    }
165
}
166
167
#[derive(Debug, Clone, Serialize, Deserialize)]
168
pub enum DataStorageFormat {
169
    Parquet,
170
    Arrow,
171
    Json,
172
    Csv,
173
    CSV,
174
    HDF5,
175
}
176
177
#[derive(Debug, Clone, Serialize, Deserialize)]
178
pub struct DataStorageConfig {
179
    pub format: DataStorageFormat,
180
    pub compression: DataCompressionConfig,
181
    pub path: String,
182
    pub base_directory: std::path::PathBuf,
183
    pub partition_by: Vec<String>,
184
    pub versioning: DataVersioningConfig,
185
    pub retention: DataRetentionConfig,
186
}
187
188
impl Default for DataStorageConfig {
189
0
    fn default() -> Self {
190
0
        Self {
191
0
            format: DataStorageFormat::Parquet,
192
0
            compression: DataCompressionConfig::default(),
193
0
            path: "./data".to_owned(),
194
0
            base_directory: std::path::PathBuf::from("./data"),
195
0
            partition_by: vec!["symbol".to_owned(), "date".to_owned()],
196
0
            versioning: DataVersioningConfig::default(),
197
0
            retention: DataRetentionConfig::default(),
198
0
        }
199
0
    }
200
}
201
202
#[derive(Debug, Clone, Serialize, Deserialize)]
203
pub struct DataRegimeDetectionConfig {
204
    pub enable_hmm: bool,
205
    pub enable_clustering: bool,
206
    pub window_size: usize,
207
    pub n_states: usize,
208
    pub volatility_regime: bool,
209
    pub trend_regime: bool,
210
    pub volume_regime: bool,
211
    pub correlation_regime: bool,
212
    pub lookback_period: usize,
213
}
214
215
impl Default for DataRegimeDetectionConfig {
216
0
    fn default() -> Self {
217
0
        Self {
218
0
            enable_hmm: false,
219
0
            enable_clustering: false,
220
0
            window_size: 100,
221
0
            n_states: 3,
222
0
            volatility_regime: true,
223
0
            trend_regime: true,
224
0
            volume_regime: false,
225
0
            correlation_regime: false,
226
0
            lookback_period: 252,
227
0
        }
228
0
    }
229
}
230
231
#[derive(Debug, Clone, Serialize, Deserialize)]
232
pub struct DataProcessingConfig {
233
    pub worker_threads: usize,
234
    pub batch_size: usize,
235
    pub buffer_size: usize,
236
    pub timeout: u64,
237
    pub parallel_processing: bool,
238
}
239
240
impl Default for DataProcessingConfig {
241
0
    fn default() -> Self {
242
0
        Self {
243
0
            worker_threads: num_cpus::get(),
244
0
            batch_size: 1000,
245
0
            buffer_size: 10000,
246
0
            timeout: 300,
247
0
            parallel_processing: true,
248
0
        }
249
0
    }
250
}
251
252
#[derive(Debug, Clone, Serialize, Deserialize)]
253
pub struct DataTrainingConfig {
254
    pub batch_size: usize,
255
    pub sequence_length: usize,
256
    pub validation_split: f64,
257
    pub test_split: f64,
258
    pub sources: DataSourcesConfig,
259
    pub features: TrainingFeatureEngineeringConfig,
260
    pub validation: DataValidationConfig,
261
    pub storage: DataStorageConfig,
262
    pub processing: DataProcessingConfig,
263
    pub rate_limit: usize,
264
}
265
266
impl Default for DataTrainingConfig {
267
0
    fn default() -> Self {
268
0
        Self {
269
0
            batch_size: 32,
270
0
            sequence_length: 100,
271
0
            validation_split: 0.2,
272
0
            test_split: 0.1,
273
0
            sources: DataSourcesConfig::default(),
274
0
            features: TrainingFeatureEngineeringConfig::default(),
275
0
            validation: DataValidationConfig::default(),
276
0
            storage: DataStorageConfig::default(),
277
0
            processing: DataProcessingConfig::default(),
278
0
            rate_limit: 100,
279
0
        }
280
0
    }
281
}
282
283
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284
pub struct DataSourcesConfig {
285
    pub databento: Option<DatabentoConfig>,
286
    pub benzinga: Option<TrainingBenzingaConfig>,
287
    #[serde(default)]
288
    pub enable_realtime: bool,
289
    pub interactive_brokers: Option<InteractiveBrokersConfig>,
290
    pub icmarkets: Option<ICMarketsConfig>,
291
    pub historical: Option<HistoricalDataConfig>,
292
}
293
294
#[derive(Debug, Clone, Serialize, Deserialize)]
295
pub struct InteractiveBrokersConfig {
296
    pub host: String,
297
    pub port: u16,
298
    pub client_id: i32,
299
    pub timeout_seconds: u64,
300
}
301
302
#[derive(Debug, Clone, Serialize, Deserialize)]
303
pub struct ICMarketsConfig {
304
    pub api_key: String,
305
    pub environment: String,
306
}
307
308
#[derive(Debug, Clone, Serialize, Deserialize)]
309
pub struct HistoricalDataConfig {
310
    pub enabled: bool,
311
    pub batch_size: usize,
312
    pub parallel_downloads: usize,
313
}
314
315
#[derive(Debug, Clone, Serialize, Deserialize)]
316
pub struct DatabentoConfig {
317
    pub api_key: String,
318
    pub dataset: String,
319
    pub symbols: Vec<String>,
320
    pub schema: String,
321
    pub stype_in: String,
322
}
323
324
#[derive(Debug, Clone, Serialize, Deserialize)]
325
pub struct DataValidationConfig {
326
    #[serde(default)]
327
    pub enable_price_validation: bool,
328
    #[serde(default)]
329
    pub enable_volume_validation: bool,
330
    #[serde(default)]
331
    pub price_threshold: f64,
332
    #[serde(default)]
333
    pub volume_threshold: f64,
334
    #[serde(default)]
335
    pub outlier_method: OutlierDetectionMethod,
336
    #[serde(default)]
337
    pub max_price_change: f64,
338
    #[serde(default)]
339
    pub max_volume_change: f64,
340
    #[serde(default)]
341
    pub max_timestamp_drift: i64,
342
    #[serde(default)]
343
    pub price_validation: bool,
344
    #[serde(default)]
345
    pub volume_validation: bool,
346
    #[serde(default)]
347
    pub timestamp_validation: bool,
348
    #[serde(default)]
349
    pub outlier_detection: bool,
350
    #[serde(default)]
351
    pub missing_data_handling: MissingDataHandling,
352
}
353
354
impl Default for DataValidationConfig {
355
0
    fn default() -> Self {
356
0
        Self {
357
0
            enable_price_validation: true,
358
0
            enable_volume_validation: true,
359
0
            price_threshold: 0.1,
360
0
            volume_threshold: 0.2,
361
0
            outlier_method: OutlierDetectionMethod::ZScore,
362
0
            max_price_change: 0.05,
363
0
            max_volume_change: 2.0,
364
0
            max_timestamp_drift: 1000,
365
0
            price_validation: true,
366
0
            volume_validation: true,
367
0
            timestamp_validation: true,
368
0
            outlier_detection: true,
369
0
            missing_data_handling: MissingDataHandling::Skip,
370
0
        }
371
0
    }
372
}
373
374
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
375
pub enum MissingDataHandling {
376
    #[default]
377
    Skip,
378
    Drop,
379
    Interpolate,
380
    ForwardFill,
381
    BackwardFill,
382
    FillForward,
383
    FillBackward,
384
    Mean,
385
    Median,
386
    Error,
387
}
388
389
#[derive(Debug, Clone, Serialize, Deserialize)]
390
pub struct TrainingFeatureEngineeringConfig {
391
    pub enable_normalization: bool,
392
    pub enable_scaling: bool,
393
    pub enable_log_returns: bool,
394
    pub lookback_window: usize,
395
    pub regime_detection: DataRegimeDetectionConfig,
396
    pub technical_indicators: DataTechnicalIndicatorsConfig,
397
    pub microstructure: DataMicrostructureConfig,
398
}
399
400
impl Default for TrainingFeatureEngineeringConfig {
401
0
    fn default() -> Self {
402
0
        Self {
403
0
            enable_normalization: true,
404
0
            enable_scaling: true,
405
0
            enable_log_returns: true,
406
0
            lookback_window: 100,
407
0
            regime_detection: DataRegimeDetectionConfig::default(),
408
0
            technical_indicators: DataTechnicalIndicatorsConfig::default(),
409
0
            microstructure: DataMicrostructureConfig::default(),
410
0
        }
411
0
    }
412
}
413
414
#[derive(Debug, Clone, Serialize, Deserialize)]
415
pub struct DataTemporalConfig {
416
    pub enable_time_features: bool,
417
    pub enable_seasonal: bool,
418
    pub timezone: String,
419
    pub business_hours_only: bool,
420
    pub market_session: bool,
421
    pub holiday_effects: bool,
422
    pub expiration_effects: bool,
423
}
424
425
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426
pub enum OutlierDetectionMethod {
427
    #[default]
428
    ZScore,
429
    IQR,
430
    Isolation,
431
    IsolationForest,
432
    LocalOutlierFactor,
433
    None,
434
}
435
436
#[derive(Debug, Clone, Serialize, Deserialize)]
437
pub struct DataModuleConfig {
438
    pub data_path: String,
439
    pub batch_size: usize,
440
    pub num_workers: usize,
441
    pub cache_size: usize,
442
    pub settings: DataModuleSettings,
443
    pub interactive_brokers: Option<InteractiveBrokersConfig>,
444
}
445
446
#[derive(Debug, Clone, Serialize, Deserialize)]
447
pub struct DataModuleSettings {
448
    pub enable_preprocessing: bool,
449
    pub enable_validation: bool,
450
    pub max_memory_usage: usize,
451
    pub market_data_buffer_size: usize,
452
    pub order_event_buffer_size: usize,
453
}
454
455
#[derive(Debug, Clone, Serialize, Deserialize)]
456
pub struct DataMACDConfig {
457
    pub fast_period: usize,
458
    pub slow_period: usize,
459
    pub signal_period: usize,
460
    pub enabled: bool,
461
}
462
463
impl Default for DataMACDConfig {
464
0
    fn default() -> Self {
465
0
        Self {
466
0
            fast_period: 12,
467
0
            slow_period: 26,
468
0
            signal_period: 9,
469
0
            enabled: true,
470
0
        }
471
0
    }
472
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html deleted file mode 100644 index 186663ddf..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs
Line
Count
Source
1
//! Data provider endpoint configuration
2
//!
3
//! Centralizes all hardcoded API endpoints for data providers, enabling
4
//! environment-specific configurations and easy switching between dev/staging/prod.
5
6
use serde::{Deserialize, Serialize};
7
8
/// Environment specification for data providers
9
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10
pub enum DataProviderEnvironment {
11
    /// Development environment with potentially mocked or sandbox endpoints
12
    Development,
13
    /// Staging environment for pre-production testing
14
    Staging,
15
    /// Production environment with live data
16
    Production,
17
}
18
19
impl DataProviderEnvironment {
20
    /// Detect environment from FOXHUNT_ENV environment variable
21
0
    pub fn from_env() -> Self {
22
0
        match std::env::var("FOXHUNT_ENV")
23
0
            .unwrap_or_else(|_| "development".to_owned())
24
0
            .to_lowercase()
25
0
            .as_str()
26
        {
27
0
            "prod" | "production" => Self::Production,
28
0
            "staging" | "stage" => Self::Staging,
29
0
            _ => Self::Development,
30
        }
31
0
    }
32
}
33
34
/// Databento endpoint configuration
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct DatabentoEndpoints {
37
    /// WebSocket URL for real-time data streaming
38
    pub websocket_url: String,
39
    /// HTTP base URL for historical data queries
40
    pub historical_base_url: String,
41
}
42
43
impl DatabentoEndpoints {
44
    /// Create configuration from environment variables with fallback to defaults
45
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
46
0
        let (ws_default, http_default) = match environment {
47
0
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
48
0
                "wss://gateway.databento.com/v0/subscribe",
49
0
                "https://hist.databento.com",
50
0
            ),
51
0
            DataProviderEnvironment::Staging => (
52
0
                "wss://staging-gateway.databento.com/v0/subscribe",
53
0
                "https://staging-hist.databento.com",
54
0
            ),
55
        };
56
57
        Self {
58
0
            websocket_url: std::env::var("DATABENTO_WS_URL")
59
0
                .unwrap_or_else(|_| ws_default.to_owned()),
60
0
            historical_base_url: std::env::var("DATABENTO_HTTP_URL")
61
0
                .unwrap_or_else(|_| http_default.to_owned()),
62
        }
63
0
    }
64
}
65
66
impl Default for DatabentoEndpoints {
67
0
    fn default() -> Self {
68
0
        Self::from_env(DataProviderEnvironment::from_env())
69
0
    }
70
}
71
72
/// Benzinga endpoint configuration
73
#[derive(Debug, Clone, Serialize, Deserialize)]
74
pub struct BenzingaEndpoints {
75
    /// WebSocket URL for real-time news and sentiment streaming
76
    pub websocket_url: String,
77
    /// HTTP base URL for API queries
78
    pub api_base_url: String,
79
}
80
81
impl BenzingaEndpoints {
82
    /// Create configuration from environment variables with fallback to defaults
83
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
84
0
        let (ws_default, api_default) = match environment {
85
0
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
86
0
                "wss://api.benzinga.com/api/v1/stream",
87
0
                "https://api.benzinga.com/api/v2",
88
0
            ),
89
0
            DataProviderEnvironment::Staging => (
90
0
                "wss://staging-api.benzinga.com/api/v1/stream",
91
0
                "https://staging-api.benzinga.com/api/v2",
92
0
            ),
93
        };
94
95
        Self {
96
0
            websocket_url: std::env::var("BENZINGA_WS_URL")
97
0
                .unwrap_or_else(|_| ws_default.to_owned()),
98
0
            api_base_url: std::env::var("BENZINGA_API_URL")
99
0
                .unwrap_or_else(|_| api_default.to_owned()),
100
        }
101
0
    }
102
}
103
104
impl Default for BenzingaEndpoints {
105
0
    fn default() -> Self {
106
0
        Self::from_env(DataProviderEnvironment::from_env())
107
0
    }
108
}
109
110
/// Alpaca endpoint configuration
111
#[derive(Debug, Clone, Serialize, Deserialize)]
112
pub struct AlpacaEndpoints {
113
    /// Base URL for trading operations (paper or live)
114
    pub trading_base_url: String,
115
    /// Base URL for market data queries
116
    pub data_base_url: String,
117
}
118
119
impl AlpacaEndpoints {
120
    /// Create configuration from environment variables with fallback to defaults
121
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
122
0
        let (trading_default, data_default) = match environment {
123
0
            DataProviderEnvironment::Development => (
124
0
                "https://paper-api.alpaca.markets",
125
0
                "https://data.alpaca.markets",
126
0
            ),
127
0
            DataProviderEnvironment::Staging => (
128
0
                "https://paper-api.alpaca.markets",
129
0
                "https://data.alpaca.markets",
130
0
            ),
131
0
            DataProviderEnvironment::Production => (
132
0
                "https://api.alpaca.markets",
133
0
                "https://data.alpaca.markets",
134
0
            ),
135
        };
136
137
        Self {
138
0
            trading_base_url: std::env::var("ALPACA_TRADING_URL")
139
0
                .unwrap_or_else(|_| trading_default.to_owned()),
140
0
            data_base_url: std::env::var("ALPACA_DATA_URL")
141
0
                .unwrap_or_else(|_| data_default.to_owned()),
142
        }
143
0
    }
144
}
145
146
impl Default for AlpacaEndpoints {
147
0
    fn default() -> Self {
148
0
        Self::from_env(DataProviderEnvironment::from_env())
149
0
    }
150
}
151
152
/// Interactive Brokers Gateway configuration
153
#[derive(Debug, Clone, Serialize, Deserialize)]
154
pub struct IBGatewayConfig {
155
    /// Gateway host (typically localhost for local TWS/Gateway)
156
    pub host: String,
157
    /// Gateway port (7497 for paper trading, 7496 for live, 4001 for IB Gateway)
158
    pub port: u16,
159
}
160
161
impl IBGatewayConfig {
162
    /// Create configuration from environment variables with fallback to defaults
163
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
164
0
        let (host_default, port_default) = match environment {
165
0
            DataProviderEnvironment::Development => ("127.0.0.1", 7497), // Paper trading
166
0
            DataProviderEnvironment::Staging => ("127.0.0.1", 7497),     // Paper trading
167
0
            DataProviderEnvironment::Production => ("127.0.0.1", 7496),  // Live trading
168
        };
169
170
        Self {
171
0
            host: std::env::var("IB_GATEWAY_HOST")
172
0
                .unwrap_or_else(|_| host_default.to_owned()),
173
0
            port: std::env::var("IB_GATEWAY_PORT")
174
0
                .ok()
175
0
                .and_then(|s| s.parse().ok())
176
0
                .unwrap_or(port_default),
177
        }
178
0
    }
179
}
180
181
impl Default for IBGatewayConfig {
182
0
    fn default() -> Self {
183
0
        Self::from_env(DataProviderEnvironment::from_env())
184
0
    }
185
}
186
187
/// Master configuration for all data provider endpoints
188
#[derive(Debug, Clone, Serialize, Deserialize)]
189
pub struct DataProviderConfig {
190
    /// Current environment
191
    pub environment: DataProviderEnvironment,
192
    /// Databento endpoints
193
    pub databento: DatabentoEndpoints,
194
    /// Benzinga endpoints
195
    pub benzinga: BenzingaEndpoints,
196
    /// Alpaca endpoints
197
    pub alpaca: AlpacaEndpoints,
198
    /// Interactive Brokers Gateway configuration
199
    pub ib_gateway: IBGatewayConfig,
200
}
201
202
impl DataProviderConfig {
203
    /// Create configuration from environment
204
0
    pub fn from_env() -> Self {
205
0
        let environment = DataProviderEnvironment::from_env();
206
0
        Self {
207
0
            databento: DatabentoEndpoints::from_env(environment),
208
0
            benzinga: BenzingaEndpoints::from_env(environment),
209
0
            alpaca: AlpacaEndpoints::from_env(environment),
210
0
            ib_gateway: IBGatewayConfig::from_env(environment),
211
0
            environment,
212
0
        }
213
0
    }
214
215
    /// Create configuration for specific environment
216
0
    pub fn for_environment(environment: DataProviderEnvironment) -> Self {
217
0
        Self {
218
0
            databento: DatabentoEndpoints::from_env(environment),
219
0
            benzinga: BenzingaEndpoints::from_env(environment),
220
0
            alpaca: AlpacaEndpoints::from_env(environment),
221
0
            ib_gateway: IBGatewayConfig::from_env(environment),
222
0
            environment,
223
0
        }
224
0
    }
225
}
226
227
impl Default for DataProviderConfig {
228
0
    fn default() -> Self {
229
0
        Self::from_env()
230
0
    }
231
}
232
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236
237
    #[test]
238
    fn test_environment_detection() {
239
        std::env::set_var("FOXHUNT_ENV", "production");
240
        assert_eq!(
241
            DataProviderEnvironment::from_env(),
242
            DataProviderEnvironment::Production
243
        );
244
245
        std::env::set_var("FOXHUNT_ENV", "staging");
246
        assert_eq!(
247
            DataProviderEnvironment::from_env(),
248
            DataProviderEnvironment::Staging
249
        );
250
251
        std::env::set_var("FOXHUNT_ENV", "development");
252
        assert_eq!(
253
            DataProviderEnvironment::from_env(),
254
            DataProviderEnvironment::Development
255
        );
256
257
        std::env::remove_var("FOXHUNT_ENV");
258
        assert_eq!(
259
            DataProviderEnvironment::from_env(),
260
            DataProviderEnvironment::Development
261
        );
262
    }
263
264
    #[test]
265
    fn test_databento_defaults() {
266
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
267
        assert_eq!(
268
            config.websocket_url,
269
            "wss://gateway.databento.com/v0/subscribe"
270
        );
271
        assert_eq!(config.historical_base_url, "https://hist.databento.com");
272
    }
273
274
    #[test]
275
    fn test_benzinga_defaults() {
276
        let config = BenzingaEndpoints::from_env(DataProviderEnvironment::Production);
277
        assert_eq!(
278
            config.websocket_url,
279
            "wss://api.benzinga.com/api/v1/stream"
280
        );
281
        assert_eq!(config.api_base_url, "https://api.benzinga.com/api/v2");
282
    }
283
284
    #[test]
285
    fn test_alpaca_defaults() {
286
        let dev_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Development);
287
        assert_eq!(
288
            dev_config.trading_base_url,
289
            "https://paper-api.alpaca.markets"
290
        );
291
292
        let prod_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Production);
293
        assert_eq!(prod_config.trading_base_url, "https://api.alpaca.markets");
294
    }
295
296
    #[test]
297
    fn test_ib_gateway_defaults() {
298
        let dev_config = IBGatewayConfig::from_env(DataProviderEnvironment::Development);
299
        assert_eq!(dev_config.host, "127.0.0.1");
300
        assert_eq!(dev_config.port, 7497);
301
302
        let prod_config = IBGatewayConfig::from_env(DataProviderEnvironment::Production);
303
        assert_eq!(prod_config.port, 7496);
304
    }
305
306
    #[test]
307
    fn test_environment_variable_override() {
308
        std::env::set_var("DATABENTO_WS_URL", "wss://custom.databento.com");
309
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
310
        assert_eq!(config.websocket_url, "wss://custom.databento.com");
311
        std::env::remove_var("DATABENTO_WS_URL");
312
    }
313
314
    #[test]
315
    fn test_master_config() {
316
        let config = DataProviderConfig::for_environment(DataProviderEnvironment::Production);
317
        assert_eq!(config.environment, DataProviderEnvironment::Production);
318
        assert!(!config.databento.websocket_url.is_empty());
319
        assert!(!config.benzinga.websocket_url.is_empty());
320
        assert!(!config.alpaca.trading_base_url.is_empty());
321
        assert!(!config.ib_gateway.host.is_empty());
322
    }
323
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html deleted file mode 100644 index 7e9fb6d99..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/database.rs
Line
Count
Source
1
//! Database configuration for PostgreSQL connections and connection pooling.
2
//!
3
//! This module provides comprehensive database configuration structures for managing
4
//! PostgreSQL connections, connection pools, and transaction settings in the Foxhunt
5
//! HFT trading system. It supports connection pooling, timeout management, and
6
//! transaction isolation levels optimized for high-frequency trading workloads.
7
8
use serde::{Deserialize, Serialize};
9
use std::time::Duration;
10
11
#[cfg(feature = "postgres")]
12
use sqlx::Row;
13
14
/// Main database configuration structure for PostgreSQL connections.
15
///
16
/// Provides comprehensive database connection settings including connection pooling,
17
/// timeouts, logging, and transaction management. Optimized for high-frequency
18
/// trading workloads with appropriate defaults for low-latency operations.
19
#[derive(Debug, Clone, Serialize, Deserialize)]
20
#[allow(clippy::module_name_repetitions)]
21
pub struct DatabaseConfig {
22
    /// PostgreSQL connection URL (e.g., "postgresql://user:pass@host:port/database")
23
    pub url: String,
24
    /// Maximum number of connections in the pool
25
    pub max_connections: u32,
26
    /// Minimum number of connections to maintain in the pool
27
    pub min_connections: u32,
28
    /// Timeout for establishing new database connections
29
    pub connect_timeout: std::time::Duration,
30
    /// Timeout for individual query execution
31
    pub query_timeout: std::time::Duration,
32
    /// Enable detailed query logging for debugging
33
    pub enable_query_logging: bool,
34
    /// Application name to identify connections in PostgreSQL logs
35
    pub application_name: Option<String>,
36
    /// Connection pool configuration settings
37
    pub pool: PoolConfig,
38
    /// Transaction management configuration
39
    pub transaction: TransactionConfig,
40
}
41
42
impl Default for DatabaseConfig {
43
0
    fn default() -> Self {
44
0
        Self::new()
45
0
    }
46
}
47
48
impl DatabaseConfig {
49
    /// Creates a new DatabaseConfig with sensible defaults for development.
50
    ///
51
    /// Returns a configuration suitable for local development with a PostgreSQL
52
    /// database running on localhost. Production deployments should override
53
    /// these settings through environment variables or configuration files.
54
0
    pub fn new() -> Self {
55
        // Get database URL from environment, with fallback to development default
56
0
        let url = std::env::var("DATABASE_URL")
57
0
            .unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_owned());
58
    
59
0
        Self {
60
0
            url,
61
0
            max_connections: 10,
62
0
            min_connections: 1,
63
0
            connect_timeout: Duration::from_secs(30),
64
0
            query_timeout: Duration::from_secs(60),
65
0
            enable_query_logging: false,
66
0
            application_name: Some("foxhunt".to_owned()),
67
0
            pool: PoolConfig::default(),
68
0
            transaction: TransactionConfig::default(),
69
0
        }
70
0
    }
71
72
    /// Validates the database configuration for correctness.
73
    ///
74
    /// Performs basic validation checks on the configuration parameters to ensure
75
    /// they are valid before attempting to establish database connections.
76
    ///
77
    /// # Errors
78
    ///
79
    /// Returns an error string if the configuration is invalid, such as:
80
    /// - Empty database URL
81
    ///
82
    /// - Invalid connection parameters
83
0
    pub fn validate(&self) -> Result<(), String> {
84
0
        if self.url.is_empty() {
85
0
            return Err("Database URL cannot be empty".to_owned());
86
0
        }
87
0
        Ok(())
88
0
    }
89
}
90
91
/// Database connection pool configuration.
92
///
93
/// Manages the behavior of the connection pool including connection lifecycle,
94
/// timeouts, and health checking. Optimized for high-frequency trading workloads
95
/// where connection availability and low latency are critical.
96
#[derive(Debug, Clone, Serialize, Deserialize)]
97
pub struct PoolConfig {
98
    /// Minimum number of connections to maintain in the pool
99
    pub min_connections: u32,
100
    /// Maximum number of connections allowed in the pool
101
    pub max_connections: u32,
102
    /// Timeout in seconds for acquiring a connection from the pool
103
    pub acquire_timeout_secs: u64,
104
    /// Maximum lifetime in seconds for a connection before it's recycled
105
    pub max_lifetime_secs: u64,
106
    /// Timeout in seconds before idle connections are closed
107
    pub idle_timeout_secs: u64,
108
    /// Whether to test connections before returning them from the pool
109
    pub test_before_acquire: bool,
110
    /// Database URL for pool connections
111
    pub database_url: String,
112
    /// Enable periodic health checks for pool connections
113
    pub health_check_enabled: bool,
114
    /// Interval in seconds between health checks
115
    pub health_check_interval_secs: u64,
116
}
117
118
impl Default for PoolConfig {
119
0
    fn default() -> Self {
120
        // Get database URL from environment, with fallback to development default
121
0
        let database_url = std::env::var("DATABASE_URL")
122
0
            .unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_owned());
123
124
0
        Self {
125
0
            min_connections: 1,
126
0
            max_connections: 10,
127
0
            acquire_timeout_secs: 30,
128
0
            max_lifetime_secs: 3600,
129
0
            idle_timeout_secs: 3600,
130
0
            test_before_acquire: true,
131
0
            database_url,
132
0
            health_check_enabled: true,
133
0
            health_check_interval_secs: 60,
134
0
        }
135
0
    }
136
}
137
138
/// Database transaction configuration and retry policies.
139
///
140
/// Configures transaction behavior including isolation levels, timeouts,
141
/// and retry mechanisms. Critical for maintaining data consistency in
142
/// high-frequency trading operations while handling transient failures.
143
#[derive(Debug, Clone, Serialize, Deserialize)]
144
pub struct TransactionConfig {
145
    /// PostgreSQL transaction isolation level (e.g., "READ_COMMITTED", "SERIALIZABLE")
146
    pub isolation_level: String,
147
    /// Default timeout duration for transactions
148
    pub timeout: Duration,
149
    /// Default timeout in seconds for transactions
150
    pub default_timeout_secs: u64,
151
    /// Enable automatic retry on transaction failures
152
    pub enable_retry: bool,
153
    /// Maximum number of retry attempts for failed transactions
154
    pub max_retries: u32,
155
    /// Delay in milliseconds between retry attempts
156
    pub retry_delay_ms: u64,
157
    /// Maximum number of nested savepoints allowed
158
    pub max_savepoints: u32,
159
}
160
161
impl Default for TransactionConfig {
162
0
    fn default() -> Self {
163
0
        Self {
164
0
            isolation_level: "READ_COMMITTED".to_owned(),
165
0
            timeout: Duration::from_secs(30),
166
0
            default_timeout_secs: 30,
167
0
            enable_retry: true,
168
0
            max_retries: 3,
169
0
            retry_delay_ms: 100,
170
0
            max_savepoints: 10,
171
0
        }
172
0
    }
173
}
174
175
/// Database loader for symbol configurations with PostgreSQL integration.
176
///
177
/// Provides high-performance loading and caching of symbol configurations
178
/// from the PostgreSQL database. Supports real-time updates through PostgreSQL
179
///
180
/// NOTIFY/LISTEN for configuration hot-reload capabilities.
181
#[cfg(feature = "postgres")]
182
pub struct PostgresSymbolConfigLoader {
183
    /// Database connection pool
184
    pool: sqlx::PgPool,
185
    /// Configuration cache timeout
186
    cache_timeout: Duration,
187
    /// PostgreSQL listener for configuration changes
188
    listener: Option<sqlx::postgres::PgListener>,
189
}
190
191
#[cfg(feature = "postgres")]
192
impl PostgresSymbolConfigLoader {
193
    /// Creates a new PostgreSQL symbol configuration loader.
194
    ///
195
    /// # Errors
196
    /// Returns error if the operation fails
197
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
198
        let pool = sqlx::PgPool::connect(database_url).await?;
199
200
        Ok(Self {
201
            pool,
202
            cache_timeout: Duration::from_secs(300), // 5 minutes
203
            listener: None,
204
        })
205
    }
206
207
    /// Creates a new loader with an existing connection pool.
208
    pub const fn with_pool(pool: sqlx::PgPool) -> Self {
209
        Self {
210
            pool,
211
            cache_timeout: Duration::from_secs(300),
212
            listener: None,
213
        }
214
    }
215
216
    /// Loads a symbol configuration by symbol name.
217
    ///
218
    /// # Errors
219
    /// Returns error if the operation fails
220
    pub async fn load_symbol_config(
221
        &self,
222
        symbol: &str,
223
    ) -> Result<Option<crate::symbol_config::SymbolConfig>, sqlx::Error> {
224
        // Simplified implementation using basic sqlx::query instead of macros
225
        let query = "
226
            SELECT 
227
                sc.id,
228
                sc.symbol,
229
                sc.description,
230
                sc.classification,
231
                sc.primary_exchange,
232
                sc.currency,
233
                sc.tick_size,
234
                sc.lot_size,
235
                sc.min_order_size,
236
                sc.max_order_size,
237
                sc.sector,
238
                sc.industry,
239
                sc.market_cap,
240
                sc.avg_daily_volume,
241
                sc.margin_requirement,
242
                sc.position_limit,
243
                sc.risk_multiplier,
244
                sc.is_active,
245
                sc.data_source,
246
                sc.created_at,
247
                sc.updated_at,
248
                sc.last_validated
249
            FROM symbol_config sc
250
            WHERE sc.symbol = $1 AND sc.is_active = true
251
        ";
252
253
        let row = sqlx::query(query)
254
            .bind(symbol)
255
            .fetch_optional(&self.pool)
256
            .await?;
257
258
        if let Some(row) = row {
259
            // Create a basic symbol config from the row
260
            let symbol_name: String = row.get("symbol");
261
            let description: String = row.get("description");
262
            let classification_str: String = row.get("classification");
263
264
            let classification = match classification_str.as_str() {
265
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
266
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
267
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
268
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
269
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
270
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
271
                "OPTION" => crate::symbol_config::AssetClassification::Option,
272
                "ETF" => crate::symbol_config::AssetClassification::Etf,
273
                "INDEX" => crate::symbol_config::AssetClassification::Index,
274
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
275
                _ => crate::symbol_config::AssetClassification::Equity,
276
            };
277
278
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
279
            config.description = description;
280
            config.primary_exchange = row.get("primary_exchange");
281
            config.currency = row.get("currency");
282
283
            // Handle decimal conversions safely
284
            if let Ok(tick_size) = row.try_get::<rust_decimal::Decimal, _>("tick_size") {
285
                if let Ok(f) = tick_size.try_into() {
286
                    config.tick_size = f;
287
                }
288
            }
289
290
            Ok(Some(config))
291
        } else {
292
            Ok(None)
293
        }
294
    }
295
296
    /// Loads all active symbol configurations.
297
    ///
298
    /// # Errors
299
    /// Returns error if the operation fails
300
    pub async fn load_all_symbols(
301
        &self,
302
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
303
        let query = "
304
            SELECT symbol, description, classification
305
            FROM symbol_config 
306
            WHERE is_active = true
307
            ORDER BY symbol
308
        ";
309
310
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
311
312
        let mut configs = Vec::new();
313
        for row in rows {
314
            let symbol_name: String = row.get("symbol");
315
            let description: String = row.get("description");
316
            let classification_str: String = row.get("classification");
317
318
            let classification = match classification_str.as_str() {
319
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
320
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
321
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
322
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
323
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
324
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
325
                "OPTION" => crate::symbol_config::AssetClassification::Option,
326
                "ETF" => crate::symbol_config::AssetClassification::Etf,
327
                "INDEX" => crate::symbol_config::AssetClassification::Index,
328
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
329
                _ => crate::symbol_config::AssetClassification::Equity,
330
            };
331
332
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
333
            config.description = description;
334
            configs.push(config);
335
        }
336
337
        Ok(configs)
338
    }
339
340
    /// Loads symbols filtered by asset classification.
341
    ///
342
    /// # Errors
343
    /// Returns error if the operation fails
344
    pub async fn load_symbols_by_classification(
345
        &self,
346
        classification: crate::symbol_config::AssetClassification,
347
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
348
        let class_str = classification.regulatory_class();
349
350
        let query = "
351
            SELECT symbol, description, classification
352
            FROM symbol_config 
353
            WHERE is_active = true AND classification = $1
354
            ORDER BY symbol
355
        ";
356
357
        let rows = sqlx::query(query)
358
            .bind(class_str)
359
            .fetch_all(&self.pool)
360
            .await?;
361
362
        let mut configs = Vec::new();
363
        for row in rows {
364
            let symbol_name: String = row.get("symbol");
365
            let description: String = row.get("description");
366
            let mut config =
367
                crate::symbol_config::SymbolConfig::new(symbol_name, classification.clone());
368
            config.description = description;
369
            configs.push(config);
370
        }
371
372
        Ok(configs)
373
    }
374
    /// Saves or updates a symbol configuration.
375
    ///
376
    /// # Errors
377
    /// Returns error if the operation fails
378
    pub async fn save_symbol_config(
379
        &self,
380
        config: &crate::symbol_config::SymbolConfig,
381
    ) -> Result<(), sqlx::Error> {
382
        let query = "
383
            INSERT INTO symbol_config (
384
                symbol, description, classification, primary_exchange, currency
385
            ) VALUES ($1, $2, $3, $4, $5)
386
            ON CONFLICT (symbol) DO UPDATE SET
387
                description = EXCLUDED.description,
388
                classification = EXCLUDED.classification,
389
                primary_exchange = EXCLUDED.primary_exchange,
390
                currency = EXCLUDED.currency,
391
                updated_at = NOW()
392
        ";
393
394
        sqlx::query(query)
395
            .bind(&config.symbol)
396
            .bind(&config.description)
397
            .bind(config.classification.regulatory_class())
398
            .bind(&config.primary_exchange)
399
            .bind(&config.currency)
400
            .execute(&self.pool)
401
            .await?;
402
403
        Ok(())
404
    }
405
406
    /// Initializes PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
407
    ///
408
    /// # Errors
409
    /// Returns error if the operation fails
410
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
411
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
412
        listener.listen("symbol_config_changed").await?;
413
        self.listener = Some(listener);
414
        Ok(())
415
    }
416
417
    /// Checks for configuration change notifications.
418
    ///
419
    /// # Errors
420
    /// Returns error if the operation fails
421
    pub async fn check_for_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
422
        if let Some(listener) = &mut self.listener {
423
            if let Some(notification) = listener.try_recv().await? {
424
                return Ok(Some(notification.payload().to_owned()));
425
            }
426
        }
427
        Ok(None)
428
    }
429
}
430
431
/// Database integration for comprehensive asset classification system.
432
///
433
/// Provides PostgreSQL-backed storage and retrieval for asset classification
434
/// configurations with support for pattern matching, caching, and hot-reload.
435
#[cfg(feature = "postgres")]
436
pub struct PostgresAssetClassificationLoader {
437
    /// Database connection pool
438
    pool: sqlx::PgPool,
439
    /// Configuration cache timeout
440
    cache_timeout: Duration,
441
    /// PostgreSQL listener for configuration changes
442
    listener: Option<sqlx::postgres::PgListener>,
443
}
444
445
#[cfg(feature = "postgres")]
446
impl PostgresAssetClassificationLoader {
447
    /// Creates a new PostgreSQL asset classification loader.
448
    ///
449
    /// # Errors
450
    /// Returns error if the operation fails
451
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
452
        let pool = sqlx::PgPool::connect(database_url).await?;
453
454
        Ok(Self {
455
            pool,
456
            cache_timeout: Duration::from_secs(300), // 5 minutes
457
            listener: None,
458
        })
459
    }
460
461
    /// Creates a new loader with an existing connection pool.
462
    pub const fn with_pool(pool: sqlx::PgPool) -> Self {
463
        Self {
464
            pool,
465
            cache_timeout: Duration::from_secs(300),
466
            listener: None,
467
        }
468
    }
469
470
    /// Loads all active asset configurations ordered by priority.
471
    ///
472
    /// # Errors
473
    /// Returns error if the operation fails
474
    pub async fn load_asset_configurations(
475
        &self,
476
    ) -> Result<Vec<crate::asset_classification::AssetConfig>, sqlx::Error> {
477
        let query = "
478
                SELECT 
479
                    id,
480
                    name,
481
                    symbol_pattern,
482
                    asset_class_data,
483
                    volatility_profile,
484
                    trading_parameters,
485
                    priority,
486
                    is_active,
487
                    created_at,
488
                    updated_at,
489
                    trading_hours,
490
                    settlement_config
491
                FROM asset_configurations
492
                WHERE is_active = true
493
                ORDER BY priority DESC
494
            ";
495
496
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
497
498
        let mut configs = Vec::new();
499
        for row in rows {
500
            if let Ok(config) = Self::row_to_asset_config(row) {
501
                configs.push(config);
502
            }
503
        }
504
505
        Ok(configs)
506
    }
507
508
    /// Loads a specific asset configuration by ID.
509
    ///
510
    /// # Errors
511
    /// Returns error if the operation fails
512
    pub async fn load_asset_configuration_by_id(
513
        &self,
514
        id: uuid::Uuid,
515
    ) -> Result<Option<crate::asset_classification::AssetConfig>, sqlx::Error> {
516
        let query = "
517
                SELECT 
518
                    id,
519
                    name,
520
                    symbol_pattern,
521
                    asset_class_data,
522
                    volatility_profile,
523
                    trading_parameters,
524
                    priority,
525
                    is_active,
526
                    created_at,
527
                    updated_at,
528
                    trading_hours,
529
                    settlement_config
530
                FROM asset_configurations
531
                WHERE id = $1
532
            ";
533
534
        let row = sqlx::query(query)
535
            .bind(id)
536
            .fetch_optional(&self.pool)
537
            .await?;
538
539
        if let Some(row) = row {
540
            Ok(Some(Self::row_to_asset_config(row)?))
541
        } else {
542
            Ok(None)
543
        }
544
    }
545
546
    /// Saves or updates an asset configuration.
547
    ///
548
    /// # Errors
549
    /// Returns error if the operation fails
550
    pub async fn save_asset_configuration(
551
        &self,
552
        config: &crate::asset_classification::AssetConfig,
553
    ) -> Result<(), sqlx::Error> {
554
        let query = "
555
                INSERT INTO asset_configurations (
556
                    id, name, symbol_pattern, asset_class_data, volatility_profile,
557
                    trading_parameters, priority, is_active, created_at, updated_at,
558
                    trading_hours, settlement_config
559
                ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
560
                ON CONFLICT (id) DO UPDATE SET
561
                    name = EXCLUDED.name,
562
                    symbol_pattern = EXCLUDED.symbol_pattern,
563
                    asset_class_data = EXCLUDED.asset_class_data,
564
                    volatility_profile = EXCLUDED.volatility_profile,
565
                    trading_parameters = EXCLUDED.trading_parameters,
566
                    priority = EXCLUDED.priority,
567
                    is_active = EXCLUDED.is_active,
568
                    updated_at = NOW(),
569
                    trading_hours = EXCLUDED.trading_hours,
570
                    settlement_config = EXCLUDED.settlement_config
571
            ";
572
573
        let asset_class_json = serde_json::to_value(&config.asset_class)
574
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
575
        let volatility_json = serde_json::to_value(&config.volatility_profile)
576
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
577
        let trading_params_json = serde_json::to_value(&config.trading_parameters)
578
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
579
        let trading_hours_json = serde_json::to_value(&config.trading_hours)
580
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
581
        let settlement_json = serde_json::to_value(&config.settlement_config)
582
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
583
584
        sqlx::query(query)
585
            .bind(config.id)
586
            .bind(&config.name)
587
            .bind(&config.symbol_pattern)
588
            .bind(asset_class_json)
589
            .bind(volatility_json)
590
            .bind(trading_params_json)
591
            .bind(i32::try_from(config.priority).unwrap_or(0))
592
            .bind(config.is_active)
593
            .bind(config.created_at)
594
            .bind(config.updated_at)
595
            .bind(trading_hours_json)
596
            .bind(settlement_json)
597
            .execute(&self.pool)
598
            .await?;
599
600
        Ok(())
601
    }
602
603
    /// Loads explicit symbol mappings.
604
    ///
605
    /// # Errors
606
    /// Returns error if the operation fails
607
    pub async fn load_symbol_mappings(
608
        &self,
609
    ) -> Result<
610
        std::collections::HashMap<String, crate::asset_classification::AssetClass>,
611
        sqlx::Error,
612
    > {
613
        let query = "
614
                SELECT symbol, asset_class_data
615
                FROM symbol_mappings
616
                WHERE is_active = true AND (expires_at IS NULL OR expires_at > NOW())
617
            ";
618
619
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
620
621
        let mut mappings = std::collections::HashMap::new();
622
        for row in rows {
623
            let symbol: String = row.get("symbol");
624
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
625
626
            if let Ok(asset_class) =
627
                serde_json::from_value::<crate::asset_classification::AssetClass>(asset_class_json)
628
            {
629
                mappings.insert(symbol.to_uppercase(), asset_class);
630
            }
631
        }
632
633
        Ok(mappings)
634
    }
635
636
    /// Saves a symbol mapping.
637
    ///
638
    /// # Errors
639
    /// Returns error if the operation fails
640
    pub async fn save_symbol_mapping(
641
        &self,
642
        symbol: &str,
643
        asset_class: &crate::asset_classification::AssetClass,
644
        source: &str,
645
        confidence_score: f64,
646
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
647
    ) -> Result<(), sqlx::Error> {
648
        let query = "
649
                INSERT INTO symbol_mappings (
650
                    symbol, asset_class_data, source, confidence_score, expires_at
651
                ) VALUES ($1, $2, $3, $4, $5)
652
                ON CONFLICT (symbol) DO UPDATE SET
653
                    asset_class_data = EXCLUDED.asset_class_data,
654
                    source = EXCLUDED.source,
655
                    confidence_score = EXCLUDED.confidence_score,
656
                    expires_at = EXCLUDED.expires_at,
657
                    updated_at = NOW()
658
            ";
659
660
        let asset_class_json =
661
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
662
663
        sqlx::query(query)
664
            .bind(symbol.to_uppercase())
665
            .bind(asset_class_json)
666
            .bind(source)
667
            .bind(confidence_score)
668
            .bind(expires_at)
669
            .execute(&self.pool)
670
            .await?;
671
672
        Ok(())
673
    }
674
675
    /// Loads volatility profiles.
676
    ///
677
    /// # Errors
678
    /// Returns error if the operation fails
679
    pub async fn load_volatility_profiles(
680
        &self,
681
    ) -> Result<
682
        std::collections::HashMap<String, crate::asset_classification::VolatilityProfile>,
683
        sqlx::Error,
684
    > {
685
        let query = "
686
                SELECT 
687
                    name,
688
                    base_annual_volatility,
689
                    stress_volatility_multiplier,
690
                    intraday_pattern,
691
                    volatility_persistence,
692
                    jump_risk
693
                FROM volatility_profiles
694
                WHERE is_active = true
695
            ";
696
697
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
698
699
        let mut profiles = std::collections::HashMap::new();
700
        for row in rows {
701
            let name: String = row.get("name");
702
            let base_volatility: rust_decimal::Decimal = row.get("base_annual_volatility");
703
            let stress_multiplier: rust_decimal::Decimal = row.get("stress_volatility_multiplier");
704
            let persistence: rust_decimal::Decimal = row.get("volatility_persistence");
705
            let intraday_json: serde_json::Value = row.get("intraday_pattern");
706
            let jump_risk_json: serde_json::Value = row.get("jump_risk");
707
708
            if let (Ok(base_vol), Ok(stress_mult), Ok(persist), Ok(intraday), Ok(jump_risk)) = (
709
                f64::try_from(base_volatility),
710
                f64::try_from(stress_multiplier),
711
                f64::try_from(persistence),
712
                serde_json::from_value::<Vec<f64>>(intraday_json),
713
                serde_json::from_value::<crate::asset_classification::JumpRiskProfile>(
714
                    jump_risk_json,
715
                ),
716
            ) {
717
                let profile = crate::asset_classification::VolatilityProfile {
718
                    base_annual_volatility: base_vol,
719
                    stress_volatility_multiplier: stress_mult,
720
                    intraday_pattern: intraday,
721
                    volatility_persistence: persist,
722
                    jump_risk,
723
                };
724
                profiles.insert(name, profile);
725
            }
726
        }
727
728
        Ok(profiles)
729
    }
730
731
    /// Caches symbol classification for performance.
732
    ///
733
    /// # Errors
734
    /// Returns error if the operation fails
735
    pub async fn cache_symbol_classification(
736
        &self,
737
        symbol: &str,
738
        asset_class: &crate::asset_classification::AssetClass,
739
        configuration_id: Option<uuid::Uuid>,
740
    ) -> Result<(), sqlx::Error> {
741
        let query = "
742
                INSERT INTO asset_classification_cache (symbol, asset_class_data, configuration_id)
743
                VALUES ($1, $2, $3)
744
                ON CONFLICT (symbol) DO UPDATE SET
745
                    asset_class_data = EXCLUDED.asset_class_data,
746
                    configuration_id = EXCLUDED.configuration_id,
747
                    cached_at = NOW(),
748
                    expires_at = NOW() + INTERVAL '1 hour'
749
            ";
750
751
        let asset_class_json =
752
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
753
754
        sqlx::query(query)
755
            .bind(symbol.to_uppercase())
756
            .bind(asset_class_json)
757
            .bind(configuration_id)
758
            .execute(&self.pool)
759
            .await?;
760
761
        Ok(())
762
    }
763
764
    /// Retrieves cached symbol classification.
765
    ///
766
    /// # Errors
767
    /// Returns error if the operation fails
768
    pub async fn get_cached_classification(
769
        &self,
770
        symbol: &str,
771
    ) -> Result<Option<crate::asset_classification::AssetClass>, sqlx::Error> {
772
        let query = "
773
                SELECT asset_class_data
774
                FROM asset_classification_cache
775
                WHERE symbol = $1 AND expires_at > NOW()
776
            ";
777
778
        let row = sqlx::query(query)
779
            .bind(symbol.to_uppercase())
780
            .fetch_optional(&self.pool)
781
            .await?;
782
783
        if let Some(row) = row {
784
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
785
            Ok(serde_json::from_value(asset_class_json).ok())
786
        } else {
787
            Ok(None)
788
        }
789
    }
790
791
    /// Cleans up expired cache entries.
792
    ///
793
    /// # Errors
794
    /// Returns error if the operation fails
795
    pub async fn cleanup_cache(&self) -> Result<u64, sqlx::Error> {
796
        let query = "DELETE FROM asset_classification_cache WHERE expires_at < NOW()";
797
        let result = sqlx::query(query).execute(&self.pool).await?;
798
        Ok(result.rows_affected())
799
    }
800
801
    /// Logs asset classification changes for audit.
802
    ///
803
    /// # Errors
804
    /// Returns error if the operation fails
805
    pub async fn log_classification_change(
806
        &self,
807
        symbol: &str,
808
        old_classification: Option<&crate::asset_classification::AssetClass>,
809
        new_classification: &crate::asset_classification::AssetClass,
810
        changed_by: &str,
811
        reason: &str,
812
    ) -> Result<(), sqlx::Error> {
813
        let query = "
814
                INSERT INTO asset_classification_audit (
815
                    symbol, old_classification, new_classification, changed_by, change_reason
816
                ) VALUES ($1, $2, $3, $4, $5)
817
            ";
818
819
        let old_json = old_classification
820
            .map(|c| serde_json::to_value(c).ok())
821
            .flatten();
822
        let new_json = serde_json::to_value(new_classification)
823
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
824
825
        sqlx::query(query)
826
            .bind(symbol)
827
            .bind(old_json)
828
            .bind(new_json)
829
            .bind(changed_by)
830
            .bind(reason)
831
            .execute(&self.pool)
832
            .await?;
833
834
        Ok(())
835
    }
836
837
    /// Enables PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
838
    ///
839
    /// # Errors
840
    /// Returns error if the operation fails
841
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
842
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
843
        listener.listen("config_change").await?;
844
        self.listener = Some(listener);
845
        Ok(())
846
    }
847
848
    /// Checks for configuration change notifications.
849
    ///
850
    /// # Errors
851
    /// Returns error if the operation fails
852
    pub async fn check_for_config_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
853
        if let Some(listener) = &mut self.listener {
854
            if let Some(notification) = listener.try_recv().await? {
855
                return Ok(Some(notification.payload().to_owned()));
856
            }
857
        }
858
        Ok(None)
859
    }
860
861
    /// Converts a database row to AssetConfig.
862
    fn row_to_asset_config(row: sqlx::postgres::PgRow) -> Result<crate::asset_classification::AssetConfig, sqlx::Error> {
863
        let id: uuid::Uuid = row.get("id");
864
        let name: String = row.get("name");
865
        let symbol_pattern: String = row.get("symbol_pattern");
866
        let priority: i32 = row.get("priority");
867
        let is_active: bool = row.get("is_active");
868
        let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
869
        let updated_at: chrono::DateTime<chrono::Utc> = row.get("updated_at");
870
871
        let asset_class_json: serde_json::Value = row.get("asset_class_data");
872
        let volatility_json: serde_json::Value = row.get("volatility_profile");
873
        let trading_params_json: serde_json::Value = row.get("trading_parameters");
874
        let trading_hours_json: Option<serde_json::Value> = row.get("trading_hours");
875
        let settlement_json: serde_json::Value = row.get("settlement_config");
876
877
        let asset_class = serde_json::from_value(asset_class_json)
878
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
879
        let volatility_profile = serde_json::from_value(volatility_json)
880
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
881
        let trading_parameters = serde_json::from_value(trading_params_json)
882
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
883
        let trading_hours = trading_hours_json
884
            .map(|json| serde_json::from_value(json).ok())
885
            .flatten();
886
        let settlement_config = serde_json::from_value(settlement_json)
887
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
888
889
        Ok(crate::asset_classification::AssetConfig {
890
            id,
891
            name,
892
            symbol_pattern,
893
            compiled_pattern: None, // Will be compiled when loaded
894
            asset_class,
895
            volatility_profile,
896
            trading_parameters,
897
            priority: u32::try_from(priority).unwrap_or(0),
898
            is_active,
899
            created_at,
900
            updated_at,
901
            trading_hours,
902
            settlement_config,
903
        })
904
    }
905
}
906
907
/// General-purpose PostgreSQL configuration loader for various configuration types.
908
///
909
/// Provides a unified interface for loading configurations from PostgreSQL with
910
/// support for hot-reload through NOTIFY/LISTEN and caching for performance.
911
#[cfg(feature = "postgres")]
912
pub struct PostgresConfigLoader {
913
    /// Database connection pool
914
    pool: sqlx::PgPool,
915
    /// Configuration cache timeout
916
    cache_timeout: Duration,
917
}
918
919
#[cfg(feature = "postgres")]
920
impl PostgresConfigLoader {
921
    /// Creates a new PostgreSQL configuration loader.
922
    ///
923
    /// # Errors
924
    /// Returns error if the operation fails
925
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
926
        let pool = sqlx::PgPool::connect(database_url).await?;
927
928
        Ok(Self {
929
            pool,
930
            cache_timeout: Duration::from_secs(300), // 5 minutes
931
        })
932
    }
933
934
    /// Creates a new loader with an existing pool
935
    pub const fn with_pool(pool: sqlx::PgPool) -> Self {
936
        Self {
937
            pool,
938
            cache_timeout: Duration::from_secs(300),
939
        }
940
    }
941
    /// Returns a reference to the connection pool
942
    pub const fn pool(&self) -> &sqlx::PgPool {
943
        &self.pool
944
    }
945
    // ============================================================================
946
    // ADAPTIVE STRATEGY CONFIGURATION METHODS
947
    // ============================================================================
948
949
    /// Get adaptive strategy configuration by strategy ID.
950
    ///
951
    /// Loads the complete configuration including main settings, models, and features
952
    /// from the PostgreSQL database. Returns None if the strategy doesn't exist.
953
    ///
954
    /// # Arguments
955
    /// * `strategy_id` - Unique identifier for the strategy (e.g., "default", "prod_v1")
956
    ///
957
    /// # Returns
958
    /// - `Ok(Some(config))` - Configuration found and loaded successfully
959
    ///
960
    /// - `Ok(None)` - Strategy ID not found in database
961
    /// - `Err(sqlx::Error)` - Database error occurred
962
    ///
963
    /// # Example
964
    /// ```no_run
965
    /// # use config::PostgresConfigLoader;
966
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
967
    /// let config = loader.get_adaptive_strategy_config("default").await?;
968
    /// if let Some(cfg) = config {
969
    ///     println!("Loaded strategy: {}", cfg.name);
970
    /// }
971
    /// # Ok(())
972
    /// # }
973
    /// ```
974
    ///
975
    /// # Errors
976
    /// Returns error if the operation fails
977
    pub async fn get_adaptive_strategy_config(
978
        &self,
979
        strategy_id: &str,
980
    ) -> Result<Option<serde_json::Value>, sqlx::Error> {
981
        // Query main configuration
982
        let row = sqlx::query(
983
            r#"
984
            SELECT
985
                id, strategy_id, name, description,
986
                execution_interval_ms, error_backoff_duration_secs,
987
                max_concurrent_operations, strategy_timeout_secs,
988
                max_parallel_models, rebalancing_interval_secs,
989
                min_model_weight, max_model_weight,
990
                max_position_size, max_leverage, stop_loss_pct,
991
                position_sizing_method, max_portfolio_var,
992
                max_drawdown_threshold, kelly_fraction,
993
                book_depth, vpin_window, trade_classification_threshold,
994
                trade_size_buckets, microstructure_features,
995
                regime_detection_method, regime_lookback_window,
996
                regime_transition_threshold, regime_features,
997
                execution_algorithm, max_order_size, min_order_size,
998
                order_timeout_secs, max_slippage_bps,
999
                smart_routing_enabled, dark_pool_preference,
1000
                active, version, created_at, updated_at,
1001
                created_by, updated_by, metadata
1002
            FROM adaptive_strategy_config
1003
            WHERE strategy_id = $1 AND active = true
1004
            "#,
1005
        )
1006
        .bind(strategy_id)
1007
        .fetch_optional(&self.pool)
1008
        .await?;
1009
1010
        let Some(row) = row else {
1011
            return Ok(None);
1012
        };
1013
1014
        let config_id: uuid::Uuid = row.try_get("id")?;
1015
1016
        // Query associated models
1017
        let models = sqlx::query(
1018
            r#"
1019
            SELECT
1020
                id, strategy_config_id, model_id, model_name, model_type,
1021
                parameters, initial_weight, enabled, display_order,
1022
                created_at, updated_at
1023
            FROM adaptive_strategy_models
1024
            WHERE strategy_config_id = $1
1025
            ORDER BY display_order, created_at
1026
            "#,
1027
        )
1028
        .bind(config_id)
1029
        .fetch_all(&self.pool)
1030
        .await?;
1031
1032
        // Query associated features
1033
        let features = sqlx::query(
1034
            r#"
1035
            SELECT
1036
                id, strategy_config_id, feature_name, feature_type,
1037
                parameters, enabled, required,
1038
                created_at, updated_at
1039
            FROM adaptive_strategy_features
1040
            WHERE strategy_config_id = $1
1041
            ORDER BY feature_name
1042
            "#,
1043
        )
1044
        .bind(config_id)
1045
        .fetch_all(&self.pool)
1046
        .await?;
1047
1048
        // Convert to JSON for flexibility
1049
        // In production, you'd convert to a proper struct type
1050
        let config = serde_json::json!({
1051
            "id": row.try_get::<uuid::Uuid, _>("id")?,
1052
            "strategy_id": row.try_get::<String, _>("strategy_id")?,
1053
            "name": row.try_get::<String, _>("name")?,
1054
            "description": row.try_get::<Option<String>, _>("description")?,
1055
            "general": {
1056
                "execution_interval_ms": row.try_get::<i32, _>("execution_interval_ms")?,
1057
                "error_backoff_duration_secs": row.try_get::<i32, _>("error_backoff_duration_secs")?,
1058
                "max_concurrent_operations": row.try_get::<i32, _>("max_concurrent_operations")?,
1059
                "strategy_timeout_secs": row.try_get::<i32, _>("strategy_timeout_secs")?,
1060
            },
1061
            "ensemble": {
1062
                "max_parallel_models": row.try_get::<i32, _>("max_parallel_models")?,
1063
                "rebalancing_interval_secs": row.try_get::<i32, _>("rebalancing_interval_secs")?,
1064
                "min_model_weight": row.try_get::<f64, _>("min_model_weight")?,
1065
                "max_model_weight": row.try_get::<f64, _>("max_model_weight")?,
1066
            },
1067
            "risk": {
1068
                "max_position_size": row.try_get::<f64, _>("max_position_size")?,
1069
                "max_leverage": row.try_get::<f64, _>("max_leverage")?,
1070
                "stop_loss_pct": row.try_get::<f64, _>("stop_loss_pct")?,
1071
                "position_sizing_method": row.try_get::<String, _>("position_sizing_method")?,
1072
                "max_portfolio_var": row.try_get::<f64, _>("max_portfolio_var")?,
1073
                "max_drawdown_threshold": row.try_get::<f64, _>("max_drawdown_threshold")?,
1074
                "kelly_fraction": row.try_get::<f64, _>("kelly_fraction")?,
1075
            },
1076
            "microstructure": {
1077
                "book_depth": row.try_get::<i32, _>("book_depth")?,
1078
                "vpin_window": row.try_get::<i32, _>("vpin_window")?,
1079
                "trade_classification_threshold": row.try_get::<f64, _>("trade_classification_threshold")?,
1080
                "trade_size_buckets": row.try_get::<Vec<f64>, _>("trade_size_buckets")?,
1081
                "features": row.try_get::<Vec<String>, _>("microstructure_features")?,
1082
            },
1083
            "regime": {
1084
                "detection_method": row.try_get::<String, _>("regime_detection_method")?,
1085
                "lookback_window": row.try_get::<i32, _>("regime_lookback_window")?,
1086
                "transition_threshold": row.try_get::<f64, _>("regime_transition_threshold")?,
1087
                "features": row.try_get::<Vec<String>, _>("regime_features")?,
1088
            },
1089
            "execution": {
1090
                "algorithm": row.try_get::<String, _>("execution_algorithm")?,
1091
                "max_order_size": row.try_get::<f64, _>("max_order_size")?,
1092
                "min_order_size": row.try_get::<f64, _>("min_order_size")?,
1093
                "order_timeout_secs": row.try_get::<i32, _>("order_timeout_secs")?,
1094
                "max_slippage_bps": row.try_get::<f64, _>("max_slippage_bps")?,
1095
                "smart_routing_enabled": row.try_get::<bool, _>("smart_routing_enabled")?,
1096
                "dark_pool_preference": row.try_get::<f64, _>("dark_pool_preference")?,
1097
            },
1098
            "models": models.iter().map(|m| serde_json::json!({
1099
                "id": m.try_get::<uuid::Uuid, _>("id").unwrap(),
1100
                "model_id": m.try_get::<String, _>("model_id").unwrap(),
1101
                "model_name": m.try_get::<String, _>("model_name").unwrap(),
1102
                "model_type": m.try_get::<String, _>("model_type").unwrap(),
1103
                "parameters": m.try_get::<serde_json::Value, _>("parameters").unwrap(),
1104
                "initial_weight": m.try_get::<f64, _>("initial_weight").unwrap(),
1105
                "enabled": m.try_get::<bool, _>("enabled").unwrap(),
1106
            })).collect::<Vec<_>>(),
1107
            "features": features.iter().map(|f| serde_json::json!({
1108
                "name": f.try_get::<String, _>("feature_name").unwrap(),
1109
                "feature_type": f.try_get::<String, _>("feature_type").unwrap(),
1110
                "parameters": f.try_get::<serde_json::Value, _>("parameters").unwrap(),
1111
                "enabled": f.try_get::<bool, _>("enabled").unwrap(),
1112
                "required": f.try_get::<bool, _>("required").unwrap(),
1113
            })).collect::<Vec<_>>(),
1114
            "version": row.try_get::<i32, _>("version")?,
1115
            "created_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")?,
1116
            "updated_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("updated_at")?,
1117
        });
1118
1119
        Ok(Some(config))
1120
    }
1121
1122
    /// Upsert (insert or update) adaptive strategy configuration.
1123
    ///
1124
    /// Creates a new strategy configuration if it doesn't exist, or updates
1125
    /// the existing one. Automatically handles version tracking and audit trail.
1126
    ///
1127
    /// # Arguments
1128
    /// * `config` - Configuration data as JSON (allows flexibility in structure)
1129
    ///
1130
    /// # Returns
1131
    /// - `Ok(strategy_id)` - Strategy ID of the created/updated configuration
1132
    ///
1133
    /// - `Err(sqlx::Error)` - Database error occurred
1134
    ///
1135
    /// # Example
1136
    /// ```no_run
1137
    /// # use config::PostgresConfigLoader;
1138
    /// # use serde_json::json;
1139
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
1140
    /// let config = json!({
1141
    ///     "strategy_id": "my_strategy",
1142
    ///     "name": "My Trading Strategy",
1143
    ///     "risk": {
1144
    ///         "max_position_size": 0.15,
1145
    ///         "max_leverage": 3.0
1146
    ///     }
1147
    /// });
1148
    /// let id = loader.upsert_adaptive_strategy_config(&config).await?;
1149
    /// # Ok(())
1150
    /// # }
1151
    /// ```
1152
    ///
1153
    /// # Errors
1154
    /// Returns error if the operation fails
1155
    pub async fn upsert_adaptive_strategy_config(
1156
        &self,
1157
        config: &serde_json::Value,
1158
    ) -> Result<String, sqlx::Error> {
1159
        let strategy_id = config
1160
            .get("strategy_id")
1161
            .and_then(|v| v.as_str())
1162
            .ok_or_else(|| {
1163
                sqlx::Error::Decode(Box::new(std::io::Error::new(
1164
                    std::io::ErrorKind::InvalidData,
1165
                    "Missing strategy_id in config",
1166
                )))
1167
            })?;
1168
    
1169
        // Extract all configuration fields
1170
        let name = config.get("name").and_then(|v| v.as_str()).unwrap_or("Unnamed Strategy");
1171
        let description = config.get("description").and_then(|v| v.as_str());
1172
    
1173
        // Helper macro for extracting fields with defaults
1174
        macro_rules! get_i32 {
1175
            ($field:expr, $default:expr) => {
1176
                config.get($field).and_then(|v| v.as_i64()).and_then(|v| i32::try_from(v).ok()).unwrap_or($default)
1177
            };
1178
        }
1179
        macro_rules! get_f64 {
1180
            ($field:expr, $default:expr) => {
1181
                config.get($field).and_then(|v| v.as_f64()).unwrap_or($default)
1182
            };
1183
        }
1184
        macro_rules! get_bool {
1185
            ($field:expr, $default:expr) => {
1186
                config.get($field).and_then(|v| v.as_bool()).unwrap_or($default)
1187
            };
1188
        }
1189
        macro_rules! get_str {
1190
            ($field:expr, $default:expr) => {
1191
                config.get($field).and_then(|v| v.as_str()).unwrap_or($default)
1192
            };
1193
        }
1194
    
1195
        // Full upsert with all 50+ fields
1196
        let query = r#"
1197
            INSERT INTO adaptive_strategy_config (
1198
                strategy_id, name, description,
1199
                -- General config
1200
                execution_interval_ms, error_backoff_duration_secs,
1201
                max_concurrent_operations, strategy_timeout_secs,
1202
                -- Ensemble config
1203
                max_parallel_models, rebalancing_interval_secs,
1204
                min_model_weight, max_model_weight,
1205
                -- Risk config
1206
                max_position_size, max_leverage, stop_loss_pct,
1207
                position_sizing_method, max_portfolio_var,
1208
                max_drawdown_threshold, kelly_fraction,
1209
                -- Microstructure config
1210
                book_depth, vpin_window, trade_classification_threshold,
1211
                trade_size_buckets, microstructure_features,
1212
                -- Regime config
1213
                regime_detection_method, regime_lookback_window,
1214
                regime_transition_threshold, regime_features,
1215
                -- Execution config
1216
                execution_algorithm, max_order_size, min_order_size,
1217
                order_timeout_secs, max_slippage_bps,
1218
                smart_routing_enabled, dark_pool_preference
1219
            ) VALUES (
1220
                $1, $2, $3,
1221
                $4, $5, $6, $7,
1222
                $8, $9, $10, $11,
1223
                $12, $13, $14, $15, $16, $17, $18,
1224
                $19, $20, $21, $22, $23,
1225
                $24, $25, $26, $27,
1226
                $28, $29, $30, $31, $32, $33, $34
1227
            )
1228
            ON CONFLICT (strategy_id)
1229
            DO UPDATE SET
1230
                name = EXCLUDED.name,
1231
                description = EXCLUDED.description,
1232
                execution_interval_ms = EXCLUDED.execution_interval_ms,
1233
                error_backoff_duration_secs = EXCLUDED.error_backoff_duration_secs,
1234
                max_concurrent_operations = EXCLUDED.max_concurrent_operations,
1235
                strategy_timeout_secs = EXCLUDED.strategy_timeout_secs,
1236
                max_parallel_models = EXCLUDED.max_parallel_models,
1237
                rebalancing_interval_secs = EXCLUDED.rebalancing_interval_secs,
1238
                min_model_weight = EXCLUDED.min_model_weight,
1239
                max_model_weight = EXCLUDED.max_model_weight,
1240
                max_position_size = EXCLUDED.max_position_size,
1241
                max_leverage = EXCLUDED.max_leverage,
1242
                stop_loss_pct = EXCLUDED.stop_loss_pct,
1243
                position_sizing_method = EXCLUDED.position_sizing_method,
1244
                max_portfolio_var = EXCLUDED.max_portfolio_var,
1245
                max_drawdown_threshold = EXCLUDED.max_drawdown_threshold,
1246
                kelly_fraction = EXCLUDED.kelly_fraction,
1247
                book_depth = EXCLUDED.book_depth,
1248
                vpin_window = EXCLUDED.vpin_window,
1249
                trade_classification_threshold = EXCLUDED.trade_classification_threshold,
1250
                trade_size_buckets = EXCLUDED.trade_size_buckets,
1251
                microstructure_features = EXCLUDED.microstructure_features,
1252
                regime_detection_method = EXCLUDED.regime_detection_method,
1253
                regime_lookback_window = EXCLUDED.regime_lookback_window,
1254
                regime_transition_threshold = EXCLUDED.regime_transition_threshold,
1255
                regime_features = EXCLUDED.regime_features,
1256
                execution_algorithm = EXCLUDED.execution_algorithm,
1257
                max_order_size = EXCLUDED.max_order_size,
1258
                min_order_size = EXCLUDED.min_order_size,
1259
                order_timeout_secs = EXCLUDED.order_timeout_secs,
1260
                max_slippage_bps = EXCLUDED.max_slippage_bps,
1261
                smart_routing_enabled = EXCLUDED.smart_routing_enabled,
1262
                dark_pool_preference = EXCLUDED.dark_pool_preference,
1263
                updated_at = NOW()
1264
            RETURNING strategy_id
1265
        "#;
1266
    
1267
        // Extract trade_size_buckets and features arrays
1268
        let trade_size_buckets: Vec<f64> = config.get("trade_size_buckets")
1269
            .and_then(|v| v.as_array())
1270
            .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
1271
            .unwrap_or_else(|| vec![10.0, 100.0, 1000.0, 10000.0]);
1272
    
1273
        let microstructure_features: Vec<String> = config.get("microstructure_features")
1274
            .and_then(|v| v.as_array())
1275
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_owned())).collect())
1276
            .unwrap_or_else(|| vec!["vpin".to_owned(), "order_flow".to_owned(), "bid_ask_spread".to_owned()]);
1277
    
1278
        let regime_features: Vec<String> = config.get("regime_features")
1279
            .and_then(|v| v.as_array())
1280
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_owned())).collect())
1281
            .unwrap_or_else(|| vec!["volatility".to_owned(), "momentum".to_owned(), "volume".to_owned()]);
1282
    
1283
        let row = sqlx::query(query)
1284
            .bind(strategy_id)
1285
            .bind(name)
1286
            .bind(description)
1287
            // General config (4 fields)
1288
            .bind(get_i32!("execution_interval_ms", 100))
1289
            .bind(get_i32!("error_backoff_duration_secs", 1))
1290
            .bind(get_i32!("max_concurrent_operations", 10))
1291
            .bind(get_i32!("strategy_timeout_secs", 30))
1292
            // Ensemble config (4 fields)
1293
            .bind(get_i32!("max_parallel_models", 4))
1294
            .bind(get_i32!("rebalancing_interval_secs", 300))
1295
            .bind(get_f64!("min_model_weight", 0.01))
1296
            .bind(get_f64!("max_model_weight", 0.5))
1297
            // Risk config (7 fields)
1298
            .bind(get_f64!("max_position_size", 0.1))
1299
            .bind(get_f64!("max_leverage", 2.0))
1300
            .bind(get_f64!("stop_loss_pct", 0.02))
1301
            .bind(get_str!("position_sizing_method", "KELLY"))
1302
            .bind(get_f64!("max_portfolio_var", 0.02))
1303
            .bind(get_f64!("max_drawdown_threshold", 0.05))
1304
            .bind(get_f64!("kelly_fraction", 0.1))
1305
            // Microstructure config (5 fields)
1306
            .bind(get_i32!("book_depth", 10))
1307
            .bind(get_i32!("vpin_window", 50))
1308
            .bind(get_f64!("trade_classification_threshold", 0.5))
1309
            .bind(&trade_size_buckets)
1310
            .bind(&microstructure_features)
1311
            // Regime config (4 fields)
1312
            .bind(get_str!("regime_detection_method", "HMM"))
1313
            .bind(get_i32!("regime_lookback_window", 252))
1314
            .bind(get_f64!("regime_transition_threshold", 0.7))
1315
            .bind(&regime_features)
1316
            // Execution config (7 fields)
1317
            .bind(get_str!("execution_algorithm", "TWAP"))
1318
            .bind(get_f64!("max_order_size", 10000.0))
1319
            .bind(get_f64!("min_order_size", 100.0))
1320
            .bind(get_i32!("order_timeout_secs", 30))
1321
            .bind(get_f64!("max_slippage_bps", 10.0))
1322
            .bind(get_bool!("smart_routing_enabled", true))
1323
            .bind(get_f64!("dark_pool_preference", 0.3))
1324
            .fetch_one(&self.pool)
1325
            .await?;
1326
    
1327
                let result: String = row.try_get("strategy_id")?;
1328
                Ok(result)
1329
            }
1330
        
1331
            // ========================================================================
1332
            // MODEL CRUD OPERATIONS
1333
            // ========================================================================
1334
        
1335
            /// Add a model configuration to a strategy
1336
            ///
1337
            /// # Arguments
1338
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1339
            ///
1340
            /// * `model` - Model configuration as JSON
1341
            ///
1342
            /// # Returns
1343
            ///
1344
            /// UUID of the created model configuration
1345
            ///
1346
            /// # Errors
1347
            /// Returns error if the operation fails
1348
            pub async fn add_model_config(
1349
                &self,
1350
                strategy_config_id: uuid::Uuid,
1351
                model: &serde_json::Value,
1352
            ) -> Result<uuid::Uuid, sqlx::Error> {
1353
                let model_id = model.get("model_id")
1354
                    .and_then(|v| v.as_str())
1355
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1356
                        std::io::ErrorKind::InvalidData,
1357
                        "Missing model_id"
1358
                    ))))?;
1359
        
1360
                let query = r#"
1361
                    INSERT INTO adaptive_strategy_models (
1362
                        strategy_config_id, model_id, model_name, model_type,
1363
                        parameters, initial_weight, enabled, display_order
1364
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1365
                    RETURNING id
1366
                "#;
1367
        
1368
                let row = sqlx::query(query)
1369
                    .bind(strategy_config_id)
1370
                    .bind(model_id)
1371
                    .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or(model_id))
1372
                    .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1373
                    .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1374
                    .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1375
                    .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1376
                    .bind(i32::try_from(model.get("display_order").and_then(|v| v.as_i64()).unwrap_or(0)).unwrap_or(0))
1377
                    .fetch_one(&self.pool)
1378
                    .await?;
1379
        
1380
                row.try_get("id")
1381
            }
1382
        
1383
            /// Update a model configuration
1384
            ///
1385
            /// # Arguments
1386
            /// * `model_id` - UUID of the model to update
1387
            ///
1388
            /// * `updates` - Fields to update as JSON
1389
            ///
1390
            /// # Errors
1391
            /// Returns error if the operation fails
1392
            pub async fn update_model_config(
1393
                &self,
1394
                model_id: uuid::Uuid,
1395
                updates: &serde_json::Value,
1396
            ) -> Result<(), sqlx::Error> {
1397
                let query = r#"
1398
                    UPDATE adaptive_strategy_models
1399
                    SET
1400
                        model_name = COALESCE($1, model_name),
1401
                        model_type = COALESCE($2, model_type),
1402
                        parameters = COALESCE($3, parameters),
1403
                        initial_weight = COALESCE($4, initial_weight),
1404
                        enabled = COALESCE($5, enabled),
1405
                        display_order = COALESCE($6, display_order),
1406
                        updated_at = NOW()
1407
                    WHERE id = $7
1408
                "#;
1409
        
1410
                sqlx::query(query)
1411
                    .bind(updates.get("model_name").and_then(|v| v.as_str()))
1412
                    .bind(updates.get("model_type").and_then(|v| v.as_str()))
1413
                    .bind(updates.get("parameters"))
1414
                    .bind(updates.get("initial_weight").and_then(|v| v.as_f64()))
1415
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1416
                    .bind(updates.get("display_order").and_then(|v| v.as_i64()).and_then(|v| i32::try_from(v).ok()))
1417
                    .bind(model_id)
1418
                    .execute(&self.pool)
1419
                    .await?;
1420
        
1421
                Ok(())
1422
            }
1423
        
1424
            /// Remove a model configuration
1425
            ///
1426
            /// # Arguments
1427
            /// * `model_id` - UUID of the model to remove
1428
            ///
1429
            /// # Errors
1430
            /// Returns error if the operation fails
1431
            pub async fn remove_model_config(
1432
                &self,
1433
                model_id: uuid::Uuid,
1434
            ) -> Result<(), sqlx::Error> {
1435
                let query = "DELETE FROM adaptive_strategy_models WHERE id = $1";
1436
                sqlx::query(query)
1437
                    .bind(model_id)
1438
                    .execute(&self.pool)
1439
                    .await?;
1440
                Ok(())
1441
            }
1442
        
1443
            // ========================================================================
1444
            // FEATURE CRUD OPERATIONS
1445
            // ========================================================================
1446
        
1447
            /// Add a feature configuration to a strategy
1448
            ///
1449
            /// # Arguments
1450
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1451
            ///
1452
            /// * `feature` - Feature configuration as JSON
1453
            ///
1454
            /// # Returns
1455
            ///
1456
            /// UUID of the created feature configuration
1457
            ///
1458
            /// # Errors
1459
            /// Returns error if the operation fails
1460
            pub async fn add_feature_config(
1461
                &self,
1462
                strategy_config_id: uuid::Uuid,
1463
                feature: &serde_json::Value,
1464
            ) -> Result<uuid::Uuid, sqlx::Error> {
1465
                let feature_name = feature.get("feature_name")
1466
                    .and_then(|v| v.as_str())
1467
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1468
                        std::io::ErrorKind::InvalidData,
1469
                        "Missing feature_name"
1470
                    ))))?;
1471
        
1472
                let query = r#"
1473
                    INSERT INTO adaptive_strategy_features (
1474
                        strategy_config_id, feature_name, feature_type,
1475
                        parameters, enabled, required
1476
                    ) VALUES ($1, $2, $3, $4, $5, $6)
1477
                    RETURNING id
1478
                "#;
1479
        
1480
                let row = sqlx::query(query)
1481
                    .bind(strategy_config_id)
1482
                    .bind(feature_name)
1483
                    .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1484
                    .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1485
                    .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1486
                    .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1487
                    .fetch_one(&self.pool)
1488
                    .await?;
1489
        
1490
                row.try_get("id")
1491
            }
1492
        
1493
            /// Update a feature configuration
1494
            ///
1495
            /// # Arguments
1496
            /// * `feature_id` - UUID of the feature to update
1497
            ///
1498
            /// * `updates` - Fields to update as JSON
1499
            ///
1500
            /// # Errors
1501
            /// Returns error if the operation fails
1502
            pub async fn update_feature_config(
1503
                &self,
1504
                feature_id: uuid::Uuid,
1505
                updates: &serde_json::Value,
1506
            ) -> Result<(), sqlx::Error> {
1507
                let query = r#"
1508
                    UPDATE adaptive_strategy_features
1509
                    SET
1510
                        feature_type = COALESCE($1, feature_type),
1511
                        parameters = COALESCE($2, parameters),
1512
                        enabled = COALESCE($3, enabled),
1513
                        required = COALESCE($4, required),
1514
                        updated_at = NOW()
1515
                    WHERE id = $5
1516
                "#;
1517
        
1518
                sqlx::query(query)
1519
                    .bind(updates.get("feature_type").and_then(|v| v.as_str()))
1520
                    .bind(updates.get("parameters"))
1521
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1522
                    .bind(updates.get("required").and_then(|v| v.as_bool()))
1523
                    .bind(feature_id)
1524
                    .execute(&self.pool)
1525
                    .await?;
1526
        
1527
                Ok(())
1528
            }
1529
        
1530
            /// Remove a feature configuration
1531
            ///
1532
            /// # Arguments
1533
            /// * `feature_id` - UUID of the feature to remove
1534
            ///
1535
            /// # Errors
1536
            /// Returns error if the operation fails
1537
            pub async fn remove_feature_config(
1538
                &self,
1539
                feature_id: uuid::Uuid,
1540
            ) -> Result<(), sqlx::Error> {
1541
                let query = "DELETE FROM adaptive_strategy_features WHERE id = $1";
1542
                sqlx::query(query)
1543
                    .bind(feature_id)
1544
                    .execute(&self.pool)
1545
                    .await?;
1546
                Ok(())
1547
            }
1548
        
1549
            // ========================================================================
1550
            // TRANSACTION SUPPORT
1551
            // ========================================================================
1552
        
1553
            /// Update strategy configuration with models and features in a single transaction
1554
            ///
1555
            /// Provides atomic updates across all three tables:
1556
            /// - adaptive_strategy_config (main configuration)
1557
            ///
1558
            /// - adaptive_strategy_models (model configurations)
1559
            /// - adaptive_strategy_features (feature configurations)
1560
            ///
1561
            /// # Arguments
1562
            /// * `config` - Full configuration including models and features
1563
            ///
1564
            /// # Returns
1565
            ///
1566
            /// Strategy ID of the updated configuration
1567
            ///
1568
            /// # Errors
1569
            /// Returns error if the operation fails
1570
            pub async fn update_strategy_atomic(
1571
                &self,
1572
                config: &serde_json::Value,
1573
            ) -> Result<String, sqlx::Error> {
1574
                // Start transaction
1575
                let mut tx = self.pool.begin().await?;
1576
        
1577
                // 1. Upsert main configuration
1578
                let strategy_id = config.get("strategy_id")
1579
                    .and_then(|v| v.as_str())
1580
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1581
                        std::io::ErrorKind::InvalidData,
1582
                        "Missing strategy_id"
1583
                    ))))?;
1584
        
1585
                // Get or create config_id
1586
                let config_id: uuid::Uuid = sqlx::query_scalar(
1587
                    "SELECT id FROM adaptive_strategy_config WHERE strategy_id = $1"
1588
                )
1589
                .bind(strategy_id)
1590
                .fetch_optional(&mut *tx)
1591
                .await?
1592
                .unwrap_or_else(uuid::Uuid::new_v4);
1593
        
1594
                // 2. Update models if provided
1595
                if let Some(models) = config.get("models").and_then(|v| v.as_array()) {
1596
                    // Delete existing models
1597
                    sqlx::query("DELETE FROM adaptive_strategy_models WHERE strategy_config_id = $1")
1598
                        .bind(config_id)
1599
                        .execute(&mut *tx)
1600
                        .await?;
1601
        
1602
                    // Insert new models
1603
                    for model in models {
1604
                        sqlx::query(r#"
1605
                            INSERT INTO adaptive_strategy_models (
1606
                                strategy_config_id, model_id, model_name, model_type,
1607
                                parameters, initial_weight, enabled
1608
                            ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1609
                        "#)
1610
                        .bind(config_id)
1611
                        .bind(model.get("model_id").and_then(|v| v.as_str()).unwrap_or("unknown"))
1612
                        .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or("Unknown Model"))
1613
                        .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1614
                        .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1615
                        .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1616
                        .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1617
                        .execute(&mut *tx)
1618
                        .await?;
1619
                    }
1620
                }
1621
        
1622
                // 3. Update features if provided
1623
                if let Some(features) = config.get("features").and_then(|v| v.as_array()) {
1624
                    // Delete existing features
1625
                    sqlx::query("DELETE FROM adaptive_strategy_features WHERE strategy_config_id = $1")
1626
                        .bind(config_id)
1627
                        .execute(&mut *tx)
1628
                        .await?;
1629
        
1630
                    // Insert new features
1631
                    for feature in features {
1632
                        sqlx::query(r#"
1633
                            INSERT INTO adaptive_strategy_features (
1634
                                strategy_config_id, feature_name, feature_type,
1635
                                parameters, enabled, required
1636
                            ) VALUES ($1, $2, $3, $4, $5, $6)
1637
                        "#)
1638
                        .bind(config_id)
1639
                        .bind(feature.get("feature_name").and_then(|v| v.as_str()).unwrap_or("unknown"))
1640
                        .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1641
                        .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1642
                        .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1643
                        .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1644
                        .execute(&mut *tx)
1645
                        .await?;
1646
                    }
1647
                }
1648
        
1649
                // Commit transaction
1650
                tx.commit().await?;
1651
        
1652
                Ok(strategy_id.to_owned())
1653
            }
1654
        }
1655
        
1656
        #[cfg(test)]
1657
mod tests {
1658
    use super::*;
1659
1660
    #[test]
1661
    fn test_database_config_new() {
1662
        let config = DatabaseConfig::new();
1663
        assert!(!config.url.is_empty());
1664
        assert_eq!(config.max_connections, 10);
1665
        assert_eq!(config.min_connections, 1);
1666
        assert!(config.application_name.is_some());
1667
    }
1668
1669
    #[test]
1670
    fn test_database_config_validate_success() {
1671
        let config = DatabaseConfig::new();
1672
        assert!(config.validate().is_ok());
1673
    }
1674
1675
    #[test]
1676
    fn test_database_config_validate_empty_url() {
1677
        let mut config = DatabaseConfig::new();
1678
        config.url = String::new();
1679
        assert!(config.validate().is_err());
1680
    }
1681
1682
    #[test]
1683
    fn test_pool_config_default() {
1684
        let pool_config = PoolConfig::default();
1685
        assert_eq!(pool_config.min_connections, 1);
1686
        assert_eq!(pool_config.max_connections, 10);
1687
        assert!(pool_config.test_before_acquire);
1688
    }
1689
1690
    #[test]
1691
    fn test_transaction_config_default() {
1692
        let tx_config = TransactionConfig::default();
1693
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1694
        assert_eq!(tx_config.default_timeout_secs, 30);
1695
        assert!(tx_config.enable_retry);
1696
    }
1697
1698
    #[test]
1699
    fn test_transaction_config_serialization() {
1700
        let tx_config = TransactionConfig::default();
1701
        let serialized = serde_json::to_string(&tx_config).unwrap();
1702
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1703
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1704
    }
1705
1706
    #[test]
1707
    fn test_database_config_with_custom_values() {
1708
        let mut config = DatabaseConfig::new();
1709
        config.max_connections = 50;
1710
        config.min_connections = 5;
1711
        config.enable_query_logging = true;
1712
1713
        assert_eq!(config.max_connections, 50);
1714
        assert_eq!(config.min_connections, 5);
1715
        assert!(config.enable_query_logging);
1716
    }
1717
1718
    #[test]
1719
    fn test_pool_config_timeouts() {
1720
        let pool_config = PoolConfig {
1721
            acquire_timeout_secs: 30,
1722
            max_lifetime_secs: 3600,
1723
            idle_timeout_secs: 3600,
1724
            ..Default::default()
1725
        };
1726
1727
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1728
        assert_eq!(pool_config.max_lifetime_secs, 3600);
1729
        assert_eq!(pool_config.idle_timeout_secs, 3600);
1730
    }
1731
1732
    #[test]
1733
    fn test_transaction_config_isolation_levels() {
1734
        let levels = vec![
1735
            "READ_UNCOMMITTED",
1736
            "READ_COMMITTED",
1737
            "REPEATABLE_READ",
1738
            "SERIALIZABLE",
1739
        ];
1740
1741
        for level in levels {
1742
            let tx_config = TransactionConfig {
1743
                isolation_level: level.to_owned(),
1744
                ..Default::default()
1745
            };
1746
            assert_eq!(tx_config.isolation_level, level);
1747
        }
1748
    }
1749
1750
    #[test]
1751
    fn test_database_config_clone() {
1752
        let config1 = DatabaseConfig::new();
1753
        let config2 = config1.clone();
1754
1755
        assert_eq!(config1.url, config2.url);
1756
        assert_eq!(config1.max_connections, config2.max_connections);
1757
        assert_eq!(config1.min_connections, config2.min_connections);
1758
    }
1759
1760
    #[test]
1761
    fn test_pool_config_validation() {
1762
        let pool_config = PoolConfig::default();
1763
        assert!(pool_config.min_connections <= pool_config.max_connections);
1764
    }
1765
1766
    #[test]
1767
    fn test_database_url_format() {
1768
        let config = DatabaseConfig::new();
1769
        assert!(config.url.starts_with("postgresql://"));
1770
    }
1771
1772
    #[test]
1773
    fn test_transaction_config_retry_settings() {
1774
        let tx_config = TransactionConfig {
1775
            enable_retry: true,
1776
            max_retries: 5,
1777
            ..Default::default()
1778
        };
1779
        assert!(tx_config.enable_retry);
1780
        assert_eq!(tx_config.max_retries, 5);
1781
1782
        let tx_config_no_retry = TransactionConfig {
1783
            enable_retry: false,
1784
            ..Default::default()
1785
        };
1786
        assert!(!tx_config_no_retry.enable_retry);
1787
    }
1788
1789
    #[test]
1790
    fn test_pool_config_connection_settings() {
1791
        let pool_config = PoolConfig {
1792
            test_before_acquire: true,
1793
            acquire_timeout_secs: 30,
1794
            ..Default::default()
1795
        };
1796
1797
        assert!(pool_config.test_before_acquire);
1798
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1799
    }
1800
1801
    #[test]
1802
    fn test_database_config_application_name() {
1803
        let config = DatabaseConfig::new();
1804
        assert_eq!(config.application_name, Some("foxhunt".to_owned()));
1805
    }
1806
1807
    #[test]
1808
    fn test_database_config_query_logging() {
1809
        let mut config = DatabaseConfig::new();
1810
        config.enable_query_logging = true;
1811
        assert!(config.enable_query_logging);
1812
    }
1813
1814
    #[test]
1815
    fn test_pool_config_connection_limits() {
1816
        let pool_config = PoolConfig {
1817
            max_connections: 100,
1818
            min_connections: 10,
1819
            ..Default::default()
1820
        };
1821
1822
        assert_eq!(pool_config.max_connections, 100);
1823
        assert_eq!(pool_config.min_connections, 10);
1824
    }
1825
1826
    #[test]
1827
    fn test_transaction_timeout() {
1828
        let tx_config = TransactionConfig {
1829
            default_timeout_secs: 60,
1830
            timeout: Duration::from_secs(60),
1831
            ..Default::default()
1832
        };
1833
        assert_eq!(tx_config.default_timeout_secs, 60);
1834
        assert_eq!(tx_config.timeout, Duration::from_secs(60));
1835
    }
1836
1837
    #[test]
1838
    fn test_database_config_connect_timeout() {
1839
        let config = DatabaseConfig::new();
1840
        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1841
    }
1842
1843
    #[test]
1844
    fn test_database_config_query_timeout() {
1845
        let config = DatabaseConfig::new();
1846
        assert_eq!(config.query_timeout, Duration::from_secs(60));
1847
    }
1848
1849
    #[test]
1850
    fn test_pool_config_test_before_acquire() {
1851
        let pool_config = PoolConfig {
1852
            test_before_acquire: false,
1853
            ..Default::default()
1854
        };
1855
        assert!(!pool_config.test_before_acquire);
1856
1857
        let pool_config_enabled = PoolConfig {
1858
            test_before_acquire: true,
1859
            ..Default::default()
1860
        };
1861
        assert!(pool_config_enabled.test_before_acquire);
1862
    }
1863
1864
    #[test]
1865
    fn test_database_config_validation_empty_url() {
1866
        let mut config = DatabaseConfig::new();
1867
        config.url = String::new();
1868
        assert!(config.validate().is_err());
1869
        assert_eq!(
1870
            config.validate().unwrap_err(),
1871
            "Database URL cannot be empty"
1872
        );
1873
    }
1874
1875
    #[test]
1876
    fn test_database_config_validation_valid() {
1877
        let config = DatabaseConfig::new();
1878
        assert!(config.validate().is_ok());
1879
    }
1880
1881
    #[test]
1882
    fn test_pool_config_defaults() {
1883
        let pool_config = PoolConfig::default();
1884
        assert_eq!(pool_config.min_connections, 1);
1885
        assert_eq!(pool_config.max_connections, 10);
1886
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1887
        assert_eq!(pool_config.max_lifetime_secs, 3600);
1888
        assert_eq!(pool_config.idle_timeout_secs, 3600);
1889
        assert!(pool_config.test_before_acquire);
1890
        assert!(pool_config.health_check_enabled);
1891
        assert_eq!(pool_config.health_check_interval_secs, 60);
1892
    }
1893
1894
    #[test]
1895
    fn test_transaction_config_defaults() {
1896
        let tx_config = TransactionConfig::default();
1897
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1898
        assert_eq!(tx_config.timeout, Duration::from_secs(30));
1899
        assert_eq!(tx_config.default_timeout_secs, 30);
1900
        assert!(tx_config.enable_retry);
1901
        assert_eq!(tx_config.max_retries, 3);
1902
        assert_eq!(tx_config.retry_delay_ms, 100);
1903
        assert_eq!(tx_config.max_savepoints, 10);
1904
    }
1905
1906
    #[test]
1907
    fn test_transaction_config_custom_isolation() {
1908
        let tx_config = TransactionConfig {
1909
            isolation_level: "SERIALIZABLE".to_owned(),
1910
            ..Default::default()
1911
        };
1912
        assert_eq!(tx_config.isolation_level, "SERIALIZABLE");
1913
    }
1914
1915
    #[test]
1916
    fn test_pool_config_extreme_values() {
1917
        let pool_config = PoolConfig {
1918
            max_connections: 1000,
1919
            min_connections: 0,
1920
            ..Default::default()
1921
        };
1922
        assert_eq!(pool_config.max_connections, 1000);
1923
        assert_eq!(pool_config.min_connections, 0);
1924
    }
1925
1926
    #[test]
1927
    fn test_database_config_custom_application_name() {
1928
        let mut config = DatabaseConfig::new();
1929
        config.application_name = Some("custom_app".to_owned());
1930
        assert_eq!(config.application_name.unwrap(), "custom_app");
1931
    }
1932
1933
    #[test]
1934
    fn test_database_config_no_application_name() {
1935
        let mut config = DatabaseConfig::new();
1936
        config.application_name = None;
1937
        assert!(config.application_name.is_none());
1938
    }
1939
1940
    #[test]
1941
    fn test_transaction_config_retry_disabled() {
1942
        let tx_config = TransactionConfig {
1943
            enable_retry: false,
1944
            ..Default::default()
1945
        };
1946
        assert!(!tx_config.enable_retry);
1947
    }
1948
1949
    #[test]
1950
    fn test_pool_config_serialization() {
1951
        let pool_config = PoolConfig::default();
1952
        let serialized = serde_json::to_string(&pool_config).unwrap();
1953
        let deserialized: PoolConfig = serde_json::from_str(&serialized).unwrap();
1954
        assert_eq!(pool_config.max_connections, deserialized.max_connections);
1955
        assert_eq!(pool_config.min_connections, deserialized.min_connections);
1956
    }
1957
1958
    #[test]
1959
    fn test_transaction_config_serde_roundtrip() {
1960
        let tx_config = TransactionConfig::default();
1961
        let serialized = serde_json::to_string(&tx_config).unwrap();
1962
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1963
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1964
        assert_eq!(tx_config.max_retries, deserialized.max_retries);
1965
    }
1966
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html deleted file mode 100644 index 4d6c91eb6..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/lib.rs
Line
Count
Source
1
#![warn(missing_docs)]
2
//! Configuration management for Foxhunt HFT trading system
3
4
#![allow(missing_docs)] // Internal implementation details don't require documentation
5
#![allow(missing_debug_implementations)] // Not all types need Debug
6
7
// Allow pedantic lints for configuration management
8
#![allow(clippy::type_complexity)]
9
#![allow(clippy::unnecessary_map_or)]
10
#![allow(clippy::map_flatten)]
11
#![allow(dead_code)]
12
13
use serde::{Deserialize, Serialize};
14
15
// Module declarations
16
pub mod asset_classification;
17
pub mod compliance_config;
18
pub mod data_config;
19
pub mod data_providers;
20
pub mod database;
21
pub mod error;
22
pub mod manager;
23
pub mod ml_config;
24
pub mod risk_config;
25
pub mod runtime;
26
pub mod schemas;
27
pub mod storage_config;
28
pub mod structures;
29
pub mod symbol_config;
30
pub mod vault;
31
32
// Re-export commonly used types
33
pub use asset_classification::{
34
    create_default_configurations, AssetClass, AssetClassificationManager, AssetConfig,
35
    CommodityType, CryptoType, DerivativeType, EquitySector, ExecutionConfig, FixedIncomeType,
36
    ForexPairType, FutureType, GeographicRegion, JumpRiskProfile, MarketCapTier, MarketMakingConfig, OrderType,
37
    PositionLimits, RiskThresholds, SettlementConfig, TimeInForce,
38
    TradingHours as DetailedTradingHours, TradingParameters,
39
    VolatilityProfile as DetailedVolatilityProfile,
40
};
41
pub use data_config::{
42
    DataCompressionAlgorithm, DataCompressionConfig, DataConfig, DataRetentionConfig,
43
    DataStorageConfig, DataStorageFormat, DataVersioningConfig, MissingDataHandling,
44
};
45
pub use data_providers::{
46
    AlpacaEndpoints, BenzingaEndpoints, DataProviderConfig, DataProviderEnvironment,
47
    DatabentoEndpoints, IBGatewayConfig,
48
};
49
pub use compliance_config::ComplianceRuleConfig;
50
#[cfg(feature = "postgres")]
51
pub use compliance_config::PostgresComplianceRuleLoader;
52
pub use database::{DatabaseConfig, PoolConfig, TransactionConfig};
53
#[cfg(feature = "postgres")]
54
pub use database::{
55
    PostgresAssetClassificationLoader, PostgresConfigLoader, PostgresSymbolConfigLoader,
56
};
57
pub use error::{ConfigError, ConfigResult};
58
pub use manager::{ConfigManager, ConfigManagerBuilder, ServiceConfig};
59
pub use ml_config::{
60
    MLConfig, Mamba2Config, MarketState, ModelArchitectureConfig, SimulationConfig,
61
    SymbolConfig as MLSymbolConfig, TrainingConfig,
62
};
63
pub use risk_config::{
64
    AssetClass as RiskAssetClass, AssetClassMapping, RiskConfig, StressScenarioConfig,
65
};
66
pub use runtime::{
67
    CacheRuntimeConfig, DatabaseRuntimeConfig, Environment, LimitsConfig, RuntimeConfig,
68
    TimeoutConfig,
69
};
70
pub use schemas::*;
71
pub use storage_config::{ModelArchitecture, ModelMetadata, StorageConfig, TrainingMetrics};
72
pub use structures::{
73
    AssetClass as SimpleAssetClass, AssetClassificationConfig, BacktestingDatabaseConfig,
74
    BacktestingPerformanceConfig, BacktestingStrategyConfig, BrokerConfig, BrokerRoutingRule,
75
    CommissionConfig, EncryptionConfig, MarketDataConfig, TlsConfig, TradingConfig, VolatilityProfile as SimpleVolatilityProfile,
76
};
77
pub use symbol_config::{
78
    AssetClassification, SymbolConfig, SymbolConfigManager, SymbolMetadata, TradingHours,
79
    VolatilityProfile, VolatilityRegime,
80
};
81
pub use vault::VaultConfig;
82
83
/// Configuration categories for organizing different aspects of the trading system.
84
///
85
/// This enum categorizes different types of configurations to enable organized
86
/// access and management of system settings across various functional domains.
87
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
88
pub enum ConfigCategory {
89
    /// Trading system configuration including order management and execution
90
    Trading,
91
    /// Risk management configuration including position limits and VaR settings
92
    Risk,
93
    /// Market data configuration for data providers and feeds
94
    MarketData,
95
    /// Machine learning model configuration and training parameters
96
    MachineLearning,
97
    /// Broker connectivity and execution configuration
98
    Brokers,
99
    /// Performance monitoring and optimization configuration
100
    Performance,
101
    /// Symbol classification and trading parameters configuration
102
    Symbols,
103
    /// Comprehensive asset classification with advanced features
104
    AssetClassification,
105
}
106
107
/// Production-ready asset classification system integration.
108
///
109
/// This module provides a comprehensive asset classification system that integrates
110
/// with the existing config infrastructure while offering advanced features like:
111
/// - Dynamic pattern-based classification
112
///
113
/// - Regime-aware volatility profiling  
114
/// - Hot-reload configuration management
115
///
116
/// - Performance caching and audit trails
117
///
118
/// # Usage
119
///
120
/// ```rust,no_run
121
/// use config::{AssetClassificationManager, create_default_configurations};
122
///
123
/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
124
/// let mut manager = AssetClassificationManager::new();
125
/// let configs = create_default_configurations();
126
/// manager.load_configurations(configs).await?;
127
///
128
/// // Classify a symbol
129
/// let asset_class = manager.classify_symbol("AAPL");
130
///
131
/// // Get trading parameters
132
/// if let Some(params) = manager.get_trading_parameters("AAPL") {
133
///     let max_position = params.position_limits.max_position_fraction;
134
///     println!("Max position fraction for AAPL: {}", max_position);
135
/// }
136
/// # Ok(())
137
/// # }
138
/// ```
139
pub mod asset_classification_integration {
140
    pub use crate::asset_classification::*;
141
142
    /// Convenience function to create a fully configured asset classification manager
143
    /// with default configurations suitable for production use.
144
    ///
145
    /// # Errors
146
    /// Returns error if the operation fails
147
0
    pub async fn create_production_manager(
148
0
        database_pool: Option<sqlx::PgPool>,
149
0
    ) -> Result<AssetClassificationManager, Box<dyn std::error::Error + Send + Sync>> {
150
0
        let mut manager = AssetClassificationManager::new();
151
152
        // Load configurations from database if available, otherwise use defaults
153
0
        let configs = if let Some(_pool) = database_pool {
154
            // In production, load from database
155
            // let loader = crate::database::PostgresAssetClassificationLoader::with_pool(pool);
156
            // loader.load_asset_configurations().await?
157
0
            create_default_configurations()
158
        } else {
159
0
            create_default_configurations()
160
        };
161
162
0
        manager.load_configurations(configs).await?;
163
0
        Ok(manager)
164
0
    }
165
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html deleted file mode 100644 index 4a55011b0..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/manager.rs
Line
Count
Source
1
/// Builder for ConfigManager with advanced configuration options.
2
///
3
/// Provides a fluent interface for constructing ConfigManager instances
4
/// with optional asset classification, caching, and database integration.
5
pub struct ConfigManagerBuilder {
6
    config: ServiceConfig,
7
    asset_manager: Option<crate::asset_classification::AssetClassificationManager>,
8
    cache_timeout: std::time::Duration,
9
}
10
11
impl ConfigManagerBuilder {
12
    /// Creates a new ConfigManagerBuilder with the specified service configuration.
13
0
    pub const fn new(config: ServiceConfig) -> Self {
14
0
        Self {
15
0
            config,
16
0
            asset_manager: None,
17
0
            cache_timeout: std::time::Duration::from_secs(300),
18
0
        }
19
0
    }
20
21
    /// Sets the asset classification manager.
22
0
    pub fn with_asset_classification(
23
0
        mut self,
24
0
        manager: crate::asset_classification::AssetClassificationManager,
25
0
    ) -> Self {
26
0
        self.asset_manager = Some(manager);
27
0
        self
28
0
    }
29
30
    /// Sets the cache timeout duration.
31
0
    pub const fn with_cache_timeout(mut self, timeout: std::time::Duration) -> Self {
32
0
        self.cache_timeout = timeout;
33
0
        self
34
0
    }
35
36
    /// Builds the ConfigManager with the specified configuration.
37
0
    pub fn build(self) -> ConfigManager {
38
0
        ConfigManager {
39
0
            config: Arc::new(self.config),
40
0
            asset_classification: Arc::new(RwLock::new(self.asset_manager)),
41
0
            cache: Arc::new(RwLock::new(HashMap::new())),
42
0
            cache_timeout: self.cache_timeout,
43
0
        }
44
0
    }
45
46
    /// Builds the ConfigManager with database integration.
47
    ///
48
    /// # Errors
49
    /// Returns error if the operation fails
50
    #[cfg(feature = "postgres")]
51
    pub async fn build_with_database(
52
        self,
53
        database_pool: sqlx::PgPool,
54
    ) -> Result<ConfigManager, Box<dyn std::error::Error + Send + Sync>> {
55
        let manager = self.build();
56
        manager
57
            .initialize_asset_classification(database_pool)
58
            .await?;
59
        Ok(manager)
60
    }
61
}
62
63
// Configuration management and service configuration structures.
64
//
65
// This module provides the core configuration management infrastructure for
66
// the Foxhunt trading system. It handles service-specific configuration,
67
// environment management, and provides thread-safe access to configuration
68
// data across the application.
69
70
use chrono::{DateTime, Utc};
71
use serde::{Deserialize, Serialize};
72
use std::collections::HashMap;
73
use std::sync::{Arc, RwLock};
74
75
/// Service-specific configuration structure.
76
///
77
/// Contains metadata and settings for a specific service in the Foxhunt
78
/// trading system. Supports environment-specific configuration and
79
/// versioning for configuration management and deployment tracking.
80
#[derive(Debug, Clone, Serialize, Deserialize)]
81
pub struct ServiceConfig {
82
    /// Service name (e.g., "trading_service", "ml_training_service")
83
    pub name: String,
84
    /// Deployment environment (e.g., "development", "staging", "production")
85
    pub environment: String,
86
    /// Service version for deployment tracking
87
    pub version: String,
88
    /// Service-specific configuration settings as JSON
89
    pub settings: serde_json::Value,
90
}
91
92
/// Thread-safe configuration manager for comprehensive service configuration.
93
///
94
/// Provides centralized access to service configuration with support for:
95
/// - Asset classification management
96
///
97
/// - Hot-reload capabilities
98
/// - Environment-specific settings
99
///
100
/// - Thread-safe access patterns
101
///
102
/// Ensures configuration consistency across all components of a service.
103
pub struct ConfigManager {
104
    config: Arc<ServiceConfig>,
105
    /// Asset classification manager for symbol-based configuration
106
    asset_classification:
107
        Arc<RwLock<Option<crate::asset_classification::AssetClassificationManager>>>,
108
    /// Configuration cache for performance
109
    cache: Arc<RwLock<HashMap<String, (serde_json::Value, DateTime<Utc>)>>>,
110
    /// Cache timeout duration
111
    cache_timeout: std::time::Duration,
112
}
113
114
impl ConfigManager {
115
    /// Creates a new ConfigManager with the provided service configuration.
116
    ///
117
    /// The configuration is wrapped in an Arc for efficient sharing across
118
    /// multiple threads and components within the service.
119
    ///
120
    /// # Arguments
121
    ///
122
    /// * `config` - The service configuration to manage
123
0
    pub fn new(config: ServiceConfig) -> Self {
124
0
        Self {
125
0
            config: Arc::new(config),
126
0
            asset_classification: Arc::new(RwLock::new(None)),
127
0
            cache: Arc::new(RwLock::new(HashMap::new())),
128
0
            cache_timeout: std::time::Duration::from_secs(300), // 5 minutes
129
0
        }
130
0
    }
131
132
    /// Creates a new ConfigManager with asset classification support.
133
    ///
134
    /// Initializes the manager with both service configuration and
135
    /// asset classification capabilities for comprehensive trading
136
    /// parameter management.
137
    ///
138
    /// # Arguments
139
    ///
140
    /// * `config` - The service configuration to manage
141
    /// * `asset_manager` - Pre-configured asset classification manager
142
0
    pub fn with_asset_classification(
143
0
        config: ServiceConfig,
144
0
        asset_manager: crate::asset_classification::AssetClassificationManager,
145
0
    ) -> Self {
146
0
        Self {
147
0
            config: Arc::new(config),
148
0
            asset_classification: Arc::new(RwLock::new(Some(asset_manager))),
149
0
            cache: Arc::new(RwLock::new(HashMap::new())),
150
0
            cache_timeout: std::time::Duration::from_secs(300),
151
0
        }
152
0
    }
153
154
    /// Returns a shared reference to the service configuration.
155
    ///
156
    /// Provides thread-safe access to the configuration data through Arc cloning.
157
    ///
158
    /// The returned Arc can be shared across threads without additional locking.
159
    ///
160
    /// # Returns
161
    ///
162
    /// An Arc containing the service configuration
163
0
    pub fn get_config(&self) -> Arc<ServiceConfig> {
164
0
        Arc::clone(&self.config)
165
0
    }
166
167
    /// Initializes asset classification with database-backed configurations.
168
    ///
169
    /// Loads asset classification configurations from the database and
170
    /// initializes the asset classification manager for dynamic symbol
171
    /// classification and trading parameter retrieval.
172
    ///
173
    /// # Errors
174
    /// Returns error if the operation fails
175
    #[cfg(feature = "postgres")]
176
    pub async fn initialize_asset_classification(
177
        &self,
178
        database_pool: sqlx::PgPool,
179
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
180
        let loader = crate::database::PostgresAssetClassificationLoader::with_pool(database_pool);
181
        let configs = loader.load_asset_configurations().await?;
182
183
        let mut manager = crate::asset_classification::AssetClassificationManager::new();
184
        manager.load_configurations(configs).await?;
185
186
        if let Ok(mut asset_classification) = self.asset_classification.write() {
187
            *asset_classification = Some(manager);
188
        }
189
190
        Ok(())
191
    }
192
193
    /// Classifies a symbol using the asset classification manager.
194
    ///
195
    /// Returns the asset class for the given symbol based on configured
196
    /// pattern matching rules and explicit mappings.
197
    ///
198
    /// # Arguments
199
    ///
200
    /// * `symbol` - The trading symbol to classify
201
    ///
202
    /// # Returns
203
    ///
204
    /// The asset class or Unknown if classification fails
205
0
    pub fn classify_symbol(&self, symbol: &str) -> crate::asset_classification::AssetClass {
206
0
        if let Ok(asset_classification) = self.asset_classification.read() {
207
0
            if let Some(ref manager) = *asset_classification {
208
0
                return manager.classify_symbol(symbol);
209
0
            }
210
0
        }
211
0
        crate::asset_classification::AssetClass::Unknown
212
0
    }
213
214
    /// Gets trading parameters for a symbol.
215
    ///
216
    /// Retrieves comprehensive trading parameters including position limits,
217
    /// risk thresholds, and execution configuration for the specified symbol.
218
    ///
219
    /// # Arguments
220
    ///
221
    /// * `symbol` - The trading symbol
222
    ///
223
    /// # Returns
224
    ///
225
    /// Trading parameters if available, None otherwise
226
0
    pub fn get_trading_parameters(
227
0
        &self,
228
0
        symbol: &str,
229
0
    ) -> Option<crate::asset_classification::TradingParameters> {
230
0
        if let Ok(asset_classification) = self.asset_classification.read() {
231
0
            if let Some(ref manager) = *asset_classification {
232
0
                return manager.get_trading_parameters(symbol).cloned();
233
0
            }
234
0
        }
235
0
        None
236
0
    }
237
238
    /// Gets volatility profile for a symbol.
239
    ///
240
    /// Retrieves the volatility profile including base volatility,
241
    /// stress multipliers, and jump risk characteristics.
242
    ///
243
    /// # Arguments
244
    ///
245
    /// * `symbol` - The trading symbol
246
    ///
247
    /// # Returns
248
    ///
249
    /// Volatility profile if available, None otherwise
250
0
    pub fn get_volatility_profile(
251
0
        &self,
252
0
        symbol: &str,
253
0
    ) -> Option<crate::asset_classification::VolatilityProfile> {
254
0
        if let Ok(asset_classification) = self.asset_classification.read() {
255
0
            if let Some(ref manager) = *asset_classification {
256
0
                return manager.get_volatility_profile(symbol).cloned();
257
0
            }
258
0
        }
259
0
        None
260
0
    }
261
262
    /// Gets daily volatility estimate for a symbol.
263
    ///
264
    /// Calculates the daily volatility from the annual volatility
265
    /// using standard financial mathematics (annual / sqrt(252)).
266
    ///
267
    /// # Arguments
268
    ///
269
    /// * `symbol` - The trading symbol
270
    ///
271
    /// # Returns
272
    ///
273
    /// Daily volatility estimate as a decimal
274
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
275
0
        if let Ok(asset_classification) = self.asset_classification.read() {
276
0
            if let Some(ref manager) = *asset_classification {
277
0
                return manager.get_daily_volatility(symbol);
278
0
            }
279
0
        }
280
0
        0.05 // Default 5% daily volatility for unknown symbols
281
0
    }
282
283
    /// Gets position size recommendation for a symbol.
284
    ///
285
    /// Calculates recommended position size based on portfolio NAV
286
    /// and the symbol's configured position limits.
287
    ///
288
    /// # Arguments
289
    ///
290
    /// * `symbol` - The trading symbol
291
    /// * `portfolio_nav` - Current portfolio net asset value
292
    ///
293
    /// # Returns
294
    ///
295
    /// Recommended position size if available
296
0
    pub fn get_position_size_recommendation(
297
0
        &self,
298
0
        symbol: &str,
299
0
        portfolio_nav: rust_decimal::Decimal,
300
0
    ) -> Option<rust_decimal::Decimal> {
301
0
        if let Ok(asset_classification) = self.asset_classification.read() {
302
0
            if let Some(ref manager) = *asset_classification {
303
0
                return manager.get_position_size_recommendation(symbol, portfolio_nav);
304
0
            }
305
0
        }
306
0
        None
307
0
    }
308
309
    /// Checks if trading is active for a symbol at the given time.
310
    ///
311
    /// Validates trading hours and market schedule for the symbol.
312
    ///
313
    /// # Arguments
314
    ///
315
    /// * `symbol` - The trading symbol
316
    /// * `timestamp` - The timestamp to check
317
    ///
318
    /// # Returns
319
    ///
320
    /// True if trading is active, false otherwise
321
0
    pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime<Utc>) -> bool {
322
0
        if let Ok(asset_classification) = self.asset_classification.read() {
323
0
            if let Some(ref manager) = *asset_classification {
324
0
                return manager.is_trading_active(symbol, timestamp);
325
0
            }
326
0
        }
327
0
        true // Default to always active if no classification available
328
0
    }
329
330
    /// Reloads asset classification configurations.
331
    ///
332
    /// Triggers a reload of asset classification configurations
333
    /// for hot-reload functionality in production environments.
334
    ///
335
    /// # Errors
336
    /// Returns error if the operation fails
337
    #[cfg(feature = "postgres")]
338
    pub async fn reload_asset_classification(
339
        &self,
340
        database_pool: sqlx::PgPool,
341
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
342
        let needs_reload = {
343
            let asset_classification = self.asset_classification.read().ok();
344
            asset_classification
345
                .as_ref()
346
                .and_then(|ac| ac.as_ref())
347
                .map(|manager| manager.needs_reload())
348
                .unwrap_or(false)
349
        }; // Lock released here
350
351
        if needs_reload {
352
            self.initialize_asset_classification(database_pool).await?;
353
        }
354
        Ok(())
355
    }
356
357
    /// Gets cached configuration value.
358
    ///
359
    /// Retrieves a cached configuration value with automatic expiration.
360
    ///
361
    /// # Arguments
362
    ///
363
    /// * `key` - Cache key
364
    ///
365
    /// # Returns
366
    ///
367
    /// Cached value if available and not expired
368
0
    pub fn get_cached_config(&self, key: &str) -> Option<serde_json::Value> {
369
0
        if let Ok(cache) = self.cache.read() {
370
0
            if let Some((value, timestamp)) = cache.get(key) {
371
0
                let elapsed = Utc::now().signed_duration_since(*timestamp);
372
0
                if elapsed.to_std().unwrap_or_default() < self.cache_timeout {
373
0
                    return Some(value.clone());
374
0
                }
375
0
            }
376
0
        }
377
0
        None
378
0
    }
379
380
    /// Sets cached configuration value.
381
    ///
382
    /// Stores a configuration value in the cache with timestamp.
383
    ///
384
    /// # Arguments
385
    ///
386
    /// * `key` - Cache key
387
    /// * `value` - Value to cache
388
0
    pub fn set_cached_config(&self, key: String, value: serde_json::Value) {
389
0
        if let Ok(mut cache) = self.cache.write() {
390
0
            cache.insert(key, (value, Utc::now()));
391
0
        }
392
0
    }
393
394
    /// Clears expired cache entries.
395
    ///
396
    /// Removes cache entries that have exceeded the timeout duration.
397
0
    pub fn cleanup_cache(&self) {
398
0
        if let Ok(mut cache) = self.cache.write() {
399
0
            let now = Utc::now();
400
0
            cache.retain(|_, (_, timestamp)| {
401
0
                let elapsed = now.signed_duration_since(*timestamp);
402
0
                elapsed.to_std().unwrap_or_default() < self.cache_timeout
403
0
            });
404
0
        }
405
0
    }
406
}
407
408
#[cfg(test)]
409
mod tests {
410
    use super::*;
411
    use serde_json::json;
412
413
    fn create_test_config() -> ServiceConfig {
414
        ServiceConfig {
415
            name: "test_service".to_owned(),
416
            environment: "test".to_owned(),
417
            version: "1.0.0".to_owned(),
418
            settings: json!({"test_key": "test_value"}),
419
        }
420
    }
421
422
    #[test]
423
    fn test_service_config_creation() {
424
        let config = create_test_config();
425
        assert_eq!(config.name, "test_service");
426
        assert_eq!(config.environment, "test");
427
        assert_eq!(config.version, "1.0.0");
428
    }
429
430
    #[test]
431
    fn test_config_manager_new() {
432
        let config = create_test_config();
433
        let manager = ConfigManager::new(config);
434
        let retrieved_config = manager.get_config();
435
        assert_eq!(retrieved_config.name, "test_service");
436
    }
437
438
    #[test]
439
    fn test_config_manager_builder() {
440
        let config = create_test_config();
441
        let manager = ConfigManagerBuilder::new(config)
442
            .with_cache_timeout(std::time::Duration::from_secs(60))
443
            .build();
444
445
        let retrieved_config = manager.get_config();
446
        assert_eq!(retrieved_config.name, "test_service");
447
    }
448
449
    #[test]
450
    fn test_config_manager_cache_set_and_get() {
451
        let config = create_test_config();
452
        let manager = ConfigManager::new(config);
453
454
        let test_value = json!({"cached": "data"});
455
        manager.set_cached_config("test_key".to_owned(), test_value.clone());
456
457
        let retrieved = manager.get_cached_config("test_key");
458
        assert!(retrieved.is_some());
459
        assert_eq!(retrieved.unwrap(), test_value);
460
    }
461
462
    #[test]
463
    fn test_config_manager_cache_miss() {
464
        let config = create_test_config();
465
        let manager = ConfigManager::new(config);
466
467
        let retrieved = manager.get_cached_config("nonexistent_key");
468
        assert!(retrieved.is_none());
469
    }
470
471
    #[test]
472
    fn test_config_manager_cleanup_cache() {
473
        let config = create_test_config();
474
        let manager = ConfigManager::new(config);
475
476
        let test_value = json!({"cached": "data"});
477
        manager.set_cached_config("test_key".to_owned(), test_value);
478
479
        manager.cleanup_cache();
480
481
        // Cache entry should still exist since it was just created
482
        let retrieved = manager.get_cached_config("test_key");
483
        assert!(retrieved.is_some());
484
    }
485
486
    #[test]
487
    fn test_config_manager_classify_symbol_without_asset_manager() {
488
        let config = create_test_config();
489
        let manager = ConfigManager::new(config);
490
491
        let asset_class = manager.classify_symbol("AAPL");
492
        assert_eq!(
493
            asset_class,
494
            crate::asset_classification::AssetClass::Unknown
495
        );
496
    }
497
498
    #[test]
499
    fn test_config_manager_get_daily_volatility_default() {
500
        let config = create_test_config();
501
        let manager = ConfigManager::new(config);
502
503
        let volatility = manager.get_daily_volatility("AAPL");
504
        assert_eq!(volatility, 0.05); // Default value
505
    }
506
507
    #[test]
508
    fn test_config_manager_is_trading_active_default() {
509
        let config = create_test_config();
510
        let manager = ConfigManager::new(config);
511
512
        let now = chrono::Utc::now();
513
        let is_active = manager.is_trading_active("AAPL", now);
514
        assert!(is_active); // Default to always active
515
    }
516
517
    #[test]
518
    fn test_config_manager_get_trading_parameters_none() {
519
        let config = create_test_config();
520
        let manager = ConfigManager::new(config);
521
522
        let params = manager.get_trading_parameters("AAPL");
523
        assert!(params.is_none());
524
    }
525
526
    #[test]
527
    fn test_config_manager_get_volatility_profile_none() {
528
        let config = create_test_config();
529
        let manager = ConfigManager::new(config);
530
531
        let profile = manager.get_volatility_profile("AAPL");
532
        assert!(profile.is_none());
533
    }
534
535
    #[test]
536
    fn test_config_manager_get_position_size_recommendation_none() {
537
        let config = create_test_config();
538
        let manager = ConfigManager::new(config);
539
540
        let recommendation =
541
            manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0));
542
        assert!(recommendation.is_none());
543
    }
544
545
    #[test]
546
    fn test_config_manager_with_asset_classification() {
547
        let config = create_test_config();
548
        let asset_manager = crate::asset_classification::AssetClassificationManager::new();
549
        let manager = ConfigManager::with_asset_classification(config, asset_manager);
550
551
        let retrieved_config = manager.get_config();
552
        assert_eq!(retrieved_config.name, "test_service");
553
    }
554
555
    #[test]
556
    fn test_builder_with_asset_classification() {
557
        let config = create_test_config();
558
        let asset_manager = crate::asset_classification::AssetClassificationManager::new();
559
560
        let manager = ConfigManagerBuilder::new(config)
561
            .with_asset_classification(asset_manager)
562
            .build();
563
564
        let retrieved_config = manager.get_config();
565
        assert_eq!(retrieved_config.name, "test_service");
566
    }
567
568
    #[test]
569
    fn test_service_config_serialization() {
570
        let config = create_test_config();
571
        let serialized = serde_json::to_string(&config).unwrap();
572
        let deserialized: ServiceConfig = serde_json::from_str(&serialized).unwrap();
573
574
        assert_eq!(config.name, deserialized.name);
575
        assert_eq!(config.environment, deserialized.environment);
576
        assert_eq!(config.version, deserialized.version);
577
    }
578
579
    #[test]
580
    fn test_config_manager_multiple_cache_entries() {
581
        let config = create_test_config();
582
        let manager = ConfigManager::new(config);
583
584
        for i in 0..10 {
585
            manager.set_cached_config(format!("key_{}", i), json!({"value": i}));
586
        }
587
588
        for i in 0..10 {
589
            let retrieved = manager.get_cached_config(&format!("key_{}", i));
590
            assert!(retrieved.is_some());
591
        }
592
    }
593
594
    #[test]
595
    fn test_config_manager_cache_overwrite() {
596
        let config = create_test_config();
597
        let manager = ConfigManager::new(config);
598
599
        manager.set_cached_config("key".to_owned(), json!({"value": 1}));
600
        manager.set_cached_config("key".to_owned(), json!({"value": 2}));
601
602
        let retrieved = manager.get_cached_config("key");
603
        assert_eq!(retrieved.unwrap(), json!({"value": 2}));
604
    }
605
606
    #[test]
607
    fn test_builder_custom_cache_timeout() {
608
        let config = create_test_config();
609
        let custom_timeout = std::time::Duration::from_secs(120);
610
611
        let manager = ConfigManagerBuilder::new(config)
612
            .with_cache_timeout(custom_timeout)
613
            .build();
614
615
        // Cache timeout is set internally
616
        let retrieved_config = manager.get_config();
617
        assert_eq!(retrieved_config.name, "test_service");
618
    }
619
620
    #[test]
621
    fn test_config_manager_shared_config() {
622
        let config = create_test_config();
623
        let manager = ConfigManager::new(config);
624
625
        let config1 = manager.get_config();
626
        let config2 = manager.get_config();
627
628
        // Both should point to the same Arc
629
        assert_eq!(config1.name, config2.name);
630
    }
631
632
    #[test]
633
    fn test_service_config_clone() {
634
        let config1 = create_test_config();
635
        let config2 = config1.clone();
636
637
        assert_eq!(config1.name, config2.name);
638
        assert_eq!(config1.environment, config2.environment);
639
        assert_eq!(config1.version, config2.version);
640
    }
641
642
    #[test]
643
    fn test_config_manager_cache_timeout_configuration() {
644
        let config = create_test_config();
645
        let custom_timeout = std::time::Duration::from_millis(10);
646
        let manager = ConfigManagerBuilder::new(config)
647
            .with_cache_timeout(custom_timeout)
648
            .build();
649
650
        // Cache timeout is configured internally
651
        assert_eq!(manager.cache_timeout, custom_timeout);
652
653
        // Test that cache still works normally
654
        manager.set_cached_config("test_key".to_owned(), json!({"value": 42}));
655
        assert!(manager.get_cached_config("test_key").is_some());
656
    }
657
658
    #[test]
659
    fn test_config_manager_concurrent_access() {
660
        use std::sync::Arc;
661
        use std::thread;
662
663
        let config = create_test_config();
664
        let manager = Arc::new(ConfigManager::new(config));
665
666
        let mut handles = vec![];
667
668
        for i in 0..10 {
669
            let manager_clone = Arc::clone(&manager);
670
            let handle = thread::spawn(move || {
671
                manager_clone
672
                    .set_cached_config(format!("concurrent_key_{}", i), json!({"thread_id": i}));
673
                manager_clone.get_cached_config(&format!("concurrent_key_{}", i))
674
            });
675
            handles.push(handle);
676
        }
677
678
        for handle in handles {
679
            assert!(handle.join().unwrap().is_some());
680
        }
681
    }
682
683
    #[test]
684
    fn test_config_manager_daily_volatility_fallback() {
685
        let config = create_test_config();
686
        let manager = ConfigManager::new(config);
687
688
        // Should return default 5% for unknown symbols
689
        let vol = manager.get_daily_volatility("UNKNOWN_SYMBOL");
690
        assert_eq!(vol, 0.05);
691
    }
692
693
    #[test]
694
    fn test_config_manager_position_size_none() {
695
        let config = create_test_config();
696
        let manager = ConfigManager::new(config);
697
698
        // Should return None without asset classification
699
        let size =
700
            manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0));
701
        assert!(size.is_none());
702
    }
703
704
    #[test]
705
    fn test_service_config_validation() {
706
        let mut config = create_test_config();
707
708
        // Valid config
709
        assert!(!config.name.is_empty());
710
        assert!(!config.environment.is_empty());
711
712
        // Test with empty name
713
        config.name = String::new();
714
        assert!(config.name.is_empty());
715
    }
716
717
    #[test]
718
    fn test_config_manager_cache_clear() {
719
        let config = create_test_config();
720
        let manager = ConfigManager::new(config);
721
722
        // Add some cache entries
723
        manager.set_cached_config("key1".to_owned(), json!({"value": 1}));
724
        manager.set_cached_config("key2".to_owned(), json!({"value": 2}));
725
726
        assert!(manager.get_cached_config("key1").is_some());
727
        assert!(manager.get_cached_config("key2").is_some());
728
729
        // Manual clear
730
        if let Ok(mut cache) = manager.cache.write() {
731
            cache.clear();
732
        }
733
734
        assert!(manager.get_cached_config("key1").is_none());
735
        assert!(manager.get_cached_config("key2").is_none());
736
    }
737
738
    #[test]
739
    fn test_builder_default_values() {
740
        let config = create_test_config();
741
        let manager = ConfigManagerBuilder::new(config.clone()).build();
742
743
        let retrieved = manager.get_config();
744
        assert_eq!(retrieved.name, config.name);
745
        assert_eq!(retrieved.environment, config.environment);
746
    }
747
748
    #[test]
749
    fn test_config_manager_arc_cloning() {
750
        let config = create_test_config();
751
        let manager = ConfigManager::new(config);
752
753
        let config1 = Arc::clone(&manager.config);
754
        let config2 = Arc::clone(&manager.config);
755
756
        assert_eq!(config1.name, config2.name);
757
        assert_eq!(Arc::strong_count(&manager.config), 3); // Original + 2 clones
758
    }
759
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html deleted file mode 100644 index 8c138184d..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs
Line
Count
Source
1
//! Machine learning configuration
2
3
use serde::{Deserialize, Serialize};
4
use std::collections::HashMap;
5
6
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7
pub struct MLConfig {
8
    pub model_config: ModelArchitectureConfig,
9
    pub training_config: TrainingConfig,
10
    pub simulation_config: SimulationConfig,
11
}
12
13
/// Configuration for market data simulation and stress testing
14
#[derive(Debug, Clone, Serialize, Deserialize)]
15
pub struct SimulationConfig {
16
    /// Initial market state with configurable symbol prices
17
    pub initial_market_state: MarketState,
18
    /// Simulation parameters
19
    pub parameters: SimulationParameters,
20
    /// Test symbol configuration for generic testing
21
    pub test_symbols: TestSymbolConfig,
22
}
23
24
/// Initial market state configuration
25
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct MarketState {
27
    /// Symbol-specific initial prices and configuration
28
    pub symbols: HashMap<String, SymbolConfig>,
29
    /// Default configuration for unlisted symbols
30
    pub default_symbol: SymbolConfig,
31
}
32
33
/// Configuration for individual symbols
34
#[derive(Debug, Clone, Serialize, Deserialize)]
35
pub struct SymbolConfig {
36
    /// Initial price for the symbol
37
    pub initial_price: f64,
38
    /// Base volatility for the symbol
39
    pub volatility: f64,
40
    /// Base trading volume
41
    pub base_volume: f64,
42
    /// Minimum spread in basis points
43
    pub min_spread_bps: f64,
44
    /// Maximum spread in basis points
45
    pub max_spread_bps: f64,
46
    /// Market capitalization tier (affects behavior)
47
    pub market_cap_tier: MarketCapTier,
48
}
49
50
/// Market capitalization tiers for different symbol behaviors
51
#[derive(Debug, Clone, Serialize, Deserialize)]
52
pub enum MarketCapTier {
53
    /// Large cap stocks (>$10B)
54
    LargeCap,
55
    /// Mid cap stocks ($2B-$10B)
56
    MidCap,
57
    /// Small cap stocks (<$2B)
58
    SmallCap,
59
    /// Generic test symbol
60
    Test,
61
}
62
63
/// Simulation parameters
64
#[derive(Debug, Clone, Serialize, Deserialize)]
65
pub struct SimulationParameters {
66
    /// Update rate in Hz
67
    pub update_rate_hz: u32,
68
    /// Base market volatility
69
    pub base_volatility: f64,
70
    /// Market trend direction (-1.0 to 1.0)
71
    pub trend: f64,
72
    /// Enable realistic market microstructure
73
    pub enable_microstructure: bool,
74
    /// Enable correlated movements between symbols
75
    pub enable_correlation: bool,
76
}
77
78
/// Test symbol configuration for generic testing
79
#[derive(Debug, Clone, Serialize, Deserialize)]
80
pub struct TestSymbolConfig {
81
    /// Prefix for test symbols (e.g., "TEST")
82
    pub symbol_prefix: String,
83
    /// Number of test symbols to generate
84
    pub count: usize,
85
    /// Price range for test symbols
86
    pub price_range: (f64, f64),
87
    /// Volume range for test symbols
88
    pub volume_range: (f64, f64),
89
}
90
91
/// Default simulation configuration
92
impl Default for SimulationConfig {
93
0
    fn default() -> Self {
94
0
        let mut symbols = HashMap::new();
95
96
        // Production-ready major symbols with realistic configurations
97
0
        symbols.insert(
98
0
            "AAPL".to_owned(),
99
0
            SymbolConfig {
100
0
                initial_price: 150.0,
101
0
                volatility: 0.25,
102
0
                base_volume: 50000000.0,
103
0
                min_spread_bps: 1.0,
104
0
                max_spread_bps: 5.0,
105
0
                market_cap_tier: MarketCapTier::LargeCap,
106
0
            },
107
        );
108
109
0
        symbols.insert(
110
0
            "MSFT".to_owned(),
111
0
            SymbolConfig {
112
0
                initial_price: 300.0,
113
0
                volatility: 0.22,
114
0
                base_volume: 30000000.0,
115
0
                min_spread_bps: 1.0,
116
0
                max_spread_bps: 5.0,
117
0
                market_cap_tier: MarketCapTier::LargeCap,
118
0
            },
119
        );
120
121
0
        symbols.insert(
122
0
            "GOOGL".to_owned(),
123
0
            SymbolConfig {
124
0
                initial_price: 2500.0,
125
0
                volatility: 0.28,
126
0
                base_volume: 20000000.0,
127
0
                min_spread_bps: 2.0,
128
0
                max_spread_bps: 8.0,
129
0
                market_cap_tier: MarketCapTier::LargeCap,
130
0
            },
131
        );
132
133
0
        symbols.insert(
134
0
            "TSLA".to_owned(),
135
0
            SymbolConfig {
136
0
                initial_price: 800.0,
137
0
                volatility: 0.45,
138
0
                base_volume: 80000000.0,
139
0
                min_spread_bps: 2.0,
140
0
                max_spread_bps: 10.0,
141
0
                market_cap_tier: MarketCapTier::LargeCap,
142
0
            },
143
        );
144
145
0
        symbols.insert(
146
0
            "AMZN".to_owned(),
147
0
            SymbolConfig {
148
0
                initial_price: 3200.0,
149
0
                volatility: 0.30,
150
0
                base_volume: 25000000.0,
151
0
                min_spread_bps: 2.0,
152
0
                max_spread_bps: 8.0,
153
0
                market_cap_tier: MarketCapTier::LargeCap,
154
0
            },
155
        );
156
157
0
        symbols.insert(
158
0
            "NVDA".to_owned(),
159
0
            SymbolConfig {
160
0
                initial_price: 500.0,
161
0
                volatility: 0.40,
162
0
                base_volume: 40000000.0,
163
0
                min_spread_bps: 2.0,
164
0
                max_spread_bps: 8.0,
165
0
                market_cap_tier: MarketCapTier::LargeCap,
166
0
            },
167
        );
168
169
0
        Self {
170
0
            initial_market_state: MarketState {
171
0
                symbols,
172
0
                default_symbol: SymbolConfig {
173
0
                    initial_price: 100.0,
174
0
                    volatility: 0.30,
175
0
                    base_volume: 1000000.0,
176
0
                    min_spread_bps: 5.0,
177
0
                    max_spread_bps: 20.0,
178
0
                    market_cap_tier: MarketCapTier::Test,
179
0
                },
180
0
            },
181
0
            parameters: SimulationParameters {
182
0
                update_rate_hz: 1000,
183
0
                base_volatility: 0.02,
184
0
                trend: 0.0,
185
0
                enable_microstructure: true,
186
0
                enable_correlation: false,
187
0
            },
188
0
            test_symbols: TestSymbolConfig {
189
0
                symbol_prefix: "TEST".to_owned(),
190
0
                count: 10,
191
0
                price_range: (50.0, 500.0),
192
0
                volume_range: (100000.0, 10000000.0),
193
0
            },
194
0
        }
195
0
    }
196
}
197
198
#[derive(Debug, Clone, Serialize, Deserialize)]
199
pub struct ModelArchitectureConfig {
200
    pub model_type: String,
201
    pub hidden_dims: Vec<usize>,
202
    pub dropout_rate: f64,
203
    pub activation: String,
204
}
205
206
impl Default for ModelArchitectureConfig {
207
0
    fn default() -> Self {
208
0
        Self {
209
0
            model_type: "transformer".to_owned(),
210
0
            hidden_dims: vec![256, 128, 64],
211
0
            dropout_rate: 0.1,
212
0
            activation: "relu".to_owned(),
213
0
        }
214
0
    }
215
}
216
217
#[derive(Debug, Clone, Serialize, Deserialize)]
218
pub struct TrainingConfig {
219
    pub batch_size: usize,
220
    pub learning_rate: f64,
221
    pub epochs: u32,
222
    pub early_stopping_patience: u32,
223
}
224
225
impl Default for TrainingConfig {
226
0
    fn default() -> Self {
227
0
        Self {
228
0
            batch_size: 32,
229
0
            learning_rate: 0.001,
230
0
            epochs: 100,
231
0
            early_stopping_patience: 10,
232
0
        }
233
0
    }
234
}
235
236
#[derive(Debug, Clone, Serialize, Deserialize)]
237
pub struct Mamba2Config {
238
    pub d_model: usize,
239
    pub d_state: usize,
240
    pub d_conv: usize,
241
    pub expand: usize,
242
    pub dt_rank: Option<usize>,
243
    pub dt_min: f64,
244
    pub dt_max: f64,
245
    pub dt_init: String,
246
    pub dt_scale: f64,
247
    pub dt_init_floor: f64,
248
    pub conv_bias: bool,
249
    pub bias: bool,
250
    pub use_fast_path: bool,
251
    pub layer_idx: Option<usize>,
252
    pub device: Option<String>,
253
    pub dtype: Option<String>,
254
    pub d_head: usize,
255
    pub num_heads: usize,
256
    pub num_layers: usize,
257
    pub target_latency_us: u64,
258
    pub hardware_aware: bool,
259
    pub use_ssd: bool,
260
    pub use_selective_state: bool,
261
    pub max_seq_len: usize,
262
    pub batch_size: usize,
263
    pub seq_len: usize,
264
    pub dropout: f64,
265
}
266
267
impl Default for Mamba2Config {
268
0
    fn default() -> Self {
269
0
        Self {
270
0
            d_model: 768,
271
0
            d_state: 128,
272
0
            d_conv: 4,
273
0
            expand: 2,
274
0
            dt_rank: None, // Auto-calculated as ceil(d_model / 16)
275
0
            dt_min: 0.001,
276
0
            dt_max: 0.1,
277
0
            dt_init: "random".to_owned(),
278
0
            dt_scale: 1.0,
279
0
            dt_init_floor: 1e-4,
280
0
            conv_bias: true,
281
0
            bias: false,
282
0
            use_fast_path: true,
283
0
            layer_idx: None,
284
0
            device: None,
285
0
            dtype: None,
286
0
            d_head: 32,
287
0
            num_heads: 8,
288
0
            num_layers: 4,
289
0
            target_latency_us: 3,
290
0
            hardware_aware: true,
291
0
            use_ssd: true,
292
0
            use_selective_state: true,
293
0
            max_seq_len: 1024,
294
0
            batch_size: 1,
295
0
            seq_len: 256,
296
0
            dropout: 0.0,
297
0
        }
298
0
    }
299
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html deleted file mode 100644 index f1522186a..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs
Line
Count
Source
1
//! Risk management configuration structures
2
//!
3
//! Provides configuration types for risk management components including
4
//! stress testing scenarios, asset class definitions, and market shock parameters.
5
6
use serde::{Deserialize, Serialize};
7
use std::collections::HashMap;
8
9
/// Configuration for stress testing scenarios
10
///
11
/// Defines how stress scenarios are configured and applied to portfolios.
12
///
13
/// Supports both individual instrument shocks and asset class-based shocks
14
/// for more flexible and maintainable stress testing.
15
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16
pub struct StressScenarioConfig {
17
    /// Unique identifier for this stress test scenario
18
    pub id: String,
19
    /// Human-readable name describing the scenario
20
    pub name: String,
21
    /// Description of the stress scenario and its historical context
22
    pub description: String,
23
    /// Individual instrument-specific shocks (symbol -> shock percentage)
24
    pub instrument_shocks: HashMap<String, f64>,
25
    /// Asset class-based shocks that apply to all instruments in a class
26
    pub asset_class_shocks: HashMap<AssetClass, f64>,
27
    /// Global volatility multiplier to apply across all instruments
28
    pub volatility_multiplier: f64,
29
    /// Asset class-specific volatility multipliers
30
    pub volatility_multipliers: HashMap<AssetClass, f64>,
31
    /// Correlation adjustments between asset classes
32
    pub correlation_adjustments: HashMap<String, f64>,
33
    /// Liquidity haircuts to apply per asset class
34
    pub liquidity_haircuts: HashMap<AssetClass, f64>,
35
    /// Whether this scenario is active and available for use
36
    pub is_active: bool,
37
}
38
39
/// Asset class definitions for grouping instruments
40
///
41
/// Provides a hierarchical way to apply stress shocks to groups
42
/// of related instruments rather than hardcoding individual symbols.
43
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
44
pub enum AssetClass {
45
    /// Large-cap US equities (S&P 500 companies)
46
    LargeCapEquity,
47
    /// Small-cap US equities
48
    SmallCapEquity,
49
    /// Technology sector equities
50
    Technology,
51
    /// Financial sector equities
52
    Financials,
53
    /// Healthcare sector equities
54
    Healthcare,
55
    /// Energy sector equities
56
    Energy,
57
    /// Consumer discretionary equities
58
    ConsumerDiscretionary,
59
    /// Consumer staples equities
60
    ConsumerStaples,
61
    /// Industrial sector equities
62
    Industrials,
63
    /// Materials sector equities
64
    Materials,
65
    /// Real estate sector equities
66
    RealEstate,
67
    /// Utilities sector equities
68
    Utilities,
69
    /// Communication services sector equities
70
    CommunicationServices,
71
    /// US Treasury bonds
72
    USBonds,
73
    /// Corporate bonds
74
    CorporateBonds,
75
    /// High-yield bonds
76
    HighYieldBonds,
77
    /// International developed market equities
78
    InternationalEquity,
79
    /// Emerging market equities
80
    EmergingMarkets,
81
    /// Commodities
82
    Commodities,
83
    /// Foreign exchange
84
    ForeignExchange,
85
    /// Cryptocurrencies
86
    Crypto,
87
    /// Alternative investments
88
    Alternatives,
89
}
90
91
/// Asset class mapping configuration
92
///
93
/// Maps individual instrument symbols to their asset classes for
94
/// applying class-based stress shocks and risk calculations.
95
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96
pub struct AssetClassMapping {
97
    /// Symbol to asset class mappings
98
    pub mappings: HashMap<String, AssetClass>,
99
    /// Default asset class for unmapped symbols
100
    pub default_class: AssetClass,
101
}
102
103
/// Complete risk configuration containing all risk-related settings
104
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
105
pub struct RiskConfig {
106
    /// Available stress test scenarios
107
    pub stress_scenarios: Vec<StressScenarioConfig>,
108
    /// Asset class mappings for instruments
109
    pub asset_class_mapping: AssetClassMapping,
110
    /// Default volatility settings
111
    pub default_volatility_multiplier: f64,
112
    /// Maximum allowed portfolio loss percentage
113
    pub max_portfolio_loss_pct: f64,
114
    /// VaR confidence level (e.g., 0.95 for 95% confidence)
115
    pub var_confidence_level: f64,
116
    /// Time horizon for VaR calculations in days
117
    pub var_time_horizon_days: u32,
118
}
119
120
impl Default for RiskConfig {
121
0
    fn default() -> Self {
122
0
        Self {
123
0
            stress_scenarios: create_default_stress_scenarios(),
124
0
            asset_class_mapping: create_default_asset_class_mapping(),
125
0
            default_volatility_multiplier: 1.0,
126
0
            max_portfolio_loss_pct: 20.0,
127
0
            var_confidence_level: 0.95,
128
0
            var_time_horizon_days: 1,
129
0
        }
130
0
    }
131
}
132
133
impl StressScenarioConfig {
134
    /// Get the effective shock for a given instrument symbol
135
    ///
136
    /// Returns the instrument-specific shock if available, otherwise
137
    /// returns the asset class shock based on the symbol's asset class mapping.
138
0
    pub fn get_shock_for_symbol(
139
0
        &self,
140
0
        symbol: &str,
141
0
        asset_mapping: &AssetClassMapping,
142
0
    ) -> Option<f64> {
143
        // First check for instrument-specific shock
144
0
        if let Some(shock) = self.instrument_shocks.get(symbol) {
145
0
            return Some(*shock);
146
0
        }
147
148
        // Then check for asset class shock
149
0
        if let Some(asset_class) = asset_mapping.mappings.get(symbol) {
150
0
            return self.asset_class_shocks.get(asset_class).copied();
151
0
        }
152
153
        // Fall back to default asset class shock
154
0
        self.asset_class_shocks
155
0
            .get(&asset_mapping.default_class)
156
0
            .copied()
157
0
    }
158
159
    /// Get volatility multiplier for a given instrument symbol
160
0
    pub fn get_volatility_multiplier_for_symbol(
161
0
        &self,
162
0
        symbol: &str,
163
0
        asset_mapping: &AssetClassMapping,
164
0
    ) -> f64 {
165
        // Check for asset class-specific volatility multiplier
166
0
        if let Some(asset_class) = asset_mapping.mappings.get(symbol) {
167
0
            if let Some(multiplier) = self.volatility_multipliers.get(asset_class) {
168
0
                return *multiplier;
169
0
            }
170
0
        }
171
172
        // Fall back to default asset class
173
0
        if let Some(multiplier) = self
174
0
            .volatility_multipliers
175
0
            .get(&asset_mapping.default_class)
176
        {
177
0
            return *multiplier;
178
0
        }
179
180
        // Fall back to global multiplier
181
0
        self.volatility_multiplier
182
0
    }
183
}
184
185
/// Create default stress test scenarios based on historical events
186
0
fn create_default_stress_scenarios() -> Vec<StressScenarioConfig> {
187
0
    vec![
188
0
        StressScenarioConfig {
189
0
            id: "market_crash_2008".to_owned(),
190
0
            name: "2008 Financial Crisis".to_owned(),
191
0
            description: "Simulates the market conditions during the 2008 financial crisis with severe equity declines and financial sector stress".to_owned(),
192
0
            instrument_shocks: HashMap::new(),
193
0
            asset_class_shocks: {
194
0
                let mut shocks = HashMap::new();
195
0
                shocks.insert(AssetClass::LargeCapEquity, -37.0);
196
0
                shocks.insert(AssetClass::SmallCapEquity, -45.0);
197
0
                shocks.insert(AssetClass::Financials, -55.0);
198
0
                shocks.insert(AssetClass::Technology, -40.0);
199
0
                shocks.insert(AssetClass::RealEstate, -60.0);
200
0
                shocks.insert(AssetClass::EmergingMarkets, -50.0);
201
0
                shocks.insert(AssetClass::HighYieldBonds, -25.0);
202
0
                shocks
203
0
            },
204
0
            volatility_multiplier: 2.5,
205
0
            volatility_multipliers: HashMap::new(),
206
0
            correlation_adjustments: HashMap::new(),
207
0
            liquidity_haircuts: {
208
0
                let mut haircuts = HashMap::new();
209
0
                haircuts.insert(AssetClass::SmallCapEquity, 0.15);
210
0
                haircuts.insert(AssetClass::EmergingMarkets, 0.20);
211
0
                haircuts.insert(AssetClass::HighYieldBonds, 0.10);
212
0
                haircuts
213
0
            },
214
0
            is_active: true,
215
0
        },
216
0
        StressScenarioConfig {
217
0
            id: "covid_crash_2020".to_owned(),
218
0
            name: "COVID-19 Market Crash".to_owned(),
219
0
            description: "Simulates the market crash of March 2020 due to COVID-19 pandemic with broad-based equity declines".to_owned(),
220
0
            instrument_shocks: HashMap::new(),
221
0
            asset_class_shocks: {
222
0
                let mut shocks = HashMap::new();
223
0
                shocks.insert(AssetClass::LargeCapEquity, -34.0);
224
0
                shocks.insert(AssetClass::SmallCapEquity, -40.0);
225
0
                shocks.insert(AssetClass::Energy, -50.0);
226
0
                shocks.insert(AssetClass::Financials, -45.0);
227
0
                shocks.insert(AssetClass::RealEstate, -35.0);
228
0
                shocks.insert(AssetClass::Technology, -25.0);
229
0
                shocks.insert(AssetClass::EmergingMarkets, -45.0);
230
0
                shocks
231
0
            },
232
0
            volatility_multiplier: 3.0,
233
0
            volatility_multipliers: HashMap::new(),
234
0
            correlation_adjustments: HashMap::new(),
235
0
            liquidity_haircuts: HashMap::new(),
236
0
            is_active: true,
237
0
        },
238
0
        StressScenarioConfig {
239
0
            id: "flash_crash_2010".to_owned(),
240
0
            name: "Flash Crash 2010".to_owned(),
241
0
            description: "Simulates the May 6, 2010 flash crash with rapid market decline and liquidity issues".to_owned(),
242
0
            instrument_shocks: HashMap::new(),
243
0
            asset_class_shocks: {
244
0
                let mut shocks = HashMap::new();
245
0
                shocks.insert(AssetClass::LargeCapEquity, -9.0);
246
0
                shocks.insert(AssetClass::SmallCapEquity, -15.0);
247
0
                shocks.insert(AssetClass::Technology, -12.0);
248
0
                shocks
249
0
            },
250
0
            volatility_multiplier: 5.0,
251
0
            volatility_multipliers: HashMap::new(),
252
0
            correlation_adjustments: HashMap::new(),
253
0
            liquidity_haircuts: {
254
0
                let mut haircuts = HashMap::new();
255
0
                haircuts.insert(AssetClass::LargeCapEquity, 0.05);
256
0
                haircuts.insert(AssetClass::SmallCapEquity, 0.20);
257
0
                haircuts.insert(AssetClass::Technology, 0.10);
258
0
                haircuts
259
0
            },
260
0
            is_active: true,
261
0
        },
262
0
        StressScenarioConfig {
263
0
            id: "volatility_spike".to_owned(),
264
0
            name: "Volatility Spike".to_owned(),
265
0
            description: "Simulates a sudden spike in market volatility without significant price moves".to_owned(),
266
0
            instrument_shocks: HashMap::new(),
267
0
            asset_class_shocks: HashMap::new(),
268
0
            volatility_multiplier: 3.0,
269
0
            volatility_multipliers: {
270
0
                let mut multipliers = HashMap::new();
271
0
                multipliers.insert(AssetClass::SmallCapEquity, 4.0);
272
0
                multipliers.insert(AssetClass::EmergingMarkets, 3.5);
273
0
                multipliers.insert(AssetClass::HighYieldBonds, 2.5);
274
0
                multipliers
275
0
            },
276
0
            correlation_adjustments: HashMap::new(),
277
0
            liquidity_haircuts: HashMap::new(),
278
0
            is_active: true,
279
0
        },
280
0
        StressScenarioConfig {
281
0
            id: "interest_rate_shock".to_owned(),
282
0
            name: "Interest Rate Shock".to_owned(),
283
0
            description: "Simulates a sudden rise in interest rates affecting bonds and rate-sensitive sectors".to_owned(),
284
0
            instrument_shocks: HashMap::new(),
285
0
            asset_class_shocks: {
286
0
                let mut shocks = HashMap::new();
287
0
                shocks.insert(AssetClass::USBonds, -8.0);
288
0
                shocks.insert(AssetClass::CorporateBonds, -12.0);
289
0
                shocks.insert(AssetClass::RealEstate, -15.0);
290
0
                shocks.insert(AssetClass::Utilities, -10.0);
291
0
                shocks.insert(AssetClass::Financials, 5.0); // Banks benefit from higher rates
292
0
                shocks
293
0
            },
294
0
            volatility_multiplier: 1.5,
295
0
            volatility_multipliers: HashMap::new(),
296
0
            correlation_adjustments: HashMap::new(),
297
0
            liquidity_haircuts: HashMap::new(),
298
0
            is_active: true,
299
0
        },
300
    ]
301
0
}
302
303
/// Create default asset class mapping for common symbols
304
0
fn create_default_asset_class_mapping() -> AssetClassMapping {
305
0
    let mut mappings = HashMap::new();
306
307
    // Large Cap Technology
308
0
    mappings.insert("AAPL".to_owned(), AssetClass::Technology);
309
0
    mappings.insert("MSFT".to_owned(), AssetClass::Technology);
310
0
    mappings.insert("GOOGL".to_owned(), AssetClass::Technology);
311
0
    mappings.insert("GOOG".to_owned(), AssetClass::Technology);
312
0
    mappings.insert("AMZN".to_owned(), AssetClass::Technology);
313
0
    mappings.insert("META".to_owned(), AssetClass::Technology);
314
0
    mappings.insert("TSLA".to_owned(), AssetClass::Technology);
315
0
    mappings.insert("NVDA".to_owned(), AssetClass::Technology);
316
317
    // Large Cap Financials
318
0
    mappings.insert("JPM".to_owned(), AssetClass::Financials);
319
0
    mappings.insert("BAC".to_owned(), AssetClass::Financials);
320
0
    mappings.insert("WFC".to_owned(), AssetClass::Financials);
321
0
    mappings.insert("GS".to_owned(), AssetClass::Financials);
322
0
    mappings.insert("MS".to_owned(), AssetClass::Financials);
323
324
    // ETFs
325
0
    mappings.insert("SPY".to_owned(), AssetClass::LargeCapEquity);
326
0
    mappings.insert("QQQ".to_owned(), AssetClass::Technology);
327
0
    mappings.insert("IWM".to_owned(), AssetClass::SmallCapEquity);
328
0
    mappings.insert("VTI".to_owned(), AssetClass::LargeCapEquity);
329
0
    mappings.insert("EEM".to_owned(), AssetClass::EmergingMarkets);
330
0
    mappings.insert("VEA".to_owned(), AssetClass::InternationalEquity);
331
0
    mappings.insert("TLT".to_owned(), AssetClass::USBonds);
332
0
    mappings.insert("HYG".to_owned(), AssetClass::HighYieldBonds);
333
334
    // Healthcare
335
0
    mappings.insert("JNJ".to_owned(), AssetClass::Healthcare);
336
0
    mappings.insert("PFE".to_owned(), AssetClass::Healthcare);
337
0
    mappings.insert("UNH".to_owned(), AssetClass::Healthcare);
338
339
    // Energy
340
0
    mappings.insert("XOM".to_owned(), AssetClass::Energy);
341
0
    mappings.insert("CVX".to_owned(), AssetClass::Energy);
342
343
0
    AssetClassMapping {
344
0
        mappings,
345
0
        default_class: AssetClass::LargeCapEquity,
346
0
    }
347
0
}
348
349
#[cfg(test)]
350
mod tests {
351
    use super::*;
352
353
    #[test]
354
    fn test_stress_scenario_config_creation() {
355
        let config = StressScenarioConfig {
356
            id: "test".to_owned(),
357
            name: "Test Scenario".to_owned(),
358
            description: "Test description".to_owned(),
359
            instrument_shocks: HashMap::new(),
360
            asset_class_shocks: {
361
                let mut shocks = HashMap::new();
362
                shocks.insert(AssetClass::Technology, -10.0);
363
                shocks
364
            },
365
            volatility_multiplier: 2.0,
366
            volatility_multipliers: HashMap::new(),
367
            correlation_adjustments: HashMap::new(),
368
            liquidity_haircuts: HashMap::new(),
369
            is_active: true,
370
        };
371
372
        assert_eq!(config.id, "test");
373
        assert_eq!(config.volatility_multiplier, 2.0);
374
    }
375
376
    #[test]
377
    fn test_asset_class_mapping() {
378
        let mapping = create_default_asset_class_mapping();
379
380
        assert_eq!(mapping.mappings.get("AAPL"), Some(&AssetClass::Technology));
381
        assert_eq!(
382
            mapping.mappings.get("SPY"),
383
            Some(&AssetClass::LargeCapEquity)
384
        );
385
        assert_eq!(mapping.default_class, AssetClass::LargeCapEquity);
386
    }
387
388
    #[test]
389
    fn test_get_shock_for_symbol() {
390
        let config = StressScenarioConfig {
391
            id: "test".to_owned(),
392
            name: "Test".to_owned(),
393
            description: "Test".to_owned(),
394
            instrument_shocks: {
395
                let mut shocks = HashMap::new();
396
                shocks.insert("AAPL".to_owned(), -15.0);
397
                shocks
398
            },
399
            asset_class_shocks: {
400
                let mut shocks = HashMap::new();
401
                shocks.insert(AssetClass::Technology, -10.0);
402
                shocks.insert(AssetClass::LargeCapEquity, -5.0);
403
                shocks
404
            },
405
            volatility_multiplier: 1.0,
406
            volatility_multipliers: HashMap::new(),
407
            correlation_adjustments: HashMap::new(),
408
            liquidity_haircuts: HashMap::new(),
409
            is_active: true,
410
        };
411
412
        let mapping = create_default_asset_class_mapping();
413
414
        // Should get instrument-specific shock
415
        assert_eq!(config.get_shock_for_symbol("AAPL", &mapping), Some(-15.0));
416
417
        // Should get asset class shock for GOOGL (Technology)
418
        assert_eq!(config.get_shock_for_symbol("GOOGL", &mapping), Some(-10.0));
419
420
        // Should get default class shock for unknown symbol
421
        assert_eq!(config.get_shock_for_symbol("UNKNOWN", &mapping), Some(-5.0));
422
    }
423
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html deleted file mode 100644 index 9effa0baf..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/runtime.rs
Line
Count
Source
1
//! Runtime configuration layer for environment-aware defaults.
2
//!
3
//! This module provides Tier 2 runtime configuration that complements the
4
//! compile-time constants in `common::thresholds`. Values here can be overridden
5
//! via environment variables to support different deployment environments
6
//! (development, staging, production) without recompilation.
7
//!
8
//! # Architecture
9
//!
10
//! - Tier 1 (Compile-time): `common::thresholds` - Performance-critical constants
11
//! - Tier 2 (Runtime): This module - Environment-aware operational parameters
12
//! - Tier 3 (Database): Hot-reload via PostgreSQL NOTIFY/LISTEN
13
//!
14
//! # Environment Variables
15
//!
16
//! ## Database Configuration
17
//! - `DATABASE_QUERY_TIMEOUT_MS` - Query timeout in milliseconds (default: environment-aware)
18
//! - `DATABASE_CONNECTION_TIMEOUT_MS` - Connection timeout in milliseconds
19
//! - `DATABASE_POOL_SIZE` - Connection pool size
20
//! - `DATABASE_MAX_POOL_SIZE` - Maximum pool size
21
//! - `DATABASE_ACQUIRE_TIMEOUT_MS` - Pool acquire timeout in milliseconds
22
//!
23
//! ## Cache Configuration
24
//! - `CACHE_POSITION_TTL_SECS` - Position cache TTL in seconds
25
//! - `CACHE_VAR_TTL_SECS` - VaR calculation cache TTL in seconds
26
//! - `CACHE_COMPLIANCE_TTL_SECS` - Compliance check cache TTL in seconds
27
//! - `CACHE_MARKET_DATA_TTL_SECS` - Market data cache TTL in seconds
28
//! - `CACHE_MODEL_PREDICTION_TTL_SECS` - Model prediction cache TTL in seconds
29
//!
30
//! ## Network Configuration
31
//! - `NETWORK_GRPC_CONNECT_TIMEOUT_SECS` - gRPC connect timeout in seconds
32
//! - `NETWORK_GRPC_REQUEST_TIMEOUT_SECS` - gRPC request timeout in seconds
33
//! - `NETWORK_KEEP_ALIVE_INTERVAL_SECS` - Keep-alive interval in seconds
34
//! - `NETWORK_KEEP_ALIVE_TIMEOUT_SECS` - Keep-alive timeout in seconds
35
//! - `NETWORK_MAX_CONCURRENT_CONNECTIONS` - Maximum concurrent connections
36
//!
37
//! ## Retry Configuration
38
//! - `RETRY_INITIAL_DELAY_MS` - Initial retry delay in milliseconds
39
//! - `RETRY_MAX_DELAY_SECS` - Maximum retry delay in seconds
40
//! - `RETRY_MAX_ATTEMPTS` - Maximum retry attempts
41
//! - `RETRY_BACKOFF_MULTIPLIER` - Backoff multiplier for exponential backoff
42
//!
43
//! ## Safety Configuration
44
//! - `SAFETY_CHECK_TIMEOUT_MS` - Safety check timeout in milliseconds
45
//! - `SAFETY_AUTO_RECOVERY_DELAY_SECS` - Auto-recovery delay in seconds
46
//! - `SAFETY_LOSS_CHECK_INTERVAL_SECS` - Loss check interval in seconds
47
//! - `SAFETY_POSITION_CHECK_INTERVAL_SECS` - Position check interval in seconds
48
//!
49
//! ## ML Configuration
50
//! - `ML_MAX_BATCH_SIZE` - Maximum batch size for ML inference
51
//! - `ML_INFERENCE_TIMEOUT_MS` - ML inference timeout in milliseconds
52
//! - `ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS` - Model cache cleanup interval
53
//! - `ML_DRIFT_CHECK_INTERVAL_SECS` - Drift detection check interval
54
//!
55
//! ## Risk Configuration
56
//! - `RISK_VAR_LOOKBACK_DAYS` - VaR lookback period in trading days
57
//! - `RISK_VAR_CONFIDENCE` - VaR confidence level (0.0-1.0)
58
//! - `RISK_MAX_DRAWDOWN_WARNING_PCT` - Max drawdown warning threshold
59
//!
60
//! # Example
61
//!
62
//! ```rust,no_run
63
//! use config::runtime::{RuntimeConfig, Environment};
64
//!
65
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
66
//! // Auto-detect environment and load from env vars
67
//! let config = RuntimeConfig::from_env()?;
68
//!
69
//! // Or specify environment explicitly
70
//! let prod_config = RuntimeConfig::from_env_with_environment(Environment::Production)?;
71
//!
72
//! // Or use defaults for specific environment
73
//! let dev_config = RuntimeConfig::with_defaults(Environment::Development);
74
//!
75
//! println!("Database query timeout: {:?}", config.database.query_timeout);
76
//! println!("Position cache TTL: {:?}", config.cache.position_ttl);
77
//! # Ok(())
78
//! # }
79
//! ```
80
81
use crate::error::{ConfigError, ConfigResult};
82
use serde::{Deserialize, Serialize};
83
use std::time::Duration;
84
85
/// Deployment environment enumeration.
86
///
87
/// Determines default values for runtime configuration parameters.
88
///
89
/// Different environments have different performance vs safety trade-offs.
90
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91
pub enum Environment {
92
    /// Development environment - Relaxed timeouts, verbose logging
93
    Development,
94
    /// Staging environment - Production-like settings with some debug features
95
    Staging,
96
    /// Production environment - Optimized for performance and reliability
97
    Production,
98
}
99
100
impl Environment {
101
    /// Detects the environment from the ENVIRONMENT environment variable.
102
    ///
103
    /// Falls back to Development if not set or invalid.
104
0
    pub fn detect() -> Self {
105
0
        match std::env::var("ENVIRONMENT")
106
0
            .unwrap_or_else(|_| "development".to_owned())
107
0
            .to_lowercase()
108
0
            .as_str()
109
        {
110
0
            "production" | "prod" => Environment::Production,
111
0
            "staging" | "stage" => Environment::Staging,
112
0
            _ => Environment::Development,
113
        }
114
0
    }
115
116
    /// Returns true if this is a production environment.
117
0
    pub const fn is_production(&self) -> bool {
118
0
        matches!(self, Environment::Production)
119
0
    }
120
121
    /// Returns true if this is a development environment.
122
0
    pub const fn is_development(&self) -> bool {
123
0
        matches!(self, Environment::Development)
124
0
    }
125
}
126
127
/// Database runtime configuration.
128
///
129
/// Controls database connection pooling, timeouts, and query execution limits.
130
#[derive(Debug, Clone, Serialize, Deserialize)]
131
pub struct DatabaseRuntimeConfig {
132
    /// Query timeout for standard operations
133
    pub query_timeout: Duration,
134
    /// Connection establishment timeout
135
    pub connection_timeout: Duration,
136
    /// Pool acquire timeout
137
    pub acquire_timeout: Duration,
138
    /// Default pool size
139
    pub pool_size: u32,
140
    /// Maximum pool size
141
    pub max_pool_size: u32,
142
    /// Connection lifetime
143
    pub connection_lifetime: Duration,
144
    /// Idle timeout
145
    pub idle_timeout: Duration,
146
}
147
148
impl DatabaseRuntimeConfig {
149
    /// Creates configuration with environment-aware defaults.
150
0
    pub const fn with_defaults(env: Environment) -> Self {
151
0
        match env {
152
0
            Environment::Development => Self {
153
0
                query_timeout: Duration::from_millis(5000), // More relaxed for debugging
154
0
                connection_timeout: Duration::from_millis(500),
155
0
                acquire_timeout: Duration::from_millis(200),
156
0
                pool_size: 10,
157
0
                max_pool_size: 50,
158
0
                connection_lifetime: Duration::from_secs(1800), // 30 minutes
159
0
                idle_timeout: Duration::from_secs(600), // 10 minutes
160
0
            },
161
0
            Environment::Staging => Self {
162
0
                query_timeout: Duration::from_millis(2000),
163
0
                connection_timeout: Duration::from_millis(200),
164
0
                acquire_timeout: Duration::from_millis(100),
165
0
                pool_size: 15,
166
0
                max_pool_size: 75,
167
0
                connection_lifetime: Duration::from_secs(3600), // 1 hour
168
0
                idle_timeout: Duration::from_secs(300), // 5 minutes
169
0
            },
170
0
            Environment::Production => Self {
171
0
                query_timeout: Duration::from_millis(1000), // Tight timeout for HFT
172
0
                connection_timeout: Duration::from_millis(100),
173
0
                acquire_timeout: Duration::from_millis(50),
174
0
                pool_size: 20,
175
0
                max_pool_size: 100,
176
0
                connection_lifetime: Duration::from_secs(3600), // 1 hour
177
0
                idle_timeout: Duration::from_secs(300), // 5 minutes
178
0
            },
179
        }
180
0
    }
181
182
    /// Loads from environment variables with fallback to defaults.
183
    ///
184
    /// # Errors
185
    /// Returns error if the operation fails
186
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
187
0
        let defaults = Self::with_defaults(env);
188
189
        Ok(Self {
190
0
            query_timeout: parse_env_duration_ms("DATABASE_QUERY_TIMEOUT_MS", defaults.query_timeout)?,
191
0
            connection_timeout: parse_env_duration_ms("DATABASE_CONNECTION_TIMEOUT_MS", defaults.connection_timeout)?,
192
0
            acquire_timeout: parse_env_duration_ms("DATABASE_ACQUIRE_TIMEOUT_MS", defaults.acquire_timeout)?,
193
0
            pool_size: parse_env_u32("DATABASE_POOL_SIZE", defaults.pool_size)?,
194
0
            max_pool_size: parse_env_u32("DATABASE_MAX_POOL_SIZE", defaults.max_pool_size)?,
195
0
            connection_lifetime: parse_env_duration_secs("DATABASE_CONNECTION_LIFETIME_SECS", defaults.connection_lifetime)?,
196
0
            idle_timeout: parse_env_duration_secs("DATABASE_IDLE_TIMEOUT_SECS", defaults.idle_timeout)?,
197
        })
198
0
    }
199
200
    /// Validates the configuration.
201
    ///
202
    /// # Errors
203
    /// Returns error if the operation fails
204
0
    pub fn validate(&self) -> ConfigResult<()> {
205
0
        if self.query_timeout.as_millis() == 0 {
206
0
            return Err(ConfigError::Invalid("Query timeout must be positive".into()));
207
0
        }
208
0
        if self.pool_size == 0 {
209
0
            return Err(ConfigError::Invalid("Pool size must be positive".into()));
210
0
        }
211
0
        if self.pool_size > self.max_pool_size {
212
0
            return Err(ConfigError::Invalid("Pool size cannot exceed max pool size".into()));
213
0
        }
214
0
        Ok(())
215
0
    }
216
}
217
218
/// Cache TTL runtime configuration.
219
///
220
/// Controls time-to-live values for various cache types.
221
#[derive(Debug, Clone, Serialize, Deserialize)]
222
pub struct CacheRuntimeConfig {
223
    /// Position cache TTL
224
    pub position_ttl: Duration,
225
    /// VaR calculation cache TTL
226
    pub var_ttl: Duration,
227
    /// Compliance check cache TTL
228
    pub compliance_ttl: Duration,
229
    /// Market data cache TTL
230
    pub market_data_ttl: Duration,
231
    /// Model prediction cache TTL
232
    pub model_prediction_ttl: Duration,
233
}
234
235
impl CacheRuntimeConfig {
236
    /// Creates configuration with environment-aware defaults.
237
0
    pub const fn with_defaults(env: Environment) -> Self {
238
0
        match env {
239
0
            Environment::Development => Self {
240
0
                position_ttl: Duration::from_secs(120), // Longer TTL for debugging
241
0
                var_ttl: Duration::from_secs(7200), // 2 hours
242
0
                compliance_ttl: Duration::from_secs(172800), // 48 hours
243
0
                market_data_ttl: Duration::from_secs(600), // 10 minutes
244
0
                model_prediction_ttl: Duration::from_secs(120), // 2 minutes
245
0
            },
246
0
            Environment::Staging => Self {
247
0
                position_ttl: Duration::from_secs(90),
248
0
                var_ttl: Duration::from_secs(5400), // 1.5 hours
249
0
                compliance_ttl: Duration::from_secs(129600), // 36 hours
250
0
                market_data_ttl: Duration::from_secs(450), // 7.5 minutes
251
0
                model_prediction_ttl: Duration::from_secs(90),
252
0
            },
253
0
            Environment::Production => Self {
254
0
                position_ttl: Duration::from_secs(60), // 1 minute for HFT
255
0
                var_ttl: Duration::from_secs(3600), // 1 hour
256
0
                compliance_ttl: Duration::from_secs(86400), // 24 hours
257
0
                market_data_ttl: Duration::from_secs(300), // 5 minutes
258
0
                model_prediction_ttl: Duration::from_secs(60), // 1 minute
259
0
            },
260
        }
261
0
    }
262
263
    /// Loads from environment variables with fallback to defaults.
264
    ///
265
    /// # Errors
266
    /// Returns error if the operation fails
267
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
268
0
        let defaults = Self::with_defaults(env);
269
270
        Ok(Self {
271
0
            position_ttl: parse_env_duration_secs("CACHE_POSITION_TTL_SECS", defaults.position_ttl)?,
272
0
            var_ttl: parse_env_duration_secs("CACHE_VAR_TTL_SECS", defaults.var_ttl)?,
273
0
            compliance_ttl: parse_env_duration_secs("CACHE_COMPLIANCE_TTL_SECS", defaults.compliance_ttl)?,
274
0
            market_data_ttl: parse_env_duration_secs("CACHE_MARKET_DATA_TTL_SECS", defaults.market_data_ttl)?,
275
0
            model_prediction_ttl: parse_env_duration_secs("CACHE_MODEL_PREDICTION_TTL_SECS", defaults.model_prediction_ttl)?,
276
        })
277
0
    }
278
279
    /// Validates the configuration.
280
    ///
281
    /// # Errors
282
    /// Returns error if the operation fails
283
0
    pub fn validate(&self) -> ConfigResult<()> {
284
0
        if self.position_ttl.as_secs() == 0 {
285
0
            return Err(ConfigError::Invalid("Position TTL must be positive".into()));
286
0
        }
287
0
        if self.var_ttl.as_secs() == 0 {
288
0
            return Err(ConfigError::Invalid("VaR TTL must be positive".into()));
289
0
        }
290
0
        Ok(())
291
0
    }
292
}
293
294
/// Network timeout runtime configuration.
295
///
296
/// Controls gRPC and network-related timeouts.
297
#[derive(Debug, Clone, Serialize, Deserialize)]
298
pub struct TimeoutConfig {
299
    /// gRPC connect timeout
300
    pub grpc_connect_timeout: Duration,
301
    /// gRPC request timeout
302
    pub grpc_request_timeout: Duration,
303
    /// Keep-alive interval
304
    pub keep_alive_interval: Duration,
305
    /// Keep-alive timeout
306
    pub keep_alive_timeout: Duration,
307
    /// Maximum concurrent connections
308
    pub max_concurrent_connections: u32,
309
}
310
311
impl TimeoutConfig {
312
    /// Creates configuration with environment-aware defaults.
313
0
    pub const fn with_defaults(env: Environment) -> Self {
314
0
        match env {
315
0
            Environment::Development => Self {
316
0
                grpc_connect_timeout: Duration::from_secs(10),
317
0
                grpc_request_timeout: Duration::from_secs(30),
318
0
                keep_alive_interval: Duration::from_secs(60),
319
0
                keep_alive_timeout: Duration::from_secs(10),
320
0
                max_concurrent_connections: 50,
321
0
            },
322
0
            Environment::Staging => Self {
323
0
                grpc_connect_timeout: Duration::from_secs(7),
324
0
                grpc_request_timeout: Duration::from_secs(20),
325
0
                keep_alive_interval: Duration::from_secs(45),
326
0
                keep_alive_timeout: Duration::from_secs(7),
327
0
                max_concurrent_connections: 75,
328
0
            },
329
0
            Environment::Production => Self {
330
0
                grpc_connect_timeout: Duration::from_secs(5),
331
0
                grpc_request_timeout: Duration::from_secs(10),
332
0
                keep_alive_interval: Duration::from_secs(30),
333
0
                keep_alive_timeout: Duration::from_secs(5),
334
0
                max_concurrent_connections: 100,
335
0
            },
336
        }
337
0
    }
338
339
    /// Loads from environment variables with fallback to defaults.
340
    ///
341
    /// # Errors
342
    /// Returns error if the operation fails
343
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
344
0
        let defaults = Self::with_defaults(env);
345
346
        Ok(Self {
347
0
            grpc_connect_timeout: parse_env_duration_secs("NETWORK_GRPC_CONNECT_TIMEOUT_SECS", defaults.grpc_connect_timeout)?,
348
0
            grpc_request_timeout: parse_env_duration_secs("NETWORK_GRPC_REQUEST_TIMEOUT_SECS", defaults.grpc_request_timeout)?,
349
0
            keep_alive_interval: parse_env_duration_secs("NETWORK_KEEP_ALIVE_INTERVAL_SECS", defaults.keep_alive_interval)?,
350
0
            keep_alive_timeout: parse_env_duration_secs("NETWORK_KEEP_ALIVE_TIMEOUT_SECS", defaults.keep_alive_timeout)?,
351
0
            max_concurrent_connections: parse_env_u32("NETWORK_MAX_CONCURRENT_CONNECTIONS", defaults.max_concurrent_connections)?,
352
        })
353
0
    }
354
355
    /// Validates the configuration.
356
    ///
357
    /// # Errors
358
    /// Returns error if the operation fails
359
0
    pub fn validate(&self) -> ConfigResult<()> {
360
0
        if self.grpc_connect_timeout.as_secs() == 0 {
361
0
            return Err(ConfigError::Invalid("gRPC connect timeout must be positive".into()));
362
0
        }
363
0
        if self.max_concurrent_connections == 0 {
364
0
            return Err(ConfigError::Invalid("Max concurrent connections must be positive".into()));
365
0
        }
366
0
        Ok(())
367
0
    }
368
}
369
370
/// Operational limits runtime configuration.
371
///
372
/// Controls retry behavior, safety checks, ML parameters, and risk calculations.
373
#[derive(Debug, Clone, Serialize, Deserialize)]
374
pub struct LimitsConfig {
375
    // Retry configuration
376
    /// Initial retry delay
377
    pub retry_initial_delay: Duration,
378
    /// Maximum retry delay
379
    pub retry_max_delay: Duration,
380
    /// Maximum retry attempts
381
    pub retry_max_attempts: u32,
382
    /// Backoff multiplier
383
    pub retry_backoff_multiplier: f32,
384
385
    // Safety configuration
386
    /// Safety check timeout
387
    pub safety_check_timeout: Duration,
388
    /// Auto-recovery delay
389
    pub safety_auto_recovery_delay: Duration,
390
    /// Loss check interval
391
    pub safety_loss_check_interval: Duration,
392
    /// Position check interval
393
    pub safety_position_check_interval: Duration,
394
395
    // ML configuration
396
    /// Maximum batch size for ML inference
397
    pub ml_max_batch_size: usize,
398
    /// ML inference timeout
399
    pub ml_inference_timeout: Duration,
400
    /// Model cache cleanup interval
401
    pub ml_cache_cleanup_interval: Duration,
402
    /// Drift detection check interval
403
    pub ml_drift_check_interval: Duration,
404
405
    // Risk configuration
406
    /// VaR lookback period in trading days
407
    pub risk_var_lookback_days: usize,
408
    /// VaR confidence level
409
    pub risk_var_confidence: f64,
410
    /// Max drawdown warning threshold (percentage)
411
    pub risk_max_drawdown_warning_pct: u8,
412
}
413
414
impl LimitsConfig {
415
    /// Creates configuration with environment-aware defaults.
416
0
    pub const fn with_defaults(env: Environment) -> Self {
417
0
        match env {
418
0
            Environment::Development => Self {
419
0
                // Retry
420
0
                retry_initial_delay: Duration::from_millis(200),
421
0
                retry_max_delay: Duration::from_secs(60),
422
0
                retry_max_attempts: 5,
423
0
                retry_backoff_multiplier: 2.0,
424
0
425
0
                // Safety
426
0
                safety_check_timeout: Duration::from_millis(50),
427
0
                safety_auto_recovery_delay: Duration::from_secs(60),
428
0
                safety_loss_check_interval: Duration::from_secs(30),
429
0
                safety_position_check_interval: Duration::from_secs(15),
430
0
431
0
                // ML
432
0
                ml_max_batch_size: 1024,
433
0
                ml_inference_timeout: Duration::from_millis(200),
434
0
                ml_cache_cleanup_interval: Duration::from_secs(7200), // 2 hours
435
0
                ml_drift_check_interval: Duration::from_secs(600), // 10 minutes
436
0
437
0
                // Risk
438
0
                risk_var_lookback_days: 252,
439
0
                risk_var_confidence: 0.95,
440
0
                risk_max_drawdown_warning_pct: 20,
441
0
            },
442
0
            Environment::Staging => Self {
443
0
                // Retry
444
0
                retry_initial_delay: Duration::from_millis(150),
445
0
                retry_max_delay: Duration::from_secs(45),
446
0
                retry_max_attempts: 4,
447
0
                retry_backoff_multiplier: 1.75,
448
0
449
0
                // Safety
450
0
                safety_check_timeout: Duration::from_millis(25),
451
0
                safety_auto_recovery_delay: Duration::from_secs(900), // 15 minutes
452
0
                safety_loss_check_interval: Duration::from_secs(15),
453
0
                safety_position_check_interval: Duration::from_secs(7),
454
0
455
0
                // ML
456
0
                ml_max_batch_size: 4096,
457
0
                ml_inference_timeout: Duration::from_millis(150),
458
0
                ml_cache_cleanup_interval: Duration::from_secs(5400), // 1.5 hours
459
0
                ml_drift_check_interval: Duration::from_secs(450), // 7.5 minutes
460
0
461
0
                // Risk
462
0
                risk_var_lookback_days: 252,
463
0
                risk_var_confidence: 0.95,
464
0
                risk_max_drawdown_warning_pct: 17,
465
0
            },
466
0
            Environment::Production => Self {
467
0
                // Retry
468
0
                retry_initial_delay: Duration::from_millis(100),
469
0
                retry_max_delay: Duration::from_secs(30),
470
0
                retry_max_attempts: 3,
471
0
                retry_backoff_multiplier: 1.5,
472
0
473
0
                // Safety
474
0
                safety_check_timeout: Duration::from_millis(5),
475
0
                safety_auto_recovery_delay: Duration::from_secs(1800), // 30 minutes
476
0
                safety_loss_check_interval: Duration::from_secs(5),
477
0
                safety_position_check_interval: Duration::from_secs(2),
478
0
479
0
                // ML
480
0
                ml_max_batch_size: 8192,
481
0
                ml_inference_timeout: Duration::from_millis(100),
482
0
                ml_cache_cleanup_interval: Duration::from_secs(3600), // 1 hour
483
0
                ml_drift_check_interval: Duration::from_secs(300), // 5 minutes
484
0
485
0
                // Risk
486
0
                risk_var_lookback_days: 252,
487
0
                risk_var_confidence: 0.95,
488
0
                risk_max_drawdown_warning_pct: 15,
489
0
            },
490
        }
491
0
    }
492
493
    /// Loads from environment variables with fallback to defaults.
494
    ///
495
    /// # Errors
496
    /// Returns error if the operation fails
497
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
498
0
        let defaults = Self::with_defaults(env);
499
500
        Ok(Self {
501
            // Retry
502
0
            retry_initial_delay: parse_env_duration_ms("RETRY_INITIAL_DELAY_MS", defaults.retry_initial_delay)?,
503
0
            retry_max_delay: parse_env_duration_secs("RETRY_MAX_DELAY_SECS", defaults.retry_max_delay)?,
504
0
            retry_max_attempts: parse_env_u32("RETRY_MAX_ATTEMPTS", defaults.retry_max_attempts)?,
505
0
            retry_backoff_multiplier: parse_env_f32("RETRY_BACKOFF_MULTIPLIER", defaults.retry_backoff_multiplier)?,
506
507
            // Safety
508
0
            safety_check_timeout: parse_env_duration_ms("SAFETY_CHECK_TIMEOUT_MS", defaults.safety_check_timeout)?,
509
0
            safety_auto_recovery_delay: parse_env_duration_secs("SAFETY_AUTO_RECOVERY_DELAY_SECS", defaults.safety_auto_recovery_delay)?,
510
0
            safety_loss_check_interval: parse_env_duration_secs("SAFETY_LOSS_CHECK_INTERVAL_SECS", defaults.safety_loss_check_interval)?,
511
0
            safety_position_check_interval: parse_env_duration_secs("SAFETY_POSITION_CHECK_INTERVAL_SECS", defaults.safety_position_check_interval)?,
512
513
            // ML
514
0
            ml_max_batch_size: parse_env_usize("ML_MAX_BATCH_SIZE", defaults.ml_max_batch_size)?,
515
0
            ml_inference_timeout: parse_env_duration_ms("ML_INFERENCE_TIMEOUT_MS", defaults.ml_inference_timeout)?,
516
0
            ml_cache_cleanup_interval: parse_env_duration_secs("ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS", defaults.ml_cache_cleanup_interval)?,
517
0
            ml_drift_check_interval: parse_env_duration_secs("ML_DRIFT_CHECK_INTERVAL_SECS", defaults.ml_drift_check_interval)?,
518
519
            // Risk
520
0
            risk_var_lookback_days: parse_env_usize("RISK_VAR_LOOKBACK_DAYS", defaults.risk_var_lookback_days)?,
521
0
            risk_var_confidence: parse_env_f64("RISK_VAR_CONFIDENCE", defaults.risk_var_confidence)?,
522
0
            risk_max_drawdown_warning_pct: parse_env_u8("RISK_MAX_DRAWDOWN_WARNING_PCT", defaults.risk_max_drawdown_warning_pct)?,
523
        })
524
0
    }
525
526
    /// Validates the configuration.
527
    ///
528
    /// # Errors
529
    /// Returns error if the operation fails
530
0
    pub fn validate(&self) -> ConfigResult<()> {
531
0
        if self.retry_max_attempts == 0 {
532
0
            return Err(ConfigError::Invalid("Retry max attempts must be positive".into()));
533
0
        }
534
0
        if self.retry_backoff_multiplier <= 1.0 {
535
0
            return Err(ConfigError::Invalid("Backoff multiplier must be > 1.0".into()));
536
0
        }
537
0
        if self.ml_max_batch_size == 0 {
538
0
            return Err(ConfigError::Invalid("ML max batch size must be positive".into()));
539
0
        }
540
0
        if self.risk_var_confidence < 0.0_f64 || self.risk_var_confidence > 1.0_f64 {
541
0
            return Err(ConfigError::Invalid("VaR confidence must be between 0.0 and 1.0".into()));
542
0
        }
543
0
        if self.risk_var_lookback_days == 0 {
544
0
            return Err(ConfigError::Invalid("VaR lookback days must be positive".into()));
545
0
        }
546
0
        Ok(())
547
0
    }
548
}
549
550
/// Complete runtime configuration for the Foxhunt trading system.
551
///
552
/// Aggregates all runtime configuration categories with environment-aware defaults
553
/// and environment variable overrides.
554
#[derive(Debug, Clone, Serialize, Deserialize)]
555
#[allow(clippy::module_name_repetitions)]
556
pub struct RuntimeConfig {
557
    /// Detected or specified environment
558
    pub environment: Environment,
559
    /// Database configuration
560
    pub database: DatabaseRuntimeConfig,
561
    /// Cache configuration
562
    pub cache: CacheRuntimeConfig,
563
    /// Timeout configuration
564
    pub timeouts: TimeoutConfig,
565
    /// Limits and operational parameters
566
    pub limits: LimitsConfig,
567
}
568
569
impl RuntimeConfig {
570
    /// Creates runtime configuration by auto-detecting environment and loading from env vars.
571
    ///
572
    /// # Errors
573
    ///
574
    /// Returns ConfigError if environment variables contain invalid values or
575
    /// if validation fails.
576
0
    pub fn from_env() -> ConfigResult<Self> {
577
0
        let environment = Environment::detect();
578
0
        Self::from_env_with_environment(environment)
579
0
    }
580
581
    /// Creates runtime configuration with specified environment and loads from env vars.
582
    ///
583
    /// # Arguments
584
    ///
585
    /// * `environment` - The deployment environment to use for defaults
586
    ///
587
    /// # Errors
588
    ///
589
    /// Returns ConfigError if environment variables contain invalid values or
590
    /// if validation fails.
591
0
    pub fn from_env_with_environment(environment: Environment) -> ConfigResult<Self> {
592
0
        let config = Self {
593
0
            environment,
594
0
            database: DatabaseRuntimeConfig::from_env(environment)?,
595
0
            cache: CacheRuntimeConfig::from_env(environment)?,
596
0
            timeouts: TimeoutConfig::from_env(environment)?,
597
0
            limits: LimitsConfig::from_env(environment)?,
598
        };
599
600
0
        config.validate()?;
601
0
        Ok(config)
602
0
    }
603
604
    /// Creates runtime configuration with environment-specific defaults.
605
    ///
606
    /// Does not read from environment variables. Useful for testing or
607
    /// when you want pure default values.
608
    ///
609
    /// # Arguments
610
    ///
611
    /// * `environment` - The deployment environment to use for defaults
612
0
    pub const fn with_defaults(environment: Environment) -> Self {
613
0
        Self {
614
0
            environment,
615
0
            database: DatabaseRuntimeConfig::with_defaults(environment),
616
0
            cache: CacheRuntimeConfig::with_defaults(environment),
617
0
            timeouts: TimeoutConfig::with_defaults(environment),
618
0
            limits: LimitsConfig::with_defaults(environment),
619
0
        }
620
0
    }
621
622
    /// Validates the entire runtime configuration.
623
    ///
624
    /// # Errors
625
    ///
626
    /// Returns ConfigError if any configuration values are invalid.
627
0
    pub fn validate(&self) -> ConfigResult<()> {
628
0
        self.database.validate()?;
629
0
        self.cache.validate()?;
630
0
        self.timeouts.validate()?;
631
0
        self.limits.validate()?;
632
0
        Ok(())
633
0
    }
634
}
635
636
// Helper functions for parsing environment variables
637
638
0
fn parse_env_duration_ms(key: &str, default: Duration) -> ConfigResult<Duration> {
639
0
    match std::env::var(key) {
640
0
        Ok(val) => {
641
0
            let ms = val.parse::<u64>()
642
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
643
0
            Ok(Duration::from_millis(ms))
644
        }
645
0
        Err(_) => Ok(default),
646
    }
647
0
}
648
649
0
fn parse_env_duration_secs(key: &str, default: Duration) -> ConfigResult<Duration> {
650
0
    match std::env::var(key) {
651
0
        Ok(val) => {
652
0
            let secs = val.parse::<u64>()
653
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
654
0
            Ok(Duration::from_secs(secs))
655
        }
656
0
        Err(_) => Ok(default),
657
    }
658
0
}
659
660
0
fn parse_env_u32(key: &str, default: u32) -> ConfigResult<u32> {
661
0
    match std::env::var(key) {
662
0
        Ok(val) => val.parse::<u32>()
663
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u32 for {}: {}", key, e))),
664
0
        Err(_) => Ok(default),
665
    }
666
0
}
667
668
0
fn parse_env_u8(key: &str, default: u8) -> ConfigResult<u8> {
669
0
    match std::env::var(key) {
670
0
        Ok(val) => val.parse::<u8>()
671
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u8 for {}: {}", key, e))),
672
0
        Err(_) => Ok(default),
673
    }
674
0
}
675
676
0
fn parse_env_usize(key: &str, default: usize) -> ConfigResult<usize> {
677
0
    match std::env::var(key) {
678
0
        Ok(val) => val.parse::<usize>()
679
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid usize for {}: {}", key, e))),
680
0
        Err(_) => Ok(default),
681
    }
682
0
}
683
684
0
fn parse_env_f32(key: &str, default: f32) -> ConfigResult<f32> {
685
0
    match std::env::var(key) {
686
0
        Ok(val) => val.parse::<f32>()
687
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f32 for {}: {}", key, e))),
688
0
        Err(_) => Ok(default),
689
    }
690
0
}
691
692
0
fn parse_env_f64(key: &str, default: f64) -> ConfigResult<f64> {
693
0
    match std::env::var(key) {
694
0
        Ok(val) => val.parse::<f64>()
695
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f64 for {}: {}", key, e))),
696
0
        Err(_) => Ok(default),
697
    }
698
0
}
699
700
#[cfg(test)]
701
mod tests {
702
    use super::*;
703
704
    #[test]
705
    fn test_environment_detection() {
706
        // Should default to Development
707
        let env = Environment::detect();
708
        assert!(matches!(env, Environment::Development | Environment::Production | Environment::Staging));
709
    }
710
711
    #[test]
712
    fn test_environment_is_production() {
713
        assert!(Environment::Production.is_production());
714
        assert!(!Environment::Development.is_production());
715
        assert!(!Environment::Staging.is_production());
716
    }
717
718
    #[test]
719
    fn test_environment_is_development() {
720
        assert!(Environment::Development.is_development());
721
        assert!(!Environment::Production.is_development());
722
        assert!(!Environment::Staging.is_development());
723
    }
724
725
    #[test]
726
    fn test_runtime_config_with_defaults() {
727
        let config = RuntimeConfig::with_defaults(Environment::Production);
728
        assert_eq!(config.environment, Environment::Production);
729
        assert!(config.database.query_timeout.as_millis() > 0);
730
        assert!(config.cache.position_ttl.as_secs() > 0);
731
    }
732
733
    #[test]
734
    fn test_runtime_config_validation() {
735
        let config = RuntimeConfig::with_defaults(Environment::Development);
736
        assert!(config.validate().is_ok());
737
    }
738
739
    #[test]
740
    fn test_database_config_defaults() {
741
        let dev_config = DatabaseRuntimeConfig::with_defaults(Environment::Development);
742
        let prod_config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
743
744
        // Production should have tighter timeouts
745
        assert!(prod_config.query_timeout < dev_config.query_timeout);
746
        assert!(prod_config.connection_timeout < dev_config.connection_timeout);
747
    }
748
749
    #[test]
750
    fn test_cache_config_defaults() {
751
        let dev_config = CacheRuntimeConfig::with_defaults(Environment::Development);
752
        let prod_config = CacheRuntimeConfig::with_defaults(Environment::Production);
753
754
        // Production should have shorter TTLs for HFT
755
        assert!(prod_config.position_ttl < dev_config.position_ttl);
756
        assert!(prod_config.var_ttl < dev_config.var_ttl);
757
    }
758
759
    #[test]
760
    fn test_timeout_config_defaults() {
761
        let dev_config = TimeoutConfig::with_defaults(Environment::Development);
762
        let prod_config = TimeoutConfig::with_defaults(Environment::Production);
763
764
        // Production should have tighter timeouts
765
        assert!(prod_config.grpc_request_timeout < dev_config.grpc_request_timeout);
766
        assert!(prod_config.grpc_connect_timeout < dev_config.grpc_connect_timeout);
767
    }
768
769
    #[test]
770
    fn test_limits_config_defaults() {
771
        let dev_config = LimitsConfig::with_defaults(Environment::Development);
772
        let prod_config = LimitsConfig::with_defaults(Environment::Production);
773
774
        // Production should have more aggressive settings
775
        assert!(prod_config.safety_check_timeout < dev_config.safety_check_timeout);
776
        assert!(prod_config.ml_inference_timeout < dev_config.ml_inference_timeout);
777
    }
778
779
    #[test]
780
    fn test_database_config_validation() {
781
        let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
782
        assert!(config.validate().is_ok());
783
784
        config.query_timeout = Duration::from_millis(0);
785
        assert!(config.validate().is_err());
786
787
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
788
        config.pool_size = 0;
789
        assert!(config.validate().is_err());
790
791
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
792
        config.pool_size = 200;
793
        config.max_pool_size = 100;
794
        assert!(config.validate().is_err());
795
    }
796
797
    #[test]
798
    fn test_cache_config_validation() {
799
        let mut config = CacheRuntimeConfig::with_defaults(Environment::Production);
800
        assert!(config.validate().is_ok());
801
802
        config.position_ttl = Duration::from_secs(0);
803
        assert!(config.validate().is_err());
804
    }
805
806
    #[test]
807
    fn test_limits_config_validation() {
808
        let mut config = LimitsConfig::with_defaults(Environment::Production);
809
        assert!(config.validate().is_ok());
810
811
        config.retry_max_attempts = 0;
812
        assert!(config.validate().is_err());
813
814
        config = LimitsConfig::with_defaults(Environment::Production);
815
        config.retry_backoff_multiplier = 0.5;
816
        assert!(config.validate().is_err());
817
818
        config = LimitsConfig::with_defaults(Environment::Production);
819
        config.risk_var_confidence = 1.5;
820
        assert!(config.validate().is_err());
821
    }
822
823
    #[test]
824
    fn test_staging_environment_defaults() {
825
        let config = RuntimeConfig::with_defaults(Environment::Staging);
826
827
        // Staging should be between dev and prod
828
        let dev_config = RuntimeConfig::with_defaults(Environment::Development);
829
        let prod_config = RuntimeConfig::with_defaults(Environment::Production);
830
831
        assert!(config.database.query_timeout > prod_config.database.query_timeout);
832
        assert!(config.database.query_timeout < dev_config.database.query_timeout);
833
    }
834
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html deleted file mode 100644 index f92be15eb..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/schemas.rs
Line
Count
Source
1
//! Configuration schemas and cloud storage configurations.
2
//!
3
//! This module defines configuration schemas for various cloud storage backends
4
//! and configuration versioning. Primarily focused on S3-compatible storage
5
//! for model artifacts and configuration management in the Foxhunt trading system.
6
7
use chrono::{DateTime, Utc};
8
use serde::{Deserialize, Serialize};
9
use std::collections::HashMap;
10
use std::time::Duration;
11
use uuid::Uuid;
12
13
/// Configuration schema metadata for versioning and tracking.
14
///
15
/// Provides versioning and audit trail information for configuration schemas.
16
///
17
/// Used to track configuration changes over time and maintain compatibility
18
/// across different versions of the trading system.
19
#[derive(Debug, Clone, Serialize, Deserialize)]
20
pub struct ConfigSchema {
21
    /// Unique identifier for this configuration schema
22
    pub id: Uuid,
23
    /// Semantic version string (e.g., "1.2.3")
24
    pub version: String,
25
    /// Timestamp when this schema was created
26
    pub created_at: DateTime<Utc>,
27
    /// Timestamp when this schema was last updated
28
    pub updated_at: DateTime<Utc>,
29
}
30
31
/// Amazon S3 and S3-compatible storage configuration.
32
///
33
/// Configures access to S3 or S3-compatible storage services for storing
34
///
35
/// ML model artifacts, configuration backups, and other binary data.
36
///
37
/// Supports various authentication methods and connection options.
38
#[derive(Debug, Clone, Serialize, Deserialize)]
39
pub struct S3Config {
40
    /// S3 bucket name for storing model artifacts and data
41
    pub bucket_name: String,
42
    /// AWS region or S3-compatible service region
43
    pub region: String,
44
    /// AWS access key ID (optional, can use IAM roles or environment variables)
45
    pub access_key_id: Option<String>,
46
    /// AWS secret access key (optional, can use IAM roles or environment variables)
47
    pub secret_access_key: Option<String>,
48
    /// AWS session token for temporary credentials (optional)
49
    pub session_token: Option<String>,
50
    /// Custom S3-compatible endpoint URL (e.g., MinIO, DigitalOcean Spaces)
51
    pub endpoint_url: Option<String>,
52
    /// Force path-style URLs instead of virtual-hosted-style URLs
53
    pub force_path_style: bool,
54
    /// Request timeout duration for S3 operations
55
    pub timeout: Duration,
56
    /// Maximum number of retry attempts for failed requests
57
    pub max_retry_attempts: u32,
58
    /// Enable SSL/TLS for S3 connections
59
    pub use_ssl: bool,
60
}
61
62
impl S3Config {
63
    /// Validates the S3 configuration for correctness.
64
    ///
65
    /// Performs validation checks on the S3 configuration to ensure all
66
    /// required fields are present and have valid values before attempting
67
    /// to establish connections to S3 services.
68
    ///
69
    /// # Errors
70
    ///
71
    /// Returns an error string if the configuration is invalid:
72
    /// - Empty bucket name
73
    ///
74
    /// - Empty region
75
    /// - Invalid endpoint URL format
76
0
    pub fn validate(&self) -> Result<(), String> {
77
0
        if self.bucket_name.is_empty() {
78
0
            return Err("S3 bucket name cannot be empty".to_owned());
79
0
        }
80
0
        if self.region.is_empty() {
81
0
            return Err("S3 region cannot be empty".to_owned());
82
0
        }
83
0
        Ok(())
84
0
    }
85
86
    /// Create S3Config for testing purposes (generic testing)
87
    #[cfg(test)]
88
    pub fn default_for_testing(bucket: &str) -> Self {
89
        Self {
90
            bucket_name: bucket.to_owned(),
91
            region: "us-east-1".to_owned(),
92
            access_key_id: None,
93
            secret_access_key: None,
94
            session_token: None,
95
            endpoint_url: None,
96
            force_path_style: false,
97
            timeout: Duration::from_secs(30),
98
            max_retry_attempts: 3,
99
            use_ssl: true,
100
        }
101
    }
102
103
    /// Create S3Config for MinIO testing (real S3-compatible testing)
104
    ///
105
    /// Available in test mode or when test-utils feature is enabled
106
0
    pub fn for_minio_testing(bucket: &str) -> Self {
107
0
        Self {
108
0
            bucket_name: bucket.to_owned(),
109
0
            region: "us-east-1".to_owned(),
110
0
            access_key_id: Some("foxhunt_test".to_owned()),
111
0
            secret_access_key: Some("foxhunt_test_password".to_owned()),
112
0
            session_token: None,
113
0
            endpoint_url: Some("http://localhost:9000".to_owned()),
114
0
            force_path_style: true, // MinIO requires path-style
115
0
            timeout: Duration::from_secs(30),
116
0
            max_retry_attempts: 3,
117
0
            use_ssl: false, // Local MinIO uses HTTP
118
0
        }
119
0
    }
120
}
121
122
/// Schema-level asset classification configuration for sector and type categorization.
123
///
124
/// Provides configuration-driven asset classification that replaces hardcoded
125
/// symbol-based classification logic. Supports flexible categorization rules
126
/// based on instrument properties rather than specific symbol names.
127
///
128
/// **Note**: This is a simpler schema-level config. For full asset classification
129
/// with volatility profiles and pattern rules, use `structures::AssetClassificationConfig`.
130
#[derive(Debug, Clone, Serialize, Deserialize)]
131
pub struct AssetClassificationSchema {
132
    /// Classification rules based on asset type patterns
133
    pub asset_type_rules: HashMap<String, String>,
134
    /// Default classifications for different asset categories
135
    pub default_sectors: HashMap<String, String>,
136
    /// Regex patterns for currency pair detection
137
    pub currency_patterns: Vec<String>,
138
    /// Regex patterns for cryptocurrency detection
139
    pub crypto_patterns: Vec<String>,
140
}
141
142
impl AssetClassificationSchema {
143
    /// Creates a new asset classification schema with default rules.
144
0
    pub fn new() -> Self {
145
0
        let mut asset_type_rules = HashMap::new();
146
0
        asset_type_rules.insert("EQUITY".to_owned(), "Equity".to_owned());
147
0
        asset_type_rules.insert("FOREX".to_owned(), "Currencies".to_owned());
148
0
        asset_type_rules.insert("CRYPTO".to_owned(), "Cryptocurrency".to_owned());
149
0
        asset_type_rules.insert("COMMODITY".to_owned(), "Commodities".to_owned());
150
0
        asset_type_rules.insert("BOND".to_owned(), "Fixed Income".to_owned());
151
152
0
        let mut default_sectors = HashMap::new();
153
0
        default_sectors.insert("Equity".to_owned(), "Other".to_owned());
154
0
        default_sectors.insert("Currencies".to_owned(), "Currencies".to_owned());
155
0
        default_sectors.insert("Cryptocurrency".to_owned(), "Cryptocurrency".to_owned());
156
0
        default_sectors.insert("Commodities".to_owned(), "Commodities".to_owned());
157
0
        default_sectors.insert("Fixed Income".to_owned(), "Fixed Income".to_owned());
158
159
0
        Self {
160
0
            asset_type_rules,
161
0
            default_sectors,
162
0
            currency_patterns: vec![
163
0
                r"^[A-Z]{3}[A-Z]{3}$".to_owned(), // USDEUR format
164
0
                r".*USD.*".to_owned(),
165
0
                r".*EUR.*".to_owned(),
166
0
                r".*GBP.*".to_owned(),
167
0
                r".*JPY.*".to_owned(),
168
0
            ],
169
0
            crypto_patterns: vec![
170
0
                r".*BTC.*".to_owned(),
171
0
                r".*ETH.*".to_owned(),
172
0
                r".*CRYPTO.*".to_owned(),
173
0
            ],
174
0
        }
175
0
    }
176
177
    /// Classifies an instrument based on configuration rules.
178
0
    pub fn classify_sector(&self, instrument_id: &str, asset_type: Option<&str>) -> String {
179
        // First try to classify based on asset type if provided
180
0
        if let Some(asset_type) = asset_type {
181
0
            if let Some(sector) = self.asset_type_rules.get(asset_type) {
182
0
                return sector.clone();
183
0
            }
184
0
        }
185
186
        // Check for currency patterns
187
0
        for pattern in &self.currency_patterns {
188
0
            if let Ok(regex) = regex::Regex::new(pattern) {
189
0
                if regex.is_match(instrument_id) {
190
0
                    return "Currencies".to_owned();
191
0
                }
192
0
            }
193
        }
194
195
        // Check for crypto patterns
196
0
        for pattern in &self.crypto_patterns {
197
0
            if let Ok(regex) = regex::Regex::new(pattern) {
198
0
                if regex.is_match(instrument_id) {
199
0
                    return "Cryptocurrency".to_owned();
200
0
                }
201
0
            }
202
        }
203
204
        // Default classification
205
0
        "Other".to_owned()
206
0
    }
207
}
208
209
impl Default for AssetClassificationSchema {
210
0
    fn default() -> Self {
211
0
        Self::new()
212
0
    }
213
}
214
215
impl Default for S3Config {
216
0
    fn default() -> Self {
217
0
        Self {
218
0
            bucket_name: "foxhunt-models".to_owned(),
219
0
            region: "us-east-1".to_owned(),
220
0
            access_key_id: None,
221
0
            secret_access_key: None,
222
0
            session_token: None,
223
0
            endpoint_url: None,
224
0
            force_path_style: false,
225
0
            timeout: Duration::from_secs(30),
226
0
            max_retry_attempts: 3,
227
0
            use_ssl: true,
228
0
        }
229
0
    }
230
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html deleted file mode 100644 index 615db17dc..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs
Line
Count
Source
1
//! Model storage and metadata configuration structures.
2
//!
3
//! This module defines configuration structures for managing ML model metadata,
4
//! training metrics, and architectural information. Used for model versioning,
5
//! performance tracking, and deployment management in the Foxhunt trading system.
6
7
use chrono::{DateTime, Utc};
8
use serde::{Deserialize, Serialize};
9
use std::path::PathBuf;
10
use uuid::Uuid;
11
12
/// Comprehensive metadata for ML model storage and tracking.
13
///
14
/// Contains all information necessary for model identification, versioning,
15
/// and performance tracking. Used for model lifecycle management and
16
/// deployment coordination across the trading system.
17
#[derive(Debug, Clone, Serialize, Deserialize)]
18
pub struct ModelMetadata {
19
    /// Unique identifier for this model instance
20
    pub id: Uuid,
21
    /// Human-readable model name (e.g., "mamba2-price-prediction")
22
    pub name: String,
23
    /// Semantic version string (e.g., "1.2.3")
24
    pub version: String,
25
    /// Timestamp when this model was created/trained
26
    pub created_at: DateTime<Utc>,
27
    /// Timestamp when this model metadata was last updated
28
    pub updated_at: DateTime<Utc>,
29
    /// Training performance metrics for model evaluation
30
    pub training_metrics: TrainingMetrics,
31
    /// Model architecture and hyperparameter configuration
32
    pub architecture: ModelArchitecture,
33
}
34
35
/// Training performance metrics for model evaluation.
36
///
37
/// Captures key performance indicators from model training to enable
38
/// comparison between different model versions and architectures.
39
///
40
/// Essential for model selection and performance monitoring.
41
#[derive(Debug, Clone, Serialize, Deserialize)]
42
pub struct TrainingMetrics {
43
    /// Final training accuracy (0.0 to 1.0)
44
    pub accuracy: f64,
45
    /// Final training loss value
46
    pub loss: f64,
47
    /// Final validation accuracy (0.0 to 1.0)
48
    pub validation_accuracy: f64,
49
    /// Final validation loss value
50
    pub validation_loss: f64,
51
    /// Number of training epochs completed
52
    pub epochs: u32,
53
    /// Total training time in seconds
54
    pub training_time_seconds: f64,
55
}
56
57
/// Model architecture and hyperparameter specification.
58
///
59
/// Defines the structural configuration of ML models including layer
60
/// dimensions, activation functions, and optimization parameters.
61
///
62
/// Used for model reconstruction and hyperparameter tracking.
63
#[derive(Debug, Clone, Serialize, Deserialize)]
64
pub struct ModelArchitecture {
65
    /// Model type identifier (e.g., "mamba2", "transformer", "dqn")
66
    pub model_type: String,
67
    /// Input feature dimension size
68
    pub input_dim: usize,
69
    /// Output prediction dimension size
70
    pub output_dim: usize,
71
    /// Hidden layer sizes in order from input to output
72
    pub hidden_layers: Vec<usize>,
73
    /// Activation function name (e.g., "relu", "gelu", "swish")
74
    pub activation: String,
75
    /// Optimizer type (e.g., "adam", "sgd", "adamw")
76
    pub optimizer: String,
77
    /// Learning rate used during training
78
    pub learning_rate: f64,
79
}
80
81
/// Storage configuration for model artifacts
82
#[derive(Debug, Clone, Serialize, Deserialize)]
83
pub struct StorageConfig {
84
    /// Storage type (e.g., "local", "s3")
85
    pub storage_type: String,
86
    /// Local base path for file storage (required for "local" storage type)
87
    pub local_base_path: Option<PathBuf>,
88
    /// Enable compression for stored models
89
    pub enable_compression: bool,
90
}
91
92
impl Default for StorageConfig {
93
0
    fn default() -> Self {
94
0
        Self {
95
0
            storage_type: "local".to_owned(),
96
0
            local_base_path: Some(PathBuf::from("/tmp/foxhunt/models")),
97
0
            enable_compression: false,
98
0
        }
99
0
    }
100
}
101
102
impl StorageConfig {
103
    /// Create StorageConfig from environment variables
104
    ///
105
    /// # Errors
106
    /// Returns error if the operation fails
107
0
    pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
108
0
        let storage_type = std::env::var("STORAGE_TYPE").unwrap_or_else(|_| "local".to_owned());
109
0
        let local_base_path = std::env::var("STORAGE_LOCAL_PATH")
110
0
            .ok()
111
0
            .map(PathBuf::from)
112
0
            .or_else(|| Some(PathBuf::from("/tmp/foxhunt/models")));
113
0
        let enable_compression = std::env::var("STORAGE_ENABLE_COMPRESSION")
114
0
            .ok()
115
0
            .and_then(|v| v.parse().ok())
116
0
            .unwrap_or(false);
117
118
0
        Ok(Self {
119
0
            storage_type,
120
0
            local_base_path,
121
0
            enable_compression,
122
0
        })
123
0
    }
124
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html deleted file mode 100644 index 457e1b671..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/structures.rs
Line
Count
Source
1
//! Configuration structures
2
3
use rust_decimal::Decimal;
4
use serde::{Deserialize, Serialize};
5
use std::collections::HashMap;
6
7
#[derive(Debug, Clone, Serialize, Deserialize)]
8
pub struct RiskConfig {
9
    /// Maximum single position size in base currency
10
    pub max_position_size: Decimal,
11
    /// Maximum total portfolio exposure in base currency
12
    pub max_portfolio_exposure: Decimal,
13
    /// Maximum concentration percentage for a single position (0.0-1.0)
14
    pub max_concentration_pct: Decimal,
15
    /// Maximum daily loss threshold in base currency
16
    pub max_daily_loss: Decimal,
17
    /// Maximum drawdown percentage allowed (0.0-1.0)
18
    pub max_drawdown_pct: Decimal,
19
    /// Stop loss threshold in base currency
20
    pub stop_loss_threshold: Decimal,
21
    /// VaR confidence level (e.g., 0.95 for 95%)
22
    pub var_confidence_level: f64,
23
    /// VaR time horizon in days
24
    pub var_time_horizon: u32,
25
    /// 1-day VaR limit in base currency
26
    pub var_limit_1d: Decimal,
27
    /// 10-day VaR limit in base currency
28
    pub var_limit_10d: Decimal,
29
    /// Maximum single order size in base currency
30
    pub max_order_size: Decimal,
31
    /// Maximum orders per second (rate limiting)
32
    pub max_orders_per_second: u64,
33
    /// Maximum notional value per hour in base currency
34
    pub max_notional_per_hour: Decimal,
35
    /// Kelly criterion fraction limit (0.0-1.0)
36
    pub kelly_fraction_limit: f64,
37
    /// Maximum Kelly criterion position size (0.0-1.0)
38
    pub max_kelly_position_size: f64,
39
    /// Emergency stop threshold as fraction of capital (0.0-1.0)
40
    pub emergency_stop_threshold: f64,
41
    /// VaR configuration
42
    pub var_config: VarConfig,
43
    /// Circuit breaker configuration
44
    pub circuit_breaker: CircuitBreakerConfig,
45
    /// Position limits configuration
46
    pub position_limits: PositionLimitsConfig,
47
    /// Asset classification configuration
48
    pub asset_classification: crate::schemas::AssetClassificationSchema,
49
}
50
51
impl Default for RiskConfig {
52
0
    fn default() -> Self {
53
0
        Self {
54
0
            // Position and exposure limits
55
0
            max_position_size: Decimal::new(1_000_000, 0), // $1M max single position
56
0
            max_portfolio_exposure: Decimal::new(10_000_000, 0), // $10M total portfolio exposure
57
0
            max_concentration_pct: Decimal::new(25, 2), // 25% max concentration
58
0
            
59
0
            // Loss and drawdown limits
60
0
            max_daily_loss: Decimal::new(100_000, 0), // $100K max daily loss
61
0
            max_drawdown_pct: Decimal::new(15, 2), // 15% max drawdown
62
0
            stop_loss_threshold: Decimal::new(50_000, 0), // $50K stop loss threshold
63
0
            
64
0
            // VaR configuration
65
0
            var_confidence_level: 0.95, // 95% confidence
66
0
            var_time_horizon: 1, // 1-day horizon
67
0
            var_limit_1d: Decimal::new(50_000, 0), // $50K 1-day VaR limit
68
0
            var_limit_10d: Decimal::new(150_000, 0), // $150K 10-day VaR limit
69
0
            
70
0
            // Order limits and rate limiting
71
0
            max_order_size: Decimal::new(100_000, 0), // $100K max order size
72
0
            max_orders_per_second: 100, // 100 orders/sec
73
0
            max_notional_per_hour: Decimal::new(10_000_000, 0), // $10M hourly notional
74
0
            
75
0
            // Kelly criterion parameters
76
0
            kelly_fraction_limit: 0.25, // 25% Kelly fraction limit
77
0
            max_kelly_position_size: 0.20, // 20% max Kelly position
78
0
            
79
0
            // Emergency stop
80
0
            emergency_stop_threshold: 0.10, // 10% loss triggers emergency stop
81
0
            
82
0
            // Nested configurations
83
0
            var_config: VarConfig::default(),
84
0
            circuit_breaker: CircuitBreakerConfig::default(),
85
0
            position_limits: PositionLimitsConfig::default(),
86
0
            asset_classification: crate::schemas::AssetClassificationSchema::default(),
87
0
        }
88
0
    }
89
}
90
91
#[derive(Debug, Clone, Serialize, Deserialize)]
92
pub struct VarConfig {
93
    /// VaR confidence level (0.0-1.0)
94
    pub confidence_level: f64,
95
    /// Time horizon in days
96
    pub time_horizon_days: u32,
97
    /// Historical lookback period in days
98
    pub lookback_period_days: u32,
99
    /// Calculation method (e.g., "historical", "monte_carlo")
100
    pub calculation_method: String,
101
    /// Maximum VaR limit
102
    pub max_var_limit: f64,
103
}
104
105
impl Default for VarConfig {
106
0
    fn default() -> Self {
107
0
        Self {
108
0
            confidence_level: 0.95,
109
0
            time_horizon_days: 1,
110
0
            lookback_period_days: 252,
111
0
            calculation_method: "historical".to_owned(),
112
0
            max_var_limit: 100_000.0,
113
0
        }
114
0
    }
115
}
116
117
#[derive(Debug, Clone, Serialize, Deserialize)]
118
pub struct KellyConfig {
119
    pub kelly_fraction: f64,
120
    pub max_kelly_leverage: f64,
121
    pub min_kelly_leverage: f64,
122
    pub confidence_threshold: f64,
123
    pub lookback_periods: usize,
124
    pub default_position_fraction: f64,
125
    pub enabled: bool,
126
    pub fractional_kelly: f64,
127
    pub min_kelly_fraction: f64,
128
    pub max_kelly_fraction: f64,
129
}
130
131
impl Default for KellyConfig {
132
0
    fn default() -> Self {
133
0
        Self {
134
0
            kelly_fraction: 0.25,
135
0
            max_kelly_leverage: 2.0,
136
0
            min_kelly_leverage: 0.1,
137
0
            confidence_threshold: 0.95,
138
0
            lookback_periods: 252,
139
0
            default_position_fraction: 0.02,
140
0
            enabled: true,
141
0
            fractional_kelly: 0.5,
142
0
            min_kelly_fraction: 0.01,
143
0
            max_kelly_fraction: 0.5,
144
0
        }
145
0
    }
146
}
147
148
#[derive(Debug, Clone, Serialize, Deserialize)]
149
pub struct CircuitBreakerConfig {
150
    /// Enable circuit breaker
151
    pub enabled: bool,
152
    /// Price movement threshold to trigger halt (0.0-1.0)
153
    pub price_move_threshold: f64,
154
    /// Duration to halt trading in seconds
155
    pub halt_duration_seconds: u64,
156
}
157
158
impl Default for CircuitBreakerConfig {
159
0
    fn default() -> Self {
160
0
        Self {
161
0
            enabled: true,
162
0
            price_move_threshold: 0.05, // 5% price move
163
0
            halt_duration_seconds: 300, // 5 minutes
164
0
        }
165
0
    }
166
}
167
168
#[derive(Debug, Clone, Serialize, Deserialize)]
169
pub struct PositionLimitsConfig {
170
    /// Global position limit
171
    pub global_limit: f64,
172
    /// Maximum leverage allowed
173
    pub max_leverage: f64,
174
    /// Maximum VaR limit
175
    pub max_var_limit: f64,
176
}
177
178
impl Default for PositionLimitsConfig {
179
0
    fn default() -> Self {
180
0
        Self {
181
0
            global_limit: 10_000_000.0,
182
0
            max_leverage: 3.0,
183
0
            max_var_limit: 100_000.0,
184
0
        }
185
0
    }
186
}
187
188
/// Broker configuration for order routing and execution
189
#[derive(Debug, Clone, Serialize, Deserialize)]
190
pub struct BrokerConfig {
191
    /// Broker routing rules based on symbol patterns and sizes
192
    pub routing_rules: Vec<BrokerRoutingRule>,
193
    /// Default broker when no rules match
194
    pub default_broker: String,
195
    /// Commission rates by broker
196
    pub commission_rates: HashMap<String, CommissionConfig>,
197
}
198
199
/// Rule for routing orders to specific brokers
200
#[derive(Debug, Clone, Serialize, Deserialize)]
201
pub struct BrokerRoutingRule {
202
    /// Priority (higher numbers take precedence)
203
    pub priority: u32,
204
    /// Symbol pattern (regex)
205
    pub symbol_pattern: String,
206
    /// Minimum quantity for this rule
207
    pub min_quantity: Option<f64>,
208
    /// Maximum quantity for this rule
209
    pub max_quantity: Option<f64>,
210
    /// Target broker ID
211
    pub broker_id: String,
212
    /// Rule description for debugging
213
    pub description: String,
214
}
215
216
/// Commission configuration per broker
217
#[derive(Debug, Clone, Serialize, Deserialize)]
218
pub struct CommissionConfig {
219
    /// Commission rate (basis points, e.g., 0.00007 = 0.7 bps)
220
    pub rate_bps: f64,
221
    /// Minimum commission per trade
222
    pub min_commission: f64,
223
}
224
225
impl Default for BrokerConfig {
226
0
    fn default() -> Self {
227
0
        let mut commission_rates = HashMap::new();
228
229
0
        commission_rates.insert(
230
0
            "ICMARKETS".to_owned(),
231
0
            CommissionConfig {
232
0
                rate_bps: 0.00007, // 0.7 bps
233
0
                min_commission: 0.0,
234
0
            },
235
        );
236
237
0
        commission_rates.insert(
238
0
            "IBKR".to_owned(),
239
0
            CommissionConfig {
240
0
                rate_bps: 0.00005, // 0.5 bps
241
0
                min_commission: 1.0,
242
0
            },
243
        );
244
245
0
        let routing_rules = vec![
246
0
            BrokerRoutingRule {
247
0
                priority: 100,
248
0
                symbol_pattern: r"^(BTC|ETH).*".to_owned(),
249
0
                min_quantity: None,
250
0
                max_quantity: None,
251
0
                broker_id: "ICMARKETS".to_owned(),
252
0
                description: "Route all crypto symbols to ICMarkets".to_owned(),
253
0
            },
254
0
            BrokerRoutingRule {
255
0
                priority: 90,
256
0
                symbol_pattern: r".*USD$".to_owned(),
257
0
                min_quantity: None,
258
0
                max_quantity: Some(1_000_000.0_f64),
259
0
                broker_id: "ICMARKETS".to_owned(),
260
0
                description: "Route smaller USD pairs to ICMarkets".to_owned(),
261
0
            },
262
0
            BrokerRoutingRule {
263
0
                priority: 50,
264
0
                symbol_pattern: r".*".to_owned(), // Catch-all
265
0
                min_quantity: None,
266
0
                max_quantity: None,
267
0
                broker_id: "IBKR".to_owned(),
268
0
                description: "Default routing to IBKR".to_owned(),
269
0
            },
270
        ];
271
272
0
        Self {
273
0
            routing_rules,
274
0
            default_broker: "IBKR".to_owned(),
275
0
            commission_rates,
276
0
        }
277
0
    }
278
}
279
280
impl BrokerConfig {
281
    /// Select optimal broker based on symbol and quantity using routing rules
282
0
    pub fn select_broker(&self, symbol: &str, quantity: f64) -> String {
283
0
        let symbol_upper = symbol.to_uppercase();
284
285
        // Sort rules by priority (highest first)
286
0
        let mut applicable_rules: Vec<_> = self
287
0
            .routing_rules
288
0
            .iter()
289
0
            .filter(|rule| {
290
                // Check symbol pattern
291
0
                let symbol_matches = if let Ok(regex) = regex::Regex::new(&rule.symbol_pattern) {
292
0
                    regex.is_match(&symbol_upper)
293
                } else {
294
0
                    false
295
                };
296
297
                // Check quantity bounds
298
0
                let quantity_matches = {
299
0
                    let min_ok = rule.min_quantity.map_or(true, |min| quantity >= min);
300
0
                    let max_ok = rule.max_quantity.map_or(true, |max| quantity <= max);
301
0
                    min_ok && max_ok
302
                };
303
304
0
                symbol_matches && quantity_matches
305
0
            })
306
0
            .collect();
307
308
0
        applicable_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
309
310
0
        if let Some(rule) = applicable_rules.first() {
311
0
            rule.broker_id.clone()
312
        } else {
313
0
            self.default_broker.clone()
314
        }
315
0
    }
316
317
    /// Calculate commission for a given broker and notional value
318
0
    pub fn calculate_commission(&self, broker_id: &str, notional: f64) -> f64 {
319
0
        if let Some(config) = self.commission_rates.get(broker_id) {
320
0
            notional.mul_add(config.rate_bps, 0.0).max(config.min_commission)
321
        } else {
322
            // Default commission if broker not found
323
0
            notional.mul_add(0.0001, 0.0) // 1 bps
324
        }
325
0
    }
326
}
327
328
/// Asset classification for risk management and volatility profiling
329
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
330
pub enum AssetClass {
331
    /// Equity securities and stocks
332
    Equities,
333
    /// Bonds and fixed income securities
334
    FixedIncome,
335
    /// Physical and financial commodities
336
    Commodities,
337
    /// Foreign exchange and currencies
338
    Currencies,
339
    /// Alternative investments
340
    Alternatives,
341
    /// Derivative instruments
342
    Derivatives,
343
    /// Cash and cash equivalents
344
    Cash,
345
}
346
347
/// Volatility and risk profile for an asset class
348
#[derive(Debug, Clone, Serialize, Deserialize)]
349
pub struct VolatilityProfile {
350
    /// Annual volatility (0.0 to 1.0, e.g., 0.25 = 25%)
351
    pub annual_volatility: f64,
352
    /// Maximum position size as fraction of portfolio (0.0 to 1.0)
353
    pub max_position_fraction: f64,
354
    /// Volatility threshold for risk alerts (0.0 to 1.0)
355
    pub volatility_threshold: f64,
356
    /// Maximum daily loss threshold (0.0 to 1.0)
357
    pub daily_loss_threshold: f64,
358
}
359
360
/// Asset classification configuration with symbol mappings and volatility profiles
361
#[derive(Debug, Clone, Serialize, Deserialize)]
362
pub struct AssetClassificationConfig {
363
    /// Explicit symbol to asset class mappings
364
    pub symbol_mappings: HashMap<String, AssetClass>,
365
    /// Volatility profiles for each asset class
366
    pub volatility_profiles: HashMap<AssetClass, VolatilityProfile>,
367
    /// Pattern-based classification rules (regex patterns)
368
    pub pattern_rules: Vec<PatternRule>,
369
}
370
371
/// Pattern-based rule for asset classification
372
#[derive(Debug, Clone, Serialize, Deserialize)]
373
pub struct PatternRule {
374
    /// Regex pattern to match against symbol
375
    pub pattern: String,
376
    /// Asset class to assign if pattern matches
377
    pub asset_class: AssetClass,
378
    /// Priority (higher numbers take precedence)
379
    pub priority: u32,
380
}
381
382
/// Encryption configuration for secure model storage
383
#[derive(Debug, Clone, Serialize, Deserialize)]
384
pub struct EncryptionConfig {
385
    /// Enable/disable encryption for model storage
386
    pub enable_encryption: bool,
387
    /// Encryption algorithm (e.g., "AES-256-GCM")
388
    pub algorithm: String,
389
    /// Key rotation period in days
390
    pub key_rotation_days: u64,
391
    /// Vault path for encryption keys (optional, can use local keys)
392
    pub encryption_keys_vault_path: Option<String>,
393
    /// Local key file path for development/testing
394
    pub local_key_file: Option<String>,
395
}
396
397
impl Default for EncryptionConfig {
398
0
    fn default() -> Self {
399
0
        Self {
400
0
            enable_encryption: false,
401
0
            algorithm: "AES-256-GCM".to_owned(),
402
0
            key_rotation_days: 90,
403
0
            encryption_keys_vault_path: None,
404
0
            local_key_file: None,
405
0
        }
406
0
    }
407
}
408
409
impl Default for AssetClassificationConfig {
410
0
    fn default() -> Self {
411
0
        let mut symbol_mappings = HashMap::new();
412
413
        // Equity stocks
414
0
        for symbol in [
415
0
            "AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "JPM", "JNJ", "V",
416
0
        ] {
417
0
            symbol_mappings.insert(symbol.to_owned(), AssetClass::Equities);
418
0
        }
419
420
        // Major cryptocurrencies
421
0
        for symbol in ["BTC", "ETH", "BTCUSD", "ETHUSD", "BTCUSDT", "ETHUSDT"] {
422
0
            symbol_mappings.insert(symbol.to_owned(), AssetClass::Alternatives);
423
0
        }
424
425
0
        let mut volatility_profiles = HashMap::new();
426
427
0
        volatility_profiles.insert(
428
0
            AssetClass::Equities,
429
0
            VolatilityProfile {
430
0
                annual_volatility: 0.25,
431
0
                max_position_fraction: 0.20,
432
0
                volatility_threshold: 0.025,
433
0
                daily_loss_threshold: 0.03,
434
0
            },
435
        );
436
437
0
        volatility_profiles.insert(
438
0
            AssetClass::Alternatives,
439
0
            VolatilityProfile {
440
0
                annual_volatility: 0.80,
441
0
                max_position_fraction: 0.08,
442
0
                volatility_threshold: 0.15,
443
0
                daily_loss_threshold: 0.05,
444
0
            },
445
        );
446
447
0
        volatility_profiles.insert(
448
0
            AssetClass::Currencies,
449
0
            VolatilityProfile {
450
0
                annual_volatility: 0.15,
451
0
                max_position_fraction: 0.30,
452
0
                volatility_threshold: 0.02,
453
0
                daily_loss_threshold: 0.02,
454
0
            },
455
        );
456
457
0
        volatility_profiles.insert(
458
0
            AssetClass::Cash,
459
0
            VolatilityProfile {
460
0
                annual_volatility: 0.01,
461
0
                max_position_fraction: 1.00,
462
0
                volatility_threshold: 0.001,
463
0
                daily_loss_threshold: 0.001,
464
0
            },
465
        );
466
467
0
        volatility_profiles.insert(
468
0
            AssetClass::FixedIncome,
469
0
            VolatilityProfile {
470
0
                annual_volatility: 0.25,
471
0
                max_position_fraction: 0.15,
472
0
                volatility_threshold: 0.03,
473
0
                daily_loss_threshold: 0.025,
474
0
            },
475
        );
476
477
0
        volatility_profiles.insert(
478
0
            AssetClass::Derivatives,
479
0
            VolatilityProfile {
480
0
                annual_volatility: 0.40,
481
0
                max_position_fraction: 0.10,
482
0
                volatility_threshold: 0.05,
483
0
                daily_loss_threshold: 0.04,
484
0
            },
485
        );
486
487
0
        volatility_profiles.insert(
488
0
            AssetClass::Commodities,
489
0
            VolatilityProfile {
490
0
                annual_volatility: 0.30,
491
0
                max_position_fraction: 0.15,
492
0
                volatility_threshold: 0.04,
493
0
                daily_loss_threshold: 0.03,
494
0
            },
495
        );
496
497
0
        let pattern_rules = vec![
498
0
            PatternRule {
499
0
                pattern: r"^(BTC|ETH).*".to_owned(),
500
0
                asset_class: AssetClass::Alternatives,
501
0
                priority: 100,
502
0
            },
503
0
            PatternRule {
504
0
                pattern: r".*USD$".to_owned(),
505
0
                asset_class: AssetClass::Currencies,
506
0
                priority: 80,
507
0
            },
508
0
            PatternRule {
509
0
                pattern: r".*JPY$".to_owned(),
510
0
                asset_class: AssetClass::Currencies,
511
0
                priority: 90,
512
0
            },
513
0
            PatternRule {
514
0
                pattern: r"^[A-Z]{3,6}$".to_owned(), // 3-6 letter symbols (likely equities)
515
0
                asset_class: AssetClass::Equities,
516
0
                priority: 50,
517
0
            },
518
        ];
519
520
0
        Self {
521
0
            symbol_mappings,
522
0
            volatility_profiles,
523
0
            pattern_rules,
524
0
        }
525
0
    }
526
}
527
528
impl AssetClassificationConfig {
529
    /// Classify a symbol based on explicit mappings and pattern rules
530
0
    pub fn classify_symbol(&self, symbol: &str) -> AssetClass {
531
0
        let symbol_upper = symbol.to_uppercase();
532
533
        // First check explicit mappings
534
0
        if let Some(asset_class) = self.symbol_mappings.get(&symbol_upper) {
535
0
            return asset_class.clone();
536
0
        }
537
538
        // Then check pattern rules (sorted by priority, highest first)
539
0
        let mut applicable_rules: Vec<_> = self
540
0
            .pattern_rules
541
0
            .iter()
542
0
            .filter(|rule| {
543
0
                if let Ok(regex) = regex::Regex::new(&rule.pattern) {
544
0
                    regex.is_match(&symbol_upper)
545
                } else {
546
0
                    false
547
                }
548
0
            })
549
0
            .collect();
550
551
0
        applicable_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
552
553
0
        if let Some(rule) = applicable_rules.first() {
554
0
            rule.asset_class.clone()
555
        } else {
556
0
            AssetClass::Cash // Default fallback for unknown symbols
557
        }
558
0
    }
559
560
    /// Get volatility profile for a symbol
561
0
    pub fn get_volatility_profile(&self, symbol: &str) -> VolatilityProfile {
562
0
        let asset_class = self.classify_symbol(symbol);
563
0
        self.volatility_profiles
564
0
            .get(&asset_class)
565
0
            .cloned()
566
0
            .unwrap_or(VolatilityProfile {
567
0
                annual_volatility: 0.20,
568
0
                max_position_fraction: 0.05,
569
0
                volatility_threshold: 0.02,
570
0
                daily_loss_threshold: 0.01,
571
0
            })
572
0
    }
573
574
    /// Get daily volatility for a symbol
575
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
576
0
        let profile = self.get_volatility_profile(symbol);
577
        #[allow(clippy::float_arithmetic)]
578
0
        let result = profile.annual_volatility / 252.0_f64.sqrt();
579
0
        result
580
0
    }
581
582
    /// Get risk configuration tuple (position_fraction, volatility_threshold, daily_loss_threshold)
583
0
    pub fn get_risk_config(&self, symbol: &str) -> (f64, f64, f64) {
584
0
        let profile = self.get_volatility_profile(symbol);
585
0
        (
586
0
            profile.max_position_fraction,
587
0
            profile.volatility_threshold,
588
0
            profile.daily_loss_threshold,
589
0
        )
590
0
    }
591
}
592
593
/// Configuration for backtesting database connections
594
#[derive(Debug, Clone, Serialize, Deserialize)]
595
pub struct BacktestingDatabaseConfig {
596
    /// Database connection URL
597
    pub database_url: String,
598
    /// Maximum number of database connections in the pool
599
    pub max_connections: Option<u32>,
600
    /// Minimum number of database connections in the pool
601
    pub min_connections: Option<u32>,
602
    /// Timeout in milliseconds for acquiring a connection
603
    pub acquire_timeout_ms: Option<u64>,
604
    /// Statement cache capacity
605
    pub statement_cache_capacity: Option<usize>,
606
    /// Enable SQL query logging
607
    pub enable_logging: Option<bool>,
608
}
609
610
/// Configuration for backtesting strategy execution
611
#[derive(Debug, Clone, Serialize, Deserialize)]
612
pub struct BacktestingStrategyConfig {
613
    /// Commission rate for trades (e.g., 0.001 = 0.1%)
614
    pub commission_rate: f64,
615
    /// Slippage rate for trades (e.g., 0.0005 = 0.05%)
616
    pub slippage_rate: f64,
617
    /// Maximum position size as fraction of portfolio
618
    pub max_position_size: Option<f64>,
619
    /// Enable short selling
620
    pub allow_short_selling: Option<bool>,
621
}
622
623
impl Default for BacktestingStrategyConfig {
624
0
    fn default() -> Self {
625
0
        Self {
626
0
            commission_rate: 0.0007,      // 0.07% = 7 bps
627
0
            slippage_rate: 0.0002,        // 0.02% = 2 bps
628
0
            max_position_size: Some(0.2), // 20% max position
629
0
            allow_short_selling: Some(false),
630
0
        }
631
0
    }
632
}
633
634
/// Configuration for backtesting performance analysis
635
#[derive(Debug, Clone, Serialize, Deserialize)]
636
pub struct BacktestingPerformanceConfig {
637
    /// Risk-free rate for Sharpe ratio calculations (annual rate)
638
    pub risk_free_rate: f64,
639
    /// Resolution for equity curve (number of points)
640
    pub equity_curve_resolution: usize,
641
    /// Enable advanced performance metrics
642
    pub enable_advanced_metrics: Option<bool>,
643
}
644
645
impl Default for BacktestingPerformanceConfig {
646
0
    fn default() -> Self {
647
0
        Self {
648
0
            risk_free_rate: 0.04, // 4% annual risk-free rate
649
0
            equity_curve_resolution: 1000,
650
0
            enable_advanced_metrics: Some(true),
651
0
        }
652
0
    }
653
}
654
655
/// TLS/SSL configuration for secure gRPC connections
656
#[derive(Debug, Clone, Serialize, Deserialize)]
657
pub struct TlsConfig {
658
    /// Enable/disable TLS for gRPC connections
659
    pub enabled: bool,
660
    /// Path to server certificate file
661
    pub cert_path: String,
662
    /// Path to server private key file
663
    pub key_path: String,
664
    /// Path to CA certificate for client verification (optional)
665
    pub ca_cert_path: Option<String>,
666
    /// Require client certificate verification
667
    pub require_client_cert: bool,
668
    /// TLS protocol versions to support (e.g., ["TLSv1.2", "TLSv1.3"])
669
    pub protocol_versions: Vec<String>,
670
    /// Cipher suites to use (empty means default)
671
    pub cipher_suites: Vec<String>,
672
}
673
674
impl Default for TlsConfig {
675
0
    fn default() -> Self {
676
        // Wave 75 Fix: Use environment variables with fallback to /tmp instead of /etc
677
0
        let cert_path = std::env::var("TLS_CERT_PATH")
678
0
            .unwrap_or_else(|_| "/tmp/foxhunt/certs/server.crt".to_owned());
679
0
        let key_path = std::env::var("TLS_KEY_PATH")
680
0
            .unwrap_or_else(|_| "/tmp/foxhunt/certs/server.key".to_owned());
681
0
        let ca_cert_path = std::env::var("TLS_CA_PATH").ok();
682
683
0
        Self {
684
0
            enabled: false,
685
0
            cert_path,
686
0
            key_path,
687
0
            ca_cert_path,
688
0
            require_client_cert: false,
689
0
            protocol_versions: vec!["TLSv1.3".to_owned()],
690
0
            cipher_suites: Vec::new(),
691
0
        }
692
0
    }
693
}
694
695
/// Trading system configuration
696
#[derive(Debug, Clone, Serialize, Deserialize)]
697
pub struct TradingConfig {
698
    /// Maximum order size (in base units)
699
    pub max_order_size: f64,
700
    /// Minimum order size (in base units)
701
    pub min_order_size: f64,
702
    /// Maximum price deviation from market (as fraction, e.g., 0.05 = 5%)
703
    pub max_price_deviation: f64,
704
    /// Enable symbol validation
705
    pub enable_symbol_validation: bool,
706
    /// Maximum batch notional value (total value of orders in a batch)
707
    pub max_batch_notional: f64,
708
    /// Maximum position VaR (Value at Risk) limit
709
    pub max_position_var: f64,
710
}
711
712
impl Default for TradingConfig {
713
0
    fn default() -> Self {
714
0
        Self {
715
0
            max_order_size: 1_000_000.0,
716
0
            min_order_size: 0.001,
717
0
            max_price_deviation: 0.05,
718
0
            enable_symbol_validation: false,
719
0
            max_batch_notional: 10_000_000.0, // $10M batch limit
720
0
            max_position_var: 50_000.0,        // $50K VaR limit
721
0
        }
722
0
    }
723
}
724
725
/// Market data ingestion configuration
726
#[derive(Debug, Clone, Serialize, Deserialize)]
727
pub struct MarketDataConfig {
728
    /// Market data server host
729
    pub host: String,
730
    /// WebSocket port for streaming data
731
    pub websocket_port: u16,
732
    /// API key for authentication
733
    pub api_key: String,
734
    /// Use SSL/TLS for connections
735
    pub use_ssl: bool,
736
    /// Connection timeout in seconds
737
    pub timeout_seconds: u64,
738
}
739
740
impl Default for MarketDataConfig {
741
0
    fn default() -> Self {
742
0
        Self {
743
0
            host: "localhost".to_owned(),
744
0
            websocket_port: 8080,
745
0
            api_key: String::new(),
746
0
            use_ssl: false,
747
0
            timeout_seconds: 30,
748
0
        }
749
0
    }
750
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html deleted file mode 100644 index e5a6822d7..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs
Line
Count
Source
1
//! Symbol classification and configuration management for trading instruments.
2
//!
3
//! This module provides comprehensive symbol classification and configuration
4
//! management for various financial instruments in the Foxhunt HFT trading system.
5
//! It handles asset classification, volatility profiles, trading hours, and
6
//! market-specific parameters for optimal trading execution.
7
8
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc, Weekday};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
use std::time::Duration;
12
use uuid::Uuid;
13
14
/// Asset classification enumeration for different financial instrument types.
15
///
16
/// Provides standardized classification for all tradeable instruments,
17
/// enabling type-specific risk management, execution logic, and regulatory
18
/// compliance across different asset classes.
19
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20
pub enum AssetClassification {
21
    /// Equity securities (stocks, ADRs, REITs)
22
    Equity,
23
    /// Futures contracts (commodities, financials, indices)
24
    Future,
25
    /// Foreign exchange pairs (major, minor, exotic)
26
    Forex,
27
    /// Cryptocurrency and digital assets
28
    Crypto,
29
    /// Physical commodities (metals, energy, agriculture)
30
    Commodity,
31
    /// Fixed income securities (bonds, notes, bills)
32
    FixedIncome,
33
    /// Options contracts (equity, index, commodity options)
34
    Option,
35
    /// Exchange-traded funds and products
36
    Etf,
37
    /// Indices and benchmark instruments
38
    Index,
39
    /// Structured products and derivatives
40
    Derivative,
41
}
42
43
impl AssetClassification {
44
    /// Returns the regulatory classification for compliance purposes.
45
0
    pub const fn regulatory_class(&self) -> &'static str {
46
0
        match self {
47
0
            AssetClassification::Equity => "EQUITY",
48
0
            AssetClassification::Future => "FUTURE",
49
0
            AssetClassification::Forex => "FX",
50
0
            AssetClassification::Crypto => "CRYPTO",
51
0
            AssetClassification::Commodity => "COMMODITY",
52
0
            AssetClassification::FixedIncome => "FIXED_INCOME",
53
0
            AssetClassification::Option => "OPTION",
54
0
            AssetClassification::Etf => "ETF",
55
0
            AssetClassification::Index => "INDEX",
56
0
            AssetClassification::Derivative => "DERIVATIVE",
57
        }
58
0
    }
59
60
    /// Returns whether this asset class requires T+1 settlement.
61
0
    pub const fn requires_t_plus_one_settlement(&self) -> bool {
62
0
        matches!(self, AssetClassification::Equity | AssetClassification::Etf)
63
0
    }
64
65
    /// Returns whether this asset class supports after-hours trading.
66
0
    pub const fn supports_extended_hours(&self) -> bool {
67
0
        matches!(
68
0
            self,
69
            AssetClassification::Equity
70
                | AssetClassification::Etf
71
                | AssetClassification::Forex
72
                | AssetClassification::Crypto
73
        )
74
0
    }
75
}
76
77
/// Volatility profile configuration for risk management and position sizing.
78
///
79
/// Defines volatility characteristics and risk parameters for different
80
/// instruments, enabling dynamic position sizing and risk-adjusted execution.
81
#[derive(Debug, Clone, Serialize, Deserialize)]
82
pub struct VolatilityProfile {
83
    /// Historical average volatility (annualized)
84
    pub average_volatility: f64,
85
    /// Maximum observed volatility (99th percentile)
86
    pub max_volatility: f64,
87
    /// Minimum observed volatility (1st percentile)
88
    pub min_volatility: f64,
89
    /// Beta coefficient relative to market index
90
    pub beta: f64,
91
    /// Average True Range (ATR) for recent period
92
    pub atr: f64,
93
    /// Correlation with market benchmark
94
    pub market_correlation: f64,
95
    /// Volatility regime classification
96
    pub volatility_regime: VolatilityRegime,
97
    /// Last updated timestamp for volatility metrics
98
    pub last_updated: DateTime<Utc>,
99
    /// Number of observations used for calculation
100
    pub sample_size: u32,
101
}
102
103
impl VolatilityProfile {
104
    /// Creates a new volatility profile with default values.
105
0
    pub fn new() -> Self {
106
0
        Self {
107
0
            average_volatility: 0.20,
108
0
            max_volatility: 1.00,
109
0
            min_volatility: 0.05,
110
0
            beta: 1.0,
111
0
            atr: 0.0,
112
0
            market_correlation: 0.0,
113
0
            volatility_regime: VolatilityRegime::Normal,
114
0
            last_updated: Utc::now(),
115
0
            sample_size: 0,
116
0
        }
117
0
    }
118
119
    /// Updates volatility metrics with new data point.
120
0
    pub fn update_metrics(&mut self, new_volatility: f64, new_atr: f64) {
121
        // Update exponential moving average
122
0
        {
123
0
            let alpha = 0.1_f64; // Smoothing factor
124
0
            #[allow(clippy::float_arithmetic)]
125
0
            let one_minus_alpha = 1.0_f64 - alpha;
126
0
            #[allow(clippy::float_arithmetic)]
127
0
            let volatility_term = one_minus_alpha * self.average_volatility;
128
0
            self.average_volatility = alpha.mul_add(new_volatility, volatility_term);
129
0
            
130
0
            #[allow(clippy::float_arithmetic)]
131
0
            let atr_term = one_minus_alpha * self.atr;
132
0
            self.atr = alpha.mul_add(new_atr, atr_term);
133
0
        }        self.last_updated = Utc::now();
134
0
        self.sample_size = self.sample_size.saturating_add(1);
135
136
        // Update volatility regime
137
0
        self.volatility_regime = self.classify_regime();
138
0
    }
139
140
    /// Classifies current volatility regime based on metrics.
141
0
    fn classify_regime(&self) -> VolatilityRegime {
142
        #[allow(clippy::float_arithmetic)]
143
0
        let volatility_ratio = self.average_volatility / 0.20_f64; // Relative to 20% baseline
144
145
0
        if volatility_ratio > 2.0_f64 {
146
0
            VolatilityRegime::High
147
0
        } else if volatility_ratio > 1.5 {
148
0
            VolatilityRegime::Elevated
149
0
        } else if volatility_ratio < 0.5 {
150
0
            VolatilityRegime::Low
151
        } else {
152
0
            VolatilityRegime::Normal
153
        }
154
0
    }
155
156
    /// Returns risk-adjusted position size multiplier.
157
0
    pub const fn position_size_multiplier(&self) -> f64 {
158
0
        match self.volatility_regime {
159
0
            VolatilityRegime::Low => 1.5,
160
0
            VolatilityRegime::Normal => 1.0,
161
0
            VolatilityRegime::Elevated => 0.7,
162
0
            VolatilityRegime::High => 0.4,
163
        }
164
0
    }
165
}
166
167
impl Default for VolatilityProfile {
168
0
    fn default() -> Self {
169
0
        Self::new()
170
0
    }
171
}
172
173
/// Volatility regime classification for risk management.
174
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175
pub enum VolatilityRegime {
176
    /// Low volatility environment (< 50% of normal)
177
    Low,
178
    /// Normal volatility environment
179
    Normal,
180
    /// Elevated volatility (50-100% above normal)
181
    Elevated,
182
    /// High volatility environment (> 100% above normal)
183
    High,
184
}
185
186
/// Trading hours configuration for different markets and sessions.
187
///
188
/// Defines market operating hours, pre-market and after-hours sessions,
189
/// and holiday schedules for accurate trade timing and execution.
190
#[derive(Debug, Clone, Serialize, Deserialize)]
191
pub struct TradingHours {
192
    /// Primary market timezone identifier (e.g., "America/New_York")
193
    pub timezone: String,
194
    /// Regular trading session start time
195
    pub market_open: NaiveTime,
196
    /// Regular trading session end time
197
    pub market_close: NaiveTime,
198
    /// Pre-market session start time (optional)
199
    pub pre_market_open: Option<NaiveTime>,
200
    /// After-hours session end time (optional)
201
    pub after_hours_close: Option<NaiveTime>,
202
    /// Trading days of the week
203
    pub trading_days: Vec<Weekday>,
204
    /// Market holidays (dates when market is closed)
205
    pub holidays: Vec<NaiveDate>,
206
    /// Half-day sessions with early close times
207
    pub half_days: HashMap<NaiveDate, NaiveTime>,
208
}
209
210
impl TradingHours {
211
    /// Creates US equity market trading hours configuration.
212
0
    pub fn us_equity() -> Self {
213
0
        Self {
214
0
            timezone: "America/New_York".to_owned(),
215
0
            market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
216
0
            market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
217
0
            pre_market_open: Some(NaiveTime::from_hms_opt(4, 0, 0).unwrap()),
218
0
            after_hours_close: Some(NaiveTime::from_hms_opt(20, 0, 0).unwrap()),
219
0
            trading_days: vec![
220
0
                Weekday::Mon,
221
0
                Weekday::Tue,
222
0
                Weekday::Wed,
223
0
                Weekday::Thu,
224
0
                Weekday::Fri,
225
0
            ],
226
0
            holidays: vec![],
227
0
            half_days: HashMap::new(),
228
0
        }
229
0
    }
230
231
    /// Creates 24/7 trading hours for crypto markets.
232
0
    pub fn crypto_24_7() -> Self {
233
0
        Self {
234
0
            timezone: "UTC".to_owned(),
235
0
            market_open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
236
0
            market_close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
237
0
            pre_market_open: None,
238
0
            after_hours_close: None,
239
0
            trading_days: vec![
240
0
                Weekday::Mon,
241
0
                Weekday::Tue,
242
0
                Weekday::Wed,
243
0
                Weekday::Thu,
244
0
                Weekday::Fri,
245
0
                Weekday::Sat,
246
0
                Weekday::Sun,
247
0
            ],
248
0
            holidays: vec![],
249
0
            half_days: HashMap::new(),
250
0
        }
251
0
    }
252
253
    /// Creates forex market trading hours (Sunday 5 PM to Friday 5 PM EST).
254
0
    pub fn forex() -> Self {
255
0
        Self {
256
0
            timezone: "America/New_York".to_owned(),
257
0
            market_open: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
258
0
            market_close: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
259
0
            pre_market_open: None,
260
0
            after_hours_close: None,
261
0
            trading_days: vec![
262
0
                Weekday::Sun,
263
0
                Weekday::Mon,
264
0
                Weekday::Tue,
265
0
                Weekday::Wed,
266
0
                Weekday::Thu,
267
0
                Weekday::Fri,
268
0
            ],
269
0
            holidays: vec![],
270
0
            half_days: HashMap::new(),
271
0
        }
272
0
    }
273
274
    /// Checks if market is currently open.
275
0
    pub fn is_market_open(&self, current_time: DateTime<Utc>) -> bool {
276
        // Convert to market timezone and check if within trading hours
277
        // This is a simplified implementation - production would use proper timezone handling
278
0
        let current_date = current_time.date_naive();
279
0
        let current_time = current_time.time();
280
0
        let current_weekday = current_date.weekday();
281
282
        // Check if it's a trading day
283
0
        if !self.trading_days.contains(&current_weekday) {
284
0
            return false;
285
0
        }
286
287
        // Check if it's a holiday
288
0
        if self.holidays.contains(&current_date) {
289
0
            return false;
290
0
        }
291
292
        // Check if within trading hours
293
0
        current_time >= self.market_open && current_time <= self.market_close
294
0
    }
295
296
    /// Checks if extended hours trading is active.
297
0
    pub fn is_extended_hours_open(&self, current_time: DateTime<Utc>) -> bool {
298
0
        let current_time = current_time.time();
299
300
        // Check pre-market
301
0
        if let Some(pre_open) = self.pre_market_open {
302
0
            if current_time >= pre_open && current_time < self.market_open {
303
0
                return true;
304
0
            }
305
0
        }
306
307
        // Check after-hours
308
0
        if let Some(after_close) = self.after_hours_close {
309
0
            if current_time > self.market_close && current_time <= after_close {
310
0
                return true;
311
0
            }
312
0
        }
313
314
0
        false
315
0
    }
316
}
317
318
impl Default for TradingHours {
319
0
    fn default() -> Self {
320
0
        Self::us_equity()
321
0
    }
322
}
323
324
/// Comprehensive symbol configuration containing all trading parameters.
325
///
326
/// Central configuration structure for each tradeable symbol, containing
327
/// classification, market parameters, risk settings, and execution rules.
328
#[derive(Debug, Clone, Serialize, Deserialize)]
329
pub struct SymbolConfig {
330
    /// Unique symbol identifier
331
    pub symbol: String,
332
    /// Symbol description or company name
333
    pub description: String,
334
    /// Asset classification
335
    pub classification: AssetClassification,
336
    /// Volatility and risk profile
337
    pub volatility_profile: VolatilityProfile,
338
    /// Market operating hours
339
    pub trading_hours: TradingHours,
340
    /// Minimum price increment (tick size)
341
    pub tick_size: f64,
342
    /// Standard trading unit size
343
    pub lot_size: f64,
344
    /// Minimum order quantity
345
    pub min_order_size: f64,
346
    /// Maximum order quantity
347
    pub max_order_size: f64,
348
    /// Primary exchange or venue
349
    pub primary_exchange: String,
350
    /// Currency denomination
351
    pub currency: String,
352
    /// Sector classification (for equities)
353
    pub sector: Option<String>,
354
    /// Industry classification (for equities)
355
    pub industry: Option<String>,
356
    /// Market capitalization (for equities)
357
    pub market_cap: Option<f64>,
358
    /// Average daily volume
359
    pub avg_daily_volume: f64,
360
    /// Margin requirements
361
    pub margin_requirement: f64,
362
    /// Position limits
363
    pub position_limit: Option<f64>,
364
    /// Risk multiplier for position sizing
365
    pub risk_multiplier: f64,
366
    /// Configuration metadata
367
    pub metadata: SymbolMetadata,
368
}
369
370
impl SymbolConfig {
371
    /// Creates a new symbol configuration with default values.
372
0
    pub fn new(symbol: String, classification: AssetClassification) -> Self {
373
0
        let trading_hours = match classification {
374
0
            AssetClassification::Crypto => TradingHours::crypto_24_7(),
375
0
            AssetClassification::Forex => TradingHours::forex(),
376
0
            _ => TradingHours::us_equity(),
377
        };
378
379
0
        Self {
380
0
            symbol: symbol.clone(),
381
0
            description: format!("{} - Auto-generated", symbol),
382
0
            classification,
383
0
            volatility_profile: VolatilityProfile::new(),
384
0
            trading_hours,
385
0
            tick_size: 0.01,
386
0
            lot_size: 1.0,
387
0
            min_order_size: 1.0,
388
0
            max_order_size: 1_000_000.0,
389
0
            primary_exchange: "".to_owned(),
390
0
            currency: "USD".to_owned(),
391
0
            sector: None,
392
0
            industry: None,
393
0
            market_cap: None,
394
0
            avg_daily_volume: 0.0,
395
0
            margin_requirement: 0.25,
396
0
            position_limit: None,
397
0
            risk_multiplier: 1.0,
398
0
            metadata: SymbolMetadata::new(),
399
0
        }
400
0
    }
401
402
    /// Validates the symbol configuration for correctness.
403
    ///
404
    /// # Errors
405
    /// Returns error if the operation fails
406
0
    pub fn validate(&self) -> Result<(), String> {
407
0
        if self.symbol.is_empty() {
408
0
            return Err("Symbol cannot be empty".to_owned());
409
0
        }
410
411
0
        if self.tick_size <= 0.0_f64 {
412
0
            return Err("Tick size must be positive".to_owned());
413
0
        }
414
415
0
        if self.lot_size <= 0.0_f64 {
416
0
            return Err("Lot size must be positive".to_owned());
417
0
        }
418
419
0
        if self.min_order_size <= 0.0_f64 {
420
0
            return Err("Minimum order size must be positive".to_owned());
421
0
        }
422
423
0
        if self.max_order_size <= self.min_order_size {
424
0
            return Err("Maximum order size must be greater than minimum".to_owned());
425
0
        }
426
427
0
        if self.margin_requirement < 0.0_f64 || self.margin_requirement > 1.0_f64 {
428
0
            return Err("Margin requirement must be between 0 and 1".to_owned());
429
0
        }
430
431
0
        Ok(())
432
0
    }
433
434
    /// Calculates the effective position size based on risk parameters.
435
0
    pub fn calculate_position_size(&self, base_size: f64, _account_value: f64) -> f64 {
436
0
        let volatility_multiplier = self.volatility_profile.position_size_multiplier();
437
0
        let risk_adjusted_size = base_size.mul_add(volatility_multiplier, 0.0).mul_add(self.risk_multiplier, 0.0);
438
439
        // Apply position limits
440
0
        if let Some(limit) = self.position_limit {
441
0
            risk_adjusted_size.min(limit)
442
        } else {
443
0
            risk_adjusted_size
444
        }
445
0
    }
446
447
    /// Returns the appropriate tick size for a given price level.
448
0
    pub const fn get_tick_size_for_price(&self, _price: f64) -> f64 {
449
        // Some markets have variable tick sizes based on price
450
        // This is a simplified implementation
451
0
        self.tick_size
452
0
    }
453
454
    /// Rounds price to the nearest valid tick.
455
0
    pub fn round_to_tick(&self, price: f64) -> f64 {
456
0
        let tick = self.get_tick_size_for_price(price);
457
        #[allow(clippy::float_arithmetic)]
458
0
        let result = (price / tick).round() * tick;
459
0
        result
460
0
    }
461
462
    /// Checks if the symbol is currently tradeable.
463
0
    pub fn is_tradeable(&self, current_time: DateTime<Utc>) -> bool {
464
0
        self.trading_hours.is_market_open(current_time) && self.metadata.is_active
465
0
    }
466
467
    /// Checks if extended hours trading is available.
468
0
    pub const fn supports_extended_hours(&self) -> bool {
469
0
        self.classification.supports_extended_hours()
470
0
    }
471
}
472
473
/// Symbol configuration metadata for versioning and tracking.
474
#[derive(Debug, Clone, Serialize, Deserialize)]
475
pub struct SymbolMetadata {
476
    /// Unique configuration ID
477
    pub id: Uuid,
478
    /// Configuration version
479
    pub version: u32,
480
    /// Creation timestamp
481
    pub created_at: DateTime<Utc>,
482
    /// Last update timestamp
483
    pub updated_at: DateTime<Utc>,
484
    /// Active status
485
    pub is_active: bool,
486
    /// Data source for configuration
487
    pub data_source: String,
488
    /// Last validation timestamp
489
    pub last_validated: Option<DateTime<Utc>>,
490
    /// Configuration tags for organization
491
    pub tags: Vec<String>,
492
}
493
494
impl SymbolMetadata {
495
    /// Creates new metadata with default values.
496
0
    pub fn new() -> Self {
497
0
        let now = Utc::now();
498
0
        Self {
499
0
            id: Uuid::new_v4(),
500
0
            version: 1,
501
0
            created_at: now,
502
0
            updated_at: now,
503
0
            is_active: true,
504
0
            data_source: "manual".to_owned(),
505
0
            last_validated: None,
506
0
            tags: vec![],
507
0
        }
508
0
    }
509
510
    /// Updates the metadata timestamp and version.
511
0
    pub fn update(&mut self) {
512
0
        self.updated_at = Utc::now();
513
0
        self.version = self.version.saturating_add(1);
514
0
    }
515
516
    /// Marks the configuration as validated.
517
0
    pub fn mark_validated(&mut self) {
518
0
        self.last_validated = Some(Utc::now());
519
0
    }
520
}
521
522
impl Default for SymbolMetadata {
523
0
    fn default() -> Self {
524
0
        Self::new()
525
0
    }
526
}
527
528
/// Symbol configuration manager for loading and caching symbol configurations.
529
///
530
/// Provides high-performance access to symbol configurations with caching,
531
/// hot-reload capabilities, and configuration validation.
532
#[derive(Debug)]
533
#[allow(clippy::module_name_repetitions)]
534
pub struct SymbolConfigManager {
535
    /// In-memory cache of symbol configurations
536
    symbol_cache: HashMap<String, SymbolConfig>,
537
    /// Last cache update timestamp
538
    last_updated: DateTime<Utc>,
539
    /// Cache timeout duration
540
    cache_timeout: Duration,
541
}
542
543
impl SymbolConfigManager {
544
    /// Creates a new symbol configuration manager.
545
0
    pub fn new() -> Self {
546
0
        Self {
547
0
            symbol_cache: HashMap::new(),
548
0
            last_updated: Utc::now(),
549
0
            cache_timeout: Duration::from_secs(300), // 5 minutes
550
0
        }
551
0
    }
552
553
    /// Loads symbol configuration from cache or source.
554
    ///
555
    /// # Errors
556
    /// Returns error if the operation fails
557
0
    pub async fn get_symbol_config(
558
0
        &mut self,
559
0
        symbol: &str,
560
0
    ) -> Result<Option<SymbolConfig>, String> {
561
        // Check cache first
562
0
        if let Some(config) = self.symbol_cache.get(symbol) {
563
0
            if !self.is_cache_expired() {
564
0
                return Ok(Some(config.clone()));
565
0
            }
566
0
        }
567
568
        // Load from source (this would integrate with database/external source)
569
0
        self.load_symbol_from_source(symbol).await
570
0
    }
571
572
    /// Loads all symbol configurations into cache.
573
    ///
574
    /// # Errors
575
    /// Returns error if the operation fails
576
0
    pub async fn load_all_symbols(&mut self) -> Result<usize, String> {
577
        // This would integrate with the database or external configuration source
578
0
        self.refresh_cache().await
579
0
    }
580
581
    /// Adds or updates a symbol configuration.
582
    ///
583
    /// # Errors
584
    /// Returns error if the operation fails
585
0
    pub fn upsert_symbol_config(&mut self, config: SymbolConfig) -> Result<(), String> {
586
        // Validate configuration
587
0
        config.validate()?;
588
589
        // Update cache
590
0
        self.symbol_cache.insert(config.symbol.clone(), config);
591
0
        self.last_updated = Utc::now();
592
593
0
        Ok(())
594
0
    }
595
596
    /// Removes a symbol configuration.
597
0
    pub fn remove_symbol_config(&mut self, symbol: &str) -> Option<SymbolConfig> {
598
0
        self.symbol_cache.remove(symbol)
599
0
    }
600
601
    /// Returns all cached symbol configurations.
602
0
    pub fn get_all_symbols(&self) -> Vec<&SymbolConfig> {
603
0
        self.symbol_cache.values().collect()
604
0
    }
605
606
    /// Returns symbols filtered by asset classification.
607
0
    pub fn get_symbols_by_classification(
608
0
        &self,
609
0
        classification: &AssetClassification,
610
0
    ) -> Vec<&SymbolConfig> {
611
0
        self.symbol_cache
612
0
            .values()
613
0
            .filter(|config| &config.classification == classification)
614
0
            .collect()
615
0
    }
616
617
    /// Checks if cache has expired.
618
0
    fn is_cache_expired(&self) -> bool {
619
0
        Utc::now()
620
0
            .signed_duration_since(self.last_updated)
621
0
            .to_std()
622
0
            .unwrap_or(Duration::MAX)
623
0
            > self.cache_timeout
624
0
    }
625
626
    /// Loads symbol configuration from external source.
627
0
    async fn load_symbol_from_source(
628
0
        &mut self,
629
0
        _symbol: &str,
630
0
    ) -> Result<Option<SymbolConfig>, String> {
631
        // This would integrate with database or external configuration API
632
        // For now, return None to indicate symbol not found
633
634
        // Example of creating a default config if needed:
635
        // let config = SymbolConfig::new(symbol.to_owned(), AssetClassification::Equity);
636
        // self.symbol_cache.insert(symbol.to_owned(), config.clone());
637
        // Ok(Some(config))
638
639
0
        Ok(None)
640
0
    }
641
642
    /// Refreshes the entire symbol cache from source.
643
0
    async fn refresh_cache(&mut self) -> Result<usize, String> {
644
        // This would integrate with database to load all active symbols
645
        // For now, return the current cache size
646
0
        Ok(self.symbol_cache.len())
647
0
    }
648
649
    /// Sets cache timeout duration.
650
0
    pub const fn set_cache_timeout(&mut self, timeout: Duration) {
651
0
        self.cache_timeout = timeout;
652
0
    }
653
654
    /// Forces cache refresh on next access.
655
0
    pub const fn invalidate_cache(&mut self) {
656
0
        self.last_updated = DateTime::<Utc>::MIN_UTC;
657
0
    }
658
659
    /// Returns cache statistics.
660
0
    pub fn cache_stats(&self) -> (usize, DateTime<Utc>, bool) {
661
0
        (
662
0
            self.symbol_cache.len(),
663
0
            self.last_updated,
664
0
            self.is_cache_expired(),
665
0
        )
666
0
    }
667
}
668
669
impl Default for SymbolConfigManager {
670
0
    fn default() -> Self {
671
0
        Self::new()
672
0
    }
673
}
674
675
#[cfg(test)]
676
mod tests {
677
    use super::*;
678
679
    #[test]
680
    fn test_asset_classification_regulatory_class() {
681
        assert_eq!(AssetClassification::Equity.regulatory_class(), "EQUITY");
682
        assert_eq!(AssetClassification::Forex.regulatory_class(), "FX");
683
        assert_eq!(AssetClassification::Crypto.regulatory_class(), "CRYPTO");
684
    }
685
686
    #[test]
687
    fn test_volatility_profile_update() {
688
        let mut profile = VolatilityProfile::new();
689
        profile.update_metrics(0.40, 2.5);
690
691
        // With exponential smoothing: 0.1 * 0.40 + 0.9 * 0.20 = 0.22
692
        assert!(profile.average_volatility > 0.20 && profile.average_volatility < 0.25);
693
        // With exponential smoothing: 0.1 * 2.5 + 0.9 * 0.0 = 0.25
694
        assert!((profile.atr - 0.25).abs() < 0.01);
695
        assert_eq!(profile.volatility_regime, VolatilityRegime::Normal);
696
    }
697
698
    #[test]
699
    fn test_symbol_config_validation() {
700
        let mut config = SymbolConfig::new("AAPL".to_owned(), AssetClassification::Equity);
701
        assert!(config.validate().is_ok());
702
703
        config.tick_size = -0.01;
704
        assert!(config.validate().is_err());
705
    }
706
707
    #[test]
708
    fn test_trading_hours_us_equity() {
709
        let hours = TradingHours::us_equity();
710
        assert_eq!(hours.timezone, "America/New_York");
711
        assert_eq!(
712
            hours.market_open,
713
            NaiveTime::from_hms_opt(9, 30, 0).unwrap()
714
        );
715
        assert_eq!(
716
            hours.market_close,
717
            NaiveTime::from_hms_opt(16, 0, 0).unwrap()
718
        );
719
    }
720
721
    #[test]
722
    fn test_symbol_config_manager() {
723
        let mut manager = SymbolConfigManager::new();
724
        let config = SymbolConfig::new("TEST".to_owned(), AssetClassification::Equity);
725
726
        assert!(manager.upsert_symbol_config(config).is_ok());
727
        assert_eq!(manager.get_all_symbols().len(), 1);
728
    }
729
}
\ No newline at end of file diff --git a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html b/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html deleted file mode 100644 index 73c241546..000000000 --- a/coverage_report_common/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/vault.rs
Line
Count
Source
1
//! HashiCorp Vault configuration for secure secret management.
2
//!
3
//! This module provides configuration structures for integrating with HashiCorp Vault
4
//! to securely manage secrets, API keys, and sensitive configuration data in the
5
//! Foxhunt trading system. Supports token-based authentication and namespace isolation.
6
7
use serde::{Deserialize, Serialize};
8
use secrecy::{ExposeSecret, SecretString};
9
use std::fmt;
10
11
/// HashiCorp Vault configuration for secure secret storage.
12
///
13
/// Configures connection to HashiCorp Vault for retrieving sensitive
14
/// configuration data such as API keys, database passwords, and other
15
/// secrets. Supports Vault Enterprise features like namespaces.
16
///
17
/// # Security
18
///
19
/// The Vault token is wrapped in `SecretString` to prevent accidental
20
/// exposure in logs, debug output, or memory dumps. The token is automatically
21
/// zeroized when the config is dropped.
22
#[derive(Clone, Serialize, Deserialize)]
23
#[allow(clippy::module_name_repetitions)]
24
pub struct VaultConfig {
25
    /// Vault server URL (e.g., "<https://vault.example.com:8200>")
26
    pub url: String,
27
    /// Vault authentication token for API access (securely stored)
28
    #[serde(serialize_with = "serialize_secret", deserialize_with = "deserialize_secret")]
29
    pub token: SecretString,
30
    /// Mount path for the secrets engine (e.g., "secret/")
31
    pub mount_path: String,
32
    /// Vault namespace for multi-tenant deployments (Enterprise feature)
33
    pub namespace: Option<String>,
34
}
35
36
/// Custom serializer for SecretString that prevents token exposure
37
0
fn serialize_secret<S>(_secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
38
0
where
39
0
    S: serde::Serializer,
40
{
41
    // Serialize as redacted placeholder to prevent token exposure
42
0
    serializer.serialize_str("***REDACTED***")
43
0
}
44
45
/// Custom deserializer for SecretString
46
0
fn deserialize_secret<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
47
0
where
48
0
    D: serde::Deserializer<'de>,
49
{
50
0
    let s = String::deserialize(deserializer)?;
51
0
    Ok(SecretString::from(s))
52
0
}
53
54
impl fmt::Debug for VaultConfig {
55
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56
0
        f.debug_struct("VaultConfig")
57
0
            .field("url", &self.url)
58
0
            .field("token", &"***REDACTED***")
59
0
            .field("mount_path", &self.mount_path)
60
0
            .field("namespace", &self.namespace)
61
0
            .finish()
62
0
    }
63
}
64
65
impl Drop for VaultConfig {
66
0
    fn drop(&mut self) {
67
        // Explicitly zeroize the token when VaultConfig is dropped
68
        // This ensures the secret is cleared from memory
69
        // Note: SecretString already implements ZeroizeOnDrop, but we make it explicit
70
        // for documentation purposes
71
0
    }
72
}
73
74
impl VaultConfig {
75
    /// Creates a new VaultConfig with the specified parameters.
76
    ///
77
    /// # Security
78
    ///
79
    /// The token is immediately wrapped in a `SecretString` to prevent exposure.
80
    ///
81
    /// Consider using `from_env()` or loading from secure configuration
82
    /// sources instead of passing plain strings.
83
0
    pub fn new(url: String, token: String, mount_path: String) -> Self {
84
0
        Self {
85
0
            url,
86
0
            token: SecretString::from(token),
87
0
            mount_path,
88
0
            namespace: None,
89
0
        }
90
0
    }
91
92
    /// Sets the namespace for multi-tenant Vault deployments.
93
0
    pub fn with_namespace(mut self, namespace: String) -> Self {
94
0
        self.namespace = Some(namespace);
95
0
        self
96
0
    }
97
98
    /// Gets a reference to the secret token (requires explicit exposure)
99
    ///
100
    /// # Security
101
    ///
102
    /// This method requires the caller to explicitly acknowledge they are
103
    /// exposing the secret. Use only when necessary (e.g., when making
104
    ///
105
    /// API calls to Vault) and ensure the exposed value is not logged
106
    /// or stored in insecure locations.
107
0
    pub const fn token(&self) -> &SecretString {
108
0
        &self.token
109
0
    }
110
111
    /// Validates the vault configuration.
112
    ///
113
    /// # Security
114
    ///
115
    /// Validation checks length without exposing the token value.
116
    ///
117
    /// # Errors
118
    /// Returns error if the operation fails
119
0
    pub fn validate(&self) -> Result<(), String> {
120
0
        if self.url.is_empty() {
121
0
            return Err("Vault URL cannot be empty".to_owned());
122
0
        }
123
0
        if self.token.expose_secret().is_empty() {
124
0
            return Err("Vault token cannot be empty".to_owned());
125
0
        }
126
0
        if self.mount_path.is_empty() {
127
0
            return Err("Vault mount path cannot be empty".to_owned());
128
0
        }
129
0
        Ok(())
130
0
    }
131
}
132
133
#[cfg(test)]
134
mod tests {
135
    use super::*;
136
137
    fn create_test_config() -> VaultConfig {
138
        VaultConfig::new(
139
            "https://vault.example.com:8200".to_owned(),
140
            "test-token-12345".to_owned(),
141
            "secret/".to_owned(),
142
        )
143
    }
144
145
    #[test]
146
    fn test_vault_config_creation() {
147
        let config = create_test_config();
148
        assert_eq!(config.url, "https://vault.example.com:8200");
149
        assert_eq!(config.mount_path, "secret/");
150
        assert!(config.namespace.is_none());
151
    }
152
153
    #[test]
154
    fn test_vault_config_with_namespace() {
155
        let config = create_test_config().with_namespace("production".to_owned());
156
        assert_eq!(config.namespace.as_deref(), Some("production"));
157
    }
158
159
    #[test]
160
    fn test_vault_config_validation_success() {
161
        let config = create_test_config();
162
        assert!(config.validate().is_ok());
163
    }
164
165
    #[test]
166
    fn test_vault_config_validation_empty_url() {
167
        let mut config = create_test_config();
168
        config.url = String::new();
169
        assert!(config.validate().is_err());
170
        assert_eq!(config.validate().unwrap_err(), "Vault URL cannot be empty");
171
    }
172
173
    #[test]
174
    fn test_vault_config_validation_empty_token() {
175
        let mut config = create_test_config();
176
        config.token = SecretString::from(String::new());
177
        assert!(config.validate().is_err());
178
        assert_eq!(
179
            config.validate().unwrap_err(),
180
            "Vault token cannot be empty"
181
        );
182
    }
183
184
    #[test]
185
    fn test_vault_config_validation_empty_mount_path() {
186
        let mut config = create_test_config();
187
        config.mount_path = String::new();
188
        assert!(config.validate().is_err());
189
        assert_eq!(
190
            config.validate().unwrap_err(),
191
            "Vault mount path cannot be empty"
192
        );
193
    }
194
195
    #[test]
196
    fn test_vault_config_serialization() {
197
        let config = create_test_config();
198
        let serialized = serde_json::to_string(&config).unwrap();
199
        // Token should be redacted in serialization
200
        assert!(serialized.contains("***REDACTED***"));
201
        assert!(!serialized.contains("test-token-12345"));
202
    }
203
204
    #[test]
205
    fn test_vault_config_deserialization() {
206
        let config = create_test_config();
207
        let serialized = serde_json::to_string(&config).unwrap();
208
        let deserialized: VaultConfig = serde_json::from_str(&serialized).unwrap();
209
        assert_eq!(config.url, deserialized.url);
210
        assert_eq!(config.mount_path, deserialized.mount_path);
211
    }
212
213
    #[test]
214
    fn test_vault_config_clone() {
215
        let config1 = create_test_config();
216
        let config2 = config1.clone();
217
        assert_eq!(config1.url, config2.url);
218
    }
219
220
    #[test]
221
    fn test_vault_config_debug() {
222
        let config = create_test_config();
223
        let debug_output = format!("{:?}", config);
224
        assert!(debug_output.contains("VaultConfig"));
225
        assert!(debug_output.contains("***REDACTED***"));
226
        assert!(!debug_output.contains("test-token-12345"));
227
    }
228
229
    #[test]
230
    fn test_vault_config_namespace_none() {
231
        let config = create_test_config();
232
        assert!(config.namespace.is_none());
233
    }
234
235
    #[test]
236
    fn test_vault_config_namespace_some() {
237
        let config = create_test_config().with_namespace("dev".to_owned());
238
        assert!(config.namespace.is_some());
239
        assert_eq!(config.namespace.as_deref(), Some("dev"));
240
    }
241
242
    #[test]
243
    fn test_vault_config_token_not_exposed() {
244
        let config = create_test_config();
245
        // Verify token accessor works
246
        assert_eq!(config.token().expose_secret(), "test-token-12345");
247
    }
248
249
    #[test]
250
    fn test_vault_config_token_redacted_in_display() {
251
        let config = create_test_config();
252
        let debug_str = format!("{:?}", config);
253
        assert!(!debug_str.contains("test-token-12345"));
254
    }
255
}
\ No newline at end of file diff --git a/coverage_report_common/html/index.html b/coverage_report_common/html/index.html deleted file mode 100644 index 68008dada..000000000 --- a/coverage_report_common/html/index.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-17 09:01

Click here for information about interpreting this report.

FilenameFunction CoverageLine CoverageRegion CoverageBranch Coverage
common/src/database.rs
   0.00% (0/15)
   0.00% (0/136)
   0.00% (0/131)
- (0/0)
common/src/error.rs
 100.00% (17/17)
  98.69% (151/153)
  99.10% (220/222)
- (0/0)
common/src/market_data.rs
   0.00% (0/1)
   0.00% (0/8)
   0.00% (0/13)
- (0/0)
common/src/ml_strategy.rs
  87.50% (28/32)
  91.11% (246/270)
  91.20% (373/409)
- (0/0)
common/src/thresholds.rs
 100.00% (4/4)
 100.00% (21/21)
 100.00% (28/28)
- (0/0)
common/src/trading.rs
  87.50% (14/16)
  93.10% (81/87)
  95.68% (133/139)
- (0/0)
common/src/traits.rs
   0.00% (0/2)
   0.00% (0/6)
   0.00% (0/6)
- (0/0)
common/src/types.rs
  57.69% (225/390)
  56.93% (1236/2171)
  56.35% (1771/3143)
- (0/0)
config/src/asset_classification.rs
   0.00% (0/19)
   0.00% (0/307)
   0.00% (0/262)
- (0/0)
config/src/data_config.rs
   0.00% (0/13)
   0.00% (0/145)
   0.00% (0/71)
- (0/0)
config/src/data_providers.rs
   0.00% (0/21)
   0.00% (0/113)
   0.00% (0/121)
- (0/0)
config/src/database.rs
   0.00% (0/7)
   0.00% (0/52)
   0.00% (0/34)
- (0/0)
config/src/lib.rs
   0.00% (0/2)
   0.00% (0/11)
   0.00% (0/15)
- (0/0)
config/src/manager.rs
   0.00% (0/17)
   0.00% (0/132)
   0.00% (0/168)
- (0/0)
config/src/ml_config.rs
   0.00% (0/4)
   0.00% (0/136)
   0.00% (0/55)
- (0/0)
config/src/risk_config.rs
   0.00% (0/5)
   0.00% (0/190)
   0.00% (0/334)
- (0/0)
config/src/runtime.rs
   0.00% (0/34)
   0.00% (0/344)
   0.00% (0/409)
- (0/0)
config/src/schemas.rs
   0.00% (0/6)
   0.00% (0/90)
   0.00% (0/154)
- (0/0)
config/src/storage_config.rs
   0.00% (0/5)
   0.00% (0/26)
   0.00% (0/26)
- (0/0)
config/src/structures.rs
   0.00% (0/27)
   0.00% (0/350)
   0.00% (0/296)
- (0/0)
config/src/symbol_config.rs
   0.00% (0/44)
   0.00% (0/326)
   0.00% (0/347)
- (0/0)
config/src/vault.rs
   0.00% (0/8)
   0.00% (0/48)
   0.00% (0/56)
- (0/0)
Totals
  41.80% (288/689)
  33.87% (1735/5122)
  39.21% (2525/6439)
- (0/0)
Generated by llvm-cov -- llvm version 20.1.7-rust-1.89.0-stable
\ No newline at end of file diff --git a/coverage_report_common/html/style.css b/coverage_report_common/html/style.css deleted file mode 100644 index ae4f09f69..000000000 --- a/coverage_report_common/html/style.css +++ /dev/null @@ -1,194 +0,0 @@ -.red { - background-color: #f004; -} -.cyan { - background-color: cyan; -} -html { - scroll-behavior: smooth; -} -body { - font-family: -apple-system, sans-serif; -} -pre { - margin-top: 0px !important; - margin-bottom: 0px !important; -} -.source-name-title { - padding: 5px 10px; - border-bottom: 1px solid #8888; - background-color: #0002; - line-height: 35px; -} -.centered { - display: table; - margin-left: left; - margin-right: auto; - border: 1px solid #8888; - border-radius: 3px; -} -.expansion-view { - margin-left: 0px; - margin-top: 5px; - margin-right: 5px; - margin-bottom: 5px; - border: 1px solid #8888; - border-radius: 3px; -} -table { - border-collapse: collapse; -} -.light-row { - border: 1px solid #8888; - border-left: none; - border-right: none; -} -.light-row-bold { - border: 1px solid #8888; - border-left: none; - border-right: none; - font-weight: bold; -} -.column-entry { - text-align: left; -} -.column-entry-bold { - font-weight: bold; - text-align: left; -} -.column-entry-yellow { - text-align: left; - background-color: #ff06; -} -.column-entry-red { - text-align: left; - background-color: #f004; -} -.column-entry-gray { - text-align: left; - background-color: #fff4; -} -.column-entry-green { - text-align: left; - background-color: #0f04; -} -.line-number { - text-align: right; -} -.covered-line { - text-align: right; - color: #06d; -} -.uncovered-line { - text-align: right; - color: #d00; -} -.uncovered-line.selected { - color: #f00; - font-weight: bold; -} -.region.red.selected { - background-color: #f008; - font-weight: bold; -} -.branch.red.selected { - background-color: #f008; - font-weight: bold; -} -.tooltip { - position: relative; - display: inline; - background-color: #bef; - text-decoration: none; -} -.tooltip span.tooltip-content { - position: absolute; - width: 100px; - margin-left: -50px; - color: #FFFFFF; - background: #000000; - height: 30px; - line-height: 30px; - text-align: center; - visibility: hidden; - border-radius: 6px; -} -.tooltip span.tooltip-content:after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - margin-left: -8px; - width: 0; height: 0; - border-top: 8px solid #000000; - border-right: 8px solid transparent; - border-left: 8px solid transparent; -} -:hover.tooltip span.tooltip-content { - visibility: visible; - opacity: 0.8; - bottom: 30px; - left: 50%; - z-index: 999; -} -th, td { - vertical-align: top; - padding: 2px 8px; - border-collapse: collapse; - border-right: 1px solid #8888; - border-left: 1px solid #8888; - text-align: left; -} -td pre { - display: inline-block; - text-decoration: inherit; -} -td:first-child { - border-left: none; -} -td:last-child { - border-right: none; -} -tr:hover { - background-color: #eee; -} -tr:last-child { - border-bottom: none; -} -tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { - background-color: #8884; -} -a { - color: inherit; -} -.control { - position: fixed; - top: 0em; - right: 0em; - padding: 1em; - background: #FFF8; -} -@media (prefers-color-scheme: dark) { - body { - background-color: #222; - color: whitesmoke; - } - tr:hover { - background-color: #111; - } - .covered-line { - color: #39f; - } - .uncovered-line { - color: #f55; - } - .tooltip { - background-color: #068; - } - .control { - background: #2228; - } - tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { - background-color: #8884; - } -} diff --git a/coverage_risk/html/control.js b/coverage_risk/html/control.js deleted file mode 100644 index 5897b005c..000000000 --- a/coverage_risk/html/control.js +++ /dev/null @@ -1,99 +0,0 @@ - -function next_uncovered(selector, reverse, scroll_selector) { - function visit_element(element) { - element.classList.add("seen"); - element.classList.add("selected"); - - if (!scroll_selector) { - scroll_selector = "tr:has(.selected) td.line-number" - } - - const scroll_to = document.querySelector(scroll_selector); - if (scroll_to) { - scroll_to.scrollIntoView({behavior: "smooth", block: "center", inline: "end"}); - } - } - - function select_one() { - if (!reverse) { - const previously_selected = document.querySelector(".selected"); - - if (previously_selected) { - previously_selected.classList.remove("selected"); - } - - return document.querySelector(selector + ":not(.seen)"); - } else { - const previously_selected = document.querySelector(".selected"); - - if (previously_selected) { - previously_selected.classList.remove("selected"); - previously_selected.classList.remove("seen"); - } - - const nodes = document.querySelectorAll(selector + ".seen"); - if (nodes) { - const last = nodes[nodes.length - 1]; // last - return last; - } else { - return undefined; - } - } - } - - function reset_all() { - if (!reverse) { - const all_seen = document.querySelectorAll(selector + ".seen"); - - if (all_seen) { - all_seen.forEach(e => e.classList.remove("seen")); - } - } else { - const all_seen = document.querySelectorAll(selector + ":not(.seen)"); - - if (all_seen) { - all_seen.forEach(e => e.classList.add("seen")); - } - } - - } - - const uncovered = select_one(); - - if (uncovered) { - visit_element(uncovered); - } else { - reset_all(); - - const uncovered = select_one(); - - if (uncovered) { - visit_element(uncovered); - } - } -} - -function next_line(reverse) { - next_uncovered("td.uncovered-line", reverse) -} - -function next_region(reverse) { - next_uncovered("span.red.region", reverse); -} - -function next_branch(reverse) { - next_uncovered("span.red.branch", reverse); -} - -document.addEventListener("keypress", function(event) { - const reverse = event.shiftKey; - if (event.code == "KeyL") { - next_line(reverse); - } - if (event.code == "KeyB") { - next_branch(reverse); - } - if (event.code == "KeyR") { - next_region(reverse); - } -}); diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html deleted file mode 100644 index 9f430f85f..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/database.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/database.rs
Line
Count
Source
1
//! Database connection utilities and configurations
2
//!
3
//! This module provides shared database connection management utilities
4
//! that can be used across all Foxhunt services.
5
6
use serde::{Deserialize, Serialize};
7
use sqlx::{Pool, Postgres};
8
use std::time::Duration;
9
use thiserror::Error;
10
11
// Import centralized database configuration
12
pub use config::database::DatabaseConfig;
13
use config::structures::BacktestingDatabaseConfig;
14
15
/// Database-specific errors
16
#[derive(Debug, Error)]
17
pub enum DatabaseError {
18
    /// Connection failed - wrapper around SQLx connection errors
19
    #[error("Connection failed: {0}")]
20
    Connection(#[from] sqlx::Error),
21
    /// Query exceeded maximum allowed execution time
22
    #[error("Query timeout: operation took {actual_ms}ms, max allowed {max_ms}ms")]
23
    QueryTimeout {
24
        /// Actual execution time in milliseconds
25
        actual_ms: u64,
26
        /// Maximum allowed execution time in milliseconds
27
        max_ms: u64,
28
    },
29
    /// Connection pool has no available connections
30
    #[error("Pool exhausted: no connections available")]
31
    PoolExhausted,
32
    /// Database configuration is invalid or missing required parameters
33
    #[error("Configuration error: {0}")]
34
    Configuration(String),
35
    /// Performance constraint violation detected
36
    #[error("Performance violation: {0}")]
37
    Performance(String),
38
}
39
40
/// Database connection configuration (local extended version)
41
#[derive(Debug, Clone, Deserialize, Serialize)]
42
pub struct LocalDatabaseConfig {
43
    /// Database connection URL
44
    pub url: String,
45
    /// Pool configuration
46
    pub pool: PoolConfig,
47
    /// Performance settings
48
    pub performance: PerformanceConfig,
49
}
50
51
/// Connection pool configuration
52
#[derive(Debug, Clone, Deserialize, Serialize)]
53
pub struct PoolConfig {
54
    /// Maximum number of connections in the pool
55
    pub max_connections: u32,
56
    /// Minimum number of connections to maintain
57
    pub min_connections: u32,
58
    /// Connection timeout in milliseconds
59
    pub connect_timeout_ms: u64,
60
    /// Connection acquire timeout in milliseconds
61
    pub acquire_timeout_ms: u64,
62
    /// Maximum connection lifetime in seconds
63
    pub max_lifetime_seconds: u64,
64
    /// Idle timeout in seconds
65
    pub idle_timeout_seconds: u64,
66
}
67
68
/// Performance configuration for HFT operations
69
#[derive(Debug, Clone, Deserialize, Serialize)]
70
pub struct PerformanceConfig {
71
    /// Query timeout in microseconds for HFT operations
72
    pub query_timeout_micros: u64,
73
    /// Enable connection prewarming
74
    pub enable_prewarming: bool,
75
    /// Enable statement preparation
76
    pub enable_prepared_statements: bool,
77
    /// Enable query logging for slow queries
78
    pub enable_slow_query_logging: bool,
79
    /// Slow query threshold in microseconds
80
    pub slow_query_threshold_micros: u64,
81
}
82
83
impl Default for LocalDatabaseConfig {
84
0
    fn default() -> Self {
85
0
        Self {
86
0
            url: "postgresql://foxhunt:password@localhost:5432/foxhunt".to_owned(),
87
0
            pool: PoolConfig::default(),
88
0
            performance: PerformanceConfig::default(),
89
0
        }
90
0
    }
91
}
92
93
impl Default for PoolConfig {
94
0
    fn default() -> Self {
95
0
        Self {
96
0
            max_connections: 50,
97
0
            min_connections: 10,
98
0
            connect_timeout_ms: 100,
99
0
            acquire_timeout_ms: 50,
100
0
            max_lifetime_seconds: 3600,
101
0
            idle_timeout_seconds: 300,
102
0
        }
103
0
    }
104
}
105
106
impl Default for PerformanceConfig {
107
0
    fn default() -> Self {
108
0
        Self {
109
0
            query_timeout_micros: 800, // <1ms for HFT operations
110
0
            enable_prewarming: true,
111
0
            enable_prepared_statements: true,
112
0
            enable_slow_query_logging: true,
113
0
            slow_query_threshold_micros: 1000, // Log queries >1ms
114
0
        }
115
0
    }
116
}
117
118
/// Convert from centralized config to common crate config with HFT optimizations
119
impl From<DatabaseConfig> for LocalDatabaseConfig {
120
0
    fn from(config: DatabaseConfig) -> Self {
121
0
        Self {
122
0
            url: config.url,
123
0
            pool: PoolConfig {
124
0
                max_connections: config.max_connections,
125
0
                min_connections: (config.max_connections / 5).max(2), // 20% of max, min 2
126
0
                connect_timeout_ms: config.connect_timeout.as_millis().min(100) as u64, // Convert to ms, cap at 100ms for HFT
127
0
                acquire_timeout_ms: 50,     // Fast acquire for HFT
128
0
                max_lifetime_seconds: 3600, // 1 hour default
129
0
                idle_timeout_seconds: 300,  // 5 minutes default
130
0
            },
131
0
            performance: PerformanceConfig {
132
0
                query_timeout_micros: config.query_timeout.as_micros().min(800) as u64, // Convert to microseconds, cap at 800μs for HFT
133
0
                enable_prewarming: true,
134
0
                enable_prepared_statements: true,
135
0
                enable_slow_query_logging: config.enable_query_logging,
136
0
                slow_query_threshold_micros: 1000, // 1ms threshold
137
0
            },
138
0
        }
139
0
    }
140
}
141
142
/// Convert from backtesting config to common crate config with backtesting optimizations
143
impl From<BacktestingDatabaseConfig> for LocalDatabaseConfig {
144
0
    fn from(config: BacktestingDatabaseConfig) -> Self {
145
0
        let max_conn = config.max_connections.unwrap_or(10);
146
0
        Self {
147
0
            url: config.database_url,
148
0
            pool: PoolConfig {
149
0
                max_connections: max_conn,
150
0
                min_connections: (max_conn / 4).max(2), // 25% of max, min 2
151
0
                connect_timeout_ms: config.acquire_timeout_ms.unwrap_or(1000), // Use acquire timeout as connection timeout
152
0
                acquire_timeout_ms: 100,    // Less strict for backtesting
153
0
                max_lifetime_seconds: 3600, // 1 hour default
154
0
                idle_timeout_seconds: 600,  // 10 minutes for backtesting
155
0
            },
156
0
            performance: PerformanceConfig {
157
0
                query_timeout_micros: 10000, // 10ms default for backtesting queries
158
0
                enable_prewarming: true,
159
0
                enable_prepared_statements: true,
160
0
                enable_slow_query_logging: config.enable_logging.unwrap_or(false),
161
0
                slow_query_threshold_micros: 5000, // 5ms threshold for backtesting
162
0
            },
163
0
        }
164
0
    }
165
}
166
167
/// Database connection pool wrapper
168
#[derive(Debug)]
169
pub struct DatabasePool {
170
    pool: Pool<Postgres>,
171
    config: LocalDatabaseConfig,
172
}
173
174
impl DatabasePool {
175
    /// Create a new database connection pool
176
0
    pub async fn new(config: LocalDatabaseConfig) -> Result<Self, DatabaseError> {
177
        use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
178
179
        // Parse connection options
180
0
        let mut connect_options: PgConnectOptions = config
181
0
            .url
182
0
            .parse()
183
0
            .map_err(|e| DatabaseError::Configuration(format!("Invalid URL: {}", e)))?;
184
185
        // Configure connection-level optimizations
186
0
        connect_options = connect_options
187
0
            .application_name("foxhunt-service")
188
0
            .statement_cache_capacity(1000);
189
190
        // Create connection pool with optimized settings
191
0
        let pool = PgPoolOptions::new()
192
0
            .max_connections(config.pool.max_connections)
193
0
            .min_connections(config.pool.min_connections)
194
0
            .acquire_timeout(Duration::from_millis(config.pool.acquire_timeout_ms))
195
0
            .max_lifetime(Duration::from_secs(config.pool.max_lifetime_seconds))
196
0
            .idle_timeout(Duration::from_secs(config.pool.idle_timeout_seconds))
197
0
            .test_before_acquire(true)
198
0
            .connect_with(connect_options)
199
0
            .await
200
0
            .map_err(DatabaseError::Connection)?;
201
202
        // Pre-warm connections if enabled
203
0
        if config.performance.enable_prewarming {
204
0
            for _ in 0..config.pool.min_connections {
205
0
                let _conn = pool.acquire().await.map_err(DatabaseError::Connection)?;
206
0
                sqlx::query("SELECT 1")
207
0
                    .fetch_one(&pool)
208
0
                    .await
209
0
                    .map_err(DatabaseError::Connection)?;
210
            }
211
0
        }
212
213
0
        Ok(Self { pool, config })
214
0
    }
215
216
    /// Get the underlying connection pool
217
0
    pub const fn pool(&self) -> &Pool<Postgres> {
218
0
        &self.pool
219
0
    }
220
221
    /// Get current configuration
222
0
    pub const fn config(&self) -> &LocalDatabaseConfig {
223
0
        &self.config
224
0
    }
225
226
    /// Health check for the database connection
227
0
    pub async fn health_check(&self) -> Result<(), DatabaseError> {
228
0
        let result = tokio::time::timeout(
229
0
            Duration::from_millis(100),
230
0
            sqlx::query("SELECT 1").fetch_one(&self.pool),
231
0
        )
232
0
        .await;
233
234
0
        match result {
235
0
            Ok(Ok(_)) => Ok(()),
236
0
            Ok(Err(e)) => Err(DatabaseError::Connection(e)),
237
0
            Err(_) => Err(DatabaseError::QueryTimeout {
238
0
                actual_ms: 100,
239
0
                max_ms: 100,
240
0
            }),
241
        }
242
0
    }
243
244
    /// Get connection pool statistics
245
0
    pub fn pool_stats(&self) -> PoolStats {
246
0
        PoolStats {
247
0
            size: self.pool.size(),
248
0
            idle: self.pool.num_idle() as u32,
249
0
            active: self.pool.size() - self.pool.num_idle() as u32,
250
0
            max_size: self.config.pool.max_connections,
251
0
        }
252
0
    }
253
}
254
255
/// Connection pool statistics
256
#[derive(Debug, Clone, Serialize, Deserialize)]
257
pub struct PoolStats {
258
    /// Current pool size
259
    pub size: u32,
260
    /// Number of idle connections
261
    pub idle: u32,
262
    /// Number of active connections
263
    pub active: u32,
264
    /// Maximum pool size
265
    pub max_size: u32,
266
}
267
268
impl PoolStats {
269
    /// Calculate pool utilization percentage
270
0
    pub fn utilization_percentage(&self) -> f64 {
271
0
        (self.active as f64 / self.max_size as f64) * 100.0
272
0
    }
273
274
    /// Check if pool is healthy (not over-utilized)
275
0
    pub fn is_healthy(&self) -> bool {
276
0
        self.utilization_percentage() < 80.0
277
0
    }
278
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html deleted file mode 100644 index ff95eb644..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/error.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/error.rs
Line
Count
Source
1
//! Common error types and utilities
2
//!
3
//! This module provides shared error types and utilities used across
4
//! all Foxhunt services.
5
6
use serde::{Deserialize, Serialize};
7
use std::fmt;
8
use std::time::Duration;
9
use thiserror::Error;
10
11
/// Common error type for all Foxhunt services
12
#[derive(Debug, Error)]
13
pub enum CommonError {
14
    /// Database operation failed - wraps database-specific errors
15
    #[error("Database error: {0}")]
16
    Database(#[from] crate::database::DatabaseError),
17
    /// Configuration is invalid or missing required parameters
18
    #[error("Configuration error: {0}")]
19
    Configuration(String),
20
    /// Network communication error occurred
21
    #[error("Network error: {0}")]
22
    Network(String),
23
    /// Service-specific error with categorization for metrics
24
    #[error("Service error: {category} - {message}")]
25
    Service {
26
        /// Error category for classification
27
        category: ErrorCategory,
28
        /// Descriptive error message
29
        message: String,
30
    },
31
    /// Input validation failed
32
    #[error("Validation error: {0}")]
33
    Validation(String),
34
    /// Operation exceeded maximum allowed execution time
35
    #[error("Timeout error: operation took {actual_ms}ms, max allowed {max_ms}ms")]
36
    Timeout {
37
        /// Actual execution time in milliseconds
38
        actual_ms: u64,
39
        /// Maximum allowed execution time in milliseconds
40
        max_ms: u64,
41
    },
42
}
43
44
/// Error categories for classification and metrics
45
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46
pub enum ErrorCategory {
47
    /// Market data related errors
48
    MarketData,
49
    /// Trading and order management errors
50
    Trading,
51
    /// Network and communication errors
52
    Network,
53
    /// System and infrastructure errors
54
    System,
55
    /// Configuration errors
56
    Configuration,
57
    /// Validation errors
58
    Validation,
59
    /// Critical errors requiring immediate attention
60
    Critical,
61
    /// Connection errors (data providers)
62
    Connection,
63
    /// Authentication errors
64
    Authentication,
65
    /// Rate limiting errors
66
    RateLimit,
67
    /// Data parsing errors
68
    Parse,
69
    /// Subscription errors
70
    Subscription,
71
    /// Financial safety and calculation errors
72
    FinancialSafety,
73
    /// Risk management and circuit breakers
74
    RiskManagement,
75
    /// Database and persistence layer
76
    Database,
77
    /// Broker connectivity and execution
78
    Broker,
79
    /// Machine learning and AI errors
80
    MachineLearning,
81
    /// Security and authentication errors
82
    Security,
83
    /// Business logic errors
84
    BusinessLogic,
85
    /// Resource errors (not found, conflicts)
86
    Resource,
87
    /// Development and testing errors
88
    Development,
89
    /// Risk management errors
90
    Risk,
91
    /// Machine learning errors (alias for MachineLearning)
92
    ML,
93
    /// Unknown/other errors
94
    Other,
95
}
96
97
impl fmt::Display for ErrorCategory {
98
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99
0
        match self {
100
0
            Self::MarketData => write!(f, "MARKET_DATA"),
101
0
            Self::Trading => write!(f, "TRADING"),
102
0
            Self::Network => write!(f, "NETWORK"),
103
0
            Self::System => write!(f, "SYSTEM"),
104
0
            Self::Configuration => write!(f, "CONFIGURATION"),
105
0
            Self::Validation => write!(f, "VALIDATION"),
106
0
            Self::Critical => write!(f, "CRITICAL"),
107
0
            Self::Connection => write!(f, "CONNECTION"),
108
0
            Self::Authentication => write!(f, "AUTHENTICATION"),
109
0
            Self::RateLimit => write!(f, "RATE_LIMIT"),
110
0
            Self::Parse => write!(f, "PARSE"),
111
0
            Self::Subscription => write!(f, "SUBSCRIPTION"),
112
0
            Self::FinancialSafety => write!(f, "FINANCIAL_SAFETY"),
113
0
            Self::RiskManagement => write!(f, "RISK_MANAGEMENT"),
114
0
            Self::Database => write!(f, "DATABASE"),
115
0
            Self::Broker => write!(f, "BROKER"),
116
0
            Self::MachineLearning => write!(f, "MACHINE_LEARNING"),
117
0
            Self::Security => write!(f, "SECURITY"),
118
0
            Self::BusinessLogic => write!(f, "BUSINESS_LOGIC"),
119
0
            Self::Resource => write!(f, "RESOURCE"),
120
0
            Self::Development => write!(f, "DEVELOPMENT"),
121
0
            Self::Risk => write!(f, "RISK"),
122
0
            Self::ML => write!(f, "ML"),
123
0
            Self::Other => write!(f, "OTHER"),
124
        }
125
0
    }
126
}
127
128
/// Error severity levels for prioritization and alerting
129
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130
pub enum ErrorSeverity {
131
    /// Debug level - for development and troubleshooting
132
    Debug,
133
    /// Info level - informational messages
134
    Info,
135
    /// Warning level - potentially problematic situations
136
    Warn,
137
    /// Error level - error conditions that should be addressed
138
    Error,
139
    /// Critical level - serious error conditions requiring immediate attention
140
    Critical,
141
}
142
143
impl fmt::Display for ErrorSeverity {
144
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145
0
        match self {
146
0
            Self::Debug => write!(f, "DEBUG"),
147
0
            Self::Info => write!(f, "INFO"),
148
0
            Self::Warn => write!(f, "WARN"),
149
0
            Self::Error => write!(f, "ERROR"),
150
0
            Self::Critical => write!(f, "CRITICAL"),
151
        }
152
0
    }
153
}
154
155
/// Retry strategies for error recovery
156
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157
pub enum RetryStrategy {
158
    /// Do not retry - error is permanent
159
    NoRetry,
160
    /// Retry immediately without delay
161
    Immediate,
162
    /// Linear backoff with fixed intervals
163
    Linear {
164
        /// Base delay in milliseconds between retries
165
        base_delay_ms: u64,
166
    },
167
    /// Exponential backoff with jitter
168
    Exponential {
169
        /// Base delay in milliseconds for exponential backoff
170
        base_delay_ms: u64,
171
        /// Maximum delay cap in milliseconds
172
        max_delay_ms: u64,
173
    },
174
    /// Wait for circuit breaker to close
175
    CircuitBreaker,
176
}
177
178
impl RetryStrategy {
179
    /// Calculate delay for retry attempt
180
    #[must_use]
181
0
    pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> {
182
0
        match self {
183
0
            Self::NoRetry => None,
184
0
            Self::Immediate => Some(Duration::from_millis(0)),
185
0
            Self::Linear { base_delay_ms } => {
186
0
                Some(Duration::from_millis(base_delay_ms * u64::from(attempt)))
187
            },
188
            Self::Exponential {
189
0
                base_delay_ms,
190
0
                max_delay_ms,
191
            } => {
192
0
                let delay_ms = base_delay_ms * 2_u64.pow(attempt.min(10));
193
0
                let capped_delay = delay_ms.min(*max_delay_ms);
194
195
                // Add simple jitter (±10%)
196
0
                let jitter_ms = capped_delay / 10;
197
0
                let final_delay = capped_delay.saturating_sub(jitter_ms / 2);
198
199
0
                Some(Duration::from_millis(final_delay))
200
            },
201
0
            Self::CircuitBreaker => Some(Duration::from_secs(30)),
202
        }
203
0
    }
204
205
    /// Get maximum recommended retry attempts
206
    #[must_use]
207
0
    pub const fn max_attempts(&self) -> Option<u32> {
208
0
        match self {
209
0
            Self::NoRetry => Some(0),
210
0
            Self::Immediate => Some(3),
211
0
            Self::Linear { .. } => Some(5),
212
0
            Self::Exponential { .. } => Some(7),
213
0
            Self::CircuitBreaker => Some(1),
214
        }
215
0
    }
216
}
217
218
/// Convenience functions for creating common errors
219
impl CommonError {
220
    /// Create a configuration error
221
0
    pub fn config<S: Into<String>>(message: S) -> Self {
222
0
        Self::Configuration(message.into())
223
0
    }
224
225
    /// Create a network error
226
0
    pub fn network<S: Into<String>>(message: S) -> Self {
227
0
        Self::Network(message.into())
228
0
    }
229
230
    /// Create a service error with category
231
0
    pub fn service<S: Into<String>>(category: ErrorCategory, message: S) -> Self {
232
0
        Self::Service {
233
0
            category,
234
0
            message: message.into(),
235
0
        }
236
0
    }
237
238
    /// Create a validation error
239
0
    pub fn validation<S: Into<String>>(message: S) -> Self {
240
0
        Self::Validation(message.into())
241
0
    }
242
243
    /// Create a timeout error
244
0
    pub fn timeout(actual_ms: u64, max_ms: u64) -> Self {
245
0
        Self::Timeout { actual_ms, max_ms }
246
0
    }
247
248
    /// Create a machine learning specific service error
249
0
    pub fn ml<S: Into<String>, M: Into<String>>(model_name: S, message: M) -> Self {
250
0
        Self::Service {
251
0
            category: ErrorCategory::MachineLearning,
252
0
            message: format!("{}: {}", model_name.into(), message.into()),
253
0
        }
254
0
    }
255
256
    /// Create a serialization error
257
0
    pub fn serialization<S: Into<String>>(message: S) -> Self {
258
0
        Self::Service {
259
0
            category: ErrorCategory::Parse,
260
0
            message: format!("Serialization error: {}", message.into()),
261
0
        }
262
0
    }
263
264
    /// Create an internal error
265
0
    pub fn internal<S: Into<String>>(message: S) -> Self {
266
0
        Self::Service {
267
0
            category: ErrorCategory::System,
268
0
            message: format!("Internal error: {}", message.into()),
269
0
        }
270
0
    }
271
272
    /// Create a resource exhausted error
273
0
    pub fn resource_exhausted<S: Into<String>>(resource: S) -> Self {
274
0
        Self::Service {
275
0
            category: ErrorCategory::Resource,
276
0
            message: format!("Resource exhausted: {}", resource.into()),
277
0
        }
278
0
    }
279
280
    /// Get the error category for classification and metrics
281
0
    pub fn category(&self) -> ErrorCategory {
282
0
        match self {
283
0
            Self::Database(_) => ErrorCategory::Database,
284
0
            Self::Configuration(_) => ErrorCategory::Configuration,
285
0
            Self::Network(_) => ErrorCategory::Network,
286
0
            Self::Service { category, .. } => *category,
287
0
            Self::Validation(_) => ErrorCategory::Validation,
288
0
            Self::Timeout { .. } => ErrorCategory::System,
289
        }
290
0
    }
291
292
    /// Get error severity level
293
0
    pub fn severity(&self) -> ErrorSeverity {
294
0
        match self {
295
0
            Self::Database(_) => ErrorSeverity::Critical,
296
0
            Self::Configuration(_) => ErrorSeverity::Critical,
297
0
            Self::Network(_) => ErrorSeverity::Error,
298
0
            Self::Service { category, .. } => match category {
299
                ErrorCategory::Critical
300
                | ErrorCategory::FinancialSafety
301
0
                | ErrorCategory::Authentication => ErrorSeverity::Critical,
302
                ErrorCategory::Trading
303
                | ErrorCategory::RiskManagement
304
0
                | ErrorCategory::Database => ErrorSeverity::Error,
305
0
                _ => ErrorSeverity::Warn,
306
            },
307
0
            Self::Validation(_) => ErrorSeverity::Warn,
308
0
            Self::Timeout { .. } => ErrorSeverity::Error,
309
        }
310
0
    }
311
312
    /// Check if the error is retryable
313
0
    pub fn is_retryable(&self) -> bool {
314
0
        match self {
315
0
            Self::Database(_) => true,       // Database operations can be retried
316
0
            Self::Configuration(_) => false, // Configuration errors are permanent
317
0
            Self::Network(_) => true,        // Network errors are often transient
318
0
            Self::Service { category, .. } => !matches!(
319
0
                category,
320
                ErrorCategory::Authentication
321
                    | ErrorCategory::Configuration
322
                    | ErrorCategory::Validation
323
            ),
324
0
            Self::Validation(_) => false, // Validation errors are permanent
325
0
            Self::Timeout { .. } => true, // Timeouts can be retried
326
        }
327
0
    }
328
329
    /// Get retry strategy for this error
330
0
    pub fn retry_strategy(&self) -> RetryStrategy {
331
0
        if !self.is_retryable() {
332
0
            return RetryStrategy::NoRetry;
333
0
        }
334
335
0
        match self {
336
0
            Self::Database(_) => RetryStrategy::Exponential {
337
0
                base_delay_ms: 1000,
338
0
                max_delay_ms: 10000,
339
0
            },
340
0
            Self::Network(_) => RetryStrategy::Linear { base_delay_ms: 500 },
341
0
            Self::Service { category, .. } => match category {
342
                ErrorCategory::Network | ErrorCategory::Connection => {
343
0
                    RetryStrategy::Linear { base_delay_ms: 500 }
344
                },
345
0
                ErrorCategory::RateLimit => RetryStrategy::Exponential {
346
0
                    base_delay_ms: 5000,
347
0
                    max_delay_ms: 60000,
348
0
                },
349
0
                _ => RetryStrategy::Immediate,
350
            },
351
0
            Self::Timeout { .. } => RetryStrategy::Linear {
352
0
                base_delay_ms: 1000,
353
0
            },
354
0
            _ => RetryStrategy::NoRetry,
355
        }
356
0
    }
357
}
358
359
/// Result type for common operations
360
pub type CommonResult<T> = Result<T, CommonError>;
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html deleted file mode 100644 index 2e9f842d6..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/trading.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/trading.rs
Line
Count
Source
1
//! Trading-specific types and enums
2
//!
3
//! This module contains the canonical definitions for all trading-related
4
//! types used across the Foxhunt HFT system. This is the single source
5
//! of truth for all trading types.
6
7
use chrono::{DateTime, Utc};
8
use rust_decimal::Decimal;
9
use serde::{Deserialize, Serialize};
10
use std::fmt;
11
12
// ELIMINATED: Re-exports removed to force explicit imports
13
// REMOVED: TimeInForce duplicate - use canonical definition from common::types
14
15
// Currency moved to canonical source: common::types::Currency
16
17
/// Tick type for market data
18
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19
#[cfg_attr(feature = "database", derive(sqlx::Type))]
20
#[cfg_attr(
21
    feature = "database",
22
    sqlx(type_name = "tick_type", rename_all = "snake_case")
23
)]
24
pub enum TickType {
25
    /// Trade tick
26
    Trade,
27
    /// Bid price update
28
    Bid,
29
    /// Ask price update
30
    Ask,
31
    /// Quote update (bid and ask)
32
    Quote,
33
}
34
35
impl fmt::Display for TickType {
36
    /// Format the tick type for display
37
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38
0
        match self {
39
0
            Self::Trade => write!(f, "TRADE"),
40
0
            Self::Bid => write!(f, "BID"),
41
0
            Self::Ask => write!(f, "ASK"),
42
0
            Self::Quote => write!(f, "QUOTE"),
43
        }
44
0
    }
45
}
46
47
/// Order book action type
48
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49
pub enum BookAction {
50
    /// Update price level
51
    Update,
52
    /// Delete price level
53
    Delete,
54
    /// Clear entire book
55
    Clear,
56
}
57
58
impl fmt::Display for BookAction {
59
    /// Format the book action for display
60
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61
0
        match self {
62
0
            Self::Update => write!(f, "UPDATE"),
63
0
            Self::Delete => write!(f, "DELETE"),
64
0
            Self::Clear => write!(f, "CLEAR"),
65
        }
66
0
    }
67
}
68
69
/// Market regime classification
70
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71
pub enum MarketRegime {
72
    /// Normal market conditions
73
    Normal,
74
    /// Crisis/stress market conditions
75
    Crisis,
76
    /// Trending market (strong directional movement)
77
    Trending,
78
    /// Sideways/ranging market (low volatility)
79
    Sideways,
80
    /// Bull market (sustained upward trend)
81
    Bull,
82
    /// Bear market (sustained downward trend)
83
    Bear,
84
}
85
86
impl fmt::Display for MarketRegime {
87
    /// Format the market regime for display
88
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89
0
        match self {
90
0
            Self::Normal => write!(f, "NORMAL"),
91
0
            Self::Crisis => write!(f, "CRISIS"),
92
0
            Self::Trending => write!(f, "TRENDING"),
93
0
            Self::Sideways => write!(f, "SIDEWAYS"),
94
0
            Self::Bull => write!(f, "BULL"),
95
0
            Self::Bear => write!(f, "BEAR"),
96
        }
97
0
    }
98
}
99
100
/// Core Quantity type using fixed-point arithmetic for precise calculations
101
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
102
pub struct Quantity {
103
    /// Internal representation using 6 decimal places (scale factor of 1,000,000)
104
    value: u64,
105
}
106
107
impl Quantity {
108
    /// Scale factor for fixed-point arithmetic (6 decimal places)
109
    pub const SCALE: u64 = 1_000_000;
110
111
    /// Zero quantity
112
    pub const ZERO: Self = Self { value: 0 };
113
114
    /// Create a new quantity from a floating-point value
115
0
    pub fn new(value: f64) -> Result<Self, &'static str> {
116
0
        if value < 0.0 {
117
0
            return Err("Quantity cannot be negative");
118
0
        }
119
0
        if !value.is_finite() {
120
0
            return Err("Quantity must be finite");
121
0
        }
122
123
0
        let scaled = (value * Self::SCALE as f64).round() as u64;
124
0
        Ok(Self { value: scaled })
125
0
    }
126
127
    /// Create from raw internal value
128
0
    pub const fn from_raw(value: u64) -> Self {
129
0
        Self { value }
130
0
    }
131
132
    /// Get raw internal value
133
0
    pub const fn raw(&self) -> u64 {
134
0
        self.value
135
0
    }
136
137
    /// Convert to floating-point value
138
0
    pub fn to_f64(&self) -> f64 {
139
0
        self.value as f64 / Self::SCALE as f64
140
0
    }
141
142
    /// Convert to decimal
143
0
    pub fn to_decimal(&self) -> Decimal {
144
0
        Decimal::new(self.value as i64, 6)
145
0
    }
146
147
    /// Add two quantities
148
0
    pub fn add(&self, other: Self) -> Self {
149
0
        Self {
150
0
            value: self.value + other.value,
151
0
        }
152
0
    }
153
154
    /// Subtract two quantities
155
0
    pub fn subtract(&self, other: Self) -> Self {
156
0
        Self {
157
0
            value: self.value.saturating_sub(other.value),
158
0
        }
159
0
    }
160
}
161
162
impl fmt::Display for Quantity {
163
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164
0
        write!(f, "{:.6}", self.to_f64())
165
0
    }
166
}
167
168
impl std::ops::Add for Quantity {
169
    type Output = Self;
170
171
0
    fn add(self, other: Self) -> Self::Output {
172
0
        Self {
173
0
            value: self.value + other.value,
174
0
        }
175
0
    }
176
}
177
178
impl std::ops::Sub for Quantity {
179
    type Output = Self;
180
181
0
    fn sub(self, other: Self) -> Self::Output {
182
0
        Self {
183
0
            value: self.value.saturating_sub(other.value),
184
0
        }
185
0
    }
186
}
187
188
/// Order event for tracking order lifecycle
189
#[derive(Debug, Clone, Serialize, Deserialize)]
190
pub struct OrderEvent {
191
    /// Unique order identifier
192
    pub order_id: String,
193
    /// Trading symbol
194
    pub symbol: String,
195
    /// Order type (Market, Limit, etc.)
196
    pub order_type: OrderType,
197
    /// Order side (Buy/Sell)
198
    pub side: OrderSide,
199
    /// Order quantity
200
    pub quantity: Quantity,
201
    /// Order price (None for market orders)
202
    pub price: Option<Decimal>,
203
    /// Event timestamp
204
    pub timestamp: DateTime<Utc>,
205
    /// Strategy identifier
206
    pub strategy_id: String,
207
    /// Type of order event
208
    pub event_type: OrderEventType,
209
    /// Previous quantity for modifications
210
    pub previous_quantity: Option<Quantity>,
211
    /// Previous price for modifications
212
    pub previous_price: Option<Decimal>,
213
    /// Reason for cancellation or modification
214
    pub reason: Option<String>,
215
}
216
217
/// Types of order events
218
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219
pub enum OrderEventType {
220
    /// Order was placed
221
    Placed,
222
    /// Order was modified
223
    Modified,
224
    /// Order was cancelled
225
    Cancelled,
226
    /// Order was rejected
227
    Rejected,
228
    /// Order expired
229
    Expired,
230
}
231
232
impl fmt::Display for OrderEventType {
233
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234
0
        match self {
235
0
            Self::Placed => write!(f, "PLACED"),
236
0
            Self::Modified => write!(f, "MODIFIED"),
237
0
            Self::Cancelled => write!(f, "CANCELLED"),
238
0
            Self::Rejected => write!(f, "REJECTED"),
239
0
            Self::Expired => write!(f, "EXPIRED"),
240
        }
241
0
    }
242
}
243
244
/// Order type enumeration
245
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
246
pub enum OrderType {
247
    /// Market order - execute immediately at best available price
248
    Market,
249
    /// Limit order - execute only at specified price or better
250
    Limit,
251
    /// Stop order - becomes market order when stop price is reached
252
    Stop,
253
    /// Stop-limit order - becomes limit order when stop price is reached
254
    StopLimit,
255
}
256
257
impl fmt::Display for OrderType {
258
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259
0
        match self {
260
0
            Self::Market => write!(f, "MARKET"),
261
0
            Self::Limit => write!(f, "LIMIT"),
262
0
            Self::Stop => write!(f, "STOP"),
263
0
            Self::StopLimit => write!(f, "STOP_LIMIT"),
264
        }
265
0
    }
266
}
267
268
/// Order side enumeration
269
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
270
pub enum OrderSide {
271
    /// Buy order
272
    Buy,
273
    /// Sell order
274
    Sell,
275
}
276
277
impl fmt::Display for OrderSide {
278
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279
0
        match self {
280
0
            Self::Buy => write!(f, "BUY"),
281
0
            Self::Sell => write!(f, "SELL"),
282
        }
283
0
    }
284
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html deleted file mode 100644 index 088329cf3..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/traits.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/traits.rs
Line
Count
Source
1
//! Common traits used across services
2
//!
3
//! This module provides shared traits that define common interfaces
4
//! for services in the Foxhunt HFT trading system.
5
6
use crate::error::CommonResult;
7
use crate::types::{ServiceStatus, Timestamp};
8
use async_trait::async_trait;
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
12
/// Trait for configurable components
13
#[async_trait]
14
pub trait Configurable {
15
    /// Configuration type for this component
16
    type Config: Clone + Send + Sync;
17
18
    /// Apply configuration changes
19
    async fn configure(&mut self, config: Self::Config) -> CommonResult<()>;
20
21
    /// Get current configuration
22
    fn get_config(&self) -> &Self::Config;
23
24
    /// Validate configuration before applying
25
    fn validate_config(config: &Self::Config) -> CommonResult<()>;
26
}
27
28
/// Trait for health check capabilities
29
#[async_trait]
30
pub trait HealthCheck {
31
    /// Perform a health check
32
    async fn health_check(&self) -> CommonResult<HealthStatus>;
33
34
    /// Get detailed health information
35
    async fn detailed_health(&self) -> CommonResult<DetailedHealth>;
36
}
37
38
/// Health status for components
39
#[derive(Debug, Clone, Serialize, Deserialize)]
40
pub struct HealthStatus {
41
    /// Overall health status
42
    pub status: ServiceStatus,
43
    /// Timestamp of the health check
44
    pub timestamp: Timestamp,
45
    /// Optional message
46
    pub message: Option<String>,
47
}
48
49
/// Detailed health information
50
#[derive(Debug, Clone, Serialize, Deserialize)]
51
pub struct DetailedHealth {
52
    /// Basic health status
53
    pub status: HealthStatus,
54
    /// Component-specific metrics
55
    pub metrics: HashMap<String, f64>,
56
    /// Sub-component health statuses
57
    pub components: HashMap<String, HealthStatus>,
58
}
59
60
/// Trait for metrics collection
61
pub trait Metrics {
62
    /// Metrics type for this component
63
    type Metrics: Clone + Send + Sync + Serialize;
64
65
    /// Get current metrics
66
    fn get_metrics(&self) -> Self::Metrics;
67
68
    /// Reset metrics counters
69
    fn reset_metrics(&mut self);
70
}
71
72
/// Trait for service lifecycle management
73
#[async_trait]
74
pub trait Service: Send + Sync {
75
    /// Start the service
76
    async fn start(&mut self) -> CommonResult<()>;
77
78
    /// Stop the service gracefully
79
    async fn stop(&mut self) -> CommonResult<()>;
80
81
    /// Get current service status
82
    fn status(&self) -> ServiceStatus;
83
84
    /// Get service name
85
    fn name(&self) -> &str;
86
87
    /// Get service version
88
    fn version(&self) -> &str;
89
}
90
91
/// Trait for components that can be reloaded
92
#[async_trait]
93
pub trait Reloadable {
94
    /// Reload the component (hot reload)
95
    async fn reload(&mut self) -> CommonResult<()>;
96
97
    /// Check if reload is supported
98
0
    fn supports_reload(&self) -> bool {
99
0
        true
100
0
    }
101
}
102
103
/// Trait for components with graceful shutdown
104
#[async_trait]
105
pub trait GracefulShutdown {
106
    /// Initiate graceful shutdown
107
    async fn shutdown(&mut self) -> CommonResult<()>;
108
109
    /// Force shutdown (emergency stop)
110
    async fn force_shutdown(&mut self) -> CommonResult<()>;
111
112
    /// Get shutdown timeout duration in seconds
113
0
    fn shutdown_timeout_seconds(&self) -> u64 {
114
0
        30 // Default 30 seconds
115
0
    }
116
}
117
118
/// Trait for components that support circuit breaking
119
pub trait CircuitBreaker {
120
    /// Check if circuit is open
121
    fn is_circuit_open(&self) -> bool;
122
123
    /// Get failure count
124
    fn failure_count(&self) -> u64;
125
126
    /// Reset circuit breaker
127
    fn reset_circuit(&mut self);
128
}
129
130
/// Trait for rate-limited operations
131
pub trait RateLimited {
132
    /// Check if operation is allowed under rate limits
133
    fn is_allowed(&self) -> bool;
134
135
    /// Get current rate limit status
136
    fn rate_limit_status(&self) -> RateLimitStatus;
137
}
138
139
/// Rate limit status information
140
#[derive(Debug, Clone, Serialize, Deserialize)]
141
pub struct RateLimitStatus {
142
    /// Current request count in the window
143
    pub current_count: u64,
144
    /// Maximum requests allowed in the window
145
    pub max_requests: u64,
146
    /// Time window in seconds
147
    pub window_seconds: u64,
148
    /// Seconds until window resets
149
    pub reset_in_seconds: u64,
150
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html deleted file mode 100644 index bbfd55e48..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/common/src/types.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/common/src/types.rs
Line
Count
Source
1
//! Common data types used across services
2
//!
3
//! This module provides shared data types that are used throughout
4
//! the Foxhunt HFT trading system. This includes both infrastructure types
5
//! and core trading types migrated from foxhunt-common-types.
6
7
use crate::error::ErrorCategory;
8
use chrono::{DateTime, Utc};
9
// ELIMINATED: Re-exports removed to force explicit imports
10
// NO RE-EXPORTS: Import rust_decimal::Decimal directly in each crate that needs it
11
use rust_decimal::Decimal; // Internal use only - other crates must import directly
12
use serde::{Deserialize, Serialize};
13
use serde_json::Value;
14
use std::collections::HashMap;
15
use std::sync::{Arc, Mutex, RwLock};
16
17
use crate::error::{CommonError, ErrorCategory as CommonErrorCategory};
18
use num_traits::FromPrimitive;
19
use std::convert::TryFrom;
20
use std::fmt;
21
use std::iter::Sum;
22
use std::num::ParseIntError;
23
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
24
use std::str::FromStr;
25
use uuid::Uuid;
26
27
// =============================================================================
28
// Type Aliases for Complex Types
29
// =============================================================================
30
31
/// Common error type for async operations
32
pub type AsyncResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
33
34
/// Thread-safe hash map for shared state
35
pub type SharedHashMap<K, V> = Arc<RwLock<HashMap<K, V>>>;
36
37
/// Thread-safe hash map with Mutex for shared state
38
pub type MutexHashMap<K, V> = Arc<Mutex<HashMap<K, V>>>;
39
40
/// Thread-safe container for any value
41
pub type SharedValue<T> = Arc<RwLock<T>>;
42
43
/// Thread-safe container with Mutex for any value
44
pub type MutexValue<T> = Arc<Mutex<T>>;
45
46
// Trading-specific type aliases
47
/// Map of positions by symbol
48
pub type PositionMap<T> = SharedHashMap<String, T>;
49
50
/// Map of orders by order ID
51
pub type OrderMap<T> = SharedHashMap<String, T>;
52
53
/// Map of accounts by account ID
54
pub type AccountMap<T> = SharedHashMap<String, T>;
55
56
/// Map of instruments by instrument ID
57
pub type InstrumentMap<T> = SharedHashMap<String, T>;
58
59
/// Map of market data by symbol
60
pub type MarketDataMap<T> = SharedHashMap<String, T>;
61
62
/// Cache entry with timestamp
63
pub type CacheEntry<T> = (T, DateTime<Utc>);
64
65
/// Cache map with timestamped entries
66
pub type CacheMap<K, V> = SharedHashMap<K, CacheEntry<V>>;
67
68
/// Risk factor loadings by instrument
69
pub type RiskFactorMap = SharedHashMap<String, HashMap<String, Decimal>>;
70
71
/// Performance metrics history
72
pub type PerformanceHistory<T> = SharedHashMap<String, std::collections::VecDeque<T>>;
73
74
/// Model registry for ML models
75
pub type ModelRegistry<T> = SharedHashMap<String, T>;
76
77
/// Generic configuration cache
78
pub type ConfigCache<K, V> = SharedHashMap<K, V>;
79
80
// =============================================================================
81
// Event Types - Moved from trading_engine to enforce pure client architecture
82
// =============================================================================
83
84
/// Order events for the complete order lifecycle
85
#[derive(Debug, Clone, Serialize, Deserialize)]
86
pub struct OrderEvent {
87
    /// Unique identifier for the order
88
    pub order_id: OrderId,
89
    /// Trading symbol for the order
90
    pub symbol: Symbol,
91
    /// Type of order (market, limit, stop, etc.)
92
    pub order_type: OrderType,
93
    /// Order side (buy or sell)
94
    pub side: OrderSide,
95
    /// Order quantity
96
    pub quantity: Quantity,
97
    /// Order price (None for market orders)
98
    pub price: Option<Price>,
99
    /// Timestamp when the event occurred
100
    pub timestamp: DateTime<Utc>,
101
    /// Strategy or client identifier
102
    pub strategy_id: String,
103
    /// Order event type (placed, modified, cancelled)
104
    pub event_type: OrderEventType,
105
    /// Previous quantity for modifications
106
    pub previous_quantity: Option<Quantity>,
107
    /// Previous price for modifications
108
    pub previous_price: Option<Price>,
109
    /// Reason for cancellation or modification
110
    pub reason: Option<String>,
111
}
112
113
/// Types of order events
114
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115
pub enum OrderEventType {
116
    /// Order was placed
117
    Placed,
118
    /// Order was modified
119
    Modified,
120
    /// Order was cancelled
121
    Cancelled,
122
    /// Order was rejected
123
    Rejected,
124
}
125
126
// =============================================================================
127
// Core Data Types
128
// =============================================================================
129
130
/// Unique identifier for services
131
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
132
pub struct ServiceId(pub String);
133
134
impl ServiceId {
135
    /// Create a new service ID
136
0
    pub fn new<S: Into<String>>(id: S) -> Self {
137
0
        Self(id.into())
138
0
    }
139
140
    /// Get the inner string value
141
    /// Get the execution ID as a string slice
142
    /// Get execution ID as string slice
143
0
    pub fn as_str(&self) -> &str {
144
0
        &self.0
145
0
    }
146
}
147
148
impl fmt::Display for ServiceId {
149
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150
0
        write!(f, "{}", self.0)
151
0
    }
152
}
153
154
impl From<&str> for ServiceId {
155
0
    fn from(s: &str) -> Self {
156
0
        Self(s.to_owned())
157
0
    }
158
}
159
160
impl From<String> for ServiceId {
161
0
    fn from(s: String) -> Self {
162
0
        Self(s)
163
0
    }
164
}
165
166
/// Service status enumeration
167
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168
pub enum ServiceStatus {
169
    /// Service is starting up
170
    Starting,
171
    /// Service is running normally
172
    Running,
173
    /// Service is degraded but functional
174
    Degraded,
175
    /// Service is stopping
176
    Stopping,
177
    /// Service is stopped
178
    Stopped,
179
    /// Service has encountered an error
180
    Error,
181
    /// Service is in maintenance mode
182
    Maintenance,
183
}
184
185
impl fmt::Display for ServiceStatus {
186
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187
0
        match self {
188
0
            Self::Starting => write!(f, "STARTING"),
189
0
            Self::Running => write!(f, "RUNNING"),
190
0
            Self::Degraded => write!(f, "DEGRADED"),
191
0
            Self::Stopping => write!(f, "STOPPING"),
192
0
            Self::Stopped => write!(f, "STOPPED"),
193
0
            Self::Error => write!(f, "ERROR"),
194
0
            Self::Maintenance => write!(f, "MAINTENANCE"),
195
        }
196
0
    }
197
}
198
199
impl ServiceStatus {
200
    /// Check if the service is healthy
201
0
    pub fn is_healthy(&self) -> bool {
202
0
        matches!(self, Self::Running | Self::Starting)
203
0
    }
204
205
    /// Check if the service is available for requests
206
0
    pub fn is_available(&self) -> bool {
207
0
        matches!(self, Self::Running | Self::Degraded)
208
0
    }
209
}
210
211
/// Configuration version for tracking changes
212
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213
pub struct ConfigVersion {
214
    /// Version number
215
    pub version: u64,
216
    /// Timestamp when version was created
217
    pub timestamp: DateTime<Utc>,
218
    /// Optional description of changes
219
    pub description: Option<String>,
220
}
221
222
impl ConfigVersion {
223
    /// Create a new config version
224
0
    pub fn new(version: u64) -> Self {
225
0
        Self {
226
0
            version,
227
0
            timestamp: Utc::now(),
228
0
            description: None,
229
0
        }
230
0
    }
231
232
    /// Create a new config version with description
233
0
    pub fn with_description<S: Into<String>>(version: u64, description: S) -> Self {
234
0
        Self {
235
0
            version,
236
0
            timestamp: Utc::now(),
237
0
            description: Some(description.into()),
238
0
        }
239
0
    }
240
}
241
242
// TECHNICAL DEBT ELIMINATED - Use DateTime<Utc> directly instead of Timestamp alias
243
244
/// Timestamp type alias for consistency across the system
245
pub type Timestamp = DateTime<Utc>;
246
247
/// Request ID for tracing and correlation
248
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
249
pub struct RequestId(pub Uuid);
250
251
impl Default for RequestId {
252
    /// Create a default request ID with a new UUID
253
0
    fn default() -> Self {
254
0
        Self::new()
255
0
    }
256
}
257
258
impl RequestId {
259
    /// Generate a new random request ID
260
0
    pub fn new() -> Self {
261
0
        Self(Uuid::new_v4())
262
0
    }
263
264
    /// Create from UUID
265
0
    pub fn from_uuid(uuid: Uuid) -> Self {
266
0
        Self(uuid)
267
0
    }
268
269
    /// Get the inner UUID
270
0
    pub fn as_uuid(&self) -> Uuid {
271
0
        self.0
272
0
    }
273
}
274
275
// Default implementation is now in the derive macro above
276
277
// =============================================================================
278
// MARKET DATA EVENT TYPES (Consolidated from data and trading_engine crates)
279
// =============================================================================
280
281
/// Market data event types - CANONICAL DEFINITION
282
#[derive(Debug, Clone, Serialize, Deserialize)]
283
pub enum MarketDataEvent {
284
    /// Quote update (bid/ask)
285
    Quote(QuoteEvent),
286
    /// Trade execution
287
    Trade(TradeEvent),
288
    /// Aggregate trade data
289
    Aggregate(Aggregate),
290
    /// Bar/candle data
291
    Bar(BarEvent),
292
    /// Level 2 market data update
293
    Level2(Level2Update),
294
    /// Market status update
295
    Status(MarketStatus),
296
    /// Connection status updates
297
    ConnectionStatus(ConnectionEvent),
298
    /// Error events with details
299
    Error(ErrorEvent),
300
    /// Order book update
301
    OrderBook(OrderBookEvent),
302
    /// Level 2 order book snapshot
303
    OrderBookL2Snapshot(OrderBookSnapshot),
304
    /// Level 2 order book incremental update
305
    OrderBookL2Update(OrderBookUpdate),
306
}
307
308
/// Quote event structure - CANONICAL DEFINITION
309
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
310
pub struct QuoteEvent {
311
    /// Symbol
312
    pub symbol: String,
313
    /// Bid price
314
    pub bid: Option<Decimal>,
315
    /// Ask price
316
    pub ask: Option<Decimal>,
317
    /// Bid size
318
    pub bid_size: Option<Decimal>,
319
    /// Ask size
320
    pub ask_size: Option<Decimal>,
321
    /// Exchange
322
    pub exchange: Option<String>,
323
    /// Bid exchange
324
    pub bid_exchange: Option<String>,
325
    /// Ask exchange
326
    pub ask_exchange: Option<String>,
327
    /// Quote conditions
328
    pub conditions: Vec<String>,
329
    /// Timestamp
330
    pub timestamp: DateTime<Utc>,
331
    /// Sequence number
332
    pub sequence: u64,
333
}
334
335
impl QuoteEvent {
336
    /// Create a new quote event
337
    #[must_use]
338
0
    pub fn new(symbol: String, timestamp: DateTime<Utc>) -> Self {
339
0
        Self {
340
0
            symbol,
341
0
            bid: None,
342
0
            ask: None,
343
0
            bid_size: None,
344
0
            ask_size: None,
345
0
            exchange: None,
346
0
            bid_exchange: None,
347
0
            ask_exchange: None,
348
0
            conditions: Vec::new(),
349
0
            timestamp,
350
0
            sequence: 0,
351
0
        }
352
0
    }
353
354
    /// Set bid price and size
355
0
    pub fn with_bid(mut self, price: Decimal, size: Decimal) -> Self {
356
0
        self.bid = Some(price);
357
0
        self.bid_size = Some(size);
358
0
        self
359
0
    }
360
361
    /// Set ask price and size
362
0
    pub fn with_ask(mut self, price: Decimal, size: Decimal) -> Self {
363
0
        self.ask = Some(price);
364
0
        self.ask_size = Some(size);
365
0
        self
366
0
    }
367
368
    /// Set exchange
369
0
    pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
370
0
        self.exchange = Some(exchange.into());
371
0
        self
372
0
    }
373
374
    /// Set bid exchange
375
0
    pub fn with_bid_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
376
0
        self.bid_exchange = Some(exchange.into());
377
0
        self
378
0
    }
379
380
    /// Set ask exchange
381
0
    pub fn with_ask_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
382
0
        self.ask_exchange = Some(exchange.into());
383
0
        self
384
0
    }
385
386
    /// Add quote condition
387
0
    pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
388
0
        self.conditions.push(condition.into());
389
0
        self
390
0
    }
391
392
    /// Set sequence number
393
0
    pub fn with_sequence(mut self, sequence: u64) -> Self {
394
0
        self.sequence = sequence;
395
0
        self
396
0
    }
397
398
    /// Get mid price
399
0
    pub fn mid_price(&self) -> Option<Decimal> {
400
0
        match (self.bid, self.ask) {
401
0
            (Some(bid), Some(ask)) => Some((bid + ask) / Decimal::from(2)),
402
0
            _ => None,
403
        }
404
0
    }
405
406
    /// Get spread
407
0
    pub fn spread(&self) -> Option<Decimal> {
408
0
        match (self.bid, self.ask) {
409
0
            (Some(bid), Some(ask)) => Some(ask - bid),
410
0
            _ => None,
411
        }
412
0
    }
413
}
414
415
/// Trade event structure - CANONICAL DEFINITION
416
#[derive(Debug, Clone, Serialize, Deserialize)]
417
pub struct TradeEvent {
418
    /// Symbol
419
    pub symbol: String,
420
    /// Trade price
421
    pub price: Decimal,
422
    /// Trade size
423
    pub size: Decimal,
424
    /// Trade ID
425
    pub trade_id: Option<String>,
426
    /// Exchange
427
    pub exchange: Option<String>,
428
    /// Trade conditions
429
    pub conditions: Vec<String>,
430
    /// Timestamp
431
    pub timestamp: DateTime<Utc>,
432
    /// Sequence number
433
    pub sequence: u64,
434
}
435
436
impl TradeEvent {
437
    /// Create a new trade event
438
    #[must_use]
439
0
    pub fn new(symbol: String, price: Decimal, size: Decimal, timestamp: DateTime<Utc>) -> Self {
440
0
        Self {
441
0
            symbol,
442
0
            price,
443
0
            size,
444
0
            trade_id: None,
445
0
            exchange: None,
446
0
            conditions: Vec::new(),
447
0
            timestamp,
448
0
            sequence: 0,
449
0
        }
450
0
    }
451
452
    /// Set trade ID
453
0
    pub fn with_trade_id<S: Into<String>>(mut self, trade_id: S) -> Self {
454
0
        self.trade_id = Some(trade_id.into());
455
0
        self
456
0
    }
457
458
    /// Set exchange
459
0
    pub fn with_exchange<S: Into<String>>(mut self, exchange: S) -> Self {
460
0
        self.exchange = Some(exchange.into());
461
0
        self
462
0
    }
463
464
    /// Add trade condition
465
0
    pub fn with_condition<S: Into<String>>(mut self, condition: S) -> Self {
466
0
        self.conditions.push(condition.into());
467
0
        self
468
0
    }
469
470
    /// Set sequence number
471
0
    pub fn with_sequence(mut self, sequence: u64) -> Self {
472
0
        self.sequence = sequence;
473
0
        self
474
0
    }
475
476
    /// Get notional value
477
0
    pub fn notional_value(&self) -> Decimal {
478
0
        self.price * self.size
479
0
    }
480
}
481
482
/// Aggregate trade data
483
#[derive(Debug, Clone, Serialize, Deserialize)]
484
pub struct Aggregate {
485
    /// Symbol
486
    pub symbol: String,
487
    /// Open price
488
    pub open: Decimal,
489
    /// High price
490
    pub high: Decimal,
491
    /// Low price
492
    pub low: Decimal,
493
    /// Close price
494
    pub close: Decimal,
495
    /// Volume
496
    pub volume: Decimal,
497
    /// Volume weighted average price
498
    pub vwap: Option<Decimal>,
499
    /// Start timestamp
500
    pub start_timestamp: DateTime<Utc>,
501
    /// End timestamp
502
    pub end_timestamp: DateTime<Utc>,
503
}
504
505
/// Bar/candle event structure
506
#[derive(Debug, Clone, Serialize, Deserialize)]
507
pub struct BarEvent {
508
    /// Symbol
509
    pub symbol: String,
510
    /// Open price
511
    pub open: Decimal,
512
    /// High price
513
    pub high: Decimal,
514
    /// Low price
515
    pub low: Decimal,
516
    /// Close price
517
    pub close: Decimal,
518
    /// Volume
519
    pub volume: Decimal,
520
    /// Volume weighted average price
521
    pub vwap: Option<Decimal>,
522
    /// Start timestamp
523
    pub start_timestamp: DateTime<Utc>,
524
    /// End timestamp
525
    pub end_timestamp: DateTime<Utc>,
526
    /// Timeframe (e.g., "1m", "5m", "1h")
527
    pub timeframe: String,
528
}
529
530
/// Level 2 market data update
531
#[derive(Debug, Clone, Serialize, Deserialize)]
532
pub struct Level2Update {
533
    /// Symbol
534
    pub symbol: String,
535
    /// Bid levels
536
    pub bids: Vec<PriceLevel>,
537
    /// Ask levels
538
    pub asks: Vec<PriceLevel>,
539
    /// Timestamp
540
    pub timestamp: DateTime<Utc>,
541
}
542
543
/// Price level for order book
544
#[derive(Debug, Clone, Serialize, Deserialize)]
545
pub struct PriceLevel {
546
    /// Price
547
    pub price: Decimal,
548
    /// Size at this price level
549
    pub size: Decimal,
550
}
551
552
/// Order book snapshot from providers
553
#[derive(Debug, Clone, Serialize, Deserialize)]
554
pub struct OrderBookSnapshot {
555
    /// Symbol
556
    pub symbol: String,
557
    /// Bid levels (price, size) sorted by price descending
558
    pub bids: Vec<PriceLevel>,
559
    /// Ask levels (price, size) sorted by price ascending
560
    pub asks: Vec<PriceLevel>,
561
    /// Exchange
562
    pub exchange: String,
563
    /// Timestamp of snapshot
564
    pub timestamp: DateTime<Utc>,
565
    /// Sequence number
566
    pub sequence: u64,
567
}
568
569
/// Incremental order book update from providers
570
#[derive(Debug, Clone, Serialize, Deserialize)]
571
pub struct OrderBookUpdate {
572
    /// Symbol
573
    pub symbol: String,
574
    /// Changes to bid levels
575
    pub bid_changes: Vec<PriceLevelChange>,
576
    /// Changes to ask levels
577
    pub ask_changes: Vec<PriceLevelChange>,
578
    /// Exchange
579
    pub exchange: String,
580
    /// Timestamp of update
581
    pub timestamp: DateTime<Utc>,
582
    /// Sequence number
583
    pub sequence: u64,
584
}
585
586
/// Change to a price level
587
#[derive(Debug, Clone, Serialize, Deserialize)]
588
pub struct PriceLevelChange {
589
    /// Price level being modified
590
    pub price: Decimal,
591
    /// New size (0 = remove level)
592
    pub size: Decimal,
593
    /// Type of change
594
    pub change_type: PriceLevelChangeType,
595
    /// Side (bid or ask)
596
    pub side: OrderBookSide,
597
}
598
599
/// Type of price level change
600
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
601
pub enum PriceLevelChangeType {
602
    /// Add new price level
603
    Add,
604
    /// Update existing price level
605
    Update,
606
    /// Remove price level
607
    Delete,
608
}
609
610
/// Order book side
611
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
612
pub enum OrderBookSide {
613
    /// Bid side
614
    Bid,
615
    /// Ask side
616
    Ask,
617
}
618
619
/// Market status information
620
#[derive(Debug, Clone, Serialize, Deserialize)]
621
pub struct MarketStatus {
622
    /// Market
623
    pub market: String,
624
    /// Status (open, closed, early_hours, etc.)
625
    pub status: String,
626
    /// Timestamp
627
    pub timestamp: DateTime<Utc>,
628
}
629
630
/// Connection event for status updates
631
#[derive(Debug, Clone, Serialize, Deserialize)]
632
pub struct ConnectionEvent {
633
    /// Provider name
634
    pub provider: String,
635
    /// Connection status
636
    pub status: ConnectionStatus,
637
    /// Optional message
638
    pub message: Option<String>,
639
    /// Timestamp
640
    pub timestamp: DateTime<Utc>,
641
}
642
643
/// Connection status enumeration
644
/// Connection status for data providers and brokers
645
#[derive(Debug, Clone, Serialize, Deserialize)]
646
#[cfg_attr(feature = "database", derive(sqlx::Type))]
647
#[cfg_attr(
648
    feature = "database",
649
    sqlx(type_name = "connection_status", rename_all = "snake_case")
650
)]
651
pub enum ConnectionStatus {
652
    /// Successfully connected and operational
653
    Connected,
654
    /// Disconnected from the service
655
    Disconnected,
656
    /// Currently attempting to reconnect
657
    Reconnecting,
658
}
659
660
/// Error event structure
661
#[derive(Debug, Clone, Serialize, Deserialize)]
662
pub struct ErrorEvent {
663
    /// Provider name
664
    pub provider: String,
665
    /// Error message
666
    pub message: String,
667
    /// Error category
668
    pub category: ErrorCategory,
669
    /// Timestamp
670
    pub timestamp: DateTime<Utc>,
671
}
672
673
// ErrorCategory is imported from crate::error as CommonErrorCategory
674
675
/// Order book event
676
#[derive(Debug, Clone, Serialize, Deserialize)]
677
pub struct OrderBookEvent {
678
    /// Symbol
679
    pub symbol: String,
680
    /// Timestamp
681
    pub timestamp: DateTime<Utc>,
682
    /// Bid levels
683
    pub bids: Vec<(Price, Quantity)>,
684
    /// Ask levels
685
    pub asks: Vec<(Price, Quantity)>,
686
}
687
688
/// Data types for subscription
689
#[derive(Debug, Clone, Serialize, Deserialize)]
690
pub enum DataType {
691
    /// Real-time quotes
692
    Quotes,
693
    /// Real-time trades
694
    Trades,
695
    /// Aggregate/minute bars
696
    Aggregates,
697
    /// Level 2 order book
698
    Level2,
699
    /// Market status
700
    Status,
701
    /// Historical bars/aggregates
702
    Bars,
703
    /// Order book data
704
    OrderBook,
705
    /// Volume data
706
    Volume,
707
}
708
709
/// Market data subscription request
710
#[derive(Debug, Clone, Serialize, Deserialize)]
711
pub struct Subscription {
712
    /// Symbols to subscribe to
713
    pub symbols: Vec<String>,
714
    /// Data types to subscribe to
715
    pub data_types: Vec<DataType>,
716
    /// Exchange filter (optional)
717
    pub exchanges: Vec<String>,
718
}
719
720
impl MarketDataEvent {
721
    /// Get the symbol for any market data event
722
0
    pub fn symbol(&self) -> &str {
723
0
        match self {
724
0
            MarketDataEvent::Quote(q) => &q.symbol,
725
0
            MarketDataEvent::Trade(t) => &t.symbol,
726
0
            MarketDataEvent::Aggregate(a) => &a.symbol,
727
0
            MarketDataEvent::Bar(b) => &b.symbol,
728
0
            MarketDataEvent::Level2(l) => &l.symbol,
729
0
            MarketDataEvent::Status(s) => &s.market,
730
0
            MarketDataEvent::ConnectionStatus(_) => "",
731
0
            MarketDataEvent::Error(_) => "",
732
0
            MarketDataEvent::OrderBook(o) => &o.symbol,
733
0
            MarketDataEvent::OrderBookL2Snapshot(s) => &s.symbol,
734
0
            MarketDataEvent::OrderBookL2Update(u) => &u.symbol,
735
        }
736
0
    }
737
738
    /// Get the timestamp for any market data event
739
0
    pub fn timestamp(&self) -> Option<DateTime<Utc>> {
740
0
        match self {
741
0
            MarketDataEvent::Quote(q) => Some(q.timestamp),
742
0
            MarketDataEvent::Trade(t) => Some(t.timestamp),
743
0
            MarketDataEvent::Aggregate(a) => Some(a.end_timestamp),
744
0
            MarketDataEvent::Bar(b) => Some(b.end_timestamp),
745
0
            MarketDataEvent::Level2(l) => Some(l.timestamp),
746
0
            MarketDataEvent::Status(s) => Some(s.timestamp),
747
0
            MarketDataEvent::ConnectionStatus(c) => Some(c.timestamp),
748
0
            MarketDataEvent::Error(e) => Some(e.timestamp),
749
0
            MarketDataEvent::OrderBook(o) => Some(o.timestamp),
750
0
            MarketDataEvent::OrderBookL2Snapshot(s) => Some(s.timestamp),
751
0
            MarketDataEvent::OrderBookL2Update(u) => Some(u.timestamp),
752
        }
753
0
    }
754
}
755
impl fmt::Display for RequestId {
756
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
757
0
        write!(f, "{}", self.0)
758
0
    }
759
}
760
761
/// Connection information for services
762
#[derive(Debug, Clone, Serialize, Deserialize)]
763
pub struct ConnectionInfo {
764
    /// Host address
765
    pub host: String,
766
    /// Port number
767
    pub port: u16,
768
    /// Whether TLS is enabled
769
    pub tls: bool,
770
    /// Connection timeout in milliseconds
771
    pub timeout_ms: u64,
772
}
773
774
impl ConnectionInfo {
775
    /// Create new connection info
776
0
    pub fn new<S: Into<String>>(host: S, port: u16) -> Self {
777
0
        Self {
778
0
            host: host.into(),
779
0
            port,
780
0
            tls: false,
781
0
            timeout_ms: 5000,
782
0
        }
783
0
    }
784
785
    /// Enable TLS
786
0
    pub fn with_tls(mut self) -> Self {
787
0
        self.tls = true;
788
0
        self
789
0
    }
790
791
    /// Set timeout
792
0
    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
793
0
        self.timeout_ms = timeout_ms;
794
0
        self
795
0
    }
796
797
    /// Get connection URL
798
0
    pub fn url(&self) -> String {
799
0
        let scheme = if self.tls { "https" } else { "http" };
800
0
        format!("{}://{}:{}", scheme, self.host, self.port)
801
0
    }
802
}
803
804
/// Resource limits for services
805
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
806
pub struct ResourceLimits {
807
    /// Maximum memory usage in bytes
808
    pub max_memory_bytes: Option<u64>,
809
    /// Maximum CPU usage as percentage (0-100)
810
    pub max_cpu_percent: Option<f64>,
811
    /// Maximum number of open file descriptors
812
    pub max_file_descriptors: Option<u32>,
813
    /// Maximum number of network connections
814
    pub max_connections: Option<u32>,
815
}
816
817
// =============================================================================
818
// TRADING TYPES (Migrated from foxhunt-common-types)
819
// =============================================================================
820
821
/// Common error types for trading operations
822
///
823
/// This error type implements Send + Sync for use in async contexts
824
#[derive(thiserror::Error, Debug)]
825
pub enum CommonTypeError {
826
    /// Invalid price value
827
    #[error("Invalid price: {value} - {reason}")]
828
    InvalidPrice {
829
        /// The invalid price value as string
830
        value: String,
831
        /// Reason why the price is invalid
832
        reason: String,
833
    },
834
835
    /// Invalid quantity value
836
    #[error("Invalid quantity: {value} - {reason}")]
837
    InvalidQuantity {
838
        /// The invalid quantity value as string
839
        value: String,
840
        /// Reason why the quantity is invalid
841
        reason: String,
842
    },
843
844
    /// Invalid identifier
845
    #[error("Invalid {field}: {reason}")]
846
    InvalidIdentifier {
847
        /// The field name that contains the invalid identifier
848
        field: String,
849
        /// Reason why the identifier is invalid
850
        reason: String,
851
    },
852
853
    /// Validation error
854
    #[error("Validation error for {field}: {reason}")]
855
    ValidationError {
856
        /// The field name that failed validation
857
        field: String,
858
        /// Reason why the validation failed
859
        reason: String,
860
    },
861
862
    /// Conversion error
863
    #[error("Conversion error: {message}")]
864
    ConversionError {
865
        /// Detailed error message describing the conversion failure
866
        message: String,
867
    },
868
869
    /// I/O error
870
    #[error("I/O error: {0}")]
871
    IoError(#[from] std::io::Error),
872
873
    /// JSON serialization/deserialization error
874
    #[error("JSON error: {0}")]
875
    JsonError(#[from] serde_json::Error),
876
877
    /// Float parsing error
878
    #[error("Float parsing error: {0}")]
879
    ParseFloatError(#[from] std::num::ParseFloatError),
880
881
    /// Integer parsing error
882
    #[error("Integer parsing error: {0}")]
883
    ParseIntError(#[from] std::num::ParseIntError),
884
}
885
886
// Manual trait implementations for CommonTypeError
887
// (Cannot derive Clone, PartialEq, Eq, Serialize due to std::io::Error and serde_json::Error)
888
889
impl Clone for CommonTypeError {
890
    /// Clone the error, converting IO and JSON errors to conversion errors
891
0
    fn clone(&self) -> Self {
892
0
        match self {
893
0
            Self::InvalidPrice { value, reason } => Self::InvalidPrice {
894
0
                value: value.clone(),
895
0
                reason: reason.clone(),
896
0
            },
897
0
            Self::InvalidQuantity { value, reason } => Self::InvalidQuantity {
898
0
                value: value.clone(),
899
0
                reason: reason.clone(),
900
0
            },
901
0
            Self::InvalidIdentifier { field, reason } => Self::InvalidIdentifier {
902
0
                field: field.clone(),
903
0
                reason: reason.clone(),
904
0
            },
905
0
            Self::ValidationError { field, reason } => Self::ValidationError {
906
0
                field: field.clone(),
907
0
                reason: reason.clone(),
908
0
            },
909
0
            Self::ConversionError { message } => Self::ConversionError {
910
0
                message: message.clone(),
911
0
            },
912
            // Cannot clone std::io::Error or serde_json::Error, so create new instances
913
0
            Self::IoError(e) => Self::ConversionError {
914
0
                message: format!("I/O error: {}", e),
915
0
            },
916
0
            Self::JsonError(e) => Self::ConversionError {
917
0
                message: format!("JSON error: {}", e),
918
0
            },
919
0
            Self::ParseFloatError(e) => Self::ParseFloatError(e.clone()),
920
0
            Self::ParseIntError(e) => Self::ParseIntError(e.clone()),
921
        }
922
0
    }
923
}
924
impl PartialEq for CommonTypeError {
925
    /// Compare two errors for equality
926
0
    fn eq(&self, other: &Self) -> bool {
927
0
        match (self, other) {
928
            (
929
                Self::InvalidPrice {
930
0
                    value: v1,
931
0
                    reason: r1,
932
                },
933
                Self::InvalidPrice {
934
0
                    value: v2,
935
0
                    reason: r2,
936
                },
937
0
            ) => v1 == v2 && r1 == r2,
938
            (
939
                Self::InvalidQuantity {
940
0
                    value: v1,
941
0
                    reason: r1,
942
                },
943
                Self::InvalidQuantity {
944
0
                    value: v2,
945
0
                    reason: r2,
946
                },
947
0
            ) => v1 == v2 && r1 == r2,
948
            (
949
                Self::InvalidIdentifier {
950
0
                    field: f1,
951
0
                    reason: r1,
952
                },
953
                Self::InvalidIdentifier {
954
0
                    field: f2,
955
0
                    reason: r2,
956
                },
957
0
            ) => f1 == f2 && r1 == r2,
958
            (
959
                Self::ValidationError {
960
0
                    field: f1,
961
0
                    reason: r1,
962
                },
963
                Self::ValidationError {
964
0
                    field: f2,
965
0
                    reason: r2,
966
                },
967
0
            ) => f1 == f2 && r1 == r2,
968
0
            (Self::ConversionError { message: m1 }, Self::ConversionError { message: m2 }) => {
969
0
                m1 == m2
970
            },
971
0
            (Self::ParseFloatError(e1), Self::ParseFloatError(e2)) => e1 == e2,
972
0
            (Self::ParseIntError(e1), Self::ParseIntError(e2)) => e1 == e2,
973
            // std::io::Error and serde_json::Error don't implement PartialEq, so they're never equal
974
0
            (Self::IoError(_), Self::IoError(_)) => false,
975
0
            (Self::JsonError(_), Self::JsonError(_)) => false,
976
0
            _ => false,
977
        }
978
0
    }
979
}
980
981
impl Eq for CommonTypeError {}
982
983
// Note: Display is automatically implemented by thiserror::Error derive
984
// based on the #[error("...")] attributes on each variant
985
impl Serialize for CommonTypeError {
986
0
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
987
0
    where
988
0
        S: serde::Serializer,
989
    {
990
        use serde::ser::SerializeStruct;
991
0
        match self {
992
0
            Self::InvalidPrice { value, reason } => {
993
0
                let mut state = serializer.serialize_struct("InvalidPrice", 2)?;
994
0
                state.serialize_field("value", value)?;
995
0
                state.serialize_field("reason", reason)?;
996
0
                state.end()
997
            },
998
0
            Self::InvalidQuantity { value, reason } => {
999
0
                let mut state = serializer.serialize_struct("InvalidQuantity", 2)?;
1000
0
                state.serialize_field("value", value)?;
1001
0
                state.serialize_field("reason", reason)?;
1002
0
                state.end()
1003
            },
1004
0
            Self::InvalidIdentifier { field, reason } => {
1005
0
                let mut state = serializer.serialize_struct("InvalidIdentifier", 2)?;
1006
0
                state.serialize_field("field", field)?;
1007
0
                state.serialize_field("reason", reason)?;
1008
0
                state.end()
1009
            },
1010
0
            Self::ValidationError { field, reason } => {
1011
0
                let mut state = serializer.serialize_struct("ValidationError", 2)?;
1012
0
                state.serialize_field("field", field)?;
1013
0
                state.serialize_field("reason", reason)?;
1014
0
                state.end()
1015
            },
1016
0
            Self::ConversionError { message } => {
1017
0
                let mut state = serializer.serialize_struct("ConversionError", 1)?;
1018
0
                state.serialize_field("message", message)?;
1019
0
                state.end()
1020
            },
1021
0
            Self::IoError(e) => {
1022
0
                let mut state = serializer.serialize_struct("IoError", 1)?;
1023
0
                state.serialize_field("message", &format!("I/O error: {}", e))?;
1024
0
                state.end()
1025
            },
1026
0
            Self::JsonError(e) => {
1027
0
                let mut state = serializer.serialize_struct("JsonError", 1)?;
1028
0
                state.serialize_field("message", &format!("JSON error: {}", e))?;
1029
0
                state.end()
1030
            },
1031
0
            Self::ParseFloatError(e) => {
1032
0
                let mut state = serializer.serialize_struct("ParseFloatError", 1)?;
1033
0
                state.serialize_field("message", &format!("Float parsing error: {}", e))?;
1034
0
                state.end()
1035
            },
1036
0
            Self::ParseIntError(e) => {
1037
0
                let mut state = serializer.serialize_struct("ParseIntError", 1)?;
1038
0
                state.serialize_field("message", &format!("Integer parsing error: {}", e))?;
1039
0
                state.end()
1040
            },
1041
        }
1042
0
    }
1043
}
1044
1045
impl<'de> Deserialize<'de> for CommonTypeError {
1046
0
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1047
0
    where
1048
0
        D: serde::Deserializer<'de>,
1049
    {
1050
        // For deserialization, we'll convert everything to ConversionError since
1051
        // we can't reconstruct std::io::Error or serde_json::Error from serialized form
1052
        use serde::de::{MapAccess, Visitor};
1053
        use std::fmt;
1054
1055
        struct CommonTypeErrorVisitor;
1056
1057
        impl<'de> Visitor<'de> for CommonTypeErrorVisitor {
1058
            type Value = CommonTypeError;
1059
1060
0
            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1061
0
                formatter.write_str("a CommonTypeError")
1062
0
            }
1063
1064
0
            fn visit_map<V>(self, mut map: V) -> Result<CommonTypeError, V::Error>
1065
0
            where
1066
0
                V: MapAccess<'de>,
1067
            {
1068
                // For simplicity, deserialize everything as ConversionError
1069
0
                let mut message = String::new();
1070
0
                while let Some(key) = map.next_key::<String>()? {
1071
0
                    let value: serde_json::Value = map.next_value()?;
1072
0
                    if key == "message" {
1073
0
                        if let Some(msg) = value.as_str() {
1074
0
                            message = msg.to_string();
1075
0
                        }
1076
0
                    } else {
1077
0
                        message = format!("Deserialized error: {}: {}", key, value);
1078
0
                    }
1079
                }
1080
0
                if message.is_empty() {
1081
0
                    message = "Unknown deserialized error".to_string();
1082
0
                }
1083
0
                Ok(CommonTypeError::ConversionError { message })
1084
0
            }
1085
        }
1086
1087
0
        deserializer.deserialize_struct(
1088
            "CommonTypeError",
1089
0
            &["value", "reason", "field", "message"],
1090
0
            CommonTypeErrorVisitor,
1091
        )
1092
0
    }
1093
}
1094
1095
// =============================================================================
1096
// ORDER TYPES (Moved from trading_engine)
1097
// =============================================================================
1098
1099
/// Order type specifying execution behavior - CANONICAL DEFINITION
1100
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1101
#[non_exhaustive]
1102
pub enum OrderType {
1103
    /// Market order - executes immediately at current market price
1104
    Market,
1105
    /// Limit order - executes only at specified price or better
1106
    Limit,
1107
    /// Stop order - becomes market order when stop price is reached
1108
    Stop,
1109
    /// Stop-limit order - becomes limit order when stop price is reached
1110
    StopLimit,
1111
    /// Iceberg order - large order split into smaller visible portions
1112
    Iceberg,
1113
    /// Trailing stop order - stop price adjusts with favorable price movement
1114
    TrailingStop,
1115
    /// Hidden order - not displayed in order book
1116
    Hidden,
1117
}
1118
1119
impl fmt::Display for OrderType {
1120
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1121
0
        match self {
1122
0
            Self::Market => write!(f, "MARKET"),
1123
0
            Self::Limit => write!(f, "LIMIT"),
1124
0
            Self::Stop => write!(f, "STOP"),
1125
0
            Self::StopLimit => write!(f, "STOP_LIMIT"),
1126
0
            Self::Iceberg => write!(f, "ICEBERG"),
1127
0
            Self::TrailingStop => write!(f, "TRAILING_STOP"),
1128
0
            Self::Hidden => write!(f, "HIDDEN"),
1129
        }
1130
0
    }
1131
}
1132
1133
impl Default for OrderType {
1134
    /// Returns the default order type (Market)
1135
0
    fn default() -> Self {
1136
0
        Self::Market
1137
0
    }
1138
}
1139
1140
impl TryFrom<i32> for OrderType {
1141
    type Error = String;
1142
1143
0
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1144
0
        match value {
1145
0
            0 => Ok(OrderType::Market),
1146
0
            1 => Ok(OrderType::Limit),
1147
0
            2 => Ok(OrderType::Stop),
1148
0
            3 => Ok(OrderType::StopLimit),
1149
0
            4 => Ok(OrderType::Iceberg),
1150
0
            5 => Ok(OrderType::TrailingStop),
1151
0
            6 => Ok(OrderType::Hidden),
1152
0
            _ => Err(format!("Invalid OrderType: {}", value)),
1153
        }
1154
0
    }
1155
}
1156
1157
/// Supported broker types - CANONICAL DEFINITION
1158
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1159
pub enum BrokerType {
1160
    /// Interactive Brokers TWS/API
1161
    InteractiveBrokers,
1162
    /// IC Markets FIX API
1163
    ICMarkets,
1164
    /// Paper trading simulation
1165
    PaperTrading,
1166
    /// Demo/Test broker
1167
    Demo,
1168
}
1169
1170
impl Default for BrokerType {
1171
    /// Returns the default broker type (InteractiveBrokers)
1172
0
    fn default() -> Self {
1173
0
        Self::InteractiveBrokers
1174
0
    }
1175
}
1176
1177
/// Order status throughout its lifecycle - CANONICAL DEFINITION
1178
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1179
#[non_exhaustive]
1180
pub enum OrderStatus {
1181
    /// Order has been created but not yet submitted to broker
1182
    Created,
1183
    /// Order has been submitted to broker for execution
1184
    Submitted,
1185
    /// Order has been partially executed with remaining quantity
1186
    PartiallyFilled,
1187
    /// Order has been completely executed
1188
    Filled,
1189
    /// Order was rejected by broker or exchange
1190
    Rejected,
1191
    /// Order was cancelled by user or system
1192
    Cancelled,
1193
    /// New order accepted by broker
1194
    New,
1195
    /// Order expired due to time restrictions
1196
    Expired,
1197
    /// Order is pending broker acceptance
1198
    Pending,
1199
    /// Order is actively working in the market
1200
    Working,
1201
    /// Order status is unknown or not yet determined
1202
    Unknown,
1203
    /// Order is temporarily suspended
1204
    Suspended,
1205
    /// Order cancellation is pending
1206
    PendingCancel,
1207
    /// Order modification is pending
1208
    PendingReplace,
1209
}
1210
impl fmt::Display for OrderStatus {
1211
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1212
0
        match self {
1213
0
            Self::Created => write!(f, "CREATED"),
1214
0
            Self::Submitted => write!(f, "SUBMITTED"),
1215
0
            Self::PartiallyFilled => write!(f, "PARTIALLY_FILLED"),
1216
0
            Self::Filled => write!(f, "FILLED"),
1217
0
            Self::Rejected => write!(f, "REJECTED"),
1218
0
            Self::Cancelled => write!(f, "CANCELLED"),
1219
0
            Self::New => write!(f, "NEW"),
1220
0
            Self::Expired => write!(f, "EXPIRED"),
1221
0
            Self::Pending => write!(f, "PENDING"),
1222
0
            Self::Working => write!(f, "WORKING"),
1223
0
            Self::Unknown => write!(f, "UNKNOWN"),
1224
0
            Self::Suspended => write!(f, "SUSPENDED"),
1225
0
            Self::PendingCancel => write!(f, "PENDING_CANCEL"),
1226
0
            Self::PendingReplace => write!(f, "PENDING_REPLACE"),
1227
        }
1228
0
    }
1229
}
1230
1231
impl Default for OrderStatus {
1232
    /// Returns the default order status (Created)
1233
0
    fn default() -> Self {
1234
0
        Self::Created
1235
0
    }
1236
}
1237
1238
impl TryFrom<i32> for OrderStatus {
1239
    type Error = String;
1240
1241
0
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1242
0
        match value {
1243
0
            0 => Ok(OrderStatus::Created),
1244
0
            1 => Ok(OrderStatus::Submitted),
1245
0
            2 => Ok(OrderStatus::PartiallyFilled),
1246
0
            3 => Ok(OrderStatus::Filled),
1247
0
            4 => Ok(OrderStatus::Rejected),
1248
0
            5 => Ok(OrderStatus::Cancelled),
1249
0
            6 => Ok(OrderStatus::New),
1250
0
            7 => Ok(OrderStatus::Expired),
1251
0
            8 => Ok(OrderStatus::Pending),
1252
0
            9 => Ok(OrderStatus::Working),
1253
0
            10 => Ok(OrderStatus::Unknown),
1254
0
            11 => Ok(OrderStatus::Suspended),
1255
0
            12 => Ok(OrderStatus::PendingCancel),
1256
0
            13 => Ok(OrderStatus::PendingReplace),
1257
0
            _ => Err(format!("Invalid OrderStatus: {}", value)),
1258
        }
1259
0
    }
1260
}
1261
1262
/// Order side - whether the order is a buy or sell - CANONICAL DEFINITION
1263
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1264
pub enum OrderSide {
1265
    /// Buy order - purchasing securities
1266
    Buy,
1267
    /// Sell order - selling securities
1268
    Sell,
1269
}
1270
1271
impl fmt::Display for OrderSide {
1272
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1273
0
        match self {
1274
0
            Self::Buy => write!(f, "BUY"),
1275
0
            Self::Sell => write!(f, "SELL"),
1276
        }
1277
0
    }
1278
}
1279
1280
impl Default for OrderSide {
1281
    /// Returns the default order side (Buy)
1282
0
    fn default() -> Self {
1283
0
        Self::Buy
1284
0
    }
1285
}
1286
1287
impl TryFrom<i32> for OrderSide {
1288
    type Error = String;
1289
1290
0
    fn try_from(value: i32) -> Result<Self, Self::Error> {
1291
0
        match value {
1292
0
            0 => Ok(OrderSide::Buy),
1293
0
            1 => Ok(OrderSide::Sell),
1294
0
            _ => Err(format!("Invalid OrderSide: {}", value)),
1295
        }
1296
0
    }
1297
}
1298
1299
// REMOVED: Side alias - use OrderSide directly
1300
1301
/// Currency enumeration - CANONICAL DEFINITION
1302
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1303
#[cfg_attr(feature = "database", derive(sqlx::Type))]
1304
pub enum Currency {
1305
    /// US Dollar
1306
    USD,
1307
    /// Euro
1308
    EUR,
1309
    /// British Pound Sterling
1310
    GBP,
1311
    /// Japanese Yen
1312
    JPY,
1313
    /// Swiss Franc
1314
    CHF,
1315
    /// Canadian Dollar
1316
    CAD,
1317
    /// Australian Dollar
1318
    AUD,
1319
    /// New Zealand Dollar
1320
    NZD,
1321
    /// Bitcoin
1322
    BTC,
1323
    /// Ethereum
1324
    ETH,
1325
}
1326
1327
impl fmt::Display for Currency {
1328
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1329
0
        match self {
1330
0
            Self::USD => write!(f, "USD"),
1331
0
            Self::EUR => write!(f, "EUR"),
1332
0
            Self::GBP => write!(f, "GBP"),
1333
0
            Self::JPY => write!(f, "JPY"),
1334
0
            Self::CHF => write!(f, "CHF"),
1335
0
            Self::CAD => write!(f, "CAD"),
1336
0
            Self::AUD => write!(f, "AUD"),
1337
0
            Self::NZD => write!(f, "NZD"),
1338
0
            Self::BTC => write!(f, "BTC"),
1339
0
            Self::ETH => write!(f, "ETH"),
1340
        }
1341
0
    }
1342
}
1343
1344
impl Default for Currency {
1345
    /// Returns the default currency (USD)
1346
0
    fn default() -> Self {
1347
0
        Self::USD
1348
0
    }
1349
}
1350
1351
/// Time in force enumeration - CANONICAL DEFINITION
1352
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1353
pub enum TimeInForce {
1354
    /// Order is valid for the current trading day only
1355
    Day,
1356
    /// Order remains active until explicitly cancelled
1357
    GoodTillCancel,
1358
    /// Order must be executed immediately or cancelled
1359
    ImmediateOrCancel,
1360
    /// Order must be executed completely or cancelled
1361
    FillOrKill,
1362
}
1363
1364
impl fmt::Display for TimeInForce {
1365
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1366
0
        match self {
1367
0
            Self::Day => write!(f, "DAY"),
1368
0
            Self::GoodTillCancel => write!(f, "GTC"),
1369
0
            Self::ImmediateOrCancel => write!(f, "IOC"),
1370
0
            Self::FillOrKill => write!(f, "FOK"),
1371
        }
1372
0
    }
1373
}
1374
1375
impl Default for TimeInForce {
1376
    /// Returns the default time in force (Day)
1377
4
    fn default() -> Self {
1378
4
        Self::Day
1379
4
    }
1380
}
1381
1382
// =============================================================================
1383
// CORE ID TYPES (MIGRATED FROM TRADING_ENGINE)
1384
// =============================================================================
1385
1386
// Duplicate TradeId removed - using definition from line 1008
1387
1388
/// Event identifier for tracking system events
1389
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1390
pub struct EventId(String);
1391
1392
impl EventId {
1393
    /// Create a new random event ID
1394
0
    pub fn new() -> Self {
1395
        use uuid::Uuid;
1396
0
        Self(Uuid::new_v4().to_string())
1397
0
    }
1398
1399
    /// Create an event ID from a string, generating new if empty
1400
0
    pub fn from_string<S: Into<String>>(id: S) -> Self {
1401
0
        let id = id.into();
1402
0
        if id.is_empty() {
1403
0
            Self::new() // Generate new ID if empty
1404
        } else {
1405
0
            Self(id)
1406
        }
1407
0
    }
1408
1409
    /// Get the string value of the event ID
1410
0
    pub fn value(&self) -> &str {
1411
0
        &self.0
1412
0
    }
1413
}
1414
1415
impl fmt::Display for EventId {
1416
    /// Format the event ID for display
1417
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1418
0
        write!(f, "{}", self.0)
1419
0
    }
1420
}
1421
1422
impl From<String> for EventId {
1423
    /// Create an EventId from a String
1424
0
    fn from(s: String) -> Self {
1425
0
        Self(s)
1426
0
    }
1427
}
1428
1429
impl Default for EventId {
1430
    /// Create a default EventId with a new UUID
1431
0
    fn default() -> Self {
1432
0
        Self::new()
1433
0
    }
1434
}
1435
1436
/// Fill identifier with validation
1437
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1438
pub struct FillId(String);
1439
1440
impl FillId {
1441
    /// Create a new fill ID with validation
1442
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1443
0
        let id = id.into();
1444
0
        if id.is_empty() {
1445
0
            return Err(CommonTypeError::ValidationError {
1446
0
                field: "fill_id".to_owned(),
1447
0
                reason: "Fill ID cannot be empty".to_owned(),
1448
0
            });
1449
0
        }
1450
0
        Ok(Self(id))
1451
0
    }
1452
1453
    /// Get the fill ID as a string slice
1454
0
    pub fn as_str(&self) -> &str {
1455
0
        &self.0
1456
0
    }
1457
    /// Convert the fill ID into an owned string
1458
    /// Convert the execution ID into an owned string
1459
    /// Convert execution ID into owned string
1460
0
    pub fn into_string(self) -> String {
1461
0
        self.0
1462
0
    }
1463
}
1464
1465
impl fmt::Display for FillId {
1466
    /// Format the fill ID for display
1467
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1468
0
        write!(f, "{}", self.0)
1469
0
    }
1470
}
1471
1472
/// Aggregate identifier with validation
1473
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1474
pub struct AggregateId(String);
1475
1476
impl AggregateId {
1477
    /// Create a new aggregate ID with validation
1478
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1479
0
        let id = id.into();
1480
0
        if id.is_empty() {
1481
0
            return Err(CommonTypeError::ValidationError {
1482
0
                field: "aggregate_id".to_owned(),
1483
0
                reason: "Aggregate ID cannot be empty".to_owned(),
1484
0
            });
1485
0
        }
1486
0
        Ok(Self(id))
1487
0
    }
1488
1489
    /// Get the aggregate ID as a string slice
1490
0
    pub fn as_str(&self) -> &str {
1491
0
        &self.0
1492
0
    }
1493
    /// Convert the aggregate ID into an owned string
1494
0
    pub fn into_string(self) -> String {
1495
0
        self.0
1496
0
    }
1497
}
1498
1499
impl fmt::Display for AggregateId {
1500
    /// Format the aggregate ID for display
1501
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1502
0
        write!(f, "{}", self.0)
1503
0
    }
1504
}
1505
1506
/// Asset identifier with validation
1507
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1508
pub struct AssetId(String);
1509
1510
impl AssetId {
1511
    /// Create a new asset ID with validation
1512
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1513
0
        let id = id.into();
1514
0
        if id.is_empty() {
1515
0
            return Err(CommonTypeError::ValidationError {
1516
0
                field: "asset_id".to_owned(),
1517
0
                reason: "Asset ID cannot be empty".to_owned(),
1518
0
            });
1519
0
        }
1520
0
        Ok(Self(id))
1521
0
    }
1522
1523
    /// Get the asset ID as a string slice
1524
0
    pub fn as_str(&self) -> &str {
1525
0
        &self.0
1526
0
    }
1527
    /// Convert the asset ID into an owned string
1528
0
    pub fn into_string(self) -> String {
1529
0
        self.0
1530
0
    }
1531
}
1532
1533
impl fmt::Display for AssetId {
1534
    /// Format the asset ID for display
1535
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1536
0
        write!(f, "{}", self.0)
1537
0
    }
1538
}
1539
1540
/// Client identifier with validation
1541
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1542
pub struct ClientId(String);
1543
1544
impl ClientId {
1545
    /// Create a new client ID with validation
1546
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
1547
0
        let id = id.into();
1548
0
        if id.is_empty() {
1549
0
            return Err(CommonTypeError::ValidationError {
1550
0
                field: "client_id".to_owned(),
1551
0
                reason: "Client ID cannot be empty".to_owned(),
1552
0
            });
1553
0
        }
1554
0
        Ok(Self(id))
1555
0
    }
1556
1557
    /// Get the client ID as a string slice
1558
0
    pub fn as_str(&self) -> &str {
1559
0
        &self.0
1560
0
    }
1561
    /// Convert the client ID into an owned string
1562
0
    pub fn into_string(self) -> String {
1563
0
        self.0
1564
0
    }
1565
}
1566
1567
impl fmt::Display for ClientId {
1568
    /// Format the client ID for display
1569
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1570
0
        write!(f, "{}", self.0)
1571
0
    }
1572
}
1573
1574
// =============================================================================
1575
// CORE TRADING TYPES - MIGRATED FROM TRADING_ENGINE
1576
// =============================================================================
1577
1578
/// Canonical Order struct - UNIFIED DEFINITION based on Agent 1's comprehensive analysis
1579
/// This represents the single source of truth for Order across all services
1580
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1581
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
1582
pub struct Order {
1583
    // Core Identity
1584
    /// Unique order identifier
1585
    pub id: OrderId,
1586
    /// Client-provided order identifier
1587
    pub client_order_id: Option<String>,
1588
    /// Broker-assigned order identifier
1589
    pub broker_order_id: Option<String>,
1590
    /// Account identifier for the order
1591
    pub account_id: Option<String>,
1592
1593
    // Trading Details
1594
    /// Trading symbol for the order
1595
    pub symbol: Symbol,
1596
    /// Order side (buy or sell)
1597
    pub side: OrderSide,
1598
    /// Type of order (market, limit, etc.)
1599
    pub order_type: OrderType,
1600
    /// Current status of the order
1601
    pub status: OrderStatus,
1602
    /// Time in force policy
1603
    pub time_in_force: TimeInForce,
1604
1605
    // Quantities & Pricing
1606
    /// Total order quantity
1607
    pub quantity: Quantity,
1608
    /// Limit price for the order
1609
    pub price: Option<Price>,
1610
    /// Stop price for stop orders
1611
    pub stop_price: Option<Price>,
1612
    /// Quantity that has been filled
1613
    pub filled_quantity: Quantity,
1614
    /// Remaining quantity to be filled
1615
    pub remaining_quantity: Quantity,
1616
    /// Average execution price
1617
    pub average_price: Option<Price>,
1618
    /// Alias for average_price for database compatibility
1619
    pub avg_fill_price: Option<Price>,
1620
1621
    // Strategy Fields (from Agent 1)
1622
    /// Parent order ID for iceberg/algo orders
1623
    pub parent_id: Option<String>,
1624
    /// Execution algorithm name
1625
    pub execution_algorithm: Option<String>,
1626
    /// Execution algorithm parameters stored as JSON
1627
    pub execution_params: Value,
1628
1629
    // Risk Management (from Agent 1)
1630
    /// Stop loss price for risk management
1631
    pub stop_loss: Option<Price>,
1632
    /// Take profit price for profit taking
1633
    pub take_profit: Option<Price>,
1634
1635
    // Timestamps
1636
    /// Order creation timestamp
1637
    pub created_at: HftTimestamp,
1638
    /// Last update timestamp
1639
    pub updated_at: Option<HftTimestamp>,
1640
    /// Order expiration timestamp
1641
    pub expires_at: Option<HftTimestamp>,
1642
1643
    // Extensibility
1644
    /// Additional order metadata stored as JSON
1645
    pub metadata: Value,
1646
}
1647
1648
impl Order {
1649
    /// Create a new order with canonical fields
1650
4
    pub fn new(
1651
4
        symbol: Symbol,
1652
4
        side: OrderSide,
1653
4
        quantity: Quantity,
1654
4
        price: Option<Price>,
1655
4
        order_type: OrderType,
1656
4
    ) -> Self {
1657
4
        let now = HftTimestamp::now_or_zero();
1658
4
        Self {
1659
4
            // Core Identity
1660
4
            id: OrderId::new(),
1661
4
            client_order_id: None,
1662
4
            broker_order_id: None,
1663
4
            account_id: None,
1664
4
1665
4
            // Trading Details
1666
4
            symbol,
1667
4
            side,
1668
4
            order_type,
1669
4
            status: OrderStatus::Created,
1670
4
            time_in_force: TimeInForce::default(),
1671
4
1672
4
            // Quantities & Pricing
1673
4
            quantity,
1674
4
            price,
1675
4
            stop_price: None,
1676
4
            filled_quantity: Quantity::ZERO,
1677
4
            remaining_quantity: quantity,
1678
4
            average_price: None,
1679
4
            avg_fill_price: None, // Database compatibility alias
1680
4
1681
4
            // Strategy Fields
1682
4
            parent_id: None,
1683
4
            execution_algorithm: None,
1684
4
            execution_params: serde_json::json!({}),
1685
4
1686
4
            // Risk Management
1687
4
            stop_loss: None,
1688
4
            take_profit: None,
1689
4
1690
4
            // Timestamps
1691
4
            created_at: now,
1692
4
            updated_at: None,
1693
4
            expires_at: None,
1694
4
1695
4
            // Extensibility
1696
4
            metadata: serde_json::json!({}),
1697
4
        }
1698
4
    }
1699
1700
    /// Check if the order is fully filled
1701
0
    pub fn is_filled(&self) -> bool {
1702
0
        self.filled_quantity == self.quantity
1703
0
    }
1704
1705
    /// Check if the order is partially filled
1706
0
    pub fn is_partially_filled(&self) -> bool {
1707
0
        self.filled_quantity > Quantity::ZERO && self.filled_quantity < self.quantity
1708
0
    }
1709
1710
    /// Calculate fill percentage
1711
0
    pub fn fill_percentage(&self) -> f64 {
1712
0
        if self.quantity.is_zero() {
1713
0
            0.0
1714
        } else {
1715
0
            (self.filled_quantity.to_f64() / self.quantity.to_f64()) * 100.0
1716
        }
1717
0
    }
1718
1719
    /// Set client order ID for tracking
1720
0
    pub fn with_client_order_id(mut self, client_order_id: String) -> Self {
1721
0
        self.client_order_id = Some(client_order_id);
1722
0
        self
1723
0
    }
1724
1725
    /// Set account ID
1726
4
    pub fn with_account_id(mut self, account_id: String) -> Self {
1727
4
        self.account_id = Some(account_id);
1728
4
        self
1729
4
    }
1730
1731
    /// Set time in force
1732
0
    pub fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self {
1733
0
        self.time_in_force = time_in_force;
1734
0
        self
1735
0
    }
1736
1737
    /// Set stop price
1738
0
    pub fn with_stop_price(mut self, stop_price: Price) -> Self {
1739
0
        self.stop_price = Some(stop_price);
1740
0
        self
1741
0
    }
1742
1743
    /// Set execution algorithm
1744
0
    pub fn with_execution_algorithm(mut self, algorithm: String) -> Self {
1745
0
        self.execution_algorithm = Some(algorithm);
1746
0
        self
1747
0
    }
1748
1749
    /// Add execution parameter
1750
0
    pub fn with_execution_param(mut self, key: String, value: f64) -> Self {
1751
0
        if let Some(obj) = self.execution_params.as_object_mut() {
1752
0
            obj.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
1753
0
        } else {
1754
0
            let mut map = serde_json::Map::new();
1755
0
            map.insert(key, serde_json::to_value(value).unwrap_or(Value::Null));
1756
0
            self.execution_params = Value::Object(map);
1757
0
        }
1758
0
        self
1759
0
    }
1760
1761
    /// Set stop loss
1762
0
    pub fn with_stop_loss(mut self, stop_loss: Price) -> Self {
1763
0
        self.stop_loss = Some(stop_loss);
1764
0
        self
1765
0
    }
1766
1767
    /// Set take profit
1768
0
    pub fn with_take_profit(mut self, take_profit: Price) -> Self {
1769
0
        self.take_profit = Some(take_profit);
1770
0
        self
1771
0
    }
1772
1773
    /// Add metadata
1774
0
    pub fn with_metadata(mut self, key: String, value: String) -> Self {
1775
0
        if let Some(obj) = self.metadata.as_object_mut() {
1776
0
            obj.insert(key, Value::String(value));
1777
0
        } else {
1778
0
            let mut map = serde_json::Map::new();
1779
0
            map.insert(key, Value::String(value));
1780
0
            self.metadata = Value::Object(map);
1781
0
        }
1782
0
        self
1783
0
    }
1784
1785
    /// Update order status and timestamp
1786
0
    pub fn update_status(&mut self, status: OrderStatus) {
1787
0
        self.status = status;
1788
0
        self.updated_at = Some(HftTimestamp::now_or_zero());
1789
0
    }
1790
1791
    /// Fill order with given quantity and price
1792
0
    pub fn fill(
1793
0
        &mut self,
1794
0
        fill_quantity: Quantity,
1795
0
        fill_price: Price,
1796
0
    ) -> Result<(), CommonTypeError> {
1797
0
        if self.filled_quantity + fill_quantity > self.quantity {
1798
0
            return Err(CommonTypeError::ValidationError {
1799
0
                field: "fill_quantity".to_string(),
1800
0
                reason: "Fill quantity exceeds remaining quantity".to_string(),
1801
0
            });
1802
0
        }
1803
1804
        // Update filled quantity
1805
0
        let previous_filled = self.filled_quantity;
1806
0
        self.filled_quantity = self.filled_quantity + fill_quantity;
1807
0
        self.remaining_quantity = self.quantity - self.filled_quantity;
1808
1809
        // Update average price
1810
0
        if let Some(avg_price) = self.average_price {
1811
0
            let total_value = avg_price.to_f64() * previous_filled.to_f64()
1812
0
                + fill_price.to_f64() * fill_quantity.to_f64();
1813
0
            let new_avg = Some(
1814
0
                Price::from_f64(total_value / self.filled_quantity.to_f64()).unwrap_or(fill_price),
1815
0
            );
1816
0
            self.average_price = new_avg;
1817
0
            self.avg_fill_price = new_avg; // Keep in sync
1818
0
        } else {
1819
0
            self.average_price = Some(fill_price);
1820
0
            self.avg_fill_price = Some(fill_price); // Keep in sync
1821
0
        }
1822
1823
        // Update status
1824
0
        if self.is_filled() {
1825
0
            self.update_status(OrderStatus::Filled);
1826
0
        } else {
1827
0
            self.update_status(OrderStatus::PartiallyFilled);
1828
0
        }
1829
1830
0
        Ok(())
1831
0
    }
1832
1833
    /// Create a limit order - convenience constructor
1834
0
    pub fn limit(symbol: Symbol, side: OrderSide, quantity: Quantity, price: Price) -> Self {
1835
0
        Self::new(symbol, side, quantity, Some(price), OrderType::Limit)
1836
0
    }
1837
1838
    /// Create a market order - convenience constructor
1839
0
    pub fn market(symbol: Symbol, side: OrderSide, quantity: Quantity) -> Self {
1840
0
        Self::new(symbol, side, quantity, None, OrderType::Market)
1841
0
    }
1842
1843
    /// Get symbol hash for performance-critical operations
1844
0
    pub fn symbol_hash(&self) -> i64 {
1845
        use std::collections::hash_map::DefaultHasher;
1846
        use std::hash::{Hash, Hasher};
1847
1848
0
        let mut hasher = DefaultHasher::new();
1849
0
        self.symbol.as_str().hash(&mut hasher);
1850
0
        hasher.finish() as i64
1851
0
    }
1852
1853
    /// Get order timestamp
1854
0
    pub fn timestamp(&self) -> HftTimestamp {
1855
0
        self.created_at
1856
0
    }
1857
}
1858
1859
impl Default for Order {
1860
0
    fn default() -> Self {
1861
0
        Self {
1862
0
            id: OrderId::new(),
1863
0
            client_order_id: None,
1864
0
            broker_order_id: None,
1865
0
            account_id: None,
1866
0
1867
0
            symbol: Symbol::from("DEFAULT"),
1868
0
            side: OrderSide::Buy,
1869
0
            order_type: OrderType::Market,
1870
0
            status: OrderStatus::Created,
1871
0
            time_in_force: TimeInForce::Day,
1872
0
1873
0
            quantity: Quantity::ONE,
1874
0
            price: None,
1875
0
            stop_price: None,
1876
0
            filled_quantity: Quantity::ZERO,
1877
0
            remaining_quantity: Quantity::ONE,
1878
0
            average_price: None,
1879
0
            avg_fill_price: None,
1880
0
1881
0
            parent_id: None,
1882
0
            execution_algorithm: None,
1883
0
            execution_params: serde_json::json!({}),
1884
0
1885
0
            stop_loss: None,
1886
0
            take_profit: None,
1887
0
1888
0
            created_at: HftTimestamp::now().unwrap_or(HftTimestamp { nanos: 0 }),
1889
0
            updated_at: None,
1890
0
            expires_at: None,
1891
0
1892
0
            metadata: serde_json::json!({}),
1893
0
        }
1894
0
    }
1895
}
1896
1897
/// Represents a trading position - CANONICAL DEFINITION
1898
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1899
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
1900
pub struct Position {
1901
    /// Unique position identifier
1902
    pub id: Uuid,
1903
1904
    /// Trading symbol
1905
    pub symbol: String,
1906
1907
    /// Position quantity (positive for long, negative for short)
1908
    pub quantity: Decimal,
1909
1910
    /// Average entry price
1911
    pub avg_price: Decimal,
1912
1913
    /// Average cost per share
1914
    pub avg_cost: Decimal,
1915
1916
    /// Cost basis for tax calculations
1917
    pub basis: Decimal,
1918
1919
    /// Average entry price
1920
    pub average_price: Decimal,
1921
1922
    /// Market value of position
1923
    pub market_value: Decimal,
1924
1925
    /// Unrealized P&L
1926
    pub unrealized_pnl: Decimal,
1927
1928
    /// Realized P&L
1929
    pub realized_pnl: Decimal,
1930
1931
    /// Position creation timestamp
1932
    pub created_at: DateTime<Utc>,
1933
1934
    /// Last update timestamp
1935
    pub updated_at: DateTime<Utc>,
1936
1937
    /// Last updated timestamp
1938
    pub last_updated: DateTime<Utc>,
1939
1940
    /// Current market price (for P&L calculation)
1941
    pub current_price: Option<Decimal>,
1942
1943
    /// Position size in base currency
1944
    pub notional_value: Decimal,
1945
1946
    /// Margin requirement
1947
    pub margin_requirement: Decimal,
1948
}
1949
1950
impl Position {
1951
    /// Create a new position
1952
0
    pub fn new(symbol: String, quantity: Decimal, avg_price: Decimal) -> Self {
1953
0
        let now = Utc::now();
1954
0
        let notional_value = quantity.abs() * avg_price;
1955
1956
0
        Self {
1957
0
            id: Uuid::new_v4(),
1958
0
            symbol,
1959
0
            quantity,
1960
0
            avg_price,
1961
0
            avg_cost: avg_price, // Keep avg_cost synchronized with avg_price
1962
0
            basis: quantity * avg_price, // Cost basis calculation
1963
0
            average_price: avg_price, // Same as avg_price for compatibility
1964
0
            market_value: notional_value, // Initialize market value to notional value
1965
0
            unrealized_pnl: Decimal::ZERO,
1966
0
            realized_pnl: Decimal::ZERO,
1967
0
            created_at: now,
1968
0
            updated_at: now,
1969
0
            last_updated: now, // Same as updated_at for compatibility
1970
0
            current_price: None,
1971
0
            notional_value,
1972
0
            margin_requirement: notional_value
1973
0
                * Decimal::from_str_exact("0.02").unwrap_or(Decimal::ZERO), // 2% margin
1974
0
        }
1975
0
    }
1976
1977
    /// Check if position is long
1978
0
    pub fn is_long(&self) -> bool {
1979
0
        self.quantity > Decimal::ZERO
1980
0
    }
1981
1982
    /// Check if position is short
1983
0
    pub fn is_short(&self) -> bool {
1984
0
        self.quantity < Decimal::ZERO
1985
0
    }
1986
1987
    /// Calculate unrealized P&L based on current price
1988
0
    pub fn calculate_unrealized_pnl(&mut self, current_price: Decimal) {
1989
0
        self.current_price = Some(current_price);
1990
0
        self.market_value = self.quantity.abs() * current_price;
1991
        // For both long and short: quantity * (current_price - avg_price)
1992
0
        self.unrealized_pnl = self.quantity * (current_price - self.avg_price);
1993
0
        let now = Utc::now();
1994
0
        self.updated_at = now;
1995
0
        self.last_updated = now; // Keep alias synchronized
1996
0
    }
1997
1998
    /// Get total P&L (realized + unrealized)
1999
0
    pub fn total_pnl(&self) -> Decimal {
2000
0
        self.realized_pnl + self.unrealized_pnl
2001
0
    }
2002
2003
    /// Calculate return on investment percentage
2004
0
    pub fn roi_percentage(&self) -> Decimal {
2005
0
        if self.notional_value.is_zero() {
2006
0
            Decimal::ZERO
2007
        } else {
2008
0
            self.total_pnl() / self.notional_value * Decimal::from(100)
2009
        }
2010
0
    }
2011
}
2012
2013
/// Represents a trade execution - CANONICAL DEFINITION
2014
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2015
#[cfg_attr(feature = "database", derive(sqlx::FromRow))]
2016
pub struct Execution {
2017
    /// Unique execution identifier
2018
    pub id: Uuid,
2019
2020
    /// Related order ID
2021
    pub order_id: Uuid,
2022
2023
    /// Trading symbol
2024
    pub symbol: String,
2025
2026
    /// Executed quantity
2027
    pub quantity: Decimal,
2028
2029
    /// Execution price
2030
    pub price: Decimal,
2031
2032
    /// Execution side
2033
    pub side: OrderSide,
2034
2035
    /// Trading fees
2036
    pub fees: Decimal,
2037
2038
    /// Fee currency
2039
    pub fee_currency: String,
2040
2041
    /// Execution timestamp
2042
    pub executed_at: DateTime<Utc>,
2043
2044
    /// Execution timestamp
2045
    pub timestamp: DateTime<Utc>,
2046
2047
    /// Symbol hash for performance
2048
    pub symbol_hash: i64,
2049
2050
    /// Broker execution ID
2051
    pub broker_execution_id: Option<String>,
2052
2053
    /// Counterparty information
2054
    pub counterparty: Option<String>,
2055
2056
    /// Trade venue
2057
    pub venue: Option<String>,
2058
2059
    /// Gross trade value
2060
    pub gross_value: Decimal,
2061
2062
    /// Net trade value (after fees)
2063
    pub net_value: Decimal,
2064
}
2065
2066
impl Execution {
2067
    /// Create a new execution
2068
0
    pub fn new(
2069
0
        order_id: Uuid,
2070
0
        symbol: String,
2071
0
        quantity: Decimal,
2072
0
        price: Decimal,
2073
0
        side: OrderSide,
2074
0
        fees: Decimal,
2075
0
    ) -> Self {
2076
0
        let gross_value = quantity * price;
2077
0
        let net_value = if side == OrderSide::Buy {
2078
0
            gross_value + fees
2079
        } else {
2080
0
            gross_value - fees
2081
        };
2082
0
        let now = Utc::now();
2083
0
        let symbol_hash = Self::hash_symbol(&symbol);
2084
2085
0
        Self {
2086
0
            id: Uuid::new_v4(),
2087
0
            order_id,
2088
0
            symbol,
2089
0
            quantity,
2090
0
            price,
2091
0
            side,
2092
0
            fees,
2093
0
            fee_currency: "USD".to_string(), // Default to USD
2094
0
            executed_at: now,
2095
0
            timestamp: now, // Same as executed_at for compatibility
2096
0
            symbol_hash,
2097
0
            broker_execution_id: None,
2098
0
            counterparty: None,
2099
0
            venue: None,
2100
0
            gross_value,
2101
0
            net_value,
2102
0
        }
2103
0
    }
2104
2105
    /// Calculate effective price including fees
2106
0
    pub fn effective_price(&self) -> Decimal {
2107
0
        if self.quantity.is_zero() {
2108
0
            self.price
2109
        } else {
2110
0
            self.net_value / self.quantity
2111
        }
2112
0
    }
2113
2114
    /// Hash symbol for performance
2115
0
    fn hash_symbol(symbol: &str) -> i64 {
2116
        use std::collections::hash_map::DefaultHasher;
2117
        use std::hash::{Hash, Hasher};
2118
2119
0
        let mut hasher = DefaultHasher::new();
2120
0
        symbol.hash(&mut hasher);
2121
0
        hasher.finish() as i64
2122
0
    }
2123
}
2124
2125
/// Core Price type using fixed-point arithmetic for precision
2126
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2127
pub struct Price {
2128
    value: u64,
2129
}
2130
2131
impl Price {
2132
    /// Zero price constant
2133
    pub const ZERO: Self = Self { value: 0 };
2134
    /// One unit price constant (1.0)
2135
    pub const ONE: Self = Self { value: 100_000_000 };
2136
    /// One cent constant (0.01)
2137
    pub const CENT: Self = Self { value: 1_000_000 };
2138
    /// Maximum price value
2139
    pub const MAX: Self = Self { value: u64::MAX };
2140
2141
    /// Create a Price from a floating-point value
2142
16.6k
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
2143
16.6k
        if value < 0.0 || 
!value.is_finite()16.6k
{
2144
10
            return Err(CommonTypeError::InvalidPrice {
2145
10
                value: value.to_string(),
2146
10
                reason: "Price validation failed".to_owned(),
2147
10
            });
2148
16.6k
        }
2149
16.6k
        Ok(Self {
2150
16.6k
            value: (value * 100_000_000.0).round() as u64,
2151
16.6k
        })
2152
16.6k
    }
2153
2154
    /// Convert to floating-point representation
2155
    #[must_use]
2156
    /// Convert the quantity to a floating point value
2157
23.1k
    pub fn to_f64(&self) -> f64 {
2158
23.1k
        self.value as f64 / 100_000_000.0
2159
23.1k
    }
2160
2161
    /// Get floating-point representation (alias for to_f64)
2162
    /// Convert quantity to f64 representation
2163
    /// Convert quantity to f64 representation
2164
    /// Convert quantity to f64 representation
2165
    #[must_use]
2166
0
    pub fn as_f64(&self) -> f64 {
2167
0
        self.to_f64()
2168
0
    }
2169
2170
    /// Create a zero price
2171
    /// Create a zero quantity
2172
    /// Create zero quantity
2173
    #[must_use]
2174
0
    pub const fn zero() -> Self {
2175
0
        Self::ZERO
2176
0
    }
2177
2178
    /// Convert to Decimal type for precise calculations
2179
223
    pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
2180
223
        Decimal::from_f64(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidPrice {
2181
0
            value: "0.0".to_owned(),
2182
0
            reason: "Price to Decimal conversion failed".to_owned(),
2183
0
        })
2184
223
    }
2185
2186
    /// Create a Price from a Decimal value
2187
    #[must_use]
2188
52
    pub fn from_decimal(decimal: Decimal) -> Self {
2189
52
        Self::from(decimal)
2190
52
    }
2191
2192
    /// Create a new Price (alias for from_f64)
2193
    /// Create a new quantity from a floating point value
2194
    /// Create new quantity from f64 value
2195
169
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
2196
169
        Self::from_f64(value)
2197
169
    }
2198
2199
    /// Get the raw internal value representation
2200
    /// Get the raw internal value
2201
    /// Get the raw internal value representation
2202
    #[must_use]
2203
316
    pub const fn raw_value(&self) -> u64 {
2204
316
        self.value
2205
316
    }
2206
2207
    /// Get the price as a u64 value (same as raw_value)
2208
    /// Convert to u64 representation
2209
    /// Convert quantity to u64 representation
2210
    #[must_use]
2211
0
    pub const fn as_u64(&self) -> u64 {
2212
0
        self.value
2213
0
    }
2214
2215
    /// Create a Price from a raw u64 value
2216
    /// Create a quantity from raw internal value
2217
    /// Create quantity from raw u64 value
2218
    #[must_use]
2219
0
    pub const fn from_raw(value: u64) -> Self {
2220
0
        Self { value }
2221
0
    }
2222
2223
    /// Convert price to cents (divides by 1M for 8 decimal places)
2224
    #[must_use]
2225
0
    pub const fn to_cents(&self) -> u64 {
2226
0
        self.value / 1_000_000
2227
0
    }
2228
2229
    /// Create a Price from cents value
2230
    #[must_use]
2231
0
    pub const fn from_cents(cents: u64) -> Self {
2232
0
        Self {
2233
0
            value: cents * 1_000_000,
2234
0
        }
2235
0
    }
2236
2237
    /// Check if the price is zero
2238
    /// Check if the quantity is zero
2239
    /// Check if quantity is zero
2240
    #[must_use]
2241
0
    pub const fn is_zero(&self) -> bool {
2242
0
        self.value == 0
2243
0
    }
2244
2245
    /// Check if the price is non-zero (has some value)
2246
    /// Check if the quantity is non-zero (has some value)
2247
    /// Check if quantity has a non-zero value
2248
    #[must_use]
2249
0
    pub const fn is_some(&self) -> bool {
2250
0
        !self.is_zero()
2251
0
    }
2252
2253
    /// Check if the price is zero (has no value)
2254
    /// Check if the quantity is zero (has no value)
2255
    /// Check if quantity is zero (none)
2256
    #[must_use]
2257
0
    pub const fn is_none(&self) -> bool {
2258
0
        self.is_zero()
2259
0
    }
2260
2261
    /// Get a reference to this price
2262
    /// Get a reference to self
2263
    /// Get a reference to self
2264
    #[must_use]
2265
0
    pub const fn as_ref(&self) -> &Self {
2266
0
        self
2267
0
    }
2268
2269
    /// Get the absolute value of the price (prices are always positive)
2270
    /// Get the absolute value (quantities are always positive)
2271
    /// Get absolute value (always positive for Quantity)
2272
    #[must_use]
2273
10
    pub const fn abs(&self) -> Self {
2274
10
        *self
2275
10
    }
2276
2277
    /// Multiply this price by another price
2278
0
    pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
2279
0
        *self * other
2280
0
    }
2281
2282
    /// Subtract another price from this price
2283
    /// Subtract another quantity from this quantity
2284
    /// Subtract another quantity from this quantity
2285
    /// Subtract another quantity from this quantity
2286
    /// Subtract another quantity from this quantity
2287
    #[must_use]
2288
0
    pub fn subtract(&self, other: Self) -> Self {
2289
0
        *self - other
2290
0
    }
2291
2292
    /// Divide this price by a floating point divisor
2293
0
    pub fn divide(&self, divisor: f64) -> Result<Self, CommonTypeError> {
2294
0
        *self / divisor
2295
0
    }
2296
}
2297
2298
impl fmt::Display for Price {
2299
    /// Format the price for display with 8 decimal places
2300
49
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2301
49
        write!(f, "{:.8}", self.to_f64())
2302
49
    }
2303
}
2304
2305
impl Default for Price {
2306
    /// Returns the default price (zero)
2307
0
    fn default() -> Self {
2308
0
        Self::ZERO
2309
0
    }
2310
}
2311
2312
impl FromStr for Price {
2313
    type Err = CommonTypeError;
2314
2315
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2316
0
        let parsed_value = s
2317
0
            .parse::<f64>()
2318
0
            .map_err(|_| CommonTypeError::InvalidPrice {
2319
0
                value: s.to_owned(),
2320
0
                reason: format!("Cannot parse '{}' as price", s),
2321
0
            })?;
2322
0
        Self::from_f64(parsed_value)
2323
0
    }
2324
}
2325
2326
impl Add for Price {
2327
    type Output = Self;
2328
3
    fn add(self, rhs: Self) -> Self::Output {
2329
3
        Self {
2330
3
            value: self.value.saturating_add(rhs.value),
2331
3
        }
2332
3
    }
2333
}
2334
2335
impl Sub for Price {
2336
    type Output = Self;
2337
12
    fn sub(self, rhs: Self) -> Self::Output {
2338
12
        Self {
2339
12
            value: self.value.saturating_sub(rhs.value),
2340
12
        }
2341
12
    }
2342
}
2343
2344
impl Mul<f64> for Price {
2345
    type Output = Result<Self, CommonTypeError>;
2346
160
    fn mul(self, rhs: f64) -> Self::Output {
2347
160
        Self::from_f64(self.to_f64() * rhs)
2348
160
    }
2349
}
2350
2351
impl Div<f64> for Price {
2352
    type Output = Result<Self, CommonTypeError>;
2353
4
    fn div(self, rhs: f64) -> Self::Output {
2354
4
        if rhs == 0.0 {
2355
0
            return Err(CommonTypeError::ConversionError {
2356
0
                message: "Cannot divide price by zero".to_owned(),
2357
0
            });
2358
4
        }
2359
4
        Self::from_f64(self.to_f64() / rhs)
2360
4
    }
2361
}
2362
2363
impl From<Decimal> for Price {
2364
143
    fn from(decimal: Decimal) -> Self {
2365
143
        let f64_val: f64 = TryInto::<f64>::try_into(decimal).unwrap_or_else(|_| 
{0
2366
0
            tracing::warn!("Failed to convert Decimal to f64, using 0.0 as fallback");
2367
0
            0.0_f64
2368
0
        });
2369
143
        Self::from_f64(f64_val).unwrap_or_else(|_| 
{0
2370
0
            tracing::warn!(
2371
0
                "Failed to create Price from f64 value {}, using ZERO",
2372
                f64_val
2373
            );
2374
0
            Self::ZERO
2375
0
        })
2376
143
    }
2377
}
2378
2379
impl From<Price> for Decimal {
2380
0
    fn from(price: Price) -> Self {
2381
0
        price.to_decimal().unwrap_or(Decimal::ZERO)
2382
0
    }
2383
}
2384
2385
// TryFrom<Quantity> for Decimal removed due to conflicting blanket implementation
2386
// Use qty.to_decimal() directly instead
2387
impl From<Quantity> for Decimal {
2388
0
    fn from(qty: Quantity) -> Self {
2389
0
        qty.to_decimal().unwrap_or(Decimal::ZERO)
2390
0
    }
2391
}
2392
2393
// TryFrom<Quantity> for Decimal removed due to conflict with From implementation
2394
// Use the From implementation instead which handles errors by returning ZERO
2395
2396
impl Mul<Self> for Price {
2397
    type Output = Result<Self, CommonTypeError>;
2398
4
    fn mul(self, rhs: Self) -> Self::Output {
2399
4
        Self::from_f64(self.to_f64() * rhs.to_f64())
2400
4
    }
2401
}
2402
2403
impl TryFrom<String> for Price {
2404
    type Error = CommonTypeError;
2405
0
    fn try_from(s: String) -> Result<Self, Self::Error> {
2406
0
        Self::from_str(&s)
2407
0
    }
2408
}
2409
2410
impl TryFrom<&str> for Price {
2411
    type Error = CommonTypeError;
2412
0
    fn try_from(s: &str) -> Result<Self, Self::Error> {
2413
0
        Self::from_str(s)
2414
0
    }
2415
}
2416
2417
impl PartialEq<f64> for Price {
2418
0
    fn eq(&self, other: &f64) -> bool {
2419
0
        (self.to_f64() - other).abs() < f64::EPSILON
2420
0
    }
2421
}
2422
2423
impl PartialEq<Price> for f64 {
2424
0
    fn eq(&self, other: &Price) -> bool {
2425
0
        (self - other.to_f64()).abs() < f64::EPSILON
2426
0
    }
2427
}
2428
2429
impl AddAssign for Price {
2430
7
    fn add_assign(&mut self, rhs: Self) {
2431
7
        self.value = self.value.saturating_add(rhs.value);
2432
7
    }
2433
}
2434
2435
impl SubAssign for Price {
2436
0
    fn sub_assign(&mut self, rhs: Self) {
2437
0
        self.value = self.value.saturating_sub(rhs.value);
2438
0
    }
2439
}
2440
2441
impl MulAssign<f64> for Price {
2442
0
    fn mul_assign(&mut self, rhs: f64) {
2443
0
        if let Ok(result) = self.mul(rhs) {
2444
0
            *self = result;
2445
0
        }
2446
        // If multiplication fails, self remains unchanged
2447
0
    }
2448
}
2449
2450
impl DivAssign<f64> for Price {
2451
0
    fn div_assign(&mut self, rhs: f64) {
2452
0
        if let Ok(result) = self.div(rhs) {
2453
0
            *self = result;
2454
0
        }
2455
        // If division fails, self remains unchanged
2456
0
    }
2457
}
2458
2459
impl PartialOrd<f64> for Price {
2460
0
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
2461
0
        self.to_f64().partial_cmp(other)
2462
0
    }
2463
}
2464
2465
impl PartialOrd<Price> for f64 {
2466
9
    fn partial_cmp(&self, other: &Price) -> Option<std::cmp::Ordering> {
2467
9
        self.partial_cmp(&other.to_f64())
2468
9
    }
2469
}
2470
2471
/// Core Quantity type using fixed-point arithmetic
2472
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2473
pub struct Quantity {
2474
    value: u64,
2475
}
2476
2477
impl Quantity {
2478
    /// Zero quantity constant
2479
    pub const ZERO: Self = Self { value: 0 };
2480
    /// One unit quantity constant
2481
    pub const ONE: Self = Self { value: 100_000_000 };
2482
    /// Maximum possible quantity
2483
    pub const MAX: Self = Self { value: u64::MAX };
2484
2485
    /// Create a Quantity from a floating point value
2486
1.87k
    pub fn from_f64(value: f64) -> Result<Self, CommonTypeError> {
2487
1.87k
        if value < 0.0 || !value.is_finite() {
2488
0
            return Err(CommonTypeError::InvalidQuantity {
2489
0
                value: value.to_string(),
2490
0
                reason: "Quantity validation failed".to_owned(),
2491
0
            });
2492
1.87k
        }
2493
1.87k
        Ok(Self {
2494
1.87k
            value: (value * 100_000_000.0).round() as u64,
2495
1.87k
        })
2496
1.87k
    }
2497
2498
    /// Convert quantity to floating point representation
2499
    #[must_use]
2500
262
    pub fn to_f64(&self) -> f64 {
2501
262
        self.value as f64 / 100_000_000.0
2502
262
    }
2503
2504
    /// Convert the quantity to a Decimal value
2505
40
    pub fn to_decimal(&self) -> Result<Decimal, CommonTypeError> {
2506
40
        Decimal::from_f64(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidQuantity {
2507
0
            value: "0.0".to_owned(),
2508
0
            reason: "Quantity to Decimal conversion failed".to_owned(),
2509
0
        })
2510
40
    }
2511
2512
    /// Get the internal value representation
2513
    #[must_use]
2514
0
    pub const fn value(&self) -> u64 {
2515
0
        self.value
2516
0
    }
2517
2518
    /// Get the raw internal value representation
2519
    #[must_use]
2520
102
    pub const fn raw_value(&self) -> u64 {
2521
102
        self.value
2522
102
    }
2523
2524
    /// Convert quantity to u64 representation
2525
    #[must_use]
2526
0
    pub const fn as_u64(&self) -> u64 {
2527
0
        self.value
2528
0
    }
2529
2530
    /// Create quantity from raw u64 value
2531
    #[must_use]
2532
0
    pub const fn from_raw(value: u64) -> Self {
2533
0
        Self { value }
2534
0
    }
2535
2536
    /// Create new quantity from f64 value
2537
0
    pub fn new(value: f64) -> Result<Self, CommonTypeError> {
2538
0
        Self::from_f64(value)
2539
0
    }
2540
2541
    /// Create zero quantity
2542
    #[must_use]
2543
25
    pub const fn zero() -> Self {
2544
25
        Self::ZERO
2545
25
    }
2546
2547
    /// Create a quantity from an i64 value
2548
0
    pub fn from_i64(value: i64) -> Result<Self, CommonTypeError> {
2549
0
        Self::from_f64(value as f64)
2550
0
    }
2551
2552
    /// Create a quantity from a u64 value
2553
0
    pub fn from_u64(value: u64) -> Result<Self, CommonTypeError> {
2554
0
        Self::from_f64(value as f64)
2555
0
    }
2556
2557
    /// Create a quantity from a Decimal value
2558
0
    pub fn from_decimal(decimal: Decimal) -> Result<Self, CommonTypeError> {
2559
        use std::convert::TryFrom;
2560
0
        Self::try_from(decimal).map_err(|_| CommonTypeError::InvalidQuantity {
2561
0
            value: decimal.to_string(),
2562
0
            reason: "Failed to convert Decimal to Quantity".to_owned(),
2563
0
        })
2564
0
    }
2565
2566
    /// Check if quantity is zero
2567
    #[must_use]
2568
0
    pub const fn is_zero(&self) -> bool {
2569
0
        self.value == 0
2570
0
    }
2571
2572
    /// Check if quantity has a non-zero value
2573
    #[must_use]
2574
0
    pub const fn is_some(&self) -> bool {
2575
0
        !self.is_zero()
2576
0
    }
2577
2578
    /// Check if quantity is zero (none)
2579
    #[must_use]
2580
0
    pub const fn is_none(&self) -> bool {
2581
0
        self.is_zero()
2582
0
    }
2583
2584
    /// Get a reference to self
2585
    #[must_use]
2586
0
    pub const fn as_ref(&self) -> &Self {
2587
0
        self
2588
0
    }
2589
2590
    /// Get absolute value (always positive for Quantity)
2591
    #[must_use]
2592
0
    pub const fn abs(&self) -> Self {
2593
0
        *self
2594
0
    }
2595
2596
    /// Get the sign of the quantity (1.0 for positive, 0.0 for zero)
2597
    #[must_use]
2598
0
    pub const fn signum(&self) -> f64 {
2599
0
        if self.value > 0 {
2600
0
            1.0
2601
        } else {
2602
0
            0.0
2603
        }
2604
0
    }
2605
2606
    /// Check if quantity is positive
2607
    #[must_use]
2608
0
    pub const fn is_positive(&self) -> bool {
2609
0
        self.value > 0
2610
0
    }
2611
2612
    /// Check if quantity is negative (always false for Quantity)
2613
    #[must_use]
2614
0
    pub const fn is_negative(&self) -> bool {
2615
0
        false
2616
0
    }
2617
2618
    /// Convert quantity to f64 representation
2619
    #[must_use]
2620
0
    pub fn as_f64(&self) -> f64 {
2621
0
        self.to_f64()
2622
0
    }
2623
2624
    /// Create quantity from number of shares
2625
    #[must_use]
2626
0
    pub const fn from_shares(shares: u64) -> Self {
2627
0
        Self {
2628
0
            value: shares * 100_000_000,
2629
0
        }
2630
0
    }
2631
2632
    /// Convert quantity to number of shares
2633
    #[must_use]
2634
0
    pub const fn to_shares(&self) -> u64 {
2635
0
        self.value / 100_000_000
2636
0
    }
2637
2638
    /// Multiply this quantity by another quantity
2639
0
    pub fn multiply(&self, other: Self) -> Result<Self, CommonTypeError> {
2640
0
        Self::from_f64(self.to_f64() * other.to_f64())
2641
0
    }
2642
2643
    /// Subtract another quantity from this quantity
2644
    #[must_use]
2645
0
    pub fn subtract(&self, other: Self) -> Self {
2646
0
        *self - other
2647
0
    }
2648
}
2649
2650
impl Default for Quantity {
2651
0
    fn default() -> Self {
2652
0
        Self::ZERO
2653
0
    }
2654
}
2655
2656
impl FromStr for Quantity {
2657
    type Err = CommonTypeError;
2658
2659
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2660
0
        let parsed_value = s
2661
0
            .parse::<f64>()
2662
0
            .map_err(|_| CommonTypeError::InvalidQuantity {
2663
0
                value: s.to_owned(),
2664
0
                reason: format!("Cannot parse '{}' as quantity", s),
2665
0
            })?;
2666
0
        Self::from_f64(parsed_value)
2667
0
    }
2668
}
2669
2670
impl fmt::Display for Quantity {
2671
    /// Format the quantity for display with 8 decimal places
2672
10
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2673
10
        write!(f, "{:.8}", self.to_f64())
2674
10
    }
2675
}
2676
2677
impl TryFrom<i32> for Quantity {
2678
    type Error = CommonTypeError;
2679
0
    fn try_from(value: i32) -> Result<Self, Self::Error> {
2680
0
        Self::new(f64::from(value))
2681
0
    }
2682
}
2683
2684
impl TryFrom<u64> for Quantity {
2685
    type Error = CommonTypeError;
2686
0
    fn try_from(value: u64) -> Result<Self, Self::Error> {
2687
0
        Self::new(value as f64)
2688
0
    }
2689
}
2690
2691
impl TryFrom<f64> for Quantity {
2692
    type Error = CommonTypeError;
2693
0
    fn try_from(value: f64) -> Result<Self, Self::Error> {
2694
0
        Self::new(value)
2695
0
    }
2696
}
2697
2698
impl TryFrom<Decimal> for Quantity {
2699
    type Error = CommonTypeError;
2700
0
    fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
2701
0
        let f64_val: f64 =
2702
0
            TryInto::<f64>::try_into(decimal).map_err(|_| CommonTypeError::ConversionError {
2703
0
                message: "Failed to convert Decimal to f64".to_owned(),
2704
0
            })?;
2705
0
        Self::from_f64(f64_val)
2706
0
    }
2707
}
2708
2709
impl TryFrom<String> for Quantity {
2710
    type Error = CommonTypeError;
2711
0
    fn try_from(s: String) -> Result<Self, Self::Error> {
2712
0
        Self::from_str(&s)
2713
0
    }
2714
}
2715
2716
impl TryFrom<&str> for Quantity {
2717
    type Error = CommonTypeError;
2718
0
    fn try_from(s: &str) -> Result<Self, Self::Error> {
2719
0
        Self::from_str(s)
2720
0
    }
2721
}
2722
2723
impl PartialEq<f64> for Quantity {
2724
0
    fn eq(&self, other: &f64) -> bool {
2725
0
        (self.to_f64() - other).abs() < f64::EPSILON
2726
0
    }
2727
}
2728
2729
impl PartialEq<Quantity> for f64 {
2730
0
    fn eq(&self, other: &Quantity) -> bool {
2731
0
        (self - other.to_f64()).abs() < f64::EPSILON
2732
0
    }
2733
}
2734
2735
impl PartialOrd<f64> for Quantity {
2736
0
    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
2737
0
        self.to_f64().partial_cmp(other)
2738
0
    }
2739
}
2740
2741
impl PartialOrd<Quantity> for f64 {
2742
0
    fn partial_cmp(&self, other: &Quantity) -> Option<std::cmp::Ordering> {
2743
0
        self.partial_cmp(&other.to_f64())
2744
0
    }
2745
}
2746
2747
impl Add for Quantity {
2748
    type Output = Self;
2749
0
    fn add(self, rhs: Self) -> Self::Output {
2750
0
        Self {
2751
0
            value: self.value.saturating_add(rhs.value),
2752
0
        }
2753
0
    }
2754
}
2755
2756
impl Sub for Quantity {
2757
    type Output = Self;
2758
0
    fn sub(self, rhs: Self) -> Self::Output {
2759
0
        Self {
2760
0
            value: self.value.saturating_sub(rhs.value),
2761
0
        }
2762
0
    }
2763
}
2764
2765
impl Mul<f64> for Quantity {
2766
    type Output = Result<Self, CommonTypeError>;
2767
0
    fn mul(self, rhs: f64) -> Self::Output {
2768
0
        Self::from_f64(self.to_f64() * rhs)
2769
0
    }
2770
}
2771
2772
impl Div<f64> for Quantity {
2773
    type Output = Result<Self, CommonTypeError>;
2774
0
    fn div(self, rhs: f64) -> Self::Output {
2775
0
        if rhs == 0.0 {
2776
0
            return Err(CommonTypeError::ConversionError {
2777
0
                message: "Cannot divide quantity by zero".to_owned(),
2778
0
            });
2779
0
        }
2780
0
        Self::from_f64(self.to_f64() / rhs)
2781
0
    }
2782
}
2783
2784
impl Sum for Quantity {
2785
0
    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
2786
0
        iter.fold(Self::ZERO, |acc, x| acc + x)
2787
0
    }
2788
}
2789
2790
impl<'quantity> Sum<&'quantity Self> for Quantity {
2791
0
    fn sum<I: Iterator<Item = &'quantity Self>>(iter: I) -> Self {
2792
0
        iter.fold(Self::ZERO, |acc, x| acc + *x)
2793
0
    }
2794
}
2795
2796
// =============================================================================
2797
// SQLX IMPLEMENTATIONS FOR FINANCIAL TYPES
2798
// =============================================================================
2799
2800
#[cfg(feature = "database")]
2801
mod sqlx_impls {
2802
    use super::{HftTimestamp, MarketRegime, OrderSide, OrderStatus, OrderType, Price, Quantity};
2803
    use rust_decimal::Decimal as RustDecimal;
2804
    use sqlx::{
2805
        decode::Decode,
2806
        encode::{Encode, IsNull},
2807
        error::BoxDynError,
2808
        postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres},
2809
        Type,
2810
    };
2811
2812
    // SQLx implementations for Price
2813
    impl Type<Postgres> for Price {
2814
0
        fn type_info() -> PgTypeInfo {
2815
0
            PgTypeInfo::with_name("NUMERIC")
2816
0
        }
2817
    }
2818
2819
    impl<'q> Encode<'q, Postgres> for Price {
2820
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2821
            // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
2822
0
            let decimal_value = RustDecimal::new(self.raw_value() as i64, 8);
2823
0
            decimal_value.encode_by_ref(buf)
2824
0
        }
2825
    }
2826
2827
    impl<'r> Decode<'r, Postgres> for Price {
2828
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2829
            // Decode from NUMERIC to rust_decimal::Decimal
2830
0
            let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
2831
2832
            // Validate scale matches our fixed-point precision (8 decimal places)
2833
0
            if decimal_value.scale() != 8 {
2834
0
                return Err(format!(
2835
0
                    "Invalid scale for Price: expected 8, got {}",
2836
0
                    decimal_value.scale()
2837
0
                )
2838
0
                .into());
2839
0
            }
2840
2841
            // Extract mantissa and convert to our u64 representation
2842
0
            let mantissa = decimal_value.mantissa();
2843
0
            let inner_val = u64::try_from(mantissa)
2844
0
                .map_err(|_| "Failed to convert negative or overflowing NUMERIC to Price")?;
2845
2846
0
            Ok(Price::from_raw(inner_val))
2847
0
        }
2848
    }
2849
    // SQLx implementations for Quantity
2850
    impl Type<Postgres> for Quantity {
2851
0
        fn type_info() -> PgTypeInfo {
2852
0
            PgTypeInfo::with_name("NUMERIC")
2853
0
        }
2854
    }
2855
2856
    impl<'q> Encode<'q, Postgres> for Quantity {
2857
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2858
            // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places
2859
0
            let decimal_value = RustDecimal::new(self.raw_value() as i64, 8);
2860
0
            decimal_value.encode_by_ref(buf)
2861
0
        }
2862
    }
2863
2864
    impl<'r> Decode<'r, Postgres> for Quantity {
2865
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2866
            // Decode from NUMERIC to rust_decimal::Decimal
2867
0
            let decimal_value = <RustDecimal as Decode<Postgres>>::decode(value)?;
2868
2869
            // Validate scale matches our fixed-point precision (8 decimal places)
2870
0
            if decimal_value.scale() != 8 {
2871
0
                return Err(format!(
2872
0
                    "Invalid scale for Quantity: expected 8, got {}",
2873
0
                    decimal_value.scale()
2874
0
                )
2875
0
                .into());
2876
0
            }
2877
2878
            // Extract mantissa and convert to our u64 representation
2879
0
            let mantissa = decimal_value.mantissa();
2880
0
            let inner_val = u64::try_from(mantissa)
2881
0
                .map_err(|_| "Failed to convert negative or overflowing NUMERIC to Quantity")?;
2882
2883
0
            Ok(Quantity::from_raw(inner_val))
2884
0
        }
2885
    }
2886
2887
    // SQLx implementations for TimeInForce
2888
    impl Type<Postgres> for super::TimeInForce {
2889
0
        fn type_info() -> PgTypeInfo {
2890
0
            PgTypeInfo::with_name("TEXT")
2891
0
        }
2892
    }
2893
2894
    impl<'q> Encode<'q, Postgres> for super::TimeInForce {
2895
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2896
            // Use the Display trait to convert enum to string representation
2897
0
            <&str as Encode<Postgres>>::encode(self.to_string().as_str(), buf)
2898
0
        }
2899
    }
2900
2901
    impl<'r> Decode<'r, Postgres> for super::TimeInForce {
2902
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2903
            // Decode from TEXT to string, then parse to enum
2904
0
            let s = <&str as Decode<Postgres>>::decode(value)?;
2905
0
            match s {
2906
0
                "DAY" => Ok(super::TimeInForce::Day),
2907
0
                "GTC" => Ok(super::TimeInForce::GoodTillCancel),
2908
0
                "IOC" => Ok(super::TimeInForce::ImmediateOrCancel),
2909
0
                "FOK" => Ok(super::TimeInForce::FillOrKill),
2910
0
                _ => Err(format!("Invalid TimeInForce value: {}", s).into()),
2911
            }
2912
0
        }
2913
    }
2914
2915
    // SQLx implementations for OrderStatus
2916
    impl Type<Postgres> for OrderStatus {
2917
0
        fn type_info() -> PgTypeInfo {
2918
0
            PgTypeInfo::with_name("TEXT")
2919
0
        }
2920
    }
2921
2922
    impl<'q> Encode<'q, Postgres> for OrderStatus {
2923
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2924
0
            let value = match self {
2925
0
                OrderStatus::Created => "CREATED",
2926
0
                OrderStatus::Submitted => "SUBMITTED",
2927
0
                OrderStatus::PartiallyFilled => "PARTIALLY_FILLED",
2928
0
                OrderStatus::Filled => "FILLED",
2929
0
                OrderStatus::Rejected => "REJECTED",
2930
0
                OrderStatus::Cancelled => "CANCELLED",
2931
0
                OrderStatus::New => "NEW",
2932
0
                OrderStatus::Expired => "EXPIRED",
2933
0
                OrderStatus::Pending => "PENDING",
2934
0
                OrderStatus::Working => "WORKING",
2935
0
                OrderStatus::Unknown => "UNKNOWN",
2936
0
                OrderStatus::Suspended => "SUSPENDED",
2937
0
                OrderStatus::PendingCancel => "PENDING_CANCEL",
2938
0
                OrderStatus::PendingReplace => "PENDING_REPLACE",
2939
            };
2940
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
2941
0
        }
2942
    }
2943
2944
    impl<'r> Decode<'r, Postgres> for OrderStatus {
2945
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2946
0
            let s = <String as Decode<Postgres>>::decode(value)?;
2947
0
            match s.as_str() {
2948
0
                "CREATED" => Ok(OrderStatus::Created),
2949
0
                "SUBMITTED" => Ok(OrderStatus::Submitted),
2950
0
                "PARTIALLY_FILLED" => Ok(OrderStatus::PartiallyFilled),
2951
0
                "FILLED" => Ok(OrderStatus::Filled),
2952
0
                "REJECTED" => Ok(OrderStatus::Rejected),
2953
0
                "CANCELLED" => Ok(OrderStatus::Cancelled),
2954
0
                "NEW" => Ok(OrderStatus::New),
2955
0
                "EXPIRED" => Ok(OrderStatus::Expired),
2956
0
                "PENDING" => Ok(OrderStatus::Pending),
2957
0
                "WORKING" => Ok(OrderStatus::Working),
2958
0
                "UNKNOWN" => Ok(OrderStatus::Unknown),
2959
0
                "SUSPENDED" => Ok(OrderStatus::Suspended),
2960
0
                "PENDING_CANCEL" => Ok(OrderStatus::PendingCancel),
2961
0
                "PENDING_REPLACE" => Ok(OrderStatus::PendingReplace),
2962
0
                _ => Err(format!("Invalid OrderStatus value: {}", s).into()),
2963
            }
2964
0
        }
2965
    }
2966
2967
    // SQLx implementations for OrderSide
2968
    impl Type<Postgres> for OrderSide {
2969
0
        fn type_info() -> PgTypeInfo {
2970
0
            PgTypeInfo::with_name("TEXT")
2971
0
        }
2972
    }
2973
2974
    impl<'q> Encode<'q, Postgres> for OrderSide {
2975
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
2976
0
            let value = match self {
2977
0
                OrderSide::Buy => "BUY",
2978
0
                OrderSide::Sell => "SELL",
2979
            };
2980
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
2981
0
        }
2982
    }
2983
2984
    impl<'r> Decode<'r, Postgres> for OrderSide {
2985
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
2986
0
            let s = <String as Decode<Postgres>>::decode(value)?;
2987
0
            match s.as_str() {
2988
0
                "BUY" => Ok(OrderSide::Buy),
2989
0
                "SELL" => Ok(OrderSide::Sell),
2990
0
                _ => Err(format!("Invalid OrderSide value: {}", s).into()),
2991
            }
2992
0
        }
2993
    }
2994
2995
    // SQLx implementations for OrderType
2996
    impl Type<Postgres> for OrderType {
2997
0
        fn type_info() -> PgTypeInfo {
2998
0
            PgTypeInfo::with_name("TEXT")
2999
0
        }
3000
    }
3001
3002
    impl<'q> Encode<'q, Postgres> for OrderType {
3003
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3004
0
            let value = match self {
3005
0
                OrderType::Market => "MARKET",
3006
0
                OrderType::Limit => "LIMIT",
3007
0
                OrderType::Stop => "STOP",
3008
0
                OrderType::StopLimit => "STOP_LIMIT",
3009
0
                OrderType::Iceberg => "ICEBERG",
3010
0
                OrderType::TrailingStop => "TRAILING_STOP",
3011
0
                OrderType::Hidden => "HIDDEN",
3012
            };
3013
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3014
0
        }
3015
    }
3016
3017
    impl<'r> Decode<'r, Postgres> for OrderType {
3018
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3019
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3020
0
            match s.as_str() {
3021
0
                "MARKET" => Ok(OrderType::Market),
3022
0
                "LIMIT" => Ok(OrderType::Limit),
3023
0
                "STOP" => Ok(OrderType::Stop),
3024
0
                "STOP_LIMIT" => Ok(OrderType::StopLimit),
3025
0
                "ICEBERG" => Ok(OrderType::Iceberg),
3026
0
                "TRAILING_STOP" => Ok(OrderType::TrailingStop),
3027
0
                "HIDDEN" => Ok(OrderType::Hidden),
3028
0
                _ => Err(format!("Invalid OrderType value: {}", s).into()),
3029
            }
3030
0
        }
3031
    }
3032
3033
    // SQLx implementations for MarketRegime
3034
    impl Type<Postgres> for MarketRegime {
3035
0
        fn type_info() -> PgTypeInfo {
3036
0
            PgTypeInfo::with_name("TEXT")
3037
0
        }
3038
    }
3039
3040
    impl<'q> Encode<'q, Postgres> for MarketRegime {
3041
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3042
0
            let value = match self {
3043
0
                MarketRegime::Normal => "NORMAL",
3044
0
                MarketRegime::Crisis => "CRISIS",
3045
0
                MarketRegime::Trending => "TRENDING",
3046
0
                MarketRegime::Sideways => "SIDEWAYS",
3047
0
                MarketRegime::Bull => "BULL",
3048
0
                MarketRegime::Bear => "BEAR",
3049
0
                MarketRegime::HighVolatility => "HIGH_VOLATILITY",
3050
0
                MarketRegime::LowVolatility => "LOW_VOLATILITY",
3051
0
                MarketRegime::Volatile => "VOLATILE",
3052
0
                MarketRegime::Calm => "CALM",
3053
0
                MarketRegime::Unknown => "UNKNOWN",
3054
0
                MarketRegime::Recovery => "RECOVERY",
3055
0
                MarketRegime::Bubble => "BUBBLE",
3056
0
                MarketRegime::Correction => "CORRECTION",
3057
0
                MarketRegime::Custom(id) => {
3058
0
                    return <String as Encode<Postgres>>::encode_by_ref(
3059
0
                        &format!("CUSTOM_{}", id),
3060
0
                        buf,
3061
                    )
3062
                },
3063
            };
3064
0
            <&str as Encode<Postgres>>::encode_by_ref(&value, buf)
3065
0
        }
3066
    }
3067
3068
    impl<'r> Decode<'r, Postgres> for MarketRegime {
3069
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3070
0
            let s = <String as Decode<Postgres>>::decode(value)?;
3071
0
            match s.as_str() {
3072
0
                "NORMAL" => Ok(MarketRegime::Normal),
3073
0
                "CRISIS" => Ok(MarketRegime::Crisis),
3074
0
                "TRENDING" => Ok(MarketRegime::Trending),
3075
0
                "SIDEWAYS" => Ok(MarketRegime::Sideways),
3076
0
                "BULL" => Ok(MarketRegime::Bull),
3077
0
                "BEAR" => Ok(MarketRegime::Bear),
3078
0
                "HIGH_VOLATILITY" => Ok(MarketRegime::HighVolatility),
3079
0
                "LOW_VOLATILITY" => Ok(MarketRegime::LowVolatility),
3080
0
                "VOLATILE" => Ok(MarketRegime::Volatile),
3081
0
                "CALM" => Ok(MarketRegime::Calm),
3082
0
                "UNKNOWN" => Ok(MarketRegime::Unknown),
3083
0
                "RECOVERY" => Ok(MarketRegime::Recovery),
3084
0
                "BUBBLE" => Ok(MarketRegime::Bubble),
3085
0
                "CORRECTION" => Ok(MarketRegime::Correction),
3086
                _ => {
3087
                    // Handle Custom(id) format
3088
0
                    if let Some(id_str) = s.strip_prefix("CUSTOM_") {
3089
0
                        if let Ok(id) = id_str.parse::<usize>() {
3090
0
                            Ok(MarketRegime::Custom(id))
3091
                        } else {
3092
0
                            Err(format!("Invalid MarketRegime Custom ID: {}", id_str).into())
3093
                        }
3094
                    } else {
3095
0
                        Err(format!("Invalid MarketRegime value: {}", s).into())
3096
                    }
3097
                },
3098
            }
3099
0
        }
3100
    }
3101
3102
    // SQLx implementations for HftTimestamp
3103
    // Maps to PostgreSQL BIGINT (stores nanoseconds since Unix epoch)
3104
    // Note: Limited to i64::MAX nanoseconds (year 2262) due to PostgreSQL BIGINT constraints
3105
    impl<'q> Encode<'q, Postgres> for HftTimestamp {
3106
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3107
            // Cast u64 to i64 for PostgreSQL BIGINT compatibility
3108
0
            <i64 as Encode<Postgres>>::encode(self.nanos() as i64, buf)
3109
0
        }
3110
    }
3111
3112
    impl<'r> Decode<'r, Postgres> for HftTimestamp {
3113
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3114
0
            let val = <i64 as Decode<Postgres>>::decode(value)?;
3115
            // Cast i64 back to u64 for internal representation
3116
0
            Ok(HftTimestamp::from_nanos(val as u64))
3117
0
        }
3118
    }
3119
3120
    impl Type<Postgres> for HftTimestamp {
3121
0
        fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
3122
0
            <i64 as Type<Postgres>>::type_info()
3123
0
        }
3124
3125
0
        fn compatible(ty: &<Postgres as sqlx::Database>::TypeInfo) -> bool {
3126
0
            <i64 as Type<Postgres>>::compatible(ty)
3127
0
        }
3128
    }
3129
3130
    // SQLx implementations for OrderId (uses BIGINT for u64)
3131
    impl Type<Postgres> for super::OrderId {
3132
0
        fn type_info() -> PgTypeInfo {
3133
0
            PgTypeInfo::with_name("BIGINT")
3134
0
        }
3135
    }
3136
3137
    impl<'q> Encode<'q, Postgres> for super::OrderId {
3138
0
        fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
3139
0
            <i64 as Encode<Postgres>>::encode_by_ref(&(self.value() as i64), buf)
3140
0
        }
3141
    }
3142
3143
    impl<'r> Decode<'r, Postgres> for super::OrderId {
3144
0
        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
3145
0
            let id = <i64 as Decode<Postgres>>::decode(value)?;
3146
0
            Ok(super::OrderId::from_u64(id as u64))
3147
0
        }
3148
    }
3149
}
3150
3151
/// Volume type - alias for Quantity with the same fixed-point arithmetic
3152
/// SQLx traits are automatically inherited from Quantity
3153
pub type Volume = Quantity;
3154
3155
// ORDER TYPES ALREADY DEFINED ABOVE - No need to re-export from trading_engine
3156
// =============================================================================
3157
// CORE ID TYPES (MOVED FROM TRADING_ENGINE)
3158
// =============================================================================
3159
3160
/// Order identifier with ultra-fast atomic generation
3161
/// Replaces slow UUID generation (1ms+) with atomic increment (~5ns)
3162
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3163
pub struct OrderId(u64);
3164
3165
impl Default for OrderId {
3166
0
    fn default() -> Self {
3167
0
        Self::new()
3168
0
    }
3169
}
3170
3171
impl OrderId {
3172
    /// Generate next `OrderId` using atomic counter - <50ns performance
3173
4
    pub fn new() -> Self {
3174
        use std::sync::atomic::{AtomicU64, Ordering};
3175
        static COUNTER: AtomicU64 = AtomicU64::new(1);
3176
4
        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
3177
4
    }
3178
3179
    /// Create `OrderId` from u64 value
3180
    #[must_use]
3181
0
    pub const fn from_u64(value: u64) -> Self {
3182
0
        Self(value)
3183
0
    }
3184
3185
    /// Get u64 value
3186
    #[must_use]
3187
0
    pub const fn value(&self) -> u64 {
3188
0
        self.0
3189
0
    }
3190
3191
    /// Get u64 value for performance-critical code (alias for value)
3192
    #[must_use]
3193
0
    pub const fn as_u64(&self) -> u64 {
3194
0
        self.0
3195
0
    }
3196
3197
    /// Get as string for compatibility
3198
    #[must_use]
3199
0
    pub fn as_str(&self) -> String {
3200
0
        self.0.to_string()
3201
0
    }
3202
}
3203
3204
impl fmt::Display for OrderId {
3205
    /// Format the order ID for display
3206
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3207
0
        write!(f, "{}", self.0)
3208
0
    }
3209
}
3210
3211
impl From<u64> for OrderId {
3212
    /// Create an OrderId from a u64 value
3213
0
    fn from(value: u64) -> Self {
3214
0
        Self(value)
3215
0
    }
3216
}
3217
3218
impl From<OrderId> for u64 {
3219
    /// Convert an OrderId to u64
3220
0
    fn from(order_id: OrderId) -> Self {
3221
0
        order_id.0
3222
0
    }
3223
}
3224
3225
impl FromStr for OrderId {
3226
    type Err = ParseIntError;
3227
3228
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3229
0
        s.parse::<u64>().map(OrderId)
3230
0
    }
3231
}
3232
3233
impl From<String> for OrderId {
3234
    /// Create an OrderId from a String, generating new ID if parsing fails
3235
0
    fn from(s: String) -> Self {
3236
0
        s.parse().unwrap_or_else(|_| Self::new())
3237
0
    }
3238
}
3239
3240
impl From<&str> for OrderId {
3241
    /// Create an OrderId from a &str, generating new ID if parsing fails
3242
0
    fn from(s: &str) -> Self {
3243
0
        s.parse().unwrap_or_else(|_| Self::new())
3244
0
    }
3245
}
3246
3247
/// Execution identifier with validation
3248
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3249
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3250
pub struct ExecutionId(String);
3251
3252
impl ExecutionId {
3253
    /// Create a new execution ID with validation
3254
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3255
0
        let id = id.into();
3256
0
        if id.trim().is_empty() {
3257
0
            return Err(CommonTypeError::ValidationError {
3258
0
                field: "execution_id".to_owned(),
3259
0
                reason: "Execution ID cannot be empty".to_owned(),
3260
0
            });
3261
0
        }
3262
0
        Ok(Self(id))
3263
0
    }
3264
3265
    /// Generate a new random execution ID
3266
0
    pub fn generate() -> Self {
3267
0
        Self(uuid::Uuid::new_v4().to_string())
3268
0
    }
3269
3270
    /// Get execution ID as string slice
3271
0
    pub fn as_str(&self) -> &str {
3272
0
        &self.0
3273
0
    }
3274
3275
    /// Convert execution ID into owned string
3276
0
    pub fn into_string(self) -> String {
3277
0
        self.0
3278
0
    }
3279
}
3280
3281
impl fmt::Display for ExecutionId {
3282
    /// Format the execution ID for display
3283
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3284
0
        write!(f, "{}", self.0)
3285
0
    }
3286
}
3287
3288
impl FromStr for ExecutionId {
3289
    type Err = CommonTypeError;
3290
3291
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3292
0
        Self::new(s)
3293
0
    }
3294
}
3295
3296
/// Trade identifier with validation
3297
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3298
pub struct TradeId(String);
3299
3300
impl TradeId {
3301
    /// Create a new trade ID with validation
3302
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3303
0
        let id = id.into();
3304
0
        if id.is_empty() {
3305
0
            return Err(CommonTypeError::ValidationError {
3306
0
                field: "trade_id".to_owned(),
3307
0
                reason: "Trade ID cannot be empty".to_owned(),
3308
0
            });
3309
0
        }
3310
0
        Ok(Self(id))
3311
0
    }
3312
3313
    /// Get the trade ID as a string slice
3314
0
    pub fn as_str(&self) -> &str {
3315
0
        &self.0
3316
0
    }
3317
    /// Convert the trade ID into an owned string
3318
0
    pub fn into_string(self) -> String {
3319
0
        self.0
3320
0
    }
3321
}
3322
3323
impl fmt::Display for TradeId {
3324
    /// Format the trade ID for display
3325
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3326
0
        write!(f, "{}", self.0)
3327
0
    }
3328
}
3329
3330
/// Trading symbol with validation
3331
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3332
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3333
pub struct Symbol {
3334
    value: String,
3335
}
3336
3337
impl Symbol {
3338
    /// Create a new symbol from a string
3339
    #[must_use]
3340
358
    pub const fn new(s: String) -> Self {
3341
358
        Self { value: s }
3342
358
    }
3343
3344
    /// Create a new Symbol with validation
3345
0
    pub fn new_validated(s: String) -> Result<Self, CommonTypeError> {
3346
0
        if s.trim().is_empty() {
3347
0
            return Err(CommonTypeError::ValidationError {
3348
0
                field: "symbol".to_string(),
3349
0
                reason: "Symbol cannot be empty".to_string(),
3350
0
            });
3351
0
        }
3352
0
        Ok(Self { value: s })
3353
0
    }
3354
3355
    /// Create a Symbol from &str with validation
3356
0
    pub fn from_str_validated(s: &str) -> Result<Self, CommonTypeError> {
3357
0
        Self::new_validated(s.to_owned())
3358
0
    }
3359
3360
    /// Get the symbol as a string slice
3361
    #[must_use]
3362
6
    pub fn as_str(&self) -> &str {
3363
6
        &self.value
3364
6
    }
3365
    /// Get the symbol value as a string slice
3366
    #[must_use]
3367
0
    pub fn value(&self) -> &str {
3368
0
        &self.value
3369
0
    }
3370
    /// Get the symbol as bytes
3371
    #[must_use]
3372
0
    pub fn as_bytes(&self) -> &[u8] {
3373
0
        self.value.as_bytes()
3374
0
    }
3375
    /// Check if the symbol is empty
3376
    #[must_use]
3377
0
    pub fn is_empty(&self) -> bool {
3378
0
        self.value.is_empty()
3379
0
    }
3380
    /// Convert the symbol to uppercase
3381
    #[must_use]
3382
0
    pub fn to_uppercase(&self) -> String {
3383
0
        self.value.to_uppercase()
3384
0
    }
3385
    /// Replace occurrences in the symbol
3386
    #[must_use]
3387
0
    pub fn replace(&self, from: &str, to: &str) -> String {
3388
0
        self.value.replace(from, to)
3389
0
    }
3390
3391
    /// Helper for risk management - creates a 'NONE' symbol
3392
    #[must_use]
3393
0
    pub fn none() -> Self {
3394
0
        "NONE".parse().unwrap()
3395
0
    }
3396
3397
    /// Check if the symbol contains a pattern
3398
    #[must_use]
3399
0
    pub fn contains(&self, pattern: &str) -> bool {
3400
0
        self.value.contains(pattern)
3401
0
    }
3402
}
3403
3404
impl FromStr for Symbol {
3405
    type Err = std::convert::Infallible;
3406
3407
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3408
0
        Ok(Self {
3409
0
            value: s.to_owned(),
3410
0
        })
3411
0
    }
3412
}
3413
3414
// Additional implementation to support conversion from &Symbol to &str
3415
impl AsRef<str> for Symbol {
3416
    /// Convert symbol to string reference
3417
0
    fn as_ref(&self) -> &str {
3418
0
        &self.value
3419
0
    }
3420
}
3421
3422
impl fmt::Display for Symbol {
3423
    /// Format the symbol for display
3424
1.40k
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3425
1.40k
        write!(f, "{}", self.value)
3426
1.40k
    }
3427
}
3428
3429
impl From<String> for Symbol {
3430
    /// Create a Symbol from a String
3431
317
    fn from(s: String) -> Self {
3432
317
        Self::new(s)
3433
317
    }
3434
}
3435
impl From<&str> for Symbol {
3436
    /// Create a Symbol from a &str
3437
41
    fn from(s: &str) -> Self {
3438
41
        Self::new(s.to_owned())
3439
41
    }
3440
}
3441
3442
// TryFrom implementations removed due to conflicting blanket implementations
3443
// Use Symbol::new_validated() or Symbol::from_validated() directly instead
3444
3445
impl Default for Symbol {
3446
    /// Returns the default symbol (empty string)
3447
0
    fn default() -> Self {
3448
0
        Self::new(String::new())
3449
0
    }
3450
}
3451
3452
impl PartialEq<str> for Symbol {
3453
0
    fn eq(&self, other: &str) -> bool {
3454
0
        self.value == other
3455
0
    }
3456
}
3457
3458
impl PartialEq<&str> for Symbol {
3459
0
    fn eq(&self, other: &&str) -> bool {
3460
0
        self.value == *other
3461
0
    }
3462
}
3463
3464
impl PartialEq<String> for Symbol {
3465
0
    fn eq(&self, other: &String) -> bool {
3466
0
        &self.value == other
3467
0
    }
3468
}
3469
3470
impl PartialEq<Symbol> for &str {
3471
0
    fn eq(&self, other: &Symbol) -> bool {
3472
0
        *self == other.value
3473
0
    }
3474
}
3475
3476
impl PartialEq<Symbol> for String {
3477
0
    fn eq(&self, other: &Symbol) -> bool {
3478
0
        self == &other.value
3479
0
    }
3480
}
3481
3482
// TimeInForce moved to canonical source: common::types::TimeInForce
3483
3484
// Currency moved to canonical source: common::types::Currency
3485
3486
// Price moved to canonical source: common::types::Price
3487
3488
// Quantity moved to canonical source: common::types::Quantity
3489
// Volume moved to canonical source: common::types::Quantity (as Volume alias)
3490
3491
/// Money amount with currency
3492
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3493
pub struct Money {
3494
    /// The monetary amount
3495
    pub amount: Decimal,
3496
    /// The currency of the amount
3497
    pub currency: Currency,
3498
}
3499
3500
impl Money {
3501
    /// Create new money amount
3502
0
    pub const fn new(amount: Decimal, currency: Currency) -> Self {
3503
0
        Self { amount, currency }
3504
0
    }
3505
}
3506
3507
impl fmt::Display for Money {
3508
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3509
0
        write!(f, "{} {}", self.amount, self.currency)
3510
0
    }
3511
}
3512
3513
// OrderId moved to canonical source: common::types::OrderId
3514
3515
// TradeId moved to canonical source: common::types::TradeId
3516
3517
// Symbol moved to canonical source: common::types::Symbol
3518
3519
/// Type-safe account identifier
3520
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3521
pub struct AccountId(String);
3522
3523
impl AccountId {
3524
    /// Create a new account ID with validation
3525
0
    pub fn new<S: Into<String>>(id: S) -> Result<Self, CommonTypeError> {
3526
0
        let id = id.into();
3527
0
        if id.trim().is_empty() {
3528
0
            return Err(CommonTypeError::InvalidIdentifier {
3529
0
                field: "account_id".to_string(),
3530
0
                reason: "Account ID cannot be empty".to_string(),
3531
0
            });
3532
0
        }
3533
0
        Ok(Self(id))
3534
0
    }
3535
3536
    /// Get the ID as a string slice
3537
0
    pub fn as_str(&self) -> &str {
3538
0
        &self.0
3539
0
    }
3540
3541
    /// Convert to owned String
3542
0
    pub fn into_string(self) -> String {
3543
0
        self.0
3544
0
    }
3545
}
3546
3547
impl fmt::Display for AccountId {
3548
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3549
0
        write!(f, "{}", self.0)
3550
0
    }
3551
}
3552
3553
/// High-precision timestamp for HFT applications - CANONICAL DEFINITION
3554
/// Robust implementation with error handling for financial safety
3555
#[derive(
3556
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
3557
)]
3558
pub struct HftTimestamp {
3559
    nanos: u64,
3560
}
3561
3562
impl HftTimestamp {
3563
    /// Get current timestamp with error handling for financial safety
3564
4
    pub fn now() -> Result<Self, CommonError> {
3565
        use std::time::{SystemTime, UNIX_EPOCH};
3566
4
        let nanos = SystemTime::now()
3567
4
            .duration_since(UNIX_EPOCH)
3568
4
            .map_err(|e| CommonError::Service {
3569
0
                category: CommonErrorCategory::System,
3570
0
                message: format!("System time before UNIX epoch: {e}"),
3571
0
            })?
3572
4
            .as_nanos() as u64;
3573
4
        Ok(Self { nanos })
3574
4
    }
3575
3576
    /// Get current timestamp with error handling for financial safety (CommonTypeError version)
3577
0
    pub fn now_common() -> Result<Self, CommonTypeError> {
3578
        use std::time::{SystemTime, UNIX_EPOCH};
3579
0
        let nanos = SystemTime::now()
3580
0
            .duration_since(UNIX_EPOCH)
3581
0
            .map_err(|e| CommonTypeError::ConversionError {
3582
0
                message: format!("System time before UNIX epoch: {e}"),
3583
0
            })?
3584
0
            .as_nanos() as u64;
3585
0
        Ok(Self { nanos })
3586
0
    }
3587
3588
    /// Get current timestamp or zero if system time is invalid
3589
    #[must_use]
3590
4
    pub fn now_or_zero() -> Self {
3591
4
        Self::now().unwrap_or(Self { nanos: 0 })
3592
4
    }
3593
3594
    /// Get nanoseconds since epoch
3595
    #[must_use]
3596
0
    pub const fn nanos(self) -> u64 {
3597
0
        self.nanos
3598
0
    }
3599
3600
    /// Create from nanoseconds since epoch
3601
    #[must_use]
3602
0
    pub const fn from_nanos(nanos: u64) -> Self {
3603
0
        Self { nanos }
3604
0
    }
3605
3606
    /// Create from signed nanoseconds (cast to unsigned)
3607
    #[must_use]
3608
0
    pub const fn from_nanos_i64(nanos: i64) -> Self {
3609
0
        Self {
3610
0
            nanos: nanos as u64,
3611
0
        }
3612
0
    }
3613
3614
    /// Get nanoseconds since epoch
3615
0
    pub const fn as_nanos(&self) -> u64 {
3616
0
        self.nanos
3617
0
    }
3618
3619
    /// Convert to `DateTime<Utc>`
3620
0
    pub fn to_datetime(&self) -> DateTime<Utc> {
3621
0
        let secs = self.nanos / 1_000_000_000;
3622
0
        let nsecs = (self.nanos % 1_000_000_000) as u32;
3623
0
        DateTime::from_timestamp(secs as i64, nsecs).unwrap_or_default()
3624
0
    }
3625
}
3626
3627
impl fmt::Display for HftTimestamp {
3628
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3629
0
        write!(f, "{}", self.to_datetime())
3630
0
    }
3631
}
3632
3633
/// Generic timestamp for general use cases
3634
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3635
pub struct GenericTimestamp {
3636
    nanos: u64,
3637
}
3638
3639
impl GenericTimestamp {
3640
    /// Create from nanoseconds since epoch
3641
    #[must_use]
3642
0
    pub const fn from_nanos(nanos: u64) -> Self {
3643
0
        Self { nanos }
3644
0
    }
3645
3646
    /// Get nanoseconds since epoch
3647
    #[must_use]
3648
0
    pub const fn nanos(&self) -> u64 {
3649
0
        self.nanos
3650
0
    }
3651
}
3652
3653
// =============================================================================
3654
// MARKET TYPES (MIGRATED FROM TRADING_ENGINE)
3655
// =============================================================================
3656
3657
/// Market regime enumeration for position sizing scaling and risk management
3658
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3659
pub enum MarketRegime {
3660
    /// Normal market conditions
3661
    Normal,
3662
    /// Crisis/stress market conditions
3663
    Crisis,
3664
    /// Trending market (strong directional movement)
3665
    Trending,
3666
    /// Sideways/ranging market (low volatility)
3667
    Sideways,
3668
    /// Bull market (sustained upward trend)
3669
    Bull,
3670
    /// Bear market (sustained downward trend)
3671
    Bear,
3672
    /// High volatility market conditions
3673
    HighVolatility,
3674
    /// Low volatility market conditions
3675
    LowVolatility,
3676
    /// Volatile market conditions (alias for `HighVolatility`)
3677
    Volatile,
3678
    /// Calm market conditions (alias for `LowVolatility`)
3679
    Calm,
3680
    /// Unknown/unclassified regime
3681
    Unknown,
3682
    /// Recovery regime - transitioning from crisis
3683
    Recovery,
3684
    /// Bubble regime - unsustainable upward movement
3685
    Bubble,
3686
    /// Correction regime - temporary downward adjustment
3687
    Correction,
3688
    /// Custom regime with numeric identifier
3689
    Custom(usize),
3690
}
3691
3692
impl Default for MarketRegime {
3693
0
    fn default() -> Self {
3694
0
        Self::Normal
3695
0
    }
3696
}
3697
3698
impl fmt::Display for MarketRegime {
3699
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3700
0
        match self {
3701
0
            Self::Normal => write!(f, "Normal"),
3702
0
            Self::Crisis => write!(f, "Crisis"),
3703
0
            Self::Trending => write!(f, "Trending"),
3704
0
            Self::Sideways => write!(f, "Sideways"),
3705
0
            Self::Bull => write!(f, "Bull"),
3706
0
            Self::Bear => write!(f, "Bear"),
3707
0
            Self::HighVolatility => write!(f, "HighVolatility"),
3708
0
            Self::LowVolatility => write!(f, "LowVolatility"),
3709
0
            Self::Volatile => write!(f, "Volatile"),
3710
0
            Self::Calm => write!(f, "Calm"),
3711
0
            Self::Unknown => write!(f, "Unknown"),
3712
0
            Self::Recovery => write!(f, "Recovery"),
3713
0
            Self::Bubble => write!(f, "Bubble"),
3714
0
            Self::Correction => write!(f, "Correction"),
3715
0
            Self::Custom(id) => write!(f, "Custom({id})"),
3716
        }
3717
0
    }
3718
}
3719
3720
/// Tick type enumeration for market data
3721
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3722
#[cfg_attr(feature = "database", derive(sqlx::Type))]
3723
#[cfg_attr(
3724
    feature = "database",
3725
    sqlx(type_name = "tick_type", rename_all = "snake_case")
3726
)]
3727
pub enum TickType {
3728
    /// Trade execution tick
3729
    Trade,
3730
    /// Bid price update tick
3731
    Bid,
3732
    /// Ask price update tick
3733
    Ask,
3734
    /// Quote (bid/ask) update tick
3735
    Quote,
3736
}
3737
3738
/// Exchange enumeration for trading venues
3739
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3740
pub enum Exchange {
3741
    /// New York Stock Exchange
3742
    NYSE,
3743
    /// NASDAQ
3744
    NASDAQ,
3745
    /// Chicago Mercantile Exchange
3746
    CME,
3747
    /// Intercontinental Exchange
3748
    ICE,
3749
    /// London Stock Exchange
3750
    LSE,
3751
    /// Tokyo Stock Exchange
3752
    TSE,
3753
    /// Hong Kong Stock Exchange
3754
    HKEX,
3755
    /// Shanghai Stock Exchange
3756
    SSE,
3757
    /// Shenzhen Stock Exchange
3758
    SZSE,
3759
    /// Euronext
3760
    EURONEXT,
3761
    /// Deutsche Börse
3762
    XETRA,
3763
    /// Chicago Board of Trade
3764
    CBOT,
3765
    /// Chicago Board Options Exchange
3766
    CBOE,
3767
    /// BATS Global Markets
3768
    BATS,
3769
    /// IEX Exchange
3770
    IEX,
3771
    /// Interactive Brokers
3772
    IBKR,
3773
    /// IC Markets
3774
    ICMARKETS,
3775
    /// Forex.com
3776
    FOREX,
3777
    /// Binance
3778
    BINANCE,
3779
    /// Coinbase
3780
    COINBASE,
3781
    /// Kraken
3782
    KRAKEN,
3783
    /// Unknown or unrecognized exchange
3784
    UNKNOWN,
3785
}
3786
3787
impl Default for Exchange {
3788
0
    fn default() -> Self {
3789
0
        Self::UNKNOWN
3790
0
    }
3791
}
3792
3793
impl fmt::Display for Exchange {
3794
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3795
0
        match self {
3796
0
            Self::NYSE => write!(f, "NYSE"),
3797
0
            Self::NASDAQ => write!(f, "NASDAQ"),
3798
0
            Self::CME => write!(f, "CME"),
3799
0
            Self::ICE => write!(f, "ICE"),
3800
0
            Self::LSE => write!(f, "LSE"),
3801
0
            Self::TSE => write!(f, "TSE"),
3802
0
            Self::HKEX => write!(f, "HKEX"),
3803
0
            Self::SSE => write!(f, "SSE"),
3804
0
            Self::SZSE => write!(f, "SZSE"),
3805
0
            Self::EURONEXT => write!(f, "EURONEXT"),
3806
0
            Self::XETRA => write!(f, "XETRA"),
3807
0
            Self::CBOT => write!(f, "CBOT"),
3808
0
            Self::CBOE => write!(f, "CBOE"),
3809
0
            Self::BATS => write!(f, "BATS"),
3810
0
            Self::IEX => write!(f, "IEX"),
3811
0
            Self::IBKR => write!(f, "IBKR"),
3812
0
            Self::ICMARKETS => write!(f, "ICMARKETS"),
3813
0
            Self::FOREX => write!(f, "FOREX"),
3814
0
            Self::BINANCE => write!(f, "BINANCE"),
3815
0
            Self::COINBASE => write!(f, "COINBASE"),
3816
0
            Self::KRAKEN => write!(f, "KRAKEN"),
3817
0
            Self::UNKNOWN => write!(f, "UNKNOWN"),
3818
        }
3819
0
    }
3820
}
3821
3822
impl FromStr for Exchange {
3823
    type Err = CommonTypeError;
3824
3825
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
3826
0
        match s.to_uppercase().as_str() {
3827
0
            "NYSE" => Ok(Self::NYSE),
3828
0
            "NASDAQ" => Ok(Self::NASDAQ),
3829
0
            "CME" => Ok(Self::CME),
3830
0
            "ICE" => Ok(Self::ICE),
3831
0
            "LSE" => Ok(Self::LSE),
3832
0
            "TSE" => Ok(Self::TSE),
3833
0
            "HKEX" => Ok(Self::HKEX),
3834
0
            "SSE" => Ok(Self::SSE),
3835
0
            "SZSE" => Ok(Self::SZSE),
3836
0
            "EURONEXT" => Ok(Self::EURONEXT),
3837
0
            "XETRA" => Ok(Self::XETRA),
3838
0
            "CBOT" => Ok(Self::CBOT),
3839
0
            "CBOE" => Ok(Self::CBOE),
3840
0
            "BATS" => Ok(Self::BATS),
3841
0
            "IEX" => Ok(Self::IEX),
3842
0
            "IBKR" => Ok(Self::IBKR),
3843
0
            "ICMARKETS" => Ok(Self::ICMARKETS),
3844
0
            "FOREX" => Ok(Self::FOREX),
3845
0
            "BINANCE" => Ok(Self::BINANCE),
3846
0
            "COINBASE" => Ok(Self::COINBASE),
3847
0
            "KRAKEN" => Ok(Self::KRAKEN),
3848
0
            "UNKNOWN" => Ok(Self::UNKNOWN),
3849
0
            _ => Ok(Self::UNKNOWN), // Default to UNKNOWN for unrecognized exchanges
3850
        }
3851
0
    }
3852
}
3853
3854
/// Market tick data structure - CANONICAL SINGLE SOURCE OF TRUTH
3855
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3856
pub struct MarketTick {
3857
    /// Trading symbol
3858
    pub symbol: Symbol,
3859
    /// Tick price
3860
    pub price: Price,
3861
    /// Tick size/quantity
3862
    pub size: Quantity,
3863
    /// Tick timestamp
3864
    pub timestamp: HftTimestamp,
3865
    /// Type of tick (trade, bid, ask, quote)
3866
    pub tick_type: TickType,
3867
    /// Exchange where the tick occurred
3868
    pub exchange: Exchange,
3869
    /// Sequence number for ordering
3870
    pub sequence_number: u64,
3871
}
3872
3873
impl MarketTick {
3874
    /// Create a new market tick with current timestamp
3875
0
    pub fn new(
3876
0
        symbol: Symbol,
3877
0
        price: Price,
3878
0
        size: Quantity,
3879
0
        tick_type: TickType,
3880
0
        exchange: Exchange,
3881
0
        sequence_number: u64,
3882
0
    ) -> Result<Self, CommonError> {
3883
        Ok(Self {
3884
0
            symbol,
3885
0
            price,
3886
0
            size,
3887
0
            timestamp: HftTimestamp::now()?,
3888
0
            tick_type,
3889
0
            exchange,
3890
0
            sequence_number,
3891
        })
3892
0
    }
3893
3894
    /// Create a new market tick with specified timestamp (for backtesting)
3895
    #[must_use]
3896
0
    pub const fn with_timestamp(
3897
0
        symbol: Symbol,
3898
0
        price: Price,
3899
0
        size: Quantity,
3900
0
        timestamp: HftTimestamp,
3901
0
        tick_type: TickType,
3902
0
        exchange: Exchange,
3903
0
        sequence_number: u64,
3904
0
    ) -> Self {
3905
0
        Self {
3906
0
            symbol,
3907
0
            price,
3908
0
            size,
3909
0
            timestamp,
3910
0
            tick_type,
3911
0
            exchange,
3912
0
            sequence_number,
3913
0
        }
3914
0
    }
3915
}
3916
3917
/// Trading signal for algorithmic trading
3918
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3919
pub struct TradingSignal {
3920
    /// Signal ID
3921
    pub signal_id: Uuid,
3922
    /// Symbol this signal applies to
3923
    pub symbol: Symbol,
3924
    /// Signal strength (-1.0 to 1.0)
3925
    pub strength: f64,
3926
    /// Signal direction
3927
    pub direction: OrderSide,
3928
    /// Confidence level (0.0 to 1.0)
3929
    pub confidence: f64,
3930
    /// Signal generation timestamp
3931
    pub timestamp: HftTimestamp,
3932
    /// Signal source/strategy
3933
    pub source: String,
3934
    /// Additional metadata
3935
    pub metadata: std::collections::HashMap<String, String>,
3936
}
3937
3938
impl TradingSignal {
3939
    /// Create a new trading signal
3940
0
    pub fn new(
3941
0
        symbol: Symbol,
3942
0
        strength: f64,
3943
0
        direction: OrderSide,
3944
0
        confidence: f64,
3945
0
        source: String,
3946
0
    ) -> Result<Self, CommonTypeError> {
3947
0
        if !(0.0..=1.0).contains(&confidence) {
3948
0
            return Err(CommonTypeError::ValidationError {
3949
0
                field: "confidence".to_owned(),
3950
0
                reason: "Confidence must be between 0.0 and 1.0".to_owned(),
3951
0
            });
3952
0
        }
3953
0
        if !(-1.0..=1.0).contains(&strength) {
3954
0
            return Err(CommonTypeError::ValidationError {
3955
0
                field: "strength".to_owned(),
3956
0
                reason: "Strength must be between -1.0 and 1.0".to_owned(),
3957
0
            });
3958
0
        }
3959
3960
        Ok(Self {
3961
0
            signal_id: Uuid::new_v4(),
3962
0
            symbol,
3963
0
            strength,
3964
0
            direction,
3965
0
            confidence,
3966
0
            timestamp: HftTimestamp::now_common()?,
3967
0
            source,
3968
0
            metadata: std::collections::HashMap::new(),
3969
        })
3970
0
    }
3971
3972
    /// Add metadata to the signal
3973
    #[must_use]
3974
0
    pub fn with_metadata(mut self, key: String, value: String) -> Self {
3975
0
        self.metadata.insert(key, value);
3976
0
        self
3977
0
    }
3978
}
3979
3980
// =============================================================================
3981
// HIGH-PERFORMANCE TYPES FOR COPY/CLONE OPTIMIZATION
3982
// =============================================================================
3983
3984
/// Lightweight Order reference for high-performance contexts requiring Copy trait
3985
///
3986
/// This struct contains only the essential order data needed for performance-critical
3987
/// operations like `SmallBatchRing` processing, while maintaining Copy semantics.
3988
/// For full order details, use the complete Order struct.
3989
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3990
pub struct OrderRef {
3991
    /// Order ID (u64 for performance)
3992
    pub id: u64,
3993
    /// Symbol hash for fast lookups
3994
    pub symbol_hash: i64,
3995
    /// Order side (Buy/Sell)
3996
    pub side: OrderSide,
3997
    /// Order type
3998
    pub order_type: OrderType,
3999
    /// Quantity (fixed-point u64)
4000
    pub quantity: u64,
4001
    /// Price (fixed-point u64, 0 for market orders)
4002
    pub price: u64,
4003
    /// Timestamp (nanoseconds since epoch)
4004
    pub timestamp: u64,
4005
}
4006
4007
impl OrderRef {
4008
    /// Create `OrderRef` from a full Order struct
4009
    #[must_use]
4010
0
    pub fn from_order(order: &Order) -> Self {
4011
        Self {
4012
0
            id: order.id.value(),
4013
0
            symbol_hash: order.symbol_hash(),
4014
0
            side: order.side,
4015
0
            order_type: order.order_type,
4016
0
            quantity: order.quantity.raw_value(),
4017
0
            price: order.price.map_or(0, |p| p.raw_value()),
4018
0
            timestamp: order.created_at.nanos(),
4019
        }
4020
0
    }
4021
4022
    /// Create a limit order reference
4023
    #[must_use]
4024
0
    pub fn limit(symbol_hash: i64, side: OrderSide, quantity: u64, price: u64) -> Self {
4025
0
        Self {
4026
0
            id: OrderId::new().value(),
4027
0
            symbol_hash,
4028
0
            side,
4029
0
            order_type: OrderType::Limit,
4030
0
            quantity,
4031
0
            price,
4032
0
            timestamp: HftTimestamp::now_or_zero().nanos(),
4033
0
        }
4034
0
    }
4035
4036
    /// Create a market order reference  
4037
    #[must_use]
4038
0
    pub fn market(symbol_hash: i64, side: OrderSide, quantity: u64) -> Self {
4039
0
        Self {
4040
0
            id: OrderId::new().value(),
4041
0
            symbol_hash,
4042
0
            side,
4043
0
            order_type: OrderType::Market,
4044
0
            quantity,
4045
0
            price: 0,
4046
0
            timestamp: HftTimestamp::now_or_zero().nanos(),
4047
0
        }
4048
0
    }
4049
4050
    /// Get quantity as Quantity type
4051
    #[must_use]
4052
0
    pub const fn get_quantity(&self) -> Quantity {
4053
0
        Quantity::from_raw(self.quantity)
4054
0
    }
4055
4056
    /// Get price as Price type (None for market orders)
4057
    #[must_use]
4058
0
    pub const fn get_price(&self) -> Option<Price> {
4059
0
        if self.price == 0 {
4060
0
            None
4061
        } else {
4062
0
            Some(Price::from_raw(self.price))
4063
        }
4064
0
    }
4065
4066
    /// Check if this is a buy order
4067
    #[must_use]
4068
0
    pub fn is_buy(&self) -> bool {
4069
0
        self.side == OrderSide::Buy
4070
0
    }
4071
4072
    /// Check if this is a sell order
4073
    #[must_use]
4074
0
    pub fn is_sell(&self) -> bool {
4075
0
        self.side == OrderSide::Sell
4076
0
    }
4077
4078
    /// Check if this is a market order
4079
    #[must_use]
4080
0
    pub fn is_market_order(&self) -> bool {
4081
0
        self.order_type == OrderType::Market || self.price == 0
4082
0
    }
4083
4084
    /// Check if this is a limit order
4085
    #[must_use]
4086
0
    pub fn is_limit_order(&self) -> bool {
4087
0
        self.order_type == OrderType::Limit && self.price > 0
4088
0
    }
4089
}
4090
4091
impl Default for OrderRef {
4092
0
    fn default() -> Self {
4093
0
        Self {
4094
0
            id: 0,
4095
0
            symbol_hash: 0,
4096
0
            side: OrderSide::Buy,
4097
0
            order_type: OrderType::Market,
4098
0
            quantity: 0,
4099
0
            price: 0,
4100
0
            timestamp: 0,
4101
0
        }
4102
0
    }
4103
}
4104
4105
// =============================================================================
4106
// COMPREHENSIVE TESTS
4107
// =============================================================================
4108
4109
#[cfg(test)]
4110
mod tests {
4111
    use super::*;
4112
    use std::str::FromStr;
4113
4114
    // =============================================================================
4115
    // Price Tests
4116
    // =============================================================================
4117
4118
    #[test]
4119
    fn test_price_from_f64_valid() {
4120
        let price = Price::from_f64(100.50).unwrap();
4121
        assert_eq!(price.to_f64(), 100.50);
4122
    }
4123
4124
    #[test]
4125
    fn test_price_from_f64_negative() {
4126
        let result = Price::from_f64(-10.0);
4127
        assert!(result.is_err());
4128
    }
4129
4130
    #[test]
4131
    fn test_price_from_f64_nan() {
4132
        let result = Price::from_f64(f64::NAN);
4133
        assert!(result.is_err());
4134
    }
4135
4136
    #[test]
4137
    fn test_price_from_f64_infinity() {
4138
        let result = Price::from_f64(f64::INFINITY);
4139
        assert!(result.is_err());
4140
    }
4141
4142
    #[test]
4143
    fn test_price_constants() {
4144
        assert_eq!(Price::ZERO.to_f64(), 0.0);
4145
        assert_eq!(Price::ONE.to_f64(), 1.0);
4146
        assert_eq!(Price::CENT.to_f64(), 0.01);
4147
    }
4148
4149
    #[test]
4150
    fn test_price_addition() {
4151
        let p1 = Price::from_f64(10.0).unwrap();
4152
        let p2 = Price::from_f64(5.5).unwrap();
4153
        let result = p1 + p2;
4154
        assert!((result.to_f64() - 15.5).abs() < 0.00001);
4155
    }
4156
4157
    #[test]
4158
    fn test_price_subtraction() {
4159
        let p1 = Price::from_f64(10.0).unwrap();
4160
        let p2 = Price::from_f64(5.5).unwrap();
4161
        let result = p1 - p2;
4162
        assert!((result.to_f64() - 4.5).abs() < 0.00001);
4163
    }
4164
4165
    #[test]
4166
    fn test_price_multiplication() {
4167
        let price = Price::from_f64(10.0).unwrap();
4168
        let result = (price * 2.5).unwrap();
4169
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4170
    }
4171
4172
    #[test]
4173
    fn test_price_division() {
4174
        let price = Price::from_f64(10.0).unwrap();
4175
        let result = (price / 2.0).unwrap();
4176
        assert!((result.to_f64() - 5.0).abs() < 0.00001);
4177
    }
4178
4179
    #[test]
4180
    fn test_price_division_by_zero() {
4181
        let price = Price::from_f64(10.0).unwrap();
4182
        let result = price / 0.0;
4183
        assert!(result.is_err());
4184
    }
4185
4186
    #[test]
4187
    fn test_price_from_cents() {
4188
        let price = Price::from_cents(150);
4189
        assert!((price.to_f64() - 1.50).abs() < 0.00001);
4190
    }
4191
4192
    #[test]
4193
    fn test_price_to_cents() {
4194
        let price = Price::from_f64(1.50).unwrap();
4195
        assert_eq!(price.to_cents(), 150);
4196
    }
4197
4198
    #[test]
4199
    fn test_price_is_zero() {
4200
        assert!(Price::ZERO.is_zero());
4201
        assert!(!Price::from_f64(1.0).unwrap().is_zero());
4202
    }
4203
4204
    #[test]
4205
    fn test_price_from_str() {
4206
        let price = Price::from_str("123.45").unwrap();
4207
        assert!((price.to_f64() - 123.45).abs() < 0.00001);
4208
    }
4209
4210
    #[test]
4211
    fn test_price_from_str_invalid() {
4212
        let result = Price::from_str("invalid");
4213
        assert!(result.is_err());
4214
    }
4215
4216
    #[test]
4217
    fn test_price_display() {
4218
        let price = Price::from_f64(123.456789).unwrap();
4219
        let display = format!("{}", price);
4220
        assert!(display.starts_with("123.45678"));
4221
    }
4222
4223
    #[test]
4224
    fn test_price_partial_eq_f64() {
4225
        let price = Price::from_f64(10.0).unwrap();
4226
        assert_eq!(price, 10.0);
4227
        assert_eq!(10.0, price);
4228
    }
4229
4230
    #[test]
4231
    fn test_price_multiply_price() {
4232
        let p1 = Price::from_f64(10.0).unwrap();
4233
        let p2 = Price::from_f64(2.5).unwrap();
4234
        let result = p1.multiply(p2).unwrap();
4235
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4236
    }
4237
4238
    // =============================================================================
4239
    // Quantity Tests
4240
    // =============================================================================
4241
4242
    #[test]
4243
    fn test_quantity_from_f64_valid() {
4244
        let qty = Quantity::from_f64(100.5).unwrap();
4245
        assert_eq!(qty.to_f64(), 100.5);
4246
    }
4247
4248
    #[test]
4249
    fn test_quantity_from_f64_negative() {
4250
        let result = Quantity::from_f64(-10.0);
4251
        assert!(result.is_err());
4252
    }
4253
4254
    #[test]
4255
    fn test_quantity_from_f64_nan() {
4256
        let result = Quantity::from_f64(f64::NAN);
4257
        assert!(result.is_err());
4258
    }
4259
4260
    #[test]
4261
    fn test_quantity_constants() {
4262
        assert_eq!(Quantity::ZERO.to_f64(), 0.0);
4263
        assert_eq!(Quantity::ONE.to_f64(), 1.0);
4264
    }
4265
4266
    #[test]
4267
    fn test_quantity_addition() {
4268
        let q1 = Quantity::from_f64(10.0).unwrap();
4269
        let q2 = Quantity::from_f64(5.5).unwrap();
4270
        let result = q1 + q2;
4271
        assert!((result.to_f64() - 15.5).abs() < 0.00001);
4272
    }
4273
4274
    #[test]
4275
    fn test_quantity_subtraction() {
4276
        let q1 = Quantity::from_f64(10.0).unwrap();
4277
        let q2 = Quantity::from_f64(5.5).unwrap();
4278
        let result = q1 - q2;
4279
        assert!((result.to_f64() - 4.5).abs() < 0.00001);
4280
    }
4281
4282
    #[test]
4283
    fn test_quantity_multiplication() {
4284
        let qty = Quantity::from_f64(10.0).unwrap();
4285
        let result = (qty * 2.5).unwrap();
4286
        assert!((result.to_f64() - 25.0).abs() < 0.00001);
4287
    }
4288
4289
    #[test]
4290
    fn test_quantity_division() {
4291
        let qty = Quantity::from_f64(10.0).unwrap();
4292
        let result = (qty / 2.0).unwrap();
4293
        assert!((result.to_f64() - 5.0).abs() < 0.00001);
4294
    }
4295
4296
    #[test]
4297
    fn test_quantity_division_by_zero() {
4298
        let qty = Quantity::from_f64(10.0).unwrap();
4299
        let result = qty / 0.0;
4300
        assert!(result.is_err());
4301
    }
4302
4303
    #[test]
4304
    fn test_quantity_is_zero() {
4305
        assert!(Quantity::ZERO.is_zero());
4306
        assert!(!Quantity::from_f64(1.0).unwrap().is_zero());
4307
    }
4308
4309
    #[test]
4310
    fn test_quantity_is_positive() {
4311
        assert!(Quantity::from_f64(1.0).unwrap().is_positive());
4312
        assert!(!Quantity::ZERO.is_positive());
4313
    }
4314
4315
    #[test]
4316
    fn test_quantity_is_negative() {
4317
        // Quantity is always non-negative
4318
        assert!(!Quantity::from_f64(1.0).unwrap().is_negative());
4319
        assert!(!Quantity::ZERO.is_negative());
4320
    }
4321
4322
    #[test]
4323
    fn test_quantity_from_shares() {
4324
        let qty = Quantity::from_shares(100);
4325
        assert_eq!(qty.to_shares(), 100);
4326
    }
4327
4328
    #[test]
4329
    fn test_quantity_sum() {
4330
        let quantities = vec![
4331
            Quantity::from_f64(1.0).unwrap(),
4332
            Quantity::from_f64(2.0).unwrap(),
4333
            Quantity::from_f64(3.0).unwrap(),
4334
        ];
4335
        let sum: Quantity = quantities.into_iter().sum();
4336
        assert!((sum.to_f64() - 6.0).abs() < 0.00001);
4337
    }
4338
4339
    #[test]
4340
    fn test_quantity_try_from_i32() {
4341
        let qty = Quantity::try_from(100i32).unwrap();
4342
        assert_eq!(qty.to_f64(), 100.0);
4343
    }
4344
4345
    #[test]
4346
    fn test_quantity_try_from_string() {
4347
        let qty = Quantity::try_from("123.45").unwrap();
4348
        assert!((qty.to_f64() - 123.45).abs() < 0.00001);
4349
    }
4350
4351
    // =============================================================================
4352
    // Money Tests
4353
    // =============================================================================
4354
4355
    #[test]
4356
    fn test_money_new() {
4357
        let amount = Decimal::from_f64(100.50).unwrap();
4358
        let money = Money::new(amount, Currency::USD);
4359
        assert_eq!(money.currency, Currency::USD);
4360
        assert_eq!(money.amount, amount);
4361
    }
4362
4363
    #[test]
4364
    fn test_money_display() {
4365
        let amount = Decimal::from_f64(100.50).unwrap();
4366
        let money = Money::new(amount, Currency::USD);
4367
        let display = format!("{}", money);
4368
        assert!(display.contains("100.5"));
4369
        assert!(display.contains("USD"));
4370
    }
4371
4372
    // =============================================================================
4373
    // Symbol Tests
4374
    // =============================================================================
4375
4376
    #[test]
4377
    fn test_symbol_new() {
4378
        let symbol = Symbol::new("AAPL".to_string());
4379
        assert_eq!(symbol.as_str(), "AAPL");
4380
    }
4381
4382
    #[test]
4383
    fn test_symbol_new_validated_valid() {
4384
        let symbol = Symbol::new_validated("AAPL".to_string()).unwrap();
4385
        assert_eq!(symbol.as_str(), "AAPL");
4386
    }
4387
4388
    #[test]
4389
    fn test_symbol_new_validated_empty() {
4390
        let result = Symbol::new_validated("".to_string());
4391
        assert!(result.is_err());
4392
    }
4393
4394
    #[test]
4395
    fn test_symbol_new_validated_whitespace() {
4396
        let result = Symbol::new_validated("   ".to_string());
4397
        assert!(result.is_err());
4398
    }
4399
4400
    #[test]
4401
    fn test_symbol_from_str() {
4402
        let symbol = Symbol::from_str("AAPL").unwrap();
4403
        assert_eq!(symbol.as_str(), "AAPL");
4404
    }
4405
4406
    #[test]
4407
    fn test_symbol_to_uppercase() {
4408
        let symbol = Symbol::from_str("aapl").unwrap();
4409
        assert_eq!(symbol.to_uppercase(), "AAPL");
4410
    }
4411
4412
    #[test]
4413
    fn test_symbol_replace() {
4414
        let symbol = Symbol::from_str("AAPL.US").unwrap();
4415
        assert_eq!(symbol.replace(".US", ""), "AAPL");
4416
    }
4417
4418
    #[test]
4419
    fn test_symbol_contains() {
4420
        let symbol = Symbol::from_str("AAPL.US").unwrap();
4421
        assert!(symbol.contains("AAPL"));
4422
        assert!(!symbol.contains("MSFT"));
4423
    }
4424
4425
    #[test]
4426
    fn test_symbol_partial_eq_str() {
4427
        let symbol = Symbol::from_str("AAPL").unwrap();
4428
        assert_eq!("AAPL", symbol);
4429
        assert_eq!(symbol.as_str(), "AAPL");
4430
    }
4431
4432
    #[test]
4433
    fn test_symbol_none() {
4434
        let symbol = Symbol::none();
4435
        assert_eq!(symbol.as_str(), "NONE");
4436
    }
4437
4438
    // =============================================================================
4439
    // TimeInForce Tests
4440
    // =============================================================================
4441
4442
    #[test]
4443
    fn test_time_in_force_display() {
4444
        assert_eq!(format!("{}", TimeInForce::Day), "DAY");
4445
        assert_eq!(format!("{}", TimeInForce::GoodTillCancel), "GTC");
4446
        assert_eq!(format!("{}", TimeInForce::ImmediateOrCancel), "IOC");
4447
        assert_eq!(format!("{}", TimeInForce::FillOrKill), "FOK");
4448
    }
4449
4450
    #[test]
4451
    fn test_time_in_force_default() {
4452
        assert_eq!(TimeInForce::default(), TimeInForce::Day);
4453
    }
4454
4455
    // =============================================================================
4456
    // OrderType Tests
4457
    // =============================================================================
4458
4459
    #[test]
4460
    fn test_order_type_display() {
4461
        assert_eq!(format!("{}", OrderType::Market), "MARKET");
4462
        assert_eq!(format!("{}", OrderType::Limit), "LIMIT");
4463
        assert_eq!(format!("{}", OrderType::Stop), "STOP");
4464
        assert_eq!(format!("{}", OrderType::StopLimit), "STOP_LIMIT");
4465
    }
4466
4467
    #[test]
4468
    fn test_order_type_try_from_i32_valid() {
4469
        assert_eq!(OrderType::try_from(0).unwrap(), OrderType::Market);
4470
        assert_eq!(OrderType::try_from(1).unwrap(), OrderType::Limit);
4471
        assert_eq!(OrderType::try_from(2).unwrap(), OrderType::Stop);
4472
    }
4473
4474
    #[test]
4475
    fn test_order_type_try_from_i32_invalid() {
4476
        let result = OrderType::try_from(99);
4477
        assert!(result.is_err());
4478
    }
4479
4480
    #[test]
4481
    fn test_order_type_default() {
4482
        assert_eq!(OrderType::default(), OrderType::Market);
4483
    }
4484
4485
    // =============================================================================
4486
    // OrderStatus Tests
4487
    // =============================================================================
4488
4489
    #[test]
4490
    fn test_order_status_display() {
4491
        assert_eq!(format!("{}", OrderStatus::Created), "CREATED");
4492
        assert_eq!(format!("{}", OrderStatus::Filled), "FILLED");
4493
        assert_eq!(format!("{}", OrderStatus::Cancelled), "CANCELLED");
4494
    }
4495
4496
    #[test]
4497
    fn test_order_status_try_from_i32_valid() {
4498
        assert_eq!(OrderStatus::try_from(0).unwrap(), OrderStatus::Created);
4499
        assert_eq!(OrderStatus::try_from(3).unwrap(), OrderStatus::Filled);
4500
        assert_eq!(OrderStatus::try_from(5).unwrap(), OrderStatus::Cancelled);
4501
    }
4502
4503
    #[test]
4504
    fn test_order_status_try_from_i32_invalid() {
4505
        let result = OrderStatus::try_from(99);
4506
        assert!(result.is_err());
4507
    }
4508
4509
    // =============================================================================
4510
    // OrderSide Tests
4511
    // =============================================================================
4512
4513
    #[test]
4514
    fn test_order_side_display() {
4515
        assert_eq!(format!("{}", OrderSide::Buy), "BUY");
4516
        assert_eq!(format!("{}", OrderSide::Sell), "SELL");
4517
    }
4518
4519
    #[test]
4520
    fn test_order_side_try_from_i32_valid() {
4521
        assert_eq!(OrderSide::try_from(0).unwrap(), OrderSide::Buy);
4522
        assert_eq!(OrderSide::try_from(1).unwrap(), OrderSide::Sell);
4523
    }
4524
4525
    #[test]
4526
    fn test_order_side_try_from_i32_invalid() {
4527
        let result = OrderSide::try_from(99);
4528
        assert!(result.is_err());
4529
    }
4530
4531
    #[test]
4532
    fn test_order_side_default() {
4533
        assert_eq!(OrderSide::default(), OrderSide::Buy);
4534
    }
4535
4536
    // =============================================================================
4537
    // Currency Tests
4538
    // =============================================================================
4539
4540
    #[test]
4541
    fn test_currency_display() {
4542
        assert_eq!(format!("{}", Currency::USD), "USD");
4543
        assert_eq!(format!("{}", Currency::EUR), "EUR");
4544
        assert_eq!(format!("{}", Currency::BTC), "BTC");
4545
    }
4546
4547
    #[test]
4548
    fn test_currency_default() {
4549
        assert_eq!(Currency::default(), Currency::USD);
4550
    }
4551
4552
    // =============================================================================
4553
    // Error Type Tests
4554
    // =============================================================================
4555
4556
    #[test]
4557
    fn test_common_type_error_invalid_price() {
4558
        let error = CommonTypeError::InvalidPrice {
4559
            value: "abc".to_string(),
4560
            reason: "not a number".to_string(),
4561
        };
4562
        let display = format!("{}", error);
4563
        assert!(display.contains("abc"));
4564
    }
4565
4566
    #[test]
4567
    fn test_common_type_error_invalid_quantity() {
4568
        let error = CommonTypeError::InvalidQuantity {
4569
            value: "xyz".to_string(),
4570
            reason: "not a number".to_string(),
4571
        };
4572
        let display = format!("{}", error);
4573
        assert!(display.contains("xyz"));
4574
    }
4575
4576
    #[test]
4577
    fn test_common_type_error_validation() {
4578
        let error = CommonTypeError::ValidationError {
4579
            field: "symbol".to_string(),
4580
            reason: "cannot be empty".to_string(),
4581
        };
4582
        let display = format!("{}", error);
4583
        assert!(display.contains("symbol"));
4584
    }
4585
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html deleted file mode 100644 index e99a1c55b..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/asset_classification.rs
Line
Count
Source
1
//! Comprehensive Asset Classification Configuration System
2
//!
3
//! This module provides production-ready asset classification capabilities with:
4
//! - Sophisticated asset class hierarchies
5
//! - Dynamic trading parameter configuration
6
//! - Pattern-based symbol matching with regex support
7
//! - Database-backed configuration with hot-reload
8
//! - Volatility profiling and risk management integration
9
10
use chrono::{DateTime, Datelike, NaiveTime, Utc};
11
use log;
12
use regex::Regex;
13
use rust_decimal::{prelude::FromPrimitive, Decimal};
14
use serde::{Deserialize, Serialize};
15
use std::collections::HashMap;
16
use uuid::Uuid;
17
18
/// Comprehensive asset classification enum with detailed sub-categories
19
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20
pub enum AssetClass {
21
    /// Equity instruments with sector-specific characteristics
22
    Equity {
23
        sector: EquitySector,
24
        market_cap: MarketCapTier,
25
        region: GeographicRegion,
26
    },
27
    /// Futures contracts with underlying asset classification
28
    Future {
29
        underlying: FutureType,
30
        expiry_type: ExpiryType,
31
        exchange: String,
32
    },
33
    /// Foreign exchange pairs with specific characteristics
34
    Forex {
35
        base: String,
36
        quote: String,
37
        pair_type: ForexPairType,
38
    },
39
    /// Cryptocurrency assets with network and type classification
40
    Crypto {
41
        network: String,
42
        crypto_type: CryptoType,
43
        market_cap_rank: Option<u32>,
44
    },
45
    /// Commodity instruments with category classification
46
    Commodity {
47
        category: CommodityType,
48
        storage_type: StorageType,
49
    },
50
    /// Fixed income securities
51
    FixedIncome {
52
        instrument_type: FixedIncomeType,
53
        credit_rating: CreditRating,
54
        maturity: MaturityBucket,
55
    },
56
    /// Derivatives and structured products
57
    Derivative {
58
        underlying_class: Box<AssetClass>,
59
        derivative_type: DerivativeType,
60
    },
61
    /// Unknown or unclassified assets (conservative defaults)
62
    Unknown,
63
}
64
65
/// Equity sector classifications aligned with industry standards
66
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
67
pub enum EquitySector {
68
    Technology,
69
    Healthcare,
70
    Financial,
71
    ConsumerDiscretionary,
72
    ConsumerStaples,
73
    Industrial,
74
    Energy,
75
    Materials,
76
    Utilities,
77
    RealEstate,
78
    CommunicationServices,
79
}
80
81
/// Market capitalization tiers for equity classification
82
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
83
pub enum MarketCapTier {
84
    LargeCap, // > $10B
85
    MidCap,   // $2B - $10B
86
    SmallCap, // $300M - $2B
87
    MicroCap, // < $300M
88
}
89
90
/// Geographic regions for asset classification
91
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
92
pub enum GeographicRegion {
93
    NorthAmerica,
94
    Europe,
95
    Asia,
96
    EmergingMarkets,
97
    Global,
98
}
99
100
/// Future contract underlying asset types
101
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
102
pub enum FutureType {
103
    Equity,
104
    Currency,
105
    Commodity,
106
    Interest,
107
    Volatility,
108
}
109
110
/// Futures expiry categorization
111
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
112
pub enum ExpiryType {
113
    Weekly,
114
    Monthly,
115
    Quarterly,
116
    Annual,
117
}
118
119
/// Forex pair type classification
120
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
121
pub enum ForexPairType {
122
    Major,   // EUR/USD, GBP/USD, USD/JPY, etc.
123
    Minor,   // Cross-currency pairs without USD
124
    Exotic,  // Emerging market currencies
125
    JPYPair, // Special handling for JPY pairs
126
}
127
128
/// Cryptocurrency type classification
129
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
130
pub enum CryptoType {
131
    Bitcoin,
132
    Ethereum,
133
    Stablecoin,
134
    AltcoinMajor, // Top 20 market cap
135
    AltcoinMinor, // Beyond top 20
136
    DeFi,
137
    GameFi,
138
    Meme,
139
}
140
141
/// Commodity categories
142
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
143
pub enum CommodityType {
144
    PreciousMetals,
145
    Energy,
146
    Agricultural,
147
    IndustrialMetals,
148
    Livestock,
149
}
150
151
/// Storage characteristics for commodities
152
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
153
pub enum StorageType {
154
    Physical,
155
    Financial,
156
}
157
158
/// Fixed income instrument types
159
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
160
pub enum FixedIncomeType {
161
    Government,
162
    Corporate,
163
    Municipal,
164
    InflationProtected,
165
}
166
167
/// Credit rating classifications
168
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
169
pub enum CreditRating {
170
    AAA,
171
    AA,
172
    A,
173
    BBB,
174
    BB,
175
    B,
176
    CCC,
177
    Unrated,
178
}
179
180
/// Maturity buckets for fixed income
181
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
182
pub enum MaturityBucket {
183
    ShortTerm,  // < 2 years
184
    MediumTerm, // 2-10 years
185
    LongTerm,   // > 10 years
186
}
187
188
/// Derivative instrument types
189
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
190
pub enum DerivativeType {
191
    Option,
192
    Swap,
193
    Forward,
194
    Structured,
195
}
196
197
/// Comprehensive volatility profile with regime-aware parameters
198
#[derive(Debug, Clone, Serialize, Deserialize)]
199
pub struct VolatilityProfile {
200
    /// Base annual volatility (standard market conditions)
201
    pub base_annual_volatility: f64,
202
    /// Stress volatility multiplier for high-stress periods
203
    pub stress_volatility_multiplier: f64,
204
    /// Intraday volatility pattern (hourly multipliers)
205
    pub intraday_pattern: Vec<f64>,
206
    /// Volatility clustering parameter (GARCH-like)
207
    pub volatility_persistence: f64,
208
    /// Jump risk probability and magnitude
209
    pub jump_risk: JumpRiskProfile,
210
}
211
212
/// Jump risk characteristics
213
#[derive(Debug, Clone, Serialize, Deserialize)]
214
pub struct JumpRiskProfile {
215
    /// Probability of large price jumps per day
216
    pub jump_probability: f64,
217
    /// Average magnitude of jumps (as fraction of price)
218
    pub jump_magnitude: f64,
219
    /// Maximum expected jump size
220
    pub max_jump_size: f64,
221
}
222
223
/// Dynamic trading parameters that adapt to market conditions
224
#[derive(Debug, Clone, Serialize, Deserialize)]
225
pub struct TradingParameters {
226
    /// Position sizing constraints
227
    pub position_limits: PositionLimits,
228
    /// Risk management thresholds
229
    pub risk_thresholds: RiskThresholds,
230
    /// Execution parameters
231
    pub execution_config: ExecutionConfig,
232
    /// Market making parameters (if applicable)
233
    pub market_making: Option<MarketMakingConfig>,
234
}
235
236
/// Position sizing and exposure limits
237
#[derive(Debug, Clone, Serialize, Deserialize)]
238
pub struct PositionLimits {
239
    /// Maximum position size as fraction of portfolio NAV
240
    pub max_position_fraction: f64,
241
    /// Maximum leverage allowed for this asset
242
    pub max_leverage: f64,
243
    /// Concentration limit (max % of total positions in this asset class)
244
    pub concentration_limit: f64,
245
    /// Minimum position size (to avoid micro-positions)
246
    pub min_position_size: Decimal,
247
}
248
249
/// Risk management thresholds and limits
250
#[derive(Debug, Clone, Serialize, Deserialize)]
251
pub struct RiskThresholds {
252
    /// VaR limit as fraction of portfolio
253
    pub var_limit: f64,
254
    /// Daily loss limit
255
    pub daily_loss_limit: f64,
256
    /// Stop-loss threshold
257
    pub stop_loss_threshold: f64,
258
    /// Volatility circuit breaker threshold
259
    pub volatility_circuit_breaker: f64,
260
    /// Maximum drawdown before position reduction
261
    pub max_drawdown_threshold: f64,
262
}
263
264
/// Execution configuration parameters
265
#[derive(Debug, Clone, Serialize, Deserialize)]
266
pub struct ExecutionConfig {
267
    /// Preferred order types for this asset
268
    pub preferred_order_types: Vec<OrderType>,
269
    /// Tick size for price increments
270
    pub tick_size: Decimal,
271
    /// Minimum order size
272
    pub min_order_size: Decimal,
273
    /// Maximum order size before breaking up
274
    pub max_order_size: Decimal,
275
    /// Execution time constraints
276
    pub time_in_force_default: TimeInForce,
277
    /// Slippage tolerance
278
    pub slippage_tolerance: f64,
279
}
280
281
/// Market making specific configuration
282
#[derive(Debug, Clone, Serialize, Deserialize)]
283
pub struct MarketMakingConfig {
284
    /// Bid-ask spread targets
285
    pub target_spread: f64,
286
    /// Inventory limits
287
    pub max_inventory: Decimal,
288
    /// Quote size
289
    pub quote_size: Decimal,
290
    /// Refresh frequency
291
    pub refresh_frequency: std::time::Duration,
292
}
293
294
/// Order type enumeration
295
#[derive(Debug, Clone, Serialize, Deserialize)]
296
pub enum OrderType {
297
    Market,
298
    Limit,
299
    Stop,
300
    StopLimit,
301
    Hidden,
302
    Iceberg,
303
}
304
305
/// Time in force options
306
#[derive(Debug, Clone, Serialize, Deserialize)]
307
pub enum TimeInForce {
308
    Day,
309
    GoodTillCancel,
310
    ImmediateOrCancel,
311
    FillOrKill,
312
    GTD, // Good Till Date
313
}
314
315
/// Symbol pattern matching configuration with compiled regex
316
#[derive(Debug, Clone, Serialize, Deserialize)]
317
pub struct AssetConfig {
318
    /// UUID for database storage
319
    pub id: Uuid,
320
    /// Human-readable name for this configuration
321
    pub name: String,
322
    /// Regex pattern for symbol matching
323
    pub symbol_pattern: String,
324
    /// Compiled regex (not serialized, rebuilt on load)
325
    #[serde(skip)]
326
    pub compiled_pattern: Option<Regex>,
327
    /// Asset class classification
328
    pub asset_class: AssetClass,
329
    /// Volatility profile
330
    pub volatility_profile: VolatilityProfile,
331
    /// Trading parameters
332
    pub trading_parameters: TradingParameters,
333
    /// Priority for pattern matching (higher = checked first)
334
    pub priority: u32,
335
    /// Whether this configuration is active
336
    pub is_active: bool,
337
    /// Creation timestamp
338
    pub created_at: DateTime<Utc>,
339
    /// Last update timestamp
340
    pub updated_at: DateTime<Utc>,
341
    /// Trading hours (if applicable)
342
    pub trading_hours: Option<TradingHours>,
343
    /// Settlement details
344
    pub settlement_config: SettlementConfig,
345
}
346
347
/// Trading hours configuration
348
#[derive(Debug, Clone, Serialize, Deserialize)]
349
pub struct TradingHours {
350
    /// Regular trading session start
351
    pub market_open: NaiveTime,
352
    /// Regular trading session end
353
    pub market_close: NaiveTime,
354
    /// Pre-market session (if available)
355
    pub pre_market: Option<(NaiveTime, NaiveTime)>,
356
    /// After-hours session (if available)
357
    pub after_hours: Option<(NaiveTime, NaiveTime)>,
358
    /// Timezone for these hours
359
    pub timezone: String,
360
    /// Days of week when trading is active (0=Sunday, 6=Saturday)
361
    pub trading_days: Vec<u8>,
362
}
363
364
/// Settlement configuration
365
#[derive(Debug, Clone, Serialize, Deserialize)]
366
pub struct SettlementConfig {
367
    /// Settlement period (T+n days)
368
    pub settlement_days: u32,
369
    /// Settlement currency
370
    pub settlement_currency: String,
371
    /// Whether physical delivery is possible
372
    pub physical_settlement: bool,
373
}
374
375
/// Asset classification manager with caching and hot-reload capabilities
376
pub struct AssetClassificationManager {
377
    /// Asset configurations indexed by priority
378
    configs: Vec<AssetConfig>,
379
    /// Explicit symbol mappings for fast lookup
380
    symbol_cache: HashMap<String, AssetClass>,
381
    /// Last configuration reload timestamp
382
    last_reload: DateTime<Utc>,
383
    /// Configuration reload interval
384
    reload_interval: std::time::Duration,
385
}
386
387
impl AssetClassificationManager {
388
    /// Create a new asset classification manager
389
6
    pub fn new() -> Self {
390
6
        Self {
391
6
            configs: Vec::new(),
392
6
            symbol_cache: HashMap::new(),
393
6
            last_reload: Utc::now(),
394
6
            reload_interval: std::time::Duration::from_secs(300), // 5 minutes
395
6
        }
396
6
    }
397
398
    /// Load configurations from database
399
0
    pub async fn load_configurations(
400
0
        &mut self,
401
0
        configs: Vec<AssetConfig>,
402
0
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
403
0
        self.configs = configs;
404
        // Sort by priority (highest first)
405
0
        self.configs.sort_by(|a, b| b.priority.cmp(&a.priority));
406
407
        // Compile regex patterns
408
0
        for config in &mut self.configs {
409
0
            match Regex::new(&config.symbol_pattern) {
410
0
                Ok(regex) => config.compiled_pattern = Some(regex),
411
0
                Err(e) => {
412
0
                    log::warn!(
413
0
                        "Failed to compile regex pattern '{}': {}",
414
                        config.symbol_pattern,
415
                        e
416
                    );
417
0
                    config.is_active = false;
418
                }
419
            }
420
        }
421
422
0
        self.last_reload = Utc::now();
423
0
        log::info!(
424
0
            "Loaded {} asset classification configurations",
425
0
            self.configs.len()
426
        );
427
0
        Ok(())
428
0
    }
429
430
    /// Classify a symbol using the configured rules
431
6
    pub fn classify_symbol(&self, symbol: &str) -> AssetClass {
432
6
        let symbol_upper = symbol.to_uppercase();
433
434
        // Check cache first
435
6
        if let Some(
asset_class0
) = self.symbol_cache.get(&symbol_upper) {
436
0
            return asset_class.clone();
437
6
        }
438
439
        // Check pattern rules in priority order
440
6
        for 
config0
in &self.configs {
441
0
            if !config.is_active {
442
0
                continue;
443
0
            }
444
445
0
            if let Some(ref regex) = config.compiled_pattern {
446
0
                if regex.is_match(&symbol_upper) {
447
0
                    return config.asset_class.clone();
448
0
                }
449
0
            }
450
        }
451
452
6
        AssetClass::Unknown
453
6
    }
454
455
    /// Get complete asset configuration for a symbol
456
0
    pub fn get_asset_config(&self, symbol: &str) -> Option<&AssetConfig> {
457
0
        let symbol_upper = symbol.to_uppercase();
458
459
0
        for config in &self.configs {
460
0
            if !config.is_active {
461
0
                continue;
462
0
            }
463
464
0
            if let Some(ref regex) = config.compiled_pattern {
465
0
                if regex.is_match(&symbol_upper) {
466
0
                    return Some(config);
467
0
                }
468
0
            }
469
        }
470
471
0
        None
472
0
    }
473
474
    /// Get volatility profile for a symbol
475
0
    pub fn get_volatility_profile(&self, symbol: &str) -> Option<&VolatilityProfile> {
476
0
        self.get_asset_config(symbol)
477
0
            .map(|config| &config.volatility_profile)
478
0
    }
479
480
    /// Get trading parameters for a symbol
481
0
    pub fn get_trading_parameters(&self, symbol: &str) -> Option<&TradingParameters> {
482
0
        self.get_asset_config(symbol)
483
0
            .map(|config| &config.trading_parameters)
484
0
    }
485
486
    /// Get daily volatility estimate for a symbol
487
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
488
0
        if let Some(profile) = self.get_volatility_profile(symbol) {
489
0
            profile.base_annual_volatility / 252.0_f64.sqrt()
490
        } else {
491
0
            0.5 / 252.0_f64.sqrt() // Default high volatility
492
        }
493
0
    }
494
495
    /// Get position sizing recommendation
496
0
    pub fn get_position_size_recommendation(
497
0
        &self,
498
0
        symbol: &str,
499
0
        portfolio_nav: Decimal,
500
0
    ) -> Option<Decimal> {
501
0
        if let Some(config) = self.get_asset_config(symbol) {
502
0
            let max_fraction = config
503
0
                .trading_parameters
504
0
                .position_limits
505
0
                .max_position_fraction;
506
0
            if let Some(decimal_fraction) = Decimal::from_f64(max_fraction) {
507
0
                Some(portfolio_nav * decimal_fraction)
508
            } else {
509
0
                Some(Decimal::ZERO)
510
            }
511
        } else {
512
0
            None
513
        }
514
0
    }
515
516
    /// Check if symbol is within trading hours
517
0
    pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime<Utc>) -> bool {
518
0
        if let Some(config) = self.get_asset_config(symbol) {
519
0
            if let Some(ref trading_hours) = config.trading_hours {
520
                // Simplified check - in production would need proper timezone handling
521
0
                let weekday = timestamp.weekday().num_days_from_sunday() as u8;
522
0
                trading_hours.trading_days.contains(&weekday)
523
            } else {
524
0
                true // No trading hours restriction
525
            }
526
        } else {
527
0
            true // Default to always active for unknown symbols
528
        }
529
0
    }
530
531
    /// Add explicit symbol mapping to cache
532
0
    pub fn cache_symbol_mapping(&mut self, symbol: String, asset_class: AssetClass) {
533
0
        self.symbol_cache.insert(symbol.to_uppercase(), asset_class);
534
0
    }
535
536
    /// Clear symbol cache
537
0
    pub fn clear_cache(&mut self) {
538
0
        self.symbol_cache.clear();
539
0
    }
540
541
    /// Check if configuration needs reload
542
0
    pub fn needs_reload(&self) -> bool {
543
0
        Utc::now().signed_duration_since(self.last_reload)
544
0
            > chrono::Duration::from_std(self.reload_interval).unwrap_or_default()
545
0
    }
546
547
    /// Get all active configurations
548
0
    pub fn get_active_configurations(&self) -> Vec<&AssetConfig> {
549
0
        self.configs
550
0
            .iter()
551
0
            .filter(|config| config.is_active)
552
0
            .collect()
553
0
    }
554
555
    /// Get configurations by asset class
556
0
    pub fn get_configurations_by_class(&self, asset_class: &AssetClass) -> Vec<&AssetConfig> {
557
0
        self.configs
558
0
            .iter()
559
0
            .filter(|config| config.is_active && &config.asset_class == asset_class)
560
0
            .collect()
561
0
    }
562
}
563
564
impl Default for AssetClassificationManager {
565
0
    fn default() -> Self {
566
0
        Self::new()
567
0
    }
568
}
569
570
/// Create default asset configurations for common instruments
571
0
pub fn create_default_configurations() -> Vec<AssetConfig> {
572
0
    let mut configs = Vec::new();
573
0
    let now = Utc::now();
574
575
    // Blue chip US equities
576
0
    configs.push(AssetConfig {
577
0
        id: Uuid::new_v4(),
578
0
        name: "Blue Chip US Equities".to_string(),
579
0
        symbol_pattern: "^(AAPL|MSFT|GOOGL|AMZN|META|TSLA|NVDA|JPM|JNJ|V|PG|UNH|HD|BAC|DIS|MA|NFLX|CRM|ADBE|PYPL|INTC|CMCSA|PFE|T|VZ|MRK|WMT|KO|NKE|CVX|XOM)$".to_string(),
580
0
        compiled_pattern: None,
581
0
        asset_class: AssetClass::Equity {
582
0
            sector: EquitySector::Technology,
583
0
            market_cap: MarketCapTier::LargeCap,
584
0
            region: GeographicRegion::NorthAmerica,
585
0
        },
586
0
        volatility_profile: VolatilityProfile {
587
0
            base_annual_volatility: 0.25,
588
0
            stress_volatility_multiplier: 2.0,
589
0
            intraday_pattern: vec![1.0; 24], // Flat pattern for simplicity
590
0
            volatility_persistence: 0.85,
591
0
            jump_risk: JumpRiskProfile {
592
0
                jump_probability: 0.02,
593
0
                jump_magnitude: 0.05,
594
0
                max_jump_size: 0.15,
595
0
            },
596
0
        },
597
0
        trading_parameters: TradingParameters {
598
0
            position_limits: PositionLimits {
599
0
                max_position_fraction: 0.20,
600
0
                max_leverage: 2.0,
601
0
                concentration_limit: 0.30,
602
0
                min_position_size: Decimal::from(100),
603
0
            },
604
0
            risk_thresholds: RiskThresholds {
605
0
                var_limit: 0.05,
606
0
                daily_loss_limit: 0.03,
607
0
                stop_loss_threshold: 0.10,
608
0
                volatility_circuit_breaker: 0.05,
609
0
                max_drawdown_threshold: 0.15,
610
0
            },
611
0
            execution_config: ExecutionConfig {
612
0
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
613
0
                tick_size: "0.01".parse().unwrap(),
614
0
                min_order_size: Decimal::from(1),
615
0
                max_order_size: Decimal::from(10000),
616
0
                time_in_force_default: TimeInForce::Day,
617
0
                slippage_tolerance: 0.001,
618
0
            },
619
0
            market_making: None,
620
0
        },
621
0
        priority: 100,
622
0
        is_active: true,
623
0
        created_at: now,
624
0
        updated_at: now,
625
0
        trading_hours: Some(TradingHours {
626
0
            market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
627
0
            market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
628
0
            pre_market: Some((NaiveTime::from_hms_opt(4, 0, 0).unwrap(), NaiveTime::from_hms_opt(9, 30, 0).unwrap())),
629
0
            after_hours: Some((NaiveTime::from_hms_opt(16, 0, 0).unwrap(), NaiveTime::from_hms_opt(20, 0, 0).unwrap())),
630
0
            timezone: "America/New_York".to_string(),
631
0
            trading_days: vec![1, 2, 3, 4, 5], // Monday-Friday
632
0
        }),
633
0
        settlement_config: SettlementConfig {
634
0
            settlement_days: 2,
635
0
            settlement_currency: "USD".to_string(),
636
0
            physical_settlement: false,
637
0
        },
638
0
    });
639
640
    // Major cryptocurrency pairs
641
0
    configs.push(AssetConfig {
642
0
        id: Uuid::new_v4(),
643
0
        name: "Major Cryptocurrencies".to_string(),
644
0
        symbol_pattern: "^(BTC|ETH|BTCUSD|ETHUSD|BTCUSDT|ETHUSDT).*$".to_string(),
645
0
        compiled_pattern: None,
646
0
        asset_class: AssetClass::Crypto {
647
0
            network: "Bitcoin".to_string(),
648
0
            crypto_type: CryptoType::Bitcoin,
649
0
            market_cap_rank: Some(1),
650
0
        },
651
0
        volatility_profile: VolatilityProfile {
652
0
            base_annual_volatility: 0.80,
653
0
            stress_volatility_multiplier: 3.0,
654
0
            intraday_pattern: vec![1.0; 24],
655
0
            volatility_persistence: 0.90,
656
0
            jump_risk: JumpRiskProfile {
657
0
                jump_probability: 0.05,
658
0
                jump_magnitude: 0.10,
659
0
                max_jump_size: 0.30,
660
0
            },
661
0
        },
662
0
        trading_parameters: TradingParameters {
663
0
            position_limits: PositionLimits {
664
0
                max_position_fraction: 0.10,
665
0
                max_leverage: 1.5,
666
0
                concentration_limit: 0.15,
667
0
                min_position_size: "0.001".parse().unwrap(),
668
0
            },
669
0
            risk_thresholds: RiskThresholds {
670
0
                var_limit: 0.10,
671
0
                daily_loss_limit: 0.05,
672
0
                stop_loss_threshold: 0.15,
673
0
                volatility_circuit_breaker: 0.15,
674
0
                max_drawdown_threshold: 0.25,
675
0
            },
676
0
            execution_config: ExecutionConfig {
677
0
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
678
0
                tick_size: "0.01".parse().unwrap(),
679
0
                min_order_size: "0.001".parse().unwrap(),
680
0
                max_order_size: Decimal::from(100),
681
0
                time_in_force_default: TimeInForce::GoodTillCancel,
682
0
                slippage_tolerance: 0.005,
683
0
            },
684
0
            market_making: None,
685
0
        },
686
0
        priority: 90,
687
0
        is_active: true,
688
0
        created_at: now,
689
0
        updated_at: now,
690
0
        trading_hours: None, // 24/7 trading
691
0
        settlement_config: SettlementConfig {
692
0
            settlement_days: 0,
693
0
            settlement_currency: "USD".to_string(),
694
0
            physical_settlement: true,
695
0
        },
696
0
    });
697
698
    // Major forex pairs
699
0
    configs.push(AssetConfig {
700
0
        id: Uuid::new_v4(),
701
0
        name: "Major Forex Pairs".to_string(),
702
0
        symbol_pattern: "^(EUR|GBP|USD|JPY|AUD|CAD|CHF|NZD)(USD|EUR|GBP|JPY)$".to_string(),
703
0
        compiled_pattern: None,
704
0
        asset_class: AssetClass::Forex {
705
0
            base: "EUR".to_string(),
706
0
            quote: "USD".to_string(),
707
0
            pair_type: ForexPairType::Major,
708
0
        },
709
0
        volatility_profile: VolatilityProfile {
710
0
            base_annual_volatility: 0.12,
711
0
            stress_volatility_multiplier: 2.5,
712
0
            intraday_pattern: vec![1.0; 24],
713
0
            volatility_persistence: 0.80,
714
0
            jump_risk: JumpRiskProfile {
715
0
                jump_probability: 0.01,
716
0
                jump_magnitude: 0.02,
717
0
                max_jump_size: 0.08,
718
0
            },
719
0
        },
720
0
        trading_parameters: TradingParameters {
721
0
            position_limits: PositionLimits {
722
0
                max_position_fraction: 0.30,
723
0
                max_leverage: 10.0,
724
0
                concentration_limit: 0.40,
725
0
                min_position_size: Decimal::from(1000),
726
0
            },
727
0
            risk_thresholds: RiskThresholds {
728
0
                var_limit: 0.03,
729
0
                daily_loss_limit: 0.02,
730
0
                stop_loss_threshold: 0.05,
731
0
                volatility_circuit_breaker: 0.03,
732
0
                max_drawdown_threshold: 0.10,
733
0
            },
734
0
            execution_config: ExecutionConfig {
735
0
                preferred_order_types: vec![OrderType::Limit, OrderType::Market],
736
0
                tick_size: "0.00001".parse().unwrap(),
737
0
                min_order_size: Decimal::from(1000),
738
0
                max_order_size: Decimal::from(10000000),
739
0
                time_in_force_default: TimeInForce::GoodTillCancel,
740
0
                slippage_tolerance: 0.0002,
741
0
            },
742
0
            market_making: Some(MarketMakingConfig {
743
0
                target_spread: 0.0001,
744
0
                max_inventory: Decimal::from(100000),
745
0
                quote_size: Decimal::from(10000),
746
0
                refresh_frequency: std::time::Duration::from_millis(100),
747
0
            }),
748
0
        },
749
0
        priority: 80,
750
0
        is_active: true,
751
0
        created_at: now,
752
0
        updated_at: now,
753
0
        trading_hours: None, // 24/5 trading
754
0
        settlement_config: SettlementConfig {
755
0
            settlement_days: 2,
756
0
            settlement_currency: "USD".to_string(),
757
0
            physical_settlement: false,
758
0
        },
759
0
    });
760
761
0
    configs
762
0
}
763
764
#[cfg(test)]
765
mod tests {
766
    use super::*;
767
768
    #[tokio::test]
769
    async fn test_symbol_classification() {
770
        let mut manager = AssetClassificationManager::new();
771
        let configs = create_default_configurations();
772
        manager.load_configurations(configs).await.unwrap();
773
774
        // Test blue chip classification
775
        match manager.classify_symbol("AAPL") {
776
            AssetClass::Equity {
777
                sector: EquitySector::Technology,
778
                ..
779
            } => (),
780
            _ => panic!("AAPL should be classified as Technology equity"),
781
        }
782
783
        // Test crypto classification
784
        match manager.classify_symbol("BTCUSD") {
785
            AssetClass::Crypto {
786
                crypto_type: CryptoType::Bitcoin,
787
                ..
788
            } => (),
789
            _ => panic!("BTCUSD should be classified as Bitcoin crypto"),
790
        }
791
792
        // Test unknown symbol
793
        assert_eq!(manager.classify_symbol("UNKNOWN"), AssetClass::Unknown);
794
    }
795
796
    #[tokio::test]
797
    async fn test_volatility_profile() {
798
        let mut manager = AssetClassificationManager::new();
799
        let configs = create_default_configurations();
800
        manager.load_configurations(configs).await.unwrap();
801
802
        let profile = manager.get_volatility_profile("AAPL").unwrap();
803
        assert_eq!(profile.base_annual_volatility, 0.25);
804
805
        let daily_vol = manager.get_daily_volatility("AAPL");
806
        assert!((daily_vol - (0.25 / 252.0_f64.sqrt())).abs() < 1e-10);
807
    }
808
809
    #[tokio::test]
810
    async fn test_trading_parameters() {
811
        let mut manager = AssetClassificationManager::new();
812
        let configs = create_default_configurations();
813
        manager.load_configurations(configs).await.unwrap();
814
815
        let params = manager.get_trading_parameters("AAPL").unwrap();
816
        assert_eq!(params.position_limits.max_position_fraction, 0.20);
817
        assert_eq!(params.position_limits.max_leverage, 2.0);
818
    }
819
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html deleted file mode 100644 index 7ca0b3897..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/data_config.rs
Line
Count
Source
1
//! Data configuration
2
3
use num_cpus;
4
use serde::{Deserialize, Serialize};
5
6
#[derive(Debug, Clone, Serialize, Deserialize)]
7
pub struct DataConfig {
8
    pub provider: String,
9
    pub symbols: Vec<String>,
10
    pub batch_size: usize,
11
    pub buffer_size: usize,
12
}
13
14
#[derive(Debug, Clone, Serialize, Deserialize)]
15
pub struct DataMicrostructureConfig {
16
    pub enable_bid_ask_spread: bool,
17
    pub enable_order_flow: bool,
18
    pub tick_size: f64,
19
    pub lot_size: f64,
20
    pub bid_ask_spread: bool,
21
    pub volume_imbalance: bool,
22
    pub price_impact: bool,
23
    pub kyle_lambda: bool,
24
    pub amihud_ratio: bool,
25
}
26
27
impl Default for DataMicrostructureConfig {
28
0
    fn default() -> Self {
29
0
        Self {
30
0
            enable_bid_ask_spread: true,
31
0
            enable_order_flow: true,
32
0
            tick_size: 0.01,
33
0
            lot_size: 100.0,
34
0
            bid_ask_spread: true,
35
0
            volume_imbalance: true,
36
0
            price_impact: false,
37
0
            kyle_lambda: false,
38
0
            amihud_ratio: false,
39
0
        }
40
0
    }
41
}
42
43
#[derive(Debug, Clone, Serialize, Deserialize)]
44
pub struct DataTLOBConfig {
45
    pub depth_levels: usize,
46
    pub enable_imbalance: bool,
47
    pub enable_pressure: bool,
48
    pub window_size: usize,
49
}
50
51
#[derive(Debug, Clone, Serialize, Deserialize)]
52
pub struct DataTechnicalIndicatorsConfig {
53
    pub enable_moving_averages: bool,
54
    pub enable_momentum: bool,
55
    pub enable_volatility: bool,
56
    pub window_sizes: Vec<usize>,
57
    pub ma_periods: Vec<usize>,
58
    pub rsi_periods: Vec<usize>,
59
    pub bollinger_periods: Vec<usize>,
60
    pub macd: DataMACDConfig,
61
}
62
63
impl Default for DataTechnicalIndicatorsConfig {
64
0
    fn default() -> Self {
65
0
        Self {
66
0
            enable_moving_averages: true,
67
0
            enable_momentum: true,
68
0
            enable_volatility: true,
69
0
            window_sizes: vec![10, 20, 50],
70
0
            ma_periods: vec![10, 20, 50, 200],
71
0
            rsi_periods: vec![14],
72
0
            bollinger_periods: vec![20],
73
0
            macd: DataMACDConfig::default(),
74
0
        }
75
0
    }
76
}
77
78
#[derive(Debug, Clone, Serialize, Deserialize)]
79
pub struct TrainingBenzingaConfig {
80
    pub api_key: String,
81
    pub api_key_env: String,
82
    pub symbols: Vec<String>,
83
    pub data_types: Vec<String>,
84
    pub timeout: u64,
85
    pub rate_limit: usize,
86
    pub batch_size: usize,
87
    pub enable_caching: bool,
88
}
89
90
impl Default for TrainingBenzingaConfig {
91
0
    fn default() -> Self {
92
0
        Self {
93
0
            api_key: String::new(),
94
0
            api_key_env: "BENZINGA_API_KEY".to_string(),
95
0
            symbols: vec!["SPY".to_string(), "AAPL".to_string()],
96
0
            data_types: vec![
97
0
                "news".to_string(),
98
0
                "sentiment".to_string(),
99
0
                "ratings".to_string(),
100
0
                "options".to_string(),
101
0
            ],
102
0
            timeout: 30,
103
0
            rate_limit: 60,
104
0
            batch_size: 1000,
105
0
            enable_caching: true,
106
0
        }
107
0
    }
108
}
109
110
#[derive(Debug, Clone, Serialize, Deserialize)]
111
pub enum DataCompressionAlgorithm {
112
    GZIP,
113
    ZSTD,
114
    LZ4,
115
    Snappy,
116
    None,
117
}
118
119
#[derive(Debug, Clone, Serialize, Deserialize)]
120
pub struct DataCompressionConfig {
121
    pub algorithm: DataCompressionAlgorithm,
122
    pub enabled: bool,
123
    pub level: Option<i32>,
124
}
125
126
impl Default for DataCompressionConfig {
127
0
    fn default() -> Self {
128
0
        Self {
129
0
            algorithm: DataCompressionAlgorithm::ZSTD,
130
0
            enabled: true,
131
0
            level: Some(3),
132
0
        }
133
0
    }
134
}
135
#[derive(Debug, Clone, Serialize, Deserialize)]
136
pub struct DataVersioningConfig {
137
    pub enabled: bool,
138
    pub version_format: String,
139
    pub keep_versions: usize,
140
}
141
142
impl Default for DataVersioningConfig {
143
0
    fn default() -> Self {
144
0
        Self {
145
0
            enabled: false,
146
0
            version_format: "v%Y%m%d_%H%M%S".to_string(),
147
0
            keep_versions: 5,
148
0
        }
149
0
    }
150
}
151
152
#[derive(Debug, Clone, Serialize, Deserialize)]
153
pub struct DataRetentionConfig {
154
    pub auto_cleanup: bool,
155
    pub retention_days: u32,
156
}
157
158
impl Default for DataRetentionConfig {
159
0
    fn default() -> Self {
160
0
        Self {
161
0
            auto_cleanup: false,
162
0
            retention_days: 30,
163
0
        }
164
0
    }
165
}
166
167
#[derive(Debug, Clone, Serialize, Deserialize)]
168
pub enum DataStorageFormat {
169
    Parquet,
170
    Arrow,
171
    Json,
172
    Csv,
173
    CSV,
174
    HDF5,
175
}
176
177
#[derive(Debug, Clone, Serialize, Deserialize)]
178
pub struct DataStorageConfig {
179
    pub format: DataStorageFormat,
180
    pub compression: DataCompressionConfig,
181
    pub path: String,
182
    pub base_directory: std::path::PathBuf,
183
    pub partition_by: Vec<String>,
184
    pub versioning: DataVersioningConfig,
185
    pub retention: DataRetentionConfig,
186
}
187
188
impl Default for DataStorageConfig {
189
0
    fn default() -> Self {
190
0
        Self {
191
0
            format: DataStorageFormat::Parquet,
192
0
            compression: DataCompressionConfig::default(),
193
0
            path: "./data".to_string(),
194
0
            base_directory: std::path::PathBuf::from("./data"),
195
0
            partition_by: vec!["symbol".to_string(), "date".to_string()],
196
0
            versioning: DataVersioningConfig::default(),
197
0
            retention: DataRetentionConfig::default(),
198
0
        }
199
0
    }
200
}
201
202
#[derive(Debug, Clone, Serialize, Deserialize)]
203
pub struct DataRegimeDetectionConfig {
204
    pub enable_hmm: bool,
205
    pub enable_clustering: bool,
206
    pub window_size: usize,
207
    pub n_states: usize,
208
    pub volatility_regime: bool,
209
    pub trend_regime: bool,
210
    pub volume_regime: bool,
211
    pub correlation_regime: bool,
212
    pub lookback_period: usize,
213
}
214
215
impl Default for DataRegimeDetectionConfig {
216
0
    fn default() -> Self {
217
0
        Self {
218
0
            enable_hmm: false,
219
0
            enable_clustering: false,
220
0
            window_size: 100,
221
0
            n_states: 3,
222
0
            volatility_regime: true,
223
0
            trend_regime: true,
224
0
            volume_regime: false,
225
0
            correlation_regime: false,
226
0
            lookback_period: 252,
227
0
        }
228
0
    }
229
}
230
231
#[derive(Debug, Clone, Serialize, Deserialize)]
232
pub struct DataProcessingConfig {
233
    pub worker_threads: usize,
234
    pub batch_size: usize,
235
    pub buffer_size: usize,
236
    pub timeout: u64,
237
    pub parallel_processing: bool,
238
}
239
240
impl Default for DataProcessingConfig {
241
0
    fn default() -> Self {
242
0
        Self {
243
0
            worker_threads: num_cpus::get(),
244
0
            batch_size: 1000,
245
0
            buffer_size: 10000,
246
0
            timeout: 300,
247
0
            parallel_processing: true,
248
0
        }
249
0
    }
250
}
251
252
#[derive(Debug, Clone, Serialize, Deserialize)]
253
pub struct DataTrainingConfig {
254
    pub batch_size: usize,
255
    pub sequence_length: usize,
256
    pub validation_split: f64,
257
    pub test_split: f64,
258
    pub sources: DataSourcesConfig,
259
    pub features: TrainingFeatureEngineeringConfig,
260
    pub validation: DataValidationConfig,
261
    pub storage: DataStorageConfig,
262
    pub processing: DataProcessingConfig,
263
    pub rate_limit: usize,
264
}
265
266
impl Default for DataTrainingConfig {
267
0
    fn default() -> Self {
268
0
        Self {
269
0
            batch_size: 32,
270
0
            sequence_length: 100,
271
0
            validation_split: 0.2,
272
0
            test_split: 0.1,
273
0
            sources: DataSourcesConfig::default(),
274
0
            features: TrainingFeatureEngineeringConfig::default(),
275
0
            validation: DataValidationConfig::default(),
276
0
            storage: DataStorageConfig::default(),
277
0
            processing: DataProcessingConfig::default(),
278
0
            rate_limit: 100,
279
0
        }
280
0
    }
281
}
282
283
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
284
pub struct DataSourcesConfig {
285
    pub databento: Option<DatabentoConfig>,
286
    pub benzinga: Option<TrainingBenzingaConfig>,
287
    #[serde(default)]
288
    pub enable_realtime: bool,
289
    pub interactive_brokers: Option<InteractiveBrokersConfig>,
290
    pub icmarkets: Option<ICMarketsConfig>,
291
    pub historical: Option<HistoricalDataConfig>,
292
}
293
294
#[derive(Debug, Clone, Serialize, Deserialize)]
295
pub struct InteractiveBrokersConfig {
296
    pub host: String,
297
    pub port: u16,
298
    pub client_id: i32,
299
    pub timeout_seconds: u64,
300
}
301
302
#[derive(Debug, Clone, Serialize, Deserialize)]
303
pub struct ICMarketsConfig {
304
    pub api_key: String,
305
    pub environment: String,
306
}
307
308
#[derive(Debug, Clone, Serialize, Deserialize)]
309
pub struct HistoricalDataConfig {
310
    pub enabled: bool,
311
    pub batch_size: usize,
312
    pub parallel_downloads: usize,
313
}
314
315
#[derive(Debug, Clone, Serialize, Deserialize)]
316
pub struct DatabentoConfig {
317
    pub api_key: String,
318
    pub dataset: String,
319
    pub symbols: Vec<String>,
320
    pub schema: String,
321
    pub stype_in: String,
322
}
323
324
#[derive(Debug, Clone, Serialize, Deserialize)]
325
pub struct DataValidationConfig {
326
    #[serde(default)]
327
    pub enable_price_validation: bool,
328
    #[serde(default)]
329
    pub enable_volume_validation: bool,
330
    #[serde(default)]
331
    pub price_threshold: f64,
332
    #[serde(default)]
333
    pub volume_threshold: f64,
334
    #[serde(default)]
335
    pub outlier_method: OutlierDetectionMethod,
336
    #[serde(default)]
337
    pub max_price_change: f64,
338
    #[serde(default)]
339
    pub max_volume_change: f64,
340
    #[serde(default)]
341
    pub max_timestamp_drift: i64,
342
    #[serde(default)]
343
    pub price_validation: bool,
344
    #[serde(default)]
345
    pub volume_validation: bool,
346
    #[serde(default)]
347
    pub timestamp_validation: bool,
348
    #[serde(default)]
349
    pub outlier_detection: bool,
350
    #[serde(default)]
351
    pub missing_data_handling: MissingDataHandling,
352
}
353
354
impl Default for DataValidationConfig {
355
0
    fn default() -> Self {
356
0
        Self {
357
0
            enable_price_validation: true,
358
0
            enable_volume_validation: true,
359
0
            price_threshold: 0.1,
360
0
            volume_threshold: 0.2,
361
0
            outlier_method: OutlierDetectionMethod::ZScore,
362
0
            max_price_change: 0.05,
363
0
            max_volume_change: 2.0,
364
0
            max_timestamp_drift: 1000,
365
0
            price_validation: true,
366
0
            volume_validation: true,
367
0
            timestamp_validation: true,
368
0
            outlier_detection: true,
369
0
            missing_data_handling: MissingDataHandling::Skip,
370
0
        }
371
0
    }
372
}
373
374
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
375
pub enum MissingDataHandling {
376
    #[default]
377
    Skip,
378
    Drop,
379
    Interpolate,
380
    ForwardFill,
381
    BackwardFill,
382
    FillForward,
383
    FillBackward,
384
    Mean,
385
    Median,
386
    Error,
387
}
388
389
#[derive(Debug, Clone, Serialize, Deserialize)]
390
pub struct TrainingFeatureEngineeringConfig {
391
    pub enable_normalization: bool,
392
    pub enable_scaling: bool,
393
    pub enable_log_returns: bool,
394
    pub lookback_window: usize,
395
    pub regime_detection: DataRegimeDetectionConfig,
396
    pub technical_indicators: DataTechnicalIndicatorsConfig,
397
    pub microstructure: DataMicrostructureConfig,
398
}
399
400
impl Default for TrainingFeatureEngineeringConfig {
401
0
    fn default() -> Self {
402
0
        Self {
403
0
            enable_normalization: true,
404
0
            enable_scaling: true,
405
0
            enable_log_returns: true,
406
0
            lookback_window: 100,
407
0
            regime_detection: DataRegimeDetectionConfig::default(),
408
0
            technical_indicators: DataTechnicalIndicatorsConfig::default(),
409
0
            microstructure: DataMicrostructureConfig::default(),
410
0
        }
411
0
    }
412
}
413
414
#[derive(Debug, Clone, Serialize, Deserialize)]
415
pub struct DataTemporalConfig {
416
    pub enable_time_features: bool,
417
    pub enable_seasonal: bool,
418
    pub timezone: String,
419
    pub business_hours_only: bool,
420
    pub market_session: bool,
421
    pub holiday_effects: bool,
422
    pub expiration_effects: bool,
423
}
424
425
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
426
pub enum OutlierDetectionMethod {
427
    #[default]
428
    ZScore,
429
    IQR,
430
    Isolation,
431
    IsolationForest,
432
    LocalOutlierFactor,
433
    None,
434
}
435
436
#[derive(Debug, Clone, Serialize, Deserialize)]
437
pub struct DataModuleConfig {
438
    pub data_path: String,
439
    pub batch_size: usize,
440
    pub num_workers: usize,
441
    pub cache_size: usize,
442
    pub settings: DataModuleSettings,
443
    pub interactive_brokers: Option<InteractiveBrokersConfig>,
444
}
445
446
#[derive(Debug, Clone, Serialize, Deserialize)]
447
pub struct DataModuleSettings {
448
    pub enable_preprocessing: bool,
449
    pub enable_validation: bool,
450
    pub max_memory_usage: usize,
451
    pub market_data_buffer_size: usize,
452
    pub order_event_buffer_size: usize,
453
}
454
455
#[derive(Debug, Clone, Serialize, Deserialize)]
456
pub struct DataMACDConfig {
457
    pub fast_period: usize,
458
    pub slow_period: usize,
459
    pub signal_period: usize,
460
    pub enabled: bool,
461
}
462
463
impl Default for DataMACDConfig {
464
0
    fn default() -> Self {
465
0
        Self {
466
0
            fast_period: 12,
467
0
            slow_period: 26,
468
0
            signal_period: 9,
469
0
            enabled: true,
470
0
        }
471
0
    }
472
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html deleted file mode 100644 index 644992dcf..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/data_providers.rs
Line
Count
Source
1
//! Data provider endpoint configuration
2
//!
3
//! Centralizes all hardcoded API endpoints for data providers, enabling
4
//! environment-specific configurations and easy switching between dev/staging/prod.
5
6
use serde::{Deserialize, Serialize};
7
8
/// Environment specification for data providers
9
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10
pub enum DataProviderEnvironment {
11
    /// Development environment with potentially mocked or sandbox endpoints
12
    Development,
13
    /// Staging environment for pre-production testing
14
    Staging,
15
    /// Production environment with live data
16
    Production,
17
}
18
19
impl DataProviderEnvironment {
20
    /// Detect environment from FOXHUNT_ENV environment variable
21
0
    pub fn from_env() -> Self {
22
0
        match std::env::var("FOXHUNT_ENV")
23
0
            .unwrap_or_else(|_| "development".to_string())
24
0
            .to_lowercase()
25
0
            .as_str()
26
        {
27
0
            "prod" | "production" => Self::Production,
28
0
            "staging" | "stage" => Self::Staging,
29
0
            _ => Self::Development,
30
        }
31
0
    }
32
}
33
34
/// Databento endpoint configuration
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct DatabentoEndpoints {
37
    /// WebSocket URL for real-time data streaming
38
    pub websocket_url: String,
39
    /// HTTP base URL for historical data queries
40
    pub historical_base_url: String,
41
}
42
43
impl DatabentoEndpoints {
44
    /// Create configuration from environment variables with fallback to defaults
45
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
46
0
        let (ws_default, http_default) = match environment {
47
0
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
48
0
                "wss://gateway.databento.com/v0/subscribe",
49
0
                "https://hist.databento.com",
50
0
            ),
51
0
            DataProviderEnvironment::Staging => (
52
0
                "wss://staging-gateway.databento.com/v0/subscribe",
53
0
                "https://staging-hist.databento.com",
54
0
            ),
55
        };
56
57
        Self {
58
0
            websocket_url: std::env::var("DATABENTO_WS_URL")
59
0
                .unwrap_or_else(|_| ws_default.to_string()),
60
0
            historical_base_url: std::env::var("DATABENTO_HTTP_URL")
61
0
                .unwrap_or_else(|_| http_default.to_string()),
62
        }
63
0
    }
64
}
65
66
impl Default for DatabentoEndpoints {
67
0
    fn default() -> Self {
68
0
        Self::from_env(DataProviderEnvironment::from_env())
69
0
    }
70
}
71
72
/// Benzinga endpoint configuration
73
#[derive(Debug, Clone, Serialize, Deserialize)]
74
pub struct BenzingaEndpoints {
75
    /// WebSocket URL for real-time news and sentiment streaming
76
    pub websocket_url: String,
77
    /// HTTP base URL for API queries
78
    pub api_base_url: String,
79
}
80
81
impl BenzingaEndpoints {
82
    /// Create configuration from environment variables with fallback to defaults
83
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
84
0
        let (ws_default, api_default) = match environment {
85
0
            DataProviderEnvironment::Development | DataProviderEnvironment::Production => (
86
0
                "wss://api.benzinga.com/api/v1/stream",
87
0
                "https://api.benzinga.com/api/v2",
88
0
            ),
89
0
            DataProviderEnvironment::Staging => (
90
0
                "wss://staging-api.benzinga.com/api/v1/stream",
91
0
                "https://staging-api.benzinga.com/api/v2",
92
0
            ),
93
        };
94
95
        Self {
96
0
            websocket_url: std::env::var("BENZINGA_WS_URL")
97
0
                .unwrap_or_else(|_| ws_default.to_string()),
98
0
            api_base_url: std::env::var("BENZINGA_API_URL")
99
0
                .unwrap_or_else(|_| api_default.to_string()),
100
        }
101
0
    }
102
}
103
104
impl Default for BenzingaEndpoints {
105
0
    fn default() -> Self {
106
0
        Self::from_env(DataProviderEnvironment::from_env())
107
0
    }
108
}
109
110
/// Alpaca endpoint configuration
111
#[derive(Debug, Clone, Serialize, Deserialize)]
112
pub struct AlpacaEndpoints {
113
    /// Base URL for trading operations (paper or live)
114
    pub trading_base_url: String,
115
    /// Base URL for market data queries
116
    pub data_base_url: String,
117
}
118
119
impl AlpacaEndpoints {
120
    /// Create configuration from environment variables with fallback to defaults
121
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
122
0
        let (trading_default, data_default) = match environment {
123
0
            DataProviderEnvironment::Development => (
124
0
                "https://paper-api.alpaca.markets",
125
0
                "https://data.alpaca.markets",
126
0
            ),
127
0
            DataProviderEnvironment::Staging => (
128
0
                "https://paper-api.alpaca.markets",
129
0
                "https://data.alpaca.markets",
130
0
            ),
131
0
            DataProviderEnvironment::Production => (
132
0
                "https://api.alpaca.markets",
133
0
                "https://data.alpaca.markets",
134
0
            ),
135
        };
136
137
        Self {
138
0
            trading_base_url: std::env::var("ALPACA_TRADING_URL")
139
0
                .unwrap_or_else(|_| trading_default.to_string()),
140
0
            data_base_url: std::env::var("ALPACA_DATA_URL")
141
0
                .unwrap_or_else(|_| data_default.to_string()),
142
        }
143
0
    }
144
}
145
146
impl Default for AlpacaEndpoints {
147
0
    fn default() -> Self {
148
0
        Self::from_env(DataProviderEnvironment::from_env())
149
0
    }
150
}
151
152
/// Interactive Brokers Gateway configuration
153
#[derive(Debug, Clone, Serialize, Deserialize)]
154
pub struct IBGatewayConfig {
155
    /// Gateway host (typically localhost for local TWS/Gateway)
156
    pub host: String,
157
    /// Gateway port (7497 for paper trading, 7496 for live, 4001 for IB Gateway)
158
    pub port: u16,
159
}
160
161
impl IBGatewayConfig {
162
    /// Create configuration from environment variables with fallback to defaults
163
0
    pub fn from_env(environment: DataProviderEnvironment) -> Self {
164
0
        let (host_default, port_default) = match environment {
165
0
            DataProviderEnvironment::Development => ("127.0.0.1", 7497), // Paper trading
166
0
            DataProviderEnvironment::Staging => ("127.0.0.1", 7497),     // Paper trading
167
0
            DataProviderEnvironment::Production => ("127.0.0.1", 7496),  // Live trading
168
        };
169
170
        Self {
171
0
            host: std::env::var("IB_GATEWAY_HOST")
172
0
                .unwrap_or_else(|_| host_default.to_string()),
173
0
            port: std::env::var("IB_GATEWAY_PORT")
174
0
                .ok()
175
0
                .and_then(|s| s.parse().ok())
176
0
                .unwrap_or(port_default),
177
        }
178
0
    }
179
}
180
181
impl Default for IBGatewayConfig {
182
0
    fn default() -> Self {
183
0
        Self::from_env(DataProviderEnvironment::from_env())
184
0
    }
185
}
186
187
/// Master configuration for all data provider endpoints
188
#[derive(Debug, Clone, Serialize, Deserialize)]
189
pub struct DataProviderConfig {
190
    /// Current environment
191
    pub environment: DataProviderEnvironment,
192
    /// Databento endpoints
193
    pub databento: DatabentoEndpoints,
194
    /// Benzinga endpoints
195
    pub benzinga: BenzingaEndpoints,
196
    /// Alpaca endpoints
197
    pub alpaca: AlpacaEndpoints,
198
    /// Interactive Brokers Gateway configuration
199
    pub ib_gateway: IBGatewayConfig,
200
}
201
202
impl DataProviderConfig {
203
    /// Create configuration from environment
204
0
    pub fn from_env() -> Self {
205
0
        let environment = DataProviderEnvironment::from_env();
206
0
        Self {
207
0
            databento: DatabentoEndpoints::from_env(environment),
208
0
            benzinga: BenzingaEndpoints::from_env(environment),
209
0
            alpaca: AlpacaEndpoints::from_env(environment),
210
0
            ib_gateway: IBGatewayConfig::from_env(environment),
211
0
            environment,
212
0
        }
213
0
    }
214
215
    /// Create configuration for specific environment
216
0
    pub fn for_environment(environment: DataProviderEnvironment) -> Self {
217
0
        Self {
218
0
            databento: DatabentoEndpoints::from_env(environment),
219
0
            benzinga: BenzingaEndpoints::from_env(environment),
220
0
            alpaca: AlpacaEndpoints::from_env(environment),
221
0
            ib_gateway: IBGatewayConfig::from_env(environment),
222
0
            environment,
223
0
        }
224
0
    }
225
}
226
227
impl Default for DataProviderConfig {
228
0
    fn default() -> Self {
229
0
        Self::from_env()
230
0
    }
231
}
232
233
#[cfg(test)]
234
mod tests {
235
    use super::*;
236
237
    #[test]
238
    fn test_environment_detection() {
239
        std::env::set_var("FOXHUNT_ENV", "production");
240
        assert_eq!(
241
            DataProviderEnvironment::from_env(),
242
            DataProviderEnvironment::Production
243
        );
244
245
        std::env::set_var("FOXHUNT_ENV", "staging");
246
        assert_eq!(
247
            DataProviderEnvironment::from_env(),
248
            DataProviderEnvironment::Staging
249
        );
250
251
        std::env::set_var("FOXHUNT_ENV", "development");
252
        assert_eq!(
253
            DataProviderEnvironment::from_env(),
254
            DataProviderEnvironment::Development
255
        );
256
257
        std::env::remove_var("FOXHUNT_ENV");
258
        assert_eq!(
259
            DataProviderEnvironment::from_env(),
260
            DataProviderEnvironment::Development
261
        );
262
    }
263
264
    #[test]
265
    fn test_databento_defaults() {
266
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
267
        assert_eq!(
268
            config.websocket_url,
269
            "wss://gateway.databento.com/v0/subscribe"
270
        );
271
        assert_eq!(config.historical_base_url, "https://hist.databento.com");
272
    }
273
274
    #[test]
275
    fn test_benzinga_defaults() {
276
        let config = BenzingaEndpoints::from_env(DataProviderEnvironment::Production);
277
        assert_eq!(
278
            config.websocket_url,
279
            "wss://api.benzinga.com/api/v1/stream"
280
        );
281
        assert_eq!(config.api_base_url, "https://api.benzinga.com/api/v2");
282
    }
283
284
    #[test]
285
    fn test_alpaca_defaults() {
286
        let dev_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Development);
287
        assert_eq!(
288
            dev_config.trading_base_url,
289
            "https://paper-api.alpaca.markets"
290
        );
291
292
        let prod_config = AlpacaEndpoints::from_env(DataProviderEnvironment::Production);
293
        assert_eq!(prod_config.trading_base_url, "https://api.alpaca.markets");
294
    }
295
296
    #[test]
297
    fn test_ib_gateway_defaults() {
298
        let dev_config = IBGatewayConfig::from_env(DataProviderEnvironment::Development);
299
        assert_eq!(dev_config.host, "127.0.0.1");
300
        assert_eq!(dev_config.port, 7497);
301
302
        let prod_config = IBGatewayConfig::from_env(DataProviderEnvironment::Production);
303
        assert_eq!(prod_config.port, 7496);
304
    }
305
306
    #[test]
307
    fn test_environment_variable_override() {
308
        std::env::set_var("DATABENTO_WS_URL", "wss://custom.databento.com");
309
        let config = DatabentoEndpoints::from_env(DataProviderEnvironment::Production);
310
        assert_eq!(config.websocket_url, "wss://custom.databento.com");
311
        std::env::remove_var("DATABENTO_WS_URL");
312
    }
313
314
    #[test]
315
    fn test_master_config() {
316
        let config = DataProviderConfig::for_environment(DataProviderEnvironment::Production);
317
        assert_eq!(config.environment, DataProviderEnvironment::Production);
318
        assert!(!config.databento.websocket_url.is_empty());
319
        assert!(!config.benzinga.websocket_url.is_empty());
320
        assert!(!config.alpaca.trading_base_url.is_empty());
321
        assert!(!config.ib_gateway.host.is_empty());
322
    }
323
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html deleted file mode 100644 index 388766c35..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/database.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/database.rs
Line
Count
Source
1
//! Database configuration for PostgreSQL connections and connection pooling.
2
//!
3
//! This module provides comprehensive database configuration structures for managing
4
//! PostgreSQL connections, connection pools, and transaction settings in the Foxhunt
5
//! HFT trading system. It supports connection pooling, timeout management, and
6
//! transaction isolation levels optimized for high-frequency trading workloads.
7
8
use serde::{Deserialize, Serialize};
9
use std::time::Duration;
10
11
#[cfg(feature = "postgres")]
12
use sqlx::Row;
13
14
/// Main database configuration structure for PostgreSQL connections.
15
///
16
/// Provides comprehensive database connection settings including connection pooling,
17
/// timeouts, logging, and transaction management. Optimized for high-frequency
18
/// trading workloads with appropriate defaults for low-latency operations.
19
#[derive(Debug, Clone, Serialize, Deserialize)]
20
pub struct DatabaseConfig {
21
    /// PostgreSQL connection URL (e.g., "postgresql://user:pass@host:port/database")
22
    pub url: String,
23
    /// Maximum number of connections in the pool
24
    pub max_connections: u32,
25
    /// Minimum number of connections to maintain in the pool
26
    pub min_connections: u32,
27
    /// Timeout for establishing new database connections
28
    pub connect_timeout: std::time::Duration,
29
    /// Timeout for individual query execution
30
    pub query_timeout: std::time::Duration,
31
    /// Enable detailed query logging for debugging
32
    pub enable_query_logging: bool,
33
    /// Application name to identify connections in PostgreSQL logs
34
    pub application_name: Option<String>,
35
    /// Connection pool configuration settings
36
    pub pool: PoolConfig,
37
    /// Transaction management configuration
38
    pub transaction: TransactionConfig,
39
}
40
41
impl Default for DatabaseConfig {
42
0
    fn default() -> Self {
43
0
        Self::new()
44
0
    }
45
}
46
47
impl DatabaseConfig {
48
    /// Creates a new DatabaseConfig with sensible defaults for development.
49
    ///
50
    /// Returns a configuration suitable for local development with a PostgreSQL
51
    /// database running on localhost. Production deployments should override
52
    /// these settings through environment variables or configuration files.
53
0
    pub fn new() -> Self {
54
        // Get database URL from environment, with fallback to development default
55
0
        let url = std::env::var("DATABASE_URL")
56
0
            .unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string());
57
    
58
0
        Self {
59
0
            url,
60
0
            max_connections: 10,
61
0
            min_connections: 1,
62
0
            connect_timeout: Duration::from_secs(30),
63
0
            query_timeout: Duration::from_secs(60),
64
0
            enable_query_logging: false,
65
0
            application_name: Some("foxhunt".to_string()),
66
0
            pool: PoolConfig::default(),
67
0
            transaction: TransactionConfig::default(),
68
0
        }
69
0
    }
70
71
    /// Validates the database configuration for correctness.
72
    ///
73
    /// Performs basic validation checks on the configuration parameters to ensure
74
    /// they are valid before attempting to establish database connections.
75
    ///
76
    /// # Errors
77
    ///
78
    /// Returns an error string if the configuration is invalid, such as:
79
    /// - Empty database URL
80
    /// - Invalid connection parameters
81
0
    pub fn validate(&self) -> Result<(), String> {
82
0
        if self.url.is_empty() {
83
0
            return Err("Database URL cannot be empty".to_string());
84
0
        }
85
0
        Ok(())
86
0
    }
87
}
88
89
/// Database connection pool configuration.
90
///
91
/// Manages the behavior of the connection pool including connection lifecycle,
92
/// timeouts, and health checking. Optimized for high-frequency trading workloads
93
/// where connection availability and low latency are critical.
94
#[derive(Debug, Clone, Serialize, Deserialize)]
95
pub struct PoolConfig {
96
    /// Minimum number of connections to maintain in the pool
97
    pub min_connections: u32,
98
    /// Maximum number of connections allowed in the pool
99
    pub max_connections: u32,
100
    /// Timeout in seconds for acquiring a connection from the pool
101
    pub acquire_timeout_secs: u64,
102
    /// Maximum lifetime in seconds for a connection before it's recycled
103
    pub max_lifetime_secs: u64,
104
    /// Timeout in seconds before idle connections are closed
105
    pub idle_timeout_secs: u64,
106
    /// Whether to test connections before returning them from the pool
107
    pub test_before_acquire: bool,
108
    /// Database URL for pool connections
109
    pub database_url: String,
110
    /// Enable periodic health checks for pool connections
111
    pub health_check_enabled: bool,
112
    /// Interval in seconds between health checks
113
    pub health_check_interval_secs: u64,
114
}
115
116
impl Default for PoolConfig {
117
0
    fn default() -> Self {
118
        // Get database URL from environment, with fallback to development default
119
0
        let database_url = std::env::var("DATABASE_URL")
120
0
            .unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string());
121
122
0
        Self {
123
0
            min_connections: 1,
124
0
            max_connections: 10,
125
0
            acquire_timeout_secs: 30,
126
0
            max_lifetime_secs: 1800,
127
0
            idle_timeout_secs: 600,
128
0
            test_before_acquire: true,
129
0
            database_url,
130
0
            health_check_enabled: true,
131
0
            health_check_interval_secs: 60,
132
0
        }
133
0
    }
134
}
135
136
/// Database transaction configuration and retry policies.
137
///
138
/// Configures transaction behavior including isolation levels, timeouts,
139
/// and retry mechanisms. Critical for maintaining data consistency in
140
/// high-frequency trading operations while handling transient failures.
141
#[derive(Debug, Clone, Serialize, Deserialize)]
142
pub struct TransactionConfig {
143
    /// PostgreSQL transaction isolation level (e.g., "READ_COMMITTED", "SERIALIZABLE")
144
    pub isolation_level: String,
145
    /// Default timeout duration for transactions
146
    pub timeout: Duration,
147
    /// Default timeout in seconds for transactions
148
    pub default_timeout_secs: u64,
149
    /// Enable automatic retry on transaction failures
150
    pub enable_retry: bool,
151
    /// Maximum number of retry attempts for failed transactions
152
    pub max_retries: u32,
153
    /// Delay in milliseconds between retry attempts
154
    pub retry_delay_ms: u64,
155
    /// Maximum number of nested savepoints allowed
156
    pub max_savepoints: u32,
157
}
158
159
impl Default for TransactionConfig {
160
0
    fn default() -> Self {
161
0
        Self {
162
0
            isolation_level: "READ_COMMITTED".to_string(),
163
0
            timeout: Duration::from_secs(30),
164
0
            default_timeout_secs: 30,
165
0
            enable_retry: true,
166
0
            max_retries: 3,
167
0
            retry_delay_ms: 100,
168
0
            max_savepoints: 10,
169
0
        }
170
0
    }
171
}
172
173
/// Database loader for symbol configurations with PostgreSQL integration.
174
///
175
/// Provides high-performance loading and caching of symbol configurations
176
/// from the PostgreSQL database. Supports real-time updates through PostgreSQL
177
/// NOTIFY/LISTEN for configuration hot-reload capabilities.
178
#[cfg(feature = "postgres")]
179
pub struct PostgresSymbolConfigLoader {
180
    /// Database connection pool
181
    pool: sqlx::PgPool,
182
    /// Configuration cache timeout
183
    cache_timeout: Duration,
184
    /// PostgreSQL listener for configuration changes
185
    listener: Option<sqlx::postgres::PgListener>,
186
}
187
188
#[cfg(feature = "postgres")]
189
impl PostgresSymbolConfigLoader {
190
    /// Creates a new PostgreSQL symbol configuration loader.
191
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
192
        let pool = sqlx::PgPool::connect(database_url).await?;
193
194
        Ok(Self {
195
            pool,
196
            cache_timeout: Duration::from_secs(300), // 5 minutes
197
            listener: None,
198
        })
199
    }
200
201
    /// Creates a new loader with an existing connection pool.
202
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
203
        Self {
204
            pool,
205
            cache_timeout: Duration::from_secs(300),
206
            listener: None,
207
        }
208
    }
209
210
    /// Loads a symbol configuration by symbol name.
211
    pub async fn load_symbol_config(
212
        &self,
213
        symbol: &str,
214
    ) -> Result<Option<crate::symbol_config::SymbolConfig>, sqlx::Error> {
215
        // Simplified implementation using basic sqlx::query instead of macros
216
        let query = "
217
            SELECT 
218
                sc.id,
219
                sc.symbol,
220
                sc.description,
221
                sc.classification,
222
                sc.primary_exchange,
223
                sc.currency,
224
                sc.tick_size,
225
                sc.lot_size,
226
                sc.min_order_size,
227
                sc.max_order_size,
228
                sc.sector,
229
                sc.industry,
230
                sc.market_cap,
231
                sc.avg_daily_volume,
232
                sc.margin_requirement,
233
                sc.position_limit,
234
                sc.risk_multiplier,
235
                sc.is_active,
236
                sc.data_source,
237
                sc.created_at,
238
                sc.updated_at,
239
                sc.last_validated
240
            FROM symbol_config sc
241
            WHERE sc.symbol = $1 AND sc.is_active = true
242
        ";
243
244
        let row = sqlx::query(query)
245
            .bind(symbol)
246
            .fetch_optional(&self.pool)
247
            .await?;
248
249
        if let Some(row) = row {
250
            // Create a basic symbol config from the row
251
            let symbol_name: String = row.get("symbol");
252
            let description: String = row.get("description");
253
            let classification_str: String = row.get("classification");
254
255
            let classification = match classification_str.as_str() {
256
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
257
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
258
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
259
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
260
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
261
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
262
                "OPTION" => crate::symbol_config::AssetClassification::Option,
263
                "ETF" => crate::symbol_config::AssetClassification::Etf,
264
                "INDEX" => crate::symbol_config::AssetClassification::Index,
265
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
266
                _ => crate::symbol_config::AssetClassification::Equity,
267
            };
268
269
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
270
            config.description = description;
271
            config.primary_exchange = row.get("primary_exchange");
272
            config.currency = row.get("currency");
273
274
            // Handle decimal conversions safely
275
            if let Ok(tick_size) = row.try_get::<rust_decimal::Decimal, _>("tick_size") {
276
                if let Ok(f) = tick_size.try_into() {
277
                    config.tick_size = f;
278
                }
279
            }
280
281
            Ok(Some(config))
282
        } else {
283
            Ok(None)
284
        }
285
    }
286
287
    /// Loads all active symbol configurations.
288
    pub async fn load_all_symbols(
289
        &self,
290
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
291
        let query = "
292
            SELECT symbol, description, classification
293
            FROM symbol_config 
294
            WHERE is_active = true
295
            ORDER BY symbol
296
        ";
297
298
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
299
300
        let mut configs = Vec::new();
301
        for row in rows {
302
            let symbol_name: String = row.get("symbol");
303
            let description: String = row.get("description");
304
            let classification_str: String = row.get("classification");
305
306
            let classification = match classification_str.as_str() {
307
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
308
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
309
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
310
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
311
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
312
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
313
                "OPTION" => crate::symbol_config::AssetClassification::Option,
314
                "ETF" => crate::symbol_config::AssetClassification::Etf,
315
                "INDEX" => crate::symbol_config::AssetClassification::Index,
316
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
317
                _ => crate::symbol_config::AssetClassification::Equity,
318
            };
319
320
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
321
            config.description = description;
322
            configs.push(config);
323
        }
324
325
        Ok(configs)
326
    }
327
328
    /// Loads symbols filtered by asset classification.
329
    pub async fn load_symbols_by_classification(
330
        &self,
331
        classification: crate::symbol_config::AssetClassification,
332
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
333
        let class_str = classification.regulatory_class();
334
335
        let query = "
336
            SELECT symbol, description, classification
337
            FROM symbol_config 
338
            WHERE is_active = true AND classification = $1
339
            ORDER BY symbol
340
        ";
341
342
        let rows = sqlx::query(query)
343
            .bind(class_str)
344
            .fetch_all(&self.pool)
345
            .await?;
346
347
        let mut configs = Vec::new();
348
        for row in rows {
349
            let symbol_name: String = row.get("symbol");
350
            let description: String = row.get("description");
351
            let mut config =
352
                crate::symbol_config::SymbolConfig::new(symbol_name, classification.clone());
353
            config.description = description;
354
            configs.push(config);
355
        }
356
357
        Ok(configs)
358
    }
359
    /// Saves or updates a symbol configuration.
360
    pub async fn save_symbol_config(
361
        &self,
362
        config: &crate::symbol_config::SymbolConfig,
363
    ) -> Result<(), sqlx::Error> {
364
        let query = "
365
            INSERT INTO symbol_config (
366
                symbol, description, classification, primary_exchange, currency
367
            ) VALUES ($1, $2, $3, $4, $5)
368
            ON CONFLICT (symbol) DO UPDATE SET
369
                description = EXCLUDED.description,
370
                classification = EXCLUDED.classification,
371
                primary_exchange = EXCLUDED.primary_exchange,
372
                currency = EXCLUDED.currency,
373
                updated_at = NOW()
374
        ";
375
376
        sqlx::query(query)
377
            .bind(&config.symbol)
378
            .bind(&config.description)
379
            .bind(config.classification.regulatory_class())
380
            .bind(&config.primary_exchange)
381
            .bind(&config.currency)
382
            .execute(&self.pool)
383
            .await?;
384
385
        Ok(())
386
    }
387
388
    /// Initializes PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
389
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
390
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
391
        listener.listen("symbol_config_changed").await?;
392
        self.listener = Some(listener);
393
        Ok(())
394
    }
395
396
    /// Checks for configuration change notifications.
397
    pub async fn check_for_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
398
        if let Some(listener) = &mut self.listener {
399
            if let Some(notification) = listener.try_recv().await? {
400
                return Ok(Some(notification.payload().to_string()));
401
            }
402
        }
403
        Ok(None)
404
    }
405
}
406
407
/// Database integration for comprehensive asset classification system.
408
///
409
/// Provides PostgreSQL-backed storage and retrieval for asset classification
410
/// configurations with support for pattern matching, caching, and hot-reload.
411
#[cfg(feature = "postgres")]
412
pub struct PostgresAssetClassificationLoader {
413
    /// Database connection pool
414
    pool: sqlx::PgPool,
415
    /// Configuration cache timeout
416
    cache_timeout: Duration,
417
    /// PostgreSQL listener for configuration changes
418
    listener: Option<sqlx::postgres::PgListener>,
419
}
420
421
#[cfg(feature = "postgres")]
422
impl PostgresAssetClassificationLoader {
423
    /// Creates a new PostgreSQL asset classification loader.
424
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
425
        let pool = sqlx::PgPool::connect(database_url).await?;
426
427
        Ok(Self {
428
            pool,
429
            cache_timeout: Duration::from_secs(300), // 5 minutes
430
            listener: None,
431
        })
432
    }
433
434
    /// Creates a new loader with an existing connection pool.
435
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
436
        Self {
437
            pool,
438
            cache_timeout: Duration::from_secs(300),
439
            listener: None,
440
        }
441
    }
442
443
    /// Loads all active asset configurations ordered by priority.
444
    pub async fn load_asset_configurations(
445
        &self,
446
    ) -> Result<Vec<crate::asset_classification::AssetConfig>, sqlx::Error> {
447
        let query = "
448
                SELECT 
449
                    id,
450
                    name,
451
                    symbol_pattern,
452
                    asset_class_data,
453
                    volatility_profile,
454
                    trading_parameters,
455
                    priority,
456
                    is_active,
457
                    created_at,
458
                    updated_at,
459
                    trading_hours,
460
                    settlement_config
461
                FROM asset_configurations
462
                WHERE is_active = true
463
                ORDER BY priority DESC
464
            ";
465
466
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
467
468
        let mut configs = Vec::new();
469
        for row in rows {
470
            if let Ok(config) = self.row_to_asset_config(row) {
471
                configs.push(config);
472
            }
473
        }
474
475
        Ok(configs)
476
    }
477
478
    /// Loads a specific asset configuration by ID.
479
    pub async fn load_asset_configuration_by_id(
480
        &self,
481
        id: uuid::Uuid,
482
    ) -> Result<Option<crate::asset_classification::AssetConfig>, sqlx::Error> {
483
        let query = "
484
                SELECT 
485
                    id,
486
                    name,
487
                    symbol_pattern,
488
                    asset_class_data,
489
                    volatility_profile,
490
                    trading_parameters,
491
                    priority,
492
                    is_active,
493
                    created_at,
494
                    updated_at,
495
                    trading_hours,
496
                    settlement_config
497
                FROM asset_configurations
498
                WHERE id = $1
499
            ";
500
501
        let row = sqlx::query(query)
502
            .bind(id)
503
            .fetch_optional(&self.pool)
504
            .await?;
505
506
        if let Some(row) = row {
507
            Ok(Some(self.row_to_asset_config(row)?))
508
        } else {
509
            Ok(None)
510
        }
511
    }
512
513
    /// Saves or updates an asset configuration.
514
    pub async fn save_asset_configuration(
515
        &self,
516
        config: &crate::asset_classification::AssetConfig,
517
    ) -> Result<(), sqlx::Error> {
518
        let query = "
519
                INSERT INTO asset_configurations (
520
                    id, name, symbol_pattern, asset_class_data, volatility_profile,
521
                    trading_parameters, priority, is_active, created_at, updated_at,
522
                    trading_hours, settlement_config
523
                ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
524
                ON CONFLICT (id) DO UPDATE SET
525
                    name = EXCLUDED.name,
526
                    symbol_pattern = EXCLUDED.symbol_pattern,
527
                    asset_class_data = EXCLUDED.asset_class_data,
528
                    volatility_profile = EXCLUDED.volatility_profile,
529
                    trading_parameters = EXCLUDED.trading_parameters,
530
                    priority = EXCLUDED.priority,
531
                    is_active = EXCLUDED.is_active,
532
                    updated_at = NOW(),
533
                    trading_hours = EXCLUDED.trading_hours,
534
                    settlement_config = EXCLUDED.settlement_config
535
            ";
536
537
        let asset_class_json = serde_json::to_value(&config.asset_class)
538
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
539
        let volatility_json = serde_json::to_value(&config.volatility_profile)
540
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
541
        let trading_params_json = serde_json::to_value(&config.trading_parameters)
542
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
543
        let trading_hours_json = serde_json::to_value(&config.trading_hours)
544
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
545
        let settlement_json = serde_json::to_value(&config.settlement_config)
546
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
547
548
        sqlx::query(query)
549
            .bind(config.id)
550
            .bind(&config.name)
551
            .bind(&config.symbol_pattern)
552
            .bind(asset_class_json)
553
            .bind(volatility_json)
554
            .bind(trading_params_json)
555
            .bind(config.priority as i32)
556
            .bind(config.is_active)
557
            .bind(config.created_at)
558
            .bind(config.updated_at)
559
            .bind(trading_hours_json)
560
            .bind(settlement_json)
561
            .execute(&self.pool)
562
            .await?;
563
564
        Ok(())
565
    }
566
567
    /// Loads explicit symbol mappings.
568
    pub async fn load_symbol_mappings(
569
        &self,
570
    ) -> Result<
571
        std::collections::HashMap<String, crate::asset_classification::AssetClass>,
572
        sqlx::Error,
573
    > {
574
        let query = "
575
                SELECT symbol, asset_class_data
576
                FROM symbol_mappings
577
                WHERE is_active = true AND (expires_at IS NULL OR expires_at > NOW())
578
            ";
579
580
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
581
582
        let mut mappings = std::collections::HashMap::new();
583
        for row in rows {
584
            let symbol: String = row.get("symbol");
585
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
586
587
            if let Ok(asset_class) =
588
                serde_json::from_value::<crate::asset_classification::AssetClass>(asset_class_json)
589
            {
590
                mappings.insert(symbol.to_uppercase(), asset_class);
591
            }
592
        }
593
594
        Ok(mappings)
595
    }
596
597
    /// Saves a symbol mapping.
598
    pub async fn save_symbol_mapping(
599
        &self,
600
        symbol: &str,
601
        asset_class: &crate::asset_classification::AssetClass,
602
        source: &str,
603
        confidence_score: f64,
604
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
605
    ) -> Result<(), sqlx::Error> {
606
        let query = "
607
                INSERT INTO symbol_mappings (
608
                    symbol, asset_class_data, source, confidence_score, expires_at
609
                ) VALUES ($1, $2, $3, $4, $5)
610
                ON CONFLICT (symbol) DO UPDATE SET
611
                    asset_class_data = EXCLUDED.asset_class_data,
612
                    source = EXCLUDED.source,
613
                    confidence_score = EXCLUDED.confidence_score,
614
                    expires_at = EXCLUDED.expires_at,
615
                    updated_at = NOW()
616
            ";
617
618
        let asset_class_json =
619
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
620
621
        sqlx::query(query)
622
            .bind(symbol.to_uppercase())
623
            .bind(asset_class_json)
624
            .bind(source)
625
            .bind(confidence_score)
626
            .bind(expires_at)
627
            .execute(&self.pool)
628
            .await?;
629
630
        Ok(())
631
    }
632
633
    /// Loads volatility profiles.
634
    pub async fn load_volatility_profiles(
635
        &self,
636
    ) -> Result<
637
        std::collections::HashMap<String, crate::asset_classification::VolatilityProfile>,
638
        sqlx::Error,
639
    > {
640
        let query = "
641
                SELECT 
642
                    name,
643
                    base_annual_volatility,
644
                    stress_volatility_multiplier,
645
                    intraday_pattern,
646
                    volatility_persistence,
647
                    jump_risk
648
                FROM volatility_profiles
649
                WHERE is_active = true
650
            ";
651
652
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
653
654
        let mut profiles = std::collections::HashMap::new();
655
        for row in rows {
656
            let name: String = row.get("name");
657
            let base_volatility: rust_decimal::Decimal = row.get("base_annual_volatility");
658
            let stress_multiplier: rust_decimal::Decimal = row.get("stress_volatility_multiplier");
659
            let persistence: rust_decimal::Decimal = row.get("volatility_persistence");
660
            let intraday_json: serde_json::Value = row.get("intraday_pattern");
661
            let jump_risk_json: serde_json::Value = row.get("jump_risk");
662
663
            if let (Ok(base_vol), Ok(stress_mult), Ok(persist), Ok(intraday), Ok(jump_risk)) = (
664
                f64::try_from(base_volatility),
665
                f64::try_from(stress_multiplier),
666
                f64::try_from(persistence),
667
                serde_json::from_value::<Vec<f64>>(intraday_json),
668
                serde_json::from_value::<crate::asset_classification::JumpRiskProfile>(
669
                    jump_risk_json,
670
                ),
671
            ) {
672
                let profile = crate::asset_classification::VolatilityProfile {
673
                    base_annual_volatility: base_vol,
674
                    stress_volatility_multiplier: stress_mult,
675
                    intraday_pattern: intraday,
676
                    volatility_persistence: persist,
677
                    jump_risk,
678
                };
679
                profiles.insert(name, profile);
680
            }
681
        }
682
683
        Ok(profiles)
684
    }
685
686
    /// Caches symbol classification for performance.
687
    pub async fn cache_symbol_classification(
688
        &self,
689
        symbol: &str,
690
        asset_class: &crate::asset_classification::AssetClass,
691
        configuration_id: Option<uuid::Uuid>,
692
    ) -> Result<(), sqlx::Error> {
693
        let query = "
694
                INSERT INTO asset_classification_cache (symbol, asset_class_data, configuration_id)
695
                VALUES ($1, $2, $3)
696
                ON CONFLICT (symbol) DO UPDATE SET
697
                    asset_class_data = EXCLUDED.asset_class_data,
698
                    configuration_id = EXCLUDED.configuration_id,
699
                    cached_at = NOW(),
700
                    expires_at = NOW() + INTERVAL '1 hour'
701
            ";
702
703
        let asset_class_json =
704
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
705
706
        sqlx::query(query)
707
            .bind(symbol.to_uppercase())
708
            .bind(asset_class_json)
709
            .bind(configuration_id)
710
            .execute(&self.pool)
711
            .await?;
712
713
        Ok(())
714
    }
715
716
    /// Retrieves cached symbol classification.
717
    pub async fn get_cached_classification(
718
        &self,
719
        symbol: &str,
720
    ) -> Result<Option<crate::asset_classification::AssetClass>, sqlx::Error> {
721
        let query = "
722
                SELECT asset_class_data
723
                FROM asset_classification_cache
724
                WHERE symbol = $1 AND expires_at > NOW()
725
            ";
726
727
        let row = sqlx::query(query)
728
            .bind(symbol.to_uppercase())
729
            .fetch_optional(&self.pool)
730
            .await?;
731
732
        if let Some(row) = row {
733
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
734
            Ok(serde_json::from_value(asset_class_json).ok())
735
        } else {
736
            Ok(None)
737
        }
738
    }
739
740
    /// Cleans up expired cache entries.
741
    pub async fn cleanup_cache(&self) -> Result<u64, sqlx::Error> {
742
        let query = "DELETE FROM asset_classification_cache WHERE expires_at < NOW()";
743
        let result = sqlx::query(query).execute(&self.pool).await?;
744
        Ok(result.rows_affected())
745
    }
746
747
    /// Logs asset classification changes for audit.
748
    pub async fn log_classification_change(
749
        &self,
750
        symbol: &str,
751
        old_classification: Option<&crate::asset_classification::AssetClass>,
752
        new_classification: &crate::asset_classification::AssetClass,
753
        changed_by: &str,
754
        reason: &str,
755
    ) -> Result<(), sqlx::Error> {
756
        let query = "
757
                INSERT INTO asset_classification_audit (
758
                    symbol, old_classification, new_classification, changed_by, change_reason
759
                ) VALUES ($1, $2, $3, $4, $5)
760
            ";
761
762
        let old_json = old_classification
763
            .map(|c| serde_json::to_value(c).ok())
764
            .flatten();
765
        let new_json = serde_json::to_value(new_classification)
766
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
767
768
        sqlx::query(query)
769
            .bind(symbol)
770
            .bind(old_json)
771
            .bind(new_json)
772
            .bind(changed_by)
773
            .bind(reason)
774
            .execute(&self.pool)
775
            .await?;
776
777
        Ok(())
778
    }
779
780
    /// Enables PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
781
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
782
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
783
        listener.listen("config_change").await?;
784
        self.listener = Some(listener);
785
        Ok(())
786
    }
787
788
    /// Checks for configuration change notifications.
789
    pub async fn check_for_config_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
790
        if let Some(listener) = &mut self.listener {
791
            if let Some(notification) = listener.try_recv().await? {
792
                return Ok(Some(notification.payload().to_string()));
793
            }
794
        }
795
        Ok(None)
796
    }
797
798
    /// Converts a database row to AssetConfig.
799
    fn row_to_asset_config(
800
        &self,
801
        row: sqlx::postgres::PgRow,
802
    ) -> Result<crate::asset_classification::AssetConfig, sqlx::Error> {
803
        let id: uuid::Uuid = row.get("id");
804
        let name: String = row.get("name");
805
        let symbol_pattern: String = row.get("symbol_pattern");
806
        let priority: i32 = row.get("priority");
807
        let is_active: bool = row.get("is_active");
808
        let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
809
        let updated_at: chrono::DateTime<chrono::Utc> = row.get("updated_at");
810
811
        let asset_class_json: serde_json::Value = row.get("asset_class_data");
812
        let volatility_json: serde_json::Value = row.get("volatility_profile");
813
        let trading_params_json: serde_json::Value = row.get("trading_parameters");
814
        let trading_hours_json: Option<serde_json::Value> = row.get("trading_hours");
815
        let settlement_json: serde_json::Value = row.get("settlement_config");
816
817
        let asset_class = serde_json::from_value(asset_class_json)
818
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
819
        let volatility_profile = serde_json::from_value(volatility_json)
820
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
821
        let trading_parameters = serde_json::from_value(trading_params_json)
822
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
823
        let trading_hours = trading_hours_json
824
            .map(|json| serde_json::from_value(json).ok())
825
            .flatten();
826
        let settlement_config = serde_json::from_value(settlement_json)
827
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
828
829
        Ok(crate::asset_classification::AssetConfig {
830
            id,
831
            name,
832
            symbol_pattern,
833
            compiled_pattern: None, // Will be compiled when loaded
834
            asset_class,
835
            volatility_profile,
836
            trading_parameters,
837
            priority: priority as u32,
838
            is_active,
839
            created_at,
840
            updated_at,
841
            trading_hours,
842
            settlement_config,
843
        })
844
    }
845
}
846
847
/// General-purpose PostgreSQL configuration loader for various configuration types.
848
///
849
/// Provides a unified interface for loading configurations from PostgreSQL with
850
/// support for hot-reload through NOTIFY/LISTEN and caching for performance.
851
#[cfg(feature = "postgres")]
852
pub struct PostgresConfigLoader {
853
    /// Database connection pool
854
    pool: sqlx::PgPool,
855
    /// Configuration cache timeout
856
    cache_timeout: Duration,
857
}
858
859
#[cfg(feature = "postgres")]
860
impl PostgresConfigLoader {
861
    /// Creates a new PostgreSQL configuration loader.
862
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
863
        let pool = sqlx::PgPool::connect(database_url).await?;
864
865
        Ok(Self {
866
            pool,
867
            cache_timeout: Duration::from_secs(300), // 5 minutes
868
        })
869
    }
870
871
    /// Creates a new loader with an existing connection pool.
872
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
873
        Self {
874
            pool,
875
            cache_timeout: Duration::from_secs(300),
876
        }
877
    }
878
879
    /// Get the underlying connection pool.
880
    pub fn pool(&self) -> &sqlx::PgPool {
881
        &self.pool
882
    }
883
884
    // ============================================================================
885
    // ADAPTIVE STRATEGY CONFIGURATION METHODS
886
    // ============================================================================
887
888
    /// Get adaptive strategy configuration by strategy ID.
889
    ///
890
    /// Loads the complete configuration including main settings, models, and features
891
    /// from the PostgreSQL database. Returns None if the strategy doesn't exist.
892
    ///
893
    /// # Arguments
894
    /// * `strategy_id` - Unique identifier for the strategy (e.g., "default", "prod_v1")
895
    ///
896
    /// # Returns
897
    /// - `Ok(Some(config))` - Configuration found and loaded successfully
898
    /// - `Ok(None)` - Strategy ID not found in database
899
    /// - `Err(sqlx::Error)` - Database error occurred
900
    ///
901
    /// # Example
902
    /// ```no_run
903
    /// # use config::PostgresConfigLoader;
904
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
905
    /// let config = loader.get_adaptive_strategy_config("default").await?;
906
    /// if let Some(cfg) = config {
907
    ///     println!("Loaded strategy: {}", cfg.name);
908
    /// }
909
    /// # Ok(())
910
    /// # }
911
    /// ```
912
    pub async fn get_adaptive_strategy_config(
913
        &self,
914
        strategy_id: &str,
915
    ) -> Result<Option<serde_json::Value>, sqlx::Error> {
916
        // Query main configuration
917
        let row = sqlx::query(
918
            r#"
919
            SELECT
920
                id, strategy_id, name, description,
921
                execution_interval_ms, error_backoff_duration_secs,
922
                max_concurrent_operations, strategy_timeout_secs,
923
                max_parallel_models, rebalancing_interval_secs,
924
                min_model_weight, max_model_weight,
925
                max_position_size, max_leverage, stop_loss_pct,
926
                position_sizing_method, max_portfolio_var,
927
                max_drawdown_threshold, kelly_fraction,
928
                book_depth, vpin_window, trade_classification_threshold,
929
                trade_size_buckets, microstructure_features,
930
                regime_detection_method, regime_lookback_window,
931
                regime_transition_threshold, regime_features,
932
                execution_algorithm, max_order_size, min_order_size,
933
                order_timeout_secs, max_slippage_bps,
934
                smart_routing_enabled, dark_pool_preference,
935
                active, version, created_at, updated_at,
936
                created_by, updated_by, metadata
937
            FROM adaptive_strategy_config
938
            WHERE strategy_id = $1 AND active = true
939
            "#,
940
        )
941
        .bind(strategy_id)
942
        .fetch_optional(&self.pool)
943
        .await?;
944
945
        let Some(row) = row else {
946
            return Ok(None);
947
        };
948
949
        let config_id: uuid::Uuid = row.try_get("id")?;
950
951
        // Query associated models
952
        let models = sqlx::query(
953
            r#"
954
            SELECT
955
                id, strategy_config_id, model_id, model_name, model_type,
956
                parameters, initial_weight, enabled, display_order,
957
                created_at, updated_at
958
            FROM adaptive_strategy_models
959
            WHERE strategy_config_id = $1
960
            ORDER BY display_order, created_at
961
            "#,
962
        )
963
        .bind(config_id)
964
        .fetch_all(&self.pool)
965
        .await?;
966
967
        // Query associated features
968
        let features = sqlx::query(
969
            r#"
970
            SELECT
971
                id, strategy_config_id, feature_name, feature_type,
972
                parameters, enabled, required,
973
                created_at, updated_at
974
            FROM adaptive_strategy_features
975
            WHERE strategy_config_id = $1
976
            ORDER BY feature_name
977
            "#,
978
        )
979
        .bind(config_id)
980
        .fetch_all(&self.pool)
981
        .await?;
982
983
        // Convert to JSON for flexibility
984
        // In production, you'd convert to a proper struct type
985
        let config = serde_json::json!({
986
            "id": row.try_get::<uuid::Uuid, _>("id")?,
987
            "strategy_id": row.try_get::<String, _>("strategy_id")?,
988
            "name": row.try_get::<String, _>("name")?,
989
            "description": row.try_get::<Option<String>, _>("description")?,
990
            "general": {
991
                "execution_interval_ms": row.try_get::<i32, _>("execution_interval_ms")?,
992
                "error_backoff_duration_secs": row.try_get::<i32, _>("error_backoff_duration_secs")?,
993
                "max_concurrent_operations": row.try_get::<i32, _>("max_concurrent_operations")?,
994
                "strategy_timeout_secs": row.try_get::<i32, _>("strategy_timeout_secs")?,
995
            },
996
            "ensemble": {
997
                "max_parallel_models": row.try_get::<i32, _>("max_parallel_models")?,
998
                "rebalancing_interval_secs": row.try_get::<i32, _>("rebalancing_interval_secs")?,
999
                "min_model_weight": row.try_get::<f64, _>("min_model_weight")?,
1000
                "max_model_weight": row.try_get::<f64, _>("max_model_weight")?,
1001
            },
1002
            "risk": {
1003
                "max_position_size": row.try_get::<f64, _>("max_position_size")?,
1004
                "max_leverage": row.try_get::<f64, _>("max_leverage")?,
1005
                "stop_loss_pct": row.try_get::<f64, _>("stop_loss_pct")?,
1006
                "position_sizing_method": row.try_get::<String, _>("position_sizing_method")?,
1007
                "max_portfolio_var": row.try_get::<f64, _>("max_portfolio_var")?,
1008
                "max_drawdown_threshold": row.try_get::<f64, _>("max_drawdown_threshold")?,
1009
                "kelly_fraction": row.try_get::<f64, _>("kelly_fraction")?,
1010
            },
1011
            "microstructure": {
1012
                "book_depth": row.try_get::<i32, _>("book_depth")?,
1013
                "vpin_window": row.try_get::<i32, _>("vpin_window")?,
1014
                "trade_classification_threshold": row.try_get::<f64, _>("trade_classification_threshold")?,
1015
                "trade_size_buckets": row.try_get::<Vec<f64>, _>("trade_size_buckets")?,
1016
                "features": row.try_get::<Vec<String>, _>("microstructure_features")?,
1017
            },
1018
            "regime": {
1019
                "detection_method": row.try_get::<String, _>("regime_detection_method")?,
1020
                "lookback_window": row.try_get::<i32, _>("regime_lookback_window")?,
1021
                "transition_threshold": row.try_get::<f64, _>("regime_transition_threshold")?,
1022
                "features": row.try_get::<Vec<String>, _>("regime_features")?,
1023
            },
1024
            "execution": {
1025
                "algorithm": row.try_get::<String, _>("execution_algorithm")?,
1026
                "max_order_size": row.try_get::<f64, _>("max_order_size")?,
1027
                "min_order_size": row.try_get::<f64, _>("min_order_size")?,
1028
                "order_timeout_secs": row.try_get::<i32, _>("order_timeout_secs")?,
1029
                "max_slippage_bps": row.try_get::<f64, _>("max_slippage_bps")?,
1030
                "smart_routing_enabled": row.try_get::<bool, _>("smart_routing_enabled")?,
1031
                "dark_pool_preference": row.try_get::<f64, _>("dark_pool_preference")?,
1032
            },
1033
            "models": models.iter().map(|m| serde_json::json!({
1034
                "id": m.try_get::<uuid::Uuid, _>("id").unwrap(),
1035
                "model_id": m.try_get::<String, _>("model_id").unwrap(),
1036
                "model_name": m.try_get::<String, _>("model_name").unwrap(),
1037
                "model_type": m.try_get::<String, _>("model_type").unwrap(),
1038
                "parameters": m.try_get::<serde_json::Value, _>("parameters").unwrap(),
1039
                "initial_weight": m.try_get::<f64, _>("initial_weight").unwrap(),
1040
                "enabled": m.try_get::<bool, _>("enabled").unwrap(),
1041
            })).collect::<Vec<_>>(),
1042
            "features": features.iter().map(|f| serde_json::json!({
1043
                "name": f.try_get::<String, _>("feature_name").unwrap(),
1044
                "feature_type": f.try_get::<String, _>("feature_type").unwrap(),
1045
                "parameters": f.try_get::<serde_json::Value, _>("parameters").unwrap(),
1046
                "enabled": f.try_get::<bool, _>("enabled").unwrap(),
1047
                "required": f.try_get::<bool, _>("required").unwrap(),
1048
            })).collect::<Vec<_>>(),
1049
            "version": row.try_get::<i32, _>("version")?,
1050
            "created_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")?,
1051
            "updated_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("updated_at")?,
1052
        });
1053
1054
        Ok(Some(config))
1055
    }
1056
1057
    /// Upsert (insert or update) adaptive strategy configuration.
1058
    ///
1059
    /// Creates a new strategy configuration if it doesn't exist, or updates
1060
    /// the existing one. Automatically handles version tracking and audit trail.
1061
    ///
1062
    /// # Arguments
1063
    /// * `config` - Configuration data as JSON (allows flexibility in structure)
1064
    ///
1065
    /// # Returns
1066
    /// - `Ok(strategy_id)` - Strategy ID of the created/updated configuration
1067
    /// - `Err(sqlx::Error)` - Database error occurred
1068
    ///
1069
    /// # Example
1070
    /// ```no_run
1071
    /// # use config::PostgresConfigLoader;
1072
    /// # use serde_json::json;
1073
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
1074
    /// let config = json!({
1075
    ///     "strategy_id": "my_strategy",
1076
    ///     "name": "My Trading Strategy",
1077
    ///     "risk": {
1078
    ///         "max_position_size": 0.15,
1079
    ///         "max_leverage": 3.0
1080
    ///     }
1081
    /// });
1082
    /// let id = loader.upsert_adaptive_strategy_config(&config).await?;
1083
    /// # Ok(())
1084
    /// # }
1085
    /// ```
1086
    pub async fn upsert_adaptive_strategy_config(
1087
        &self,
1088
        config: &serde_json::Value,
1089
    ) -> Result<String, sqlx::Error> {
1090
        let strategy_id = config
1091
            .get("strategy_id")
1092
            .and_then(|v| v.as_str())
1093
            .ok_or_else(|| {
1094
                sqlx::Error::Decode(Box::new(std::io::Error::new(
1095
                    std::io::ErrorKind::InvalidData,
1096
                    "Missing strategy_id in config",
1097
                )))
1098
            })?;
1099
    
1100
        // Extract all configuration fields
1101
        let name = config.get("name").and_then(|v| v.as_str()).unwrap_or("Unnamed Strategy");
1102
        let description = config.get("description").and_then(|v| v.as_str());
1103
    
1104
        // Helper macro for extracting fields with defaults
1105
        macro_rules! get_i32 {
1106
            ($field:expr, $default:expr) => {
1107
                config.get($field).and_then(|v| v.as_i64()).map(|v| v as i32).unwrap_or($default)
1108
            };
1109
        }
1110
        macro_rules! get_f64 {
1111
            ($field:expr, $default:expr) => {
1112
                config.get($field).and_then(|v| v.as_f64()).unwrap_or($default)
1113
            };
1114
        }
1115
        macro_rules! get_bool {
1116
            ($field:expr, $default:expr) => {
1117
                config.get($field).and_then(|v| v.as_bool()).unwrap_or($default)
1118
            };
1119
        }
1120
        macro_rules! get_str {
1121
            ($field:expr, $default:expr) => {
1122
                config.get($field).and_then(|v| v.as_str()).unwrap_or($default)
1123
            };
1124
        }
1125
    
1126
        // Full upsert with all 50+ fields
1127
        let query = r#"
1128
            INSERT INTO adaptive_strategy_config (
1129
                strategy_id, name, description,
1130
                -- General config
1131
                execution_interval_ms, error_backoff_duration_secs,
1132
                max_concurrent_operations, strategy_timeout_secs,
1133
                -- Ensemble config
1134
                max_parallel_models, rebalancing_interval_secs,
1135
                min_model_weight, max_model_weight,
1136
                -- Risk config
1137
                max_position_size, max_leverage, stop_loss_pct,
1138
                position_sizing_method, max_portfolio_var,
1139
                max_drawdown_threshold, kelly_fraction,
1140
                -- Microstructure config
1141
                book_depth, vpin_window, trade_classification_threshold,
1142
                trade_size_buckets, microstructure_features,
1143
                -- Regime config
1144
                regime_detection_method, regime_lookback_window,
1145
                regime_transition_threshold, regime_features,
1146
                -- Execution config
1147
                execution_algorithm, max_order_size, min_order_size,
1148
                order_timeout_secs, max_slippage_bps,
1149
                smart_routing_enabled, dark_pool_preference
1150
            ) VALUES (
1151
                $1, $2, $3,
1152
                $4, $5, $6, $7,
1153
                $8, $9, $10, $11,
1154
                $12, $13, $14, $15, $16, $17, $18,
1155
                $19, $20, $21, $22, $23,
1156
                $24, $25, $26, $27,
1157
                $28, $29, $30, $31, $32, $33, $34
1158
            )
1159
            ON CONFLICT (strategy_id)
1160
            DO UPDATE SET
1161
                name = EXCLUDED.name,
1162
                description = EXCLUDED.description,
1163
                execution_interval_ms = EXCLUDED.execution_interval_ms,
1164
                error_backoff_duration_secs = EXCLUDED.error_backoff_duration_secs,
1165
                max_concurrent_operations = EXCLUDED.max_concurrent_operations,
1166
                strategy_timeout_secs = EXCLUDED.strategy_timeout_secs,
1167
                max_parallel_models = EXCLUDED.max_parallel_models,
1168
                rebalancing_interval_secs = EXCLUDED.rebalancing_interval_secs,
1169
                min_model_weight = EXCLUDED.min_model_weight,
1170
                max_model_weight = EXCLUDED.max_model_weight,
1171
                max_position_size = EXCLUDED.max_position_size,
1172
                max_leverage = EXCLUDED.max_leverage,
1173
                stop_loss_pct = EXCLUDED.stop_loss_pct,
1174
                position_sizing_method = EXCLUDED.position_sizing_method,
1175
                max_portfolio_var = EXCLUDED.max_portfolio_var,
1176
                max_drawdown_threshold = EXCLUDED.max_drawdown_threshold,
1177
                kelly_fraction = EXCLUDED.kelly_fraction,
1178
                book_depth = EXCLUDED.book_depth,
1179
                vpin_window = EXCLUDED.vpin_window,
1180
                trade_classification_threshold = EXCLUDED.trade_classification_threshold,
1181
                trade_size_buckets = EXCLUDED.trade_size_buckets,
1182
                microstructure_features = EXCLUDED.microstructure_features,
1183
                regime_detection_method = EXCLUDED.regime_detection_method,
1184
                regime_lookback_window = EXCLUDED.regime_lookback_window,
1185
                regime_transition_threshold = EXCLUDED.regime_transition_threshold,
1186
                regime_features = EXCLUDED.regime_features,
1187
                execution_algorithm = EXCLUDED.execution_algorithm,
1188
                max_order_size = EXCLUDED.max_order_size,
1189
                min_order_size = EXCLUDED.min_order_size,
1190
                order_timeout_secs = EXCLUDED.order_timeout_secs,
1191
                max_slippage_bps = EXCLUDED.max_slippage_bps,
1192
                smart_routing_enabled = EXCLUDED.smart_routing_enabled,
1193
                dark_pool_preference = EXCLUDED.dark_pool_preference,
1194
                updated_at = NOW()
1195
            RETURNING strategy_id
1196
        "#;
1197
    
1198
        // Extract trade_size_buckets and features arrays
1199
        let trade_size_buckets: Vec<f64> = config.get("trade_size_buckets")
1200
            .and_then(|v| v.as_array())
1201
            .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
1202
            .unwrap_or_else(|| vec![10.0, 100.0, 1000.0, 10000.0]);
1203
    
1204
        let microstructure_features: Vec<String> = config.get("microstructure_features")
1205
            .and_then(|v| v.as_array())
1206
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1207
            .unwrap_or_else(|| vec!["vpin".to_string(), "order_flow".to_string(), "bid_ask_spread".to_string()]);
1208
    
1209
        let regime_features: Vec<String> = config.get("regime_features")
1210
            .and_then(|v| v.as_array())
1211
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1212
            .unwrap_or_else(|| vec!["volatility".to_string(), "momentum".to_string(), "volume".to_string()]);
1213
    
1214
        let row = sqlx::query(query)
1215
            .bind(strategy_id)
1216
            .bind(name)
1217
            .bind(description)
1218
            // General config (4 fields)
1219
            .bind(get_i32!("execution_interval_ms", 100))
1220
            .bind(get_i32!("error_backoff_duration_secs", 1))
1221
            .bind(get_i32!("max_concurrent_operations", 10))
1222
            .bind(get_i32!("strategy_timeout_secs", 30))
1223
            // Ensemble config (4 fields)
1224
            .bind(get_i32!("max_parallel_models", 4))
1225
            .bind(get_i32!("rebalancing_interval_secs", 300))
1226
            .bind(get_f64!("min_model_weight", 0.01))
1227
            .bind(get_f64!("max_model_weight", 0.5))
1228
            // Risk config (7 fields)
1229
            .bind(get_f64!("max_position_size", 0.1))
1230
            .bind(get_f64!("max_leverage", 2.0))
1231
            .bind(get_f64!("stop_loss_pct", 0.02))
1232
            .bind(get_str!("position_sizing_method", "KELLY"))
1233
            .bind(get_f64!("max_portfolio_var", 0.02))
1234
            .bind(get_f64!("max_drawdown_threshold", 0.05))
1235
            .bind(get_f64!("kelly_fraction", 0.1))
1236
            // Microstructure config (5 fields)
1237
            .bind(get_i32!("book_depth", 10))
1238
            .bind(get_i32!("vpin_window", 50))
1239
            .bind(get_f64!("trade_classification_threshold", 0.5))
1240
            .bind(&trade_size_buckets)
1241
            .bind(&microstructure_features)
1242
            // Regime config (4 fields)
1243
            .bind(get_str!("regime_detection_method", "HMM"))
1244
            .bind(get_i32!("regime_lookback_window", 252))
1245
            .bind(get_f64!("regime_transition_threshold", 0.7))
1246
            .bind(&regime_features)
1247
            // Execution config (7 fields)
1248
            .bind(get_str!("execution_algorithm", "TWAP"))
1249
            .bind(get_f64!("max_order_size", 10000.0))
1250
            .bind(get_f64!("min_order_size", 100.0))
1251
            .bind(get_i32!("order_timeout_secs", 30))
1252
            .bind(get_f64!("max_slippage_bps", 10.0))
1253
            .bind(get_bool!("smart_routing_enabled", true))
1254
            .bind(get_f64!("dark_pool_preference", 0.3))
1255
            .fetch_one(&self.pool)
1256
            .await?;
1257
    
1258
                let result: String = row.try_get("strategy_id")?;
1259
                Ok(result)
1260
            }
1261
        
1262
            // ========================================================================
1263
            // MODEL CRUD OPERATIONS
1264
            // ========================================================================
1265
        
1266
            /// Add a model configuration to a strategy
1267
            ///
1268
            /// # Arguments
1269
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1270
            /// * `model` - Model configuration as JSON
1271
            ///
1272
            /// # Returns
1273
            /// UUID of the created model configuration
1274
            pub async fn add_model_config(
1275
                &self,
1276
                strategy_config_id: uuid::Uuid,
1277
                model: &serde_json::Value,
1278
            ) -> Result<uuid::Uuid, sqlx::Error> {
1279
                let model_id = model.get("model_id")
1280
                    .and_then(|v| v.as_str())
1281
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1282
                        std::io::ErrorKind::InvalidData,
1283
                        "Missing model_id"
1284
                    ))))?;
1285
        
1286
                let query = r#"
1287
                    INSERT INTO adaptive_strategy_models (
1288
                        strategy_config_id, model_id, model_name, model_type,
1289
                        parameters, initial_weight, enabled, display_order
1290
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1291
                    RETURNING id
1292
                "#;
1293
        
1294
                let row = sqlx::query(query)
1295
                    .bind(strategy_config_id)
1296
                    .bind(model_id)
1297
                    .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or(model_id))
1298
                    .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1299
                    .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1300
                    .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1301
                    .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1302
                    .bind(model.get("display_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32)
1303
                    .fetch_one(&self.pool)
1304
                    .await?;
1305
        
1306
                row.try_get("id")
1307
            }
1308
        
1309
            /// Update a model configuration
1310
            ///
1311
            /// # Arguments
1312
            /// * `model_id` - UUID of the model to update
1313
            /// * `updates` - Fields to update as JSON
1314
            pub async fn update_model_config(
1315
                &self,
1316
                model_id: uuid::Uuid,
1317
                updates: &serde_json::Value,
1318
            ) -> Result<(), sqlx::Error> {
1319
                let query = r#"
1320
                    UPDATE adaptive_strategy_models
1321
                    SET
1322
                        model_name = COALESCE($1, model_name),
1323
                        model_type = COALESCE($2, model_type),
1324
                        parameters = COALESCE($3, parameters),
1325
                        initial_weight = COALESCE($4, initial_weight),
1326
                        enabled = COALESCE($5, enabled),
1327
                        display_order = COALESCE($6, display_order),
1328
                        updated_at = NOW()
1329
                    WHERE id = $7
1330
                "#;
1331
        
1332
                sqlx::query(query)
1333
                    .bind(updates.get("model_name").and_then(|v| v.as_str()))
1334
                    .bind(updates.get("model_type").and_then(|v| v.as_str()))
1335
                    .bind(updates.get("parameters"))
1336
                    .bind(updates.get("initial_weight").and_then(|v| v.as_f64()))
1337
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1338
                    .bind(updates.get("display_order").and_then(|v| v.as_i64()).map(|v| v as i32))
1339
                    .bind(model_id)
1340
                    .execute(&self.pool)
1341
                    .await?;
1342
        
1343
                Ok(())
1344
            }
1345
        
1346
            /// Remove a model configuration
1347
            ///
1348
            /// # Arguments
1349
            /// * `model_id` - UUID of the model to remove
1350
            pub async fn remove_model_config(
1351
                &self,
1352
                model_id: uuid::Uuid,
1353
            ) -> Result<(), sqlx::Error> {
1354
                let query = "DELETE FROM adaptive_strategy_models WHERE id = $1";
1355
                sqlx::query(query)
1356
                    .bind(model_id)
1357
                    .execute(&self.pool)
1358
                    .await?;
1359
                Ok(())
1360
            }
1361
        
1362
            // ========================================================================
1363
            // FEATURE CRUD OPERATIONS
1364
            // ========================================================================
1365
        
1366
            /// Add a feature configuration to a strategy
1367
            ///
1368
            /// # Arguments
1369
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1370
            /// * `feature` - Feature configuration as JSON
1371
            ///
1372
            /// # Returns
1373
            /// UUID of the created feature configuration
1374
            pub async fn add_feature_config(
1375
                &self,
1376
                strategy_config_id: uuid::Uuid,
1377
                feature: &serde_json::Value,
1378
            ) -> Result<uuid::Uuid, sqlx::Error> {
1379
                let feature_name = feature.get("feature_name")
1380
                    .and_then(|v| v.as_str())
1381
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1382
                        std::io::ErrorKind::InvalidData,
1383
                        "Missing feature_name"
1384
                    ))))?;
1385
        
1386
                let query = r#"
1387
                    INSERT INTO adaptive_strategy_features (
1388
                        strategy_config_id, feature_name, feature_type,
1389
                        parameters, enabled, required
1390
                    ) VALUES ($1, $2, $3, $4, $5, $6)
1391
                    RETURNING id
1392
                "#;
1393
        
1394
                let row = sqlx::query(query)
1395
                    .bind(strategy_config_id)
1396
                    .bind(feature_name)
1397
                    .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1398
                    .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1399
                    .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1400
                    .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1401
                    .fetch_one(&self.pool)
1402
                    .await?;
1403
        
1404
                row.try_get("id")
1405
            }
1406
        
1407
            /// Update a feature configuration
1408
            ///
1409
            /// # Arguments
1410
            /// * `feature_id` - UUID of the feature to update
1411
            /// * `updates` - Fields to update as JSON
1412
            pub async fn update_feature_config(
1413
                &self,
1414
                feature_id: uuid::Uuid,
1415
                updates: &serde_json::Value,
1416
            ) -> Result<(), sqlx::Error> {
1417
                let query = r#"
1418
                    UPDATE adaptive_strategy_features
1419
                    SET
1420
                        feature_type = COALESCE($1, feature_type),
1421
                        parameters = COALESCE($2, parameters),
1422
                        enabled = COALESCE($3, enabled),
1423
                        required = COALESCE($4, required),
1424
                        updated_at = NOW()
1425
                    WHERE id = $5
1426
                "#;
1427
        
1428
                sqlx::query(query)
1429
                    .bind(updates.get("feature_type").and_then(|v| v.as_str()))
1430
                    .bind(updates.get("parameters"))
1431
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1432
                    .bind(updates.get("required").and_then(|v| v.as_bool()))
1433
                    .bind(feature_id)
1434
                    .execute(&self.pool)
1435
                    .await?;
1436
        
1437
                Ok(())
1438
            }
1439
        
1440
            /// Remove a feature configuration
1441
            ///
1442
            /// # Arguments
1443
            /// * `feature_id` - UUID of the feature to remove
1444
            pub async fn remove_feature_config(
1445
                &self,
1446
                feature_id: uuid::Uuid,
1447
            ) -> Result<(), sqlx::Error> {
1448
                let query = "DELETE FROM adaptive_strategy_features WHERE id = $1";
1449
                sqlx::query(query)
1450
                    .bind(feature_id)
1451
                    .execute(&self.pool)
1452
                    .await?;
1453
                Ok(())
1454
            }
1455
        
1456
            // ========================================================================
1457
            // TRANSACTION SUPPORT
1458
            // ========================================================================
1459
        
1460
            /// Update strategy configuration with models and features in a single transaction
1461
            ///
1462
            /// Provides atomic updates across all three tables:
1463
            /// - adaptive_strategy_config (main configuration)
1464
            /// - adaptive_strategy_models (model configurations)
1465
            /// - adaptive_strategy_features (feature configurations)
1466
            ///
1467
            /// # Arguments
1468
            /// * `config` - Full configuration including models and features
1469
            ///
1470
            /// # Returns
1471
            /// Strategy ID of the updated configuration
1472
            pub async fn update_strategy_atomic(
1473
                &self,
1474
                config: &serde_json::Value,
1475
            ) -> Result<String, sqlx::Error> {
1476
                // Start transaction
1477
                let mut tx = self.pool.begin().await?;
1478
        
1479
                // 1. Upsert main configuration
1480
                let strategy_id = config.get("strategy_id")
1481
                    .and_then(|v| v.as_str())
1482
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1483
                        std::io::ErrorKind::InvalidData,
1484
                        "Missing strategy_id"
1485
                    ))))?;
1486
        
1487
                // Get or create config_id
1488
                let config_id: uuid::Uuid = sqlx::query_scalar(
1489
                    "SELECT id FROM adaptive_strategy_config WHERE strategy_id = $1"
1490
                )
1491
                .bind(strategy_id)
1492
                .fetch_optional(&mut *tx)
1493
                .await?
1494
                .unwrap_or_else(uuid::Uuid::new_v4);
1495
        
1496
                // 2. Update models if provided
1497
                if let Some(models) = config.get("models").and_then(|v| v.as_array()) {
1498
                    // Delete existing models
1499
                    sqlx::query("DELETE FROM adaptive_strategy_models WHERE strategy_config_id = $1")
1500
                        .bind(config_id)
1501
                        .execute(&mut *tx)
1502
                        .await?;
1503
        
1504
                    // Insert new models
1505
                    for model in models {
1506
                        sqlx::query(r#"
1507
                            INSERT INTO adaptive_strategy_models (
1508
                                strategy_config_id, model_id, model_name, model_type,
1509
                                parameters, initial_weight, enabled
1510
                            ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1511
                        "#)
1512
                        .bind(config_id)
1513
                        .bind(model.get("model_id").and_then(|v| v.as_str()).unwrap_or("unknown"))
1514
                        .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or("Unknown Model"))
1515
                        .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1516
                        .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1517
                        .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1518
                        .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1519
                        .execute(&mut *tx)
1520
                        .await?;
1521
                    }
1522
                }
1523
        
1524
                // 3. Update features if provided
1525
                if let Some(features) = config.get("features").and_then(|v| v.as_array()) {
1526
                    // Delete existing features
1527
                    sqlx::query("DELETE FROM adaptive_strategy_features WHERE strategy_config_id = $1")
1528
                        .bind(config_id)
1529
                        .execute(&mut *tx)
1530
                        .await?;
1531
        
1532
                    // Insert new features
1533
                    for feature in features {
1534
                        sqlx::query(r#"
1535
                            INSERT INTO adaptive_strategy_features (
1536
                                strategy_config_id, feature_name, feature_type,
1537
                                parameters, enabled, required
1538
                            ) VALUES ($1, $2, $3, $4, $5, $6)
1539
                        "#)
1540
                        .bind(config_id)
1541
                        .bind(feature.get("feature_name").and_then(|v| v.as_str()).unwrap_or("unknown"))
1542
                        .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1543
                        .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1544
                        .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1545
                        .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1546
                        .execute(&mut *tx)
1547
                        .await?;
1548
                    }
1549
                }
1550
        
1551
                // Commit transaction
1552
                tx.commit().await?;
1553
        
1554
                Ok(strategy_id.to_string())
1555
            }
1556
        }
1557
        
1558
        #[cfg(test)]
1559
mod tests {
1560
    use super::*;
1561
1562
    #[test]
1563
    fn test_database_config_new() {
1564
        let config = DatabaseConfig::new();
1565
        assert!(!config.url.is_empty());
1566
        assert_eq!(config.max_connections, 10);
1567
        assert_eq!(config.min_connections, 1);
1568
        assert!(config.application_name.is_some());
1569
    }
1570
1571
    #[test]
1572
    fn test_database_config_validate_success() {
1573
        let config = DatabaseConfig::new();
1574
        assert!(config.validate().is_ok());
1575
    }
1576
1577
    #[test]
1578
    fn test_database_config_validate_empty_url() {
1579
        let mut config = DatabaseConfig::new();
1580
        config.url = String::new();
1581
        assert!(config.validate().is_err());
1582
    }
1583
1584
    #[test]
1585
    fn test_pool_config_default() {
1586
        let pool_config = PoolConfig::default();
1587
        assert_eq!(pool_config.min_connections, 1);
1588
        assert_eq!(pool_config.max_connections, 10);
1589
        assert!(pool_config.test_before_acquire);
1590
    }
1591
1592
    #[test]
1593
    fn test_transaction_config_default() {
1594
        let tx_config = TransactionConfig::default();
1595
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1596
        assert_eq!(tx_config.default_timeout_secs, 30);
1597
        assert!(tx_config.enable_retry);
1598
    }
1599
1600
    #[test]
1601
    fn test_transaction_config_serialization() {
1602
        let tx_config = TransactionConfig::default();
1603
        let serialized = serde_json::to_string(&tx_config).unwrap();
1604
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1605
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1606
    }
1607
1608
    #[test]
1609
    fn test_database_config_with_custom_values() {
1610
        let mut config = DatabaseConfig::new();
1611
        config.max_connections = 50;
1612
        config.min_connections = 5;
1613
        config.enable_query_logging = true;
1614
1615
        assert_eq!(config.max_connections, 50);
1616
        assert_eq!(config.min_connections, 5);
1617
        assert!(config.enable_query_logging);
1618
    }
1619
1620
    #[test]
1621
    fn test_pool_config_timeouts() {
1622
        let pool_config = PoolConfig {
1623
            acquire_timeout_secs: 30,
1624
            max_lifetime_secs: 1800,
1625
            idle_timeout_secs: 600,
1626
            ..Default::default()
1627
        };
1628
1629
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1630
        assert_eq!(pool_config.max_lifetime_secs, 1800);
1631
        assert_eq!(pool_config.idle_timeout_secs, 600);
1632
    }
1633
1634
    #[test]
1635
    fn test_transaction_config_isolation_levels() {
1636
        let levels = vec![
1637
            "READ_UNCOMMITTED",
1638
            "READ_COMMITTED",
1639
            "REPEATABLE_READ",
1640
            "SERIALIZABLE",
1641
        ];
1642
1643
        for level in levels {
1644
            let tx_config = TransactionConfig {
1645
                isolation_level: level.to_string(),
1646
                ..Default::default()
1647
            };
1648
            assert_eq!(tx_config.isolation_level, level);
1649
        }
1650
    }
1651
1652
    #[test]
1653
    fn test_database_config_clone() {
1654
        let config1 = DatabaseConfig::new();
1655
        let config2 = config1.clone();
1656
1657
        assert_eq!(config1.url, config2.url);
1658
        assert_eq!(config1.max_connections, config2.max_connections);
1659
        assert_eq!(config1.min_connections, config2.min_connections);
1660
    }
1661
1662
    #[test]
1663
    fn test_pool_config_validation() {
1664
        let pool_config = PoolConfig::default();
1665
        assert!(pool_config.min_connections <= pool_config.max_connections);
1666
    }
1667
1668
    #[test]
1669
    fn test_database_url_format() {
1670
        let config = DatabaseConfig::new();
1671
        assert!(config.url.starts_with("postgresql://"));
1672
    }
1673
1674
    #[test]
1675
    fn test_transaction_config_retry_settings() {
1676
        let tx_config = TransactionConfig {
1677
            enable_retry: true,
1678
            max_retries: 5,
1679
            ..Default::default()
1680
        };
1681
        assert!(tx_config.enable_retry);
1682
        assert_eq!(tx_config.max_retries, 5);
1683
1684
        let tx_config_no_retry = TransactionConfig {
1685
            enable_retry: false,
1686
            ..Default::default()
1687
        };
1688
        assert!(!tx_config_no_retry.enable_retry);
1689
    }
1690
1691
    #[test]
1692
    fn test_pool_config_connection_settings() {
1693
        let pool_config = PoolConfig {
1694
            test_before_acquire: true,
1695
            acquire_timeout_secs: 30,
1696
            ..Default::default()
1697
        };
1698
1699
        assert!(pool_config.test_before_acquire);
1700
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1701
    }
1702
1703
    #[test]
1704
    fn test_database_config_application_name() {
1705
        let config = DatabaseConfig::new();
1706
        assert_eq!(config.application_name, Some("foxhunt".to_string()));
1707
    }
1708
1709
    #[test]
1710
    fn test_database_config_query_logging() {
1711
        let mut config = DatabaseConfig::new();
1712
        config.enable_query_logging = true;
1713
        assert!(config.enable_query_logging);
1714
    }
1715
1716
    #[test]
1717
    fn test_pool_config_connection_limits() {
1718
        let pool_config = PoolConfig {
1719
            max_connections: 100,
1720
            min_connections: 10,
1721
            ..Default::default()
1722
        };
1723
1724
        assert_eq!(pool_config.max_connections, 100);
1725
        assert_eq!(pool_config.min_connections, 10);
1726
    }
1727
1728
    #[test]
1729
    fn test_transaction_timeout() {
1730
        let tx_config = TransactionConfig {
1731
            default_timeout_secs: 60,
1732
            timeout: Duration::from_secs(60),
1733
            ..Default::default()
1734
        };
1735
        assert_eq!(tx_config.default_timeout_secs, 60);
1736
        assert_eq!(tx_config.timeout, Duration::from_secs(60));
1737
    }
1738
1739
    #[test]
1740
    fn test_database_config_connect_timeout() {
1741
        let config = DatabaseConfig::new();
1742
        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1743
    }
1744
1745
    #[test]
1746
    fn test_database_config_query_timeout() {
1747
        let config = DatabaseConfig::new();
1748
        assert_eq!(config.query_timeout, Duration::from_secs(60));
1749
    }
1750
1751
    #[test]
1752
    fn test_pool_config_test_before_acquire() {
1753
        let pool_config = PoolConfig {
1754
            test_before_acquire: false,
1755
            ..Default::default()
1756
        };
1757
        assert!(!pool_config.test_before_acquire);
1758
1759
        let pool_config_enabled = PoolConfig {
1760
            test_before_acquire: true,
1761
            ..Default::default()
1762
        };
1763
        assert!(pool_config_enabled.test_before_acquire);
1764
    }
1765
1766
    #[test]
1767
    fn test_database_config_validation_empty_url() {
1768
        let mut config = DatabaseConfig::new();
1769
        config.url = String::new();
1770
        assert!(config.validate().is_err());
1771
        assert_eq!(
1772
            config.validate().unwrap_err(),
1773
            "Database URL cannot be empty"
1774
        );
1775
    }
1776
1777
    #[test]
1778
    fn test_database_config_validation_valid() {
1779
        let config = DatabaseConfig::new();
1780
        assert!(config.validate().is_ok());
1781
    }
1782
1783
    #[test]
1784
    fn test_pool_config_defaults() {
1785
        let pool_config = PoolConfig::default();
1786
        assert_eq!(pool_config.min_connections, 1);
1787
        assert_eq!(pool_config.max_connections, 10);
1788
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1789
        assert_eq!(pool_config.max_lifetime_secs, 1800);
1790
        assert_eq!(pool_config.idle_timeout_secs, 600);
1791
        assert!(pool_config.test_before_acquire);
1792
        assert!(pool_config.health_check_enabled);
1793
        assert_eq!(pool_config.health_check_interval_secs, 60);
1794
    }
1795
1796
    #[test]
1797
    fn test_transaction_config_defaults() {
1798
        let tx_config = TransactionConfig::default();
1799
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1800
        assert_eq!(tx_config.timeout, Duration::from_secs(30));
1801
        assert_eq!(tx_config.default_timeout_secs, 30);
1802
        assert!(tx_config.enable_retry);
1803
        assert_eq!(tx_config.max_retries, 3);
1804
        assert_eq!(tx_config.retry_delay_ms, 100);
1805
        assert_eq!(tx_config.max_savepoints, 10);
1806
    }
1807
1808
    #[test]
1809
    fn test_transaction_config_custom_isolation() {
1810
        let tx_config = TransactionConfig {
1811
            isolation_level: "SERIALIZABLE".to_string(),
1812
            ..Default::default()
1813
        };
1814
        assert_eq!(tx_config.isolation_level, "SERIALIZABLE");
1815
    }
1816
1817
    #[test]
1818
    fn test_pool_config_extreme_values() {
1819
        let pool_config = PoolConfig {
1820
            max_connections: 1000,
1821
            min_connections: 0,
1822
            ..Default::default()
1823
        };
1824
        assert_eq!(pool_config.max_connections, 1000);
1825
        assert_eq!(pool_config.min_connections, 0);
1826
    }
1827
1828
    #[test]
1829
    fn test_database_config_custom_application_name() {
1830
        let mut config = DatabaseConfig::new();
1831
        config.application_name = Some("custom_app".to_string());
1832
        assert_eq!(config.application_name.unwrap(), "custom_app");
1833
    }
1834
1835
    #[test]
1836
    fn test_database_config_no_application_name() {
1837
        let mut config = DatabaseConfig::new();
1838
        config.application_name = None;
1839
        assert!(config.application_name.is_none());
1840
    }
1841
1842
    #[test]
1843
    fn test_transaction_config_retry_disabled() {
1844
        let tx_config = TransactionConfig {
1845
            enable_retry: false,
1846
            ..Default::default()
1847
        };
1848
        assert!(!tx_config.enable_retry);
1849
    }
1850
1851
    #[test]
1852
    fn test_pool_config_serialization() {
1853
        let pool_config = PoolConfig::default();
1854
        let serialized = serde_json::to_string(&pool_config).unwrap();
1855
        let deserialized: PoolConfig = serde_json::from_str(&serialized).unwrap();
1856
        assert_eq!(pool_config.max_connections, deserialized.max_connections);
1857
        assert_eq!(pool_config.min_connections, deserialized.min_connections);
1858
    }
1859
1860
    #[test]
1861
    fn test_transaction_config_serde_roundtrip() {
1862
        let tx_config = TransactionConfig::default();
1863
        let serialized = serde_json::to_string(&tx_config).unwrap();
1864
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1865
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1866
        assert_eq!(tx_config.max_retries, deserialized.max_retries);
1867
    }
1868
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html deleted file mode 100644 index 12f46afb4..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/lib.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/lib.rs
Line
Count
Source
1
#![warn(missing_docs)]
2
//! Configuration management for Foxhunt HFT trading system
3
4
#![allow(missing_docs)] // Internal implementation details don't require documentation
5
#![allow(missing_debug_implementations)] // Not all types need Debug
6
7
// Allow pedantic lints for configuration management
8
#![allow(clippy::type_complexity)]
9
#![allow(clippy::unnecessary_map_or)]
10
#![allow(clippy::map_flatten)]
11
#![allow(dead_code)]
12
13
use serde::{Deserialize, Serialize};
14
15
// Module declarations
16
pub mod asset_classification;
17
pub mod compliance_config;
18
pub mod data_config;
19
pub mod data_providers;
20
pub mod database;
21
pub mod error;
22
pub mod manager;
23
pub mod ml_config;
24
pub mod risk_config;
25
pub mod runtime;
26
pub mod schemas;
27
pub mod storage_config;
28
pub mod structures;
29
pub mod symbol_config;
30
pub mod vault;
31
32
// Re-export commonly used types
33
pub use asset_classification::{
34
    create_default_configurations, AssetClass, AssetClassificationManager, AssetConfig,
35
    CommodityType, CryptoType, DerivativeType, EquitySector, ExecutionConfig, FixedIncomeType,
36
    ForexPairType, FutureType, GeographicRegion, JumpRiskProfile, MarketCapTier, MarketMakingConfig, OrderType,
37
    PositionLimits, RiskThresholds, SettlementConfig, TimeInForce,
38
    TradingHours as DetailedTradingHours, TradingParameters,
39
    VolatilityProfile as DetailedVolatilityProfile,
40
};
41
pub use data_config::{
42
    DataCompressionAlgorithm, DataCompressionConfig, DataConfig, DataRetentionConfig,
43
    DataStorageConfig, DataStorageFormat, DataVersioningConfig, MissingDataHandling,
44
};
45
pub use data_providers::{
46
    AlpacaEndpoints, BenzingaEndpoints, DataProviderConfig, DataProviderEnvironment,
47
    DatabentoEndpoints, IBGatewayConfig,
48
};
49
pub use compliance_config::ComplianceRuleConfig;
50
#[cfg(feature = "postgres")]
51
pub use compliance_config::PostgresComplianceRuleLoader;
52
pub use database::{DatabaseConfig, PoolConfig, TransactionConfig};
53
#[cfg(feature = "postgres")]
54
pub use database::{
55
    PostgresAssetClassificationLoader, PostgresConfigLoader, PostgresSymbolConfigLoader,
56
};
57
pub use error::{ConfigError, ConfigResult};
58
pub use manager::{ConfigManager, ConfigManagerBuilder, ServiceConfig};
59
pub use ml_config::{
60
    MLConfig, Mamba2Config, MarketState, ModelArchitectureConfig, SimulationConfig,
61
    SymbolConfig as MLSymbolConfig, TrainingConfig,
62
};
63
pub use risk_config::{
64
    AssetClass as RiskAssetClass, AssetClassMapping, RiskConfig, StressScenarioConfig,
65
};
66
pub use runtime::{
67
    CacheRuntimeConfig, DatabaseRuntimeConfig, Environment, LimitsConfig, RuntimeConfig,
68
    TimeoutConfig,
69
};
70
pub use schemas::*;
71
pub use storage_config::{ModelArchitecture, ModelMetadata, StorageConfig, TrainingMetrics};
72
pub use structures::{
73
    AssetClass as SimpleAssetClass, AssetClassificationConfig, BacktestingDatabaseConfig,
74
    BacktestingPerformanceConfig, BacktestingStrategyConfig, BrokerConfig, BrokerRoutingRule,
75
    CommissionConfig, EncryptionConfig, MarketDataConfig, TlsConfig, TradingConfig, VolatilityProfile as SimpleVolatilityProfile,
76
};
77
pub use symbol_config::{
78
    AssetClassification, SymbolConfig, SymbolConfigManager, SymbolMetadata, TradingHours,
79
    VolatilityProfile, VolatilityRegime,
80
};
81
pub use vault::VaultConfig;
82
83
/// Configuration categories for organizing different aspects of the trading system.
84
///
85
/// This enum categorizes different types of configurations to enable organized
86
/// access and management of system settings across various functional domains.
87
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
88
pub enum ConfigCategory {
89
    /// Trading system configuration including order management and execution
90
    Trading,
91
    /// Risk management configuration including position limits and VaR settings
92
    Risk,
93
    /// Market data configuration for data providers and feeds
94
    MarketData,
95
    /// Machine learning model configuration and training parameters
96
    MachineLearning,
97
    /// Broker connectivity and execution configuration
98
    Brokers,
99
    /// Performance monitoring and optimization configuration
100
    Performance,
101
    /// Symbol classification and trading parameters configuration
102
    Symbols,
103
    /// Comprehensive asset classification with advanced features
104
    AssetClassification,
105
}
106
107
/// Production-ready asset classification system integration.
108
///
109
/// This module provides a comprehensive asset classification system that integrates
110
/// with the existing config infrastructure while offering advanced features like:
111
/// - Dynamic pattern-based classification
112
/// - Regime-aware volatility profiling  
113
/// - Hot-reload configuration management
114
/// - Performance caching and audit trails
115
///
116
/// # Usage
117
///
118
/// ```rust,no_run
119
/// use config::{AssetClassificationManager, create_default_configurations};
120
///
121
/// # async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
122
/// let mut manager = AssetClassificationManager::new();
123
/// let configs = create_default_configurations();
124
/// manager.load_configurations(configs).await?;
125
///
126
/// // Classify a symbol
127
/// let asset_class = manager.classify_symbol("AAPL");
128
///
129
/// // Get trading parameters
130
/// if let Some(params) = manager.get_trading_parameters("AAPL") {
131
///     let max_position = params.position_limits.max_position_fraction;
132
///     println!("Max position fraction for AAPL: {}", max_position);
133
/// }
134
/// # Ok(())
135
/// # }
136
/// ```
137
pub mod asset_classification_integration {
138
    pub use crate::asset_classification::*;
139
140
    /// Convenience function to create a fully configured asset classification manager
141
    /// with default configurations suitable for production use.
142
0
    pub async fn create_production_manager(
143
0
        database_pool: Option<sqlx::PgPool>,
144
0
    ) -> Result<AssetClassificationManager, Box<dyn std::error::Error + Send + Sync>> {
145
0
        let mut manager = AssetClassificationManager::new();
146
147
        // Load configurations from database if available, otherwise use defaults
148
0
        let configs = if let Some(_pool) = database_pool {
149
            // In production, load from database
150
            // let loader = crate::database::PostgresAssetClassificationLoader::with_pool(pool);
151
            // loader.load_asset_configurations().await?
152
0
            create_default_configurations()
153
        } else {
154
0
            create_default_configurations()
155
        };
156
157
0
        manager.load_configurations(configs).await?;
158
0
        Ok(manager)
159
0
    }
160
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html deleted file mode 100644 index b22e0686c..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/manager.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/manager.rs
Line
Count
Source
1
/// Builder for ConfigManager with advanced configuration options.
2
///
3
/// Provides a fluent interface for constructing ConfigManager instances
4
/// with optional asset classification, caching, and database integration.
5
pub struct ConfigManagerBuilder {
6
    config: ServiceConfig,
7
    asset_manager: Option<crate::asset_classification::AssetClassificationManager>,
8
    cache_timeout: std::time::Duration,
9
}
10
11
impl ConfigManagerBuilder {
12
    /// Creates a new ConfigManagerBuilder with the specified service configuration.
13
0
    pub fn new(config: ServiceConfig) -> Self {
14
0
        Self {
15
0
            config,
16
0
            asset_manager: None,
17
0
            cache_timeout: std::time::Duration::from_secs(300),
18
0
        }
19
0
    }
20
21
    /// Sets the asset classification manager.
22
0
    pub fn with_asset_classification(
23
0
        mut self,
24
0
        manager: crate::asset_classification::AssetClassificationManager,
25
0
    ) -> Self {
26
0
        self.asset_manager = Some(manager);
27
0
        self
28
0
    }
29
30
    /// Sets the cache timeout duration.
31
0
    pub fn with_cache_timeout(mut self, timeout: std::time::Duration) -> Self {
32
0
        self.cache_timeout = timeout;
33
0
        self
34
0
    }
35
36
    /// Builds the ConfigManager with the specified configuration.
37
0
    pub fn build(self) -> ConfigManager {
38
0
        ConfigManager {
39
0
            config: Arc::new(self.config),
40
0
            asset_classification: Arc::new(RwLock::new(self.asset_manager)),
41
0
            cache: Arc::new(RwLock::new(HashMap::new())),
42
0
            cache_timeout: self.cache_timeout,
43
0
        }
44
0
    }
45
46
    /// Builds the ConfigManager with database integration.
47
    #[cfg(feature = "postgres")]
48
    pub async fn build_with_database(
49
        self,
50
        database_pool: sqlx::PgPool,
51
    ) -> Result<ConfigManager, Box<dyn std::error::Error + Send + Sync>> {
52
        let manager = self.build();
53
        manager
54
            .initialize_asset_classification(database_pool)
55
            .await?;
56
        Ok(manager)
57
    }
58
}
59
60
// Configuration management and service configuration structures.
61
//
62
// This module provides the core configuration management infrastructure for
63
// the Foxhunt trading system. It handles service-specific configuration,
64
// environment management, and provides thread-safe access to configuration
65
// data across the application.
66
67
use chrono::{DateTime, Utc};
68
use serde::{Deserialize, Serialize};
69
use std::collections::HashMap;
70
use std::sync::{Arc, RwLock};
71
72
/// Service-specific configuration structure.
73
///
74
/// Contains metadata and settings for a specific service in the Foxhunt
75
/// trading system. Supports environment-specific configuration and
76
/// versioning for configuration management and deployment tracking.
77
#[derive(Debug, Clone, Serialize, Deserialize)]
78
pub struct ServiceConfig {
79
    /// Service name (e.g., "trading_service", "ml_training_service")
80
    pub name: String,
81
    /// Deployment environment (e.g., "development", "staging", "production")
82
    pub environment: String,
83
    /// Service version for deployment tracking
84
    pub version: String,
85
    /// Service-specific configuration settings as JSON
86
    pub settings: serde_json::Value,
87
}
88
89
/// Thread-safe configuration manager for comprehensive service configuration.
90
///
91
/// Provides centralized access to service configuration with support for:
92
/// - Asset classification management
93
/// - Hot-reload capabilities
94
/// - Environment-specific settings
95
/// - Thread-safe access patterns
96
///
97
/// Ensures configuration consistency across all components of a service.
98
pub struct ConfigManager {
99
    config: Arc<ServiceConfig>,
100
    /// Asset classification manager for symbol-based configuration
101
    asset_classification:
102
        Arc<RwLock<Option<crate::asset_classification::AssetClassificationManager>>>,
103
    /// Configuration cache for performance
104
    cache: Arc<RwLock<HashMap<String, (serde_json::Value, DateTime<Utc>)>>>,
105
    /// Cache timeout duration
106
    cache_timeout: std::time::Duration,
107
}
108
109
impl ConfigManager {
110
    /// Creates a new ConfigManager with the provided service configuration.
111
    ///
112
    /// The configuration is wrapped in an Arc for efficient sharing across
113
    /// multiple threads and components within the service.
114
    ///
115
    /// # Arguments
116
    ///
117
    /// * `config` - The service configuration to manage
118
0
    pub fn new(config: ServiceConfig) -> Self {
119
0
        Self {
120
0
            config: Arc::new(config),
121
0
            asset_classification: Arc::new(RwLock::new(None)),
122
0
            cache: Arc::new(RwLock::new(HashMap::new())),
123
0
            cache_timeout: std::time::Duration::from_secs(300), // 5 minutes
124
0
        }
125
0
    }
126
127
    /// Creates a new ConfigManager with asset classification support.
128
    ///
129
    /// Initializes the manager with both service configuration and
130
    /// asset classification capabilities for comprehensive trading
131
    /// parameter management.
132
    ///
133
    /// # Arguments
134
    ///
135
    /// * `config` - The service configuration to manage
136
    /// * `asset_manager` - Pre-configured asset classification manager
137
0
    pub fn with_asset_classification(
138
0
        config: ServiceConfig,
139
0
        asset_manager: crate::asset_classification::AssetClassificationManager,
140
0
    ) -> Self {
141
0
        Self {
142
0
            config: Arc::new(config),
143
0
            asset_classification: Arc::new(RwLock::new(Some(asset_manager))),
144
0
            cache: Arc::new(RwLock::new(HashMap::new())),
145
0
            cache_timeout: std::time::Duration::from_secs(300),
146
0
        }
147
0
    }
148
149
    /// Returns a shared reference to the service configuration.
150
    ///
151
    /// Provides thread-safe access to the configuration data through Arc cloning.
152
    /// The returned Arc can be shared across threads without additional locking.
153
    ///
154
    /// # Returns
155
    ///
156
    /// An Arc containing the service configuration
157
0
    pub fn get_config(&self) -> Arc<ServiceConfig> {
158
0
        Arc::clone(&self.config)
159
0
    }
160
161
    /// Initializes asset classification with database-backed configurations.
162
    ///
163
    /// Loads asset classification configurations from the database and
164
    /// initializes the asset classification manager for dynamic symbol
165
    /// classification and trading parameter retrieval.
166
    #[cfg(feature = "postgres")]
167
    pub async fn initialize_asset_classification(
168
        &self,
169
        database_pool: sqlx::PgPool,
170
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
171
        let loader = crate::database::PostgresAssetClassificationLoader::with_pool(database_pool);
172
        let configs = loader.load_asset_configurations().await?;
173
174
        let mut manager = crate::asset_classification::AssetClassificationManager::new();
175
        manager.load_configurations(configs).await?;
176
177
        if let Ok(mut asset_classification) = self.asset_classification.write() {
178
            *asset_classification = Some(manager);
179
        }
180
181
        Ok(())
182
    }
183
184
    /// Classifies a symbol using the asset classification manager.
185
    ///
186
    /// Returns the asset class for the given symbol based on configured
187
    /// pattern matching rules and explicit mappings.
188
    ///
189
    /// # Arguments
190
    ///
191
    /// * `symbol` - The trading symbol to classify
192
    ///
193
    /// # Returns
194
    ///
195
    /// The asset class or Unknown if classification fails
196
0
    pub fn classify_symbol(&self, symbol: &str) -> crate::asset_classification::AssetClass {
197
0
        if let Ok(asset_classification) = self.asset_classification.read() {
198
0
            if let Some(ref manager) = *asset_classification {
199
0
                return manager.classify_symbol(symbol);
200
0
            }
201
0
        }
202
0
        crate::asset_classification::AssetClass::Unknown
203
0
    }
204
205
    /// Gets trading parameters for a symbol.
206
    ///
207
    /// Retrieves comprehensive trading parameters including position limits,
208
    /// risk thresholds, and execution configuration for the specified symbol.
209
    ///
210
    /// # Arguments
211
    ///
212
    /// * `symbol` - The trading symbol
213
    ///
214
    /// # Returns
215
    ///
216
    /// Trading parameters if available, None otherwise
217
0
    pub fn get_trading_parameters(
218
0
        &self,
219
0
        symbol: &str,
220
0
    ) -> Option<crate::asset_classification::TradingParameters> {
221
0
        if let Ok(asset_classification) = self.asset_classification.read() {
222
0
            if let Some(ref manager) = *asset_classification {
223
0
                return manager.get_trading_parameters(symbol).cloned();
224
0
            }
225
0
        }
226
0
        None
227
0
    }
228
229
    /// Gets volatility profile for a symbol.
230
    ///
231
    /// Retrieves the volatility profile including base volatility,
232
    /// stress multipliers, and jump risk characteristics.
233
    ///
234
    /// # Arguments
235
    ///
236
    /// * `symbol` - The trading symbol
237
    ///
238
    /// # Returns
239
    ///
240
    /// Volatility profile if available, None otherwise
241
0
    pub fn get_volatility_profile(
242
0
        &self,
243
0
        symbol: &str,
244
0
    ) -> Option<crate::asset_classification::VolatilityProfile> {
245
0
        if let Ok(asset_classification) = self.asset_classification.read() {
246
0
            if let Some(ref manager) = *asset_classification {
247
0
                return manager.get_volatility_profile(symbol).cloned();
248
0
            }
249
0
        }
250
0
        None
251
0
    }
252
253
    /// Gets daily volatility estimate for a symbol.
254
    ///
255
    /// Calculates the daily volatility from the annual volatility
256
    /// using standard financial mathematics (annual / sqrt(252)).
257
    ///
258
    /// # Arguments
259
    ///
260
    /// * `symbol` - The trading symbol
261
    ///
262
    /// # Returns
263
    ///
264
    /// Daily volatility estimate as a decimal
265
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
266
0
        if let Ok(asset_classification) = self.asset_classification.read() {
267
0
            if let Some(ref manager) = *asset_classification {
268
0
                return manager.get_daily_volatility(symbol);
269
0
            }
270
0
        }
271
0
        0.05 // Default 5% daily volatility for unknown symbols
272
0
    }
273
274
    /// Gets position size recommendation for a symbol.
275
    ///
276
    /// Calculates recommended position size based on portfolio NAV
277
    /// and the symbol's configured position limits.
278
    ///
279
    /// # Arguments
280
    ///
281
    /// * `symbol` - The trading symbol
282
    /// * `portfolio_nav` - Current portfolio net asset value
283
    ///
284
    /// # Returns
285
    ///
286
    /// Recommended position size if available
287
0
    pub fn get_position_size_recommendation(
288
0
        &self,
289
0
        symbol: &str,
290
0
        portfolio_nav: rust_decimal::Decimal,
291
0
    ) -> Option<rust_decimal::Decimal> {
292
0
        if let Ok(asset_classification) = self.asset_classification.read() {
293
0
            if let Some(ref manager) = *asset_classification {
294
0
                return manager.get_position_size_recommendation(symbol, portfolio_nav);
295
0
            }
296
0
        }
297
0
        None
298
0
    }
299
300
    /// Checks if trading is active for a symbol at the given time.
301
    ///
302
    /// Validates trading hours and market schedule for the symbol.
303
    ///
304
    /// # Arguments
305
    ///
306
    /// * `symbol` - The trading symbol
307
    /// * `timestamp` - The timestamp to check
308
    ///
309
    /// # Returns
310
    ///
311
    /// True if trading is active, false otherwise
312
0
    pub fn is_trading_active(&self, symbol: &str, timestamp: DateTime<Utc>) -> bool {
313
0
        if let Ok(asset_classification) = self.asset_classification.read() {
314
0
            if let Some(ref manager) = *asset_classification {
315
0
                return manager.is_trading_active(symbol, timestamp);
316
0
            }
317
0
        }
318
0
        true // Default to always active if no classification available
319
0
    }
320
321
    /// Reloads asset classification configurations.
322
    ///
323
    /// Triggers a reload of asset classification configurations
324
    /// for hot-reload functionality in production environments.
325
    #[cfg(feature = "postgres")]
326
    pub async fn reload_asset_classification(
327
        &self,
328
        database_pool: sqlx::PgPool,
329
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
330
        let needs_reload = {
331
            let asset_classification = self.asset_classification.read().ok();
332
            asset_classification
333
                .as_ref()
334
                .and_then(|ac| ac.as_ref())
335
                .map(|manager| manager.needs_reload())
336
                .unwrap_or(false)
337
        }; // Lock released here
338
339
        if needs_reload {
340
            self.initialize_asset_classification(database_pool).await?;
341
        }
342
        Ok(())
343
    }
344
345
    /// Gets cached configuration value.
346
    ///
347
    /// Retrieves a cached configuration value with automatic expiration.
348
    ///
349
    /// # Arguments
350
    ///
351
    /// * `key` - Cache key
352
    ///
353
    /// # Returns
354
    ///
355
    /// Cached value if available and not expired
356
0
    pub fn get_cached_config(&self, key: &str) -> Option<serde_json::Value> {
357
0
        if let Ok(cache) = self.cache.read() {
358
0
            if let Some((value, timestamp)) = cache.get(key) {
359
0
                let elapsed = Utc::now().signed_duration_since(*timestamp);
360
0
                if elapsed.to_std().unwrap_or_default() < self.cache_timeout {
361
0
                    return Some(value.clone());
362
0
                }
363
0
            }
364
0
        }
365
0
        None
366
0
    }
367
368
    /// Sets cached configuration value.
369
    ///
370
    /// Stores a configuration value in the cache with timestamp.
371
    ///
372
    /// # Arguments
373
    ///
374
    /// * `key` - Cache key
375
    /// * `value` - Value to cache
376
0
    pub fn set_cached_config(&self, key: String, value: serde_json::Value) {
377
0
        if let Ok(mut cache) = self.cache.write() {
378
0
            cache.insert(key, (value, Utc::now()));
379
0
        }
380
0
    }
381
382
    /// Clears expired cache entries.
383
    ///
384
    /// Removes cache entries that have exceeded the timeout duration.
385
0
    pub fn cleanup_cache(&self) {
386
0
        if let Ok(mut cache) = self.cache.write() {
387
0
            let now = Utc::now();
388
0
            cache.retain(|_, (_, timestamp)| {
389
0
                let elapsed = now.signed_duration_since(*timestamp);
390
0
                elapsed.to_std().unwrap_or_default() < self.cache_timeout
391
0
            });
392
0
        }
393
0
    }
394
}
395
396
#[cfg(test)]
397
mod tests {
398
    use super::*;
399
    use serde_json::json;
400
401
    fn create_test_config() -> ServiceConfig {
402
        ServiceConfig {
403
            name: "test_service".to_string(),
404
            environment: "test".to_string(),
405
            version: "1.0.0".to_string(),
406
            settings: json!({"test_key": "test_value"}),
407
        }
408
    }
409
410
    #[test]
411
    fn test_service_config_creation() {
412
        let config = create_test_config();
413
        assert_eq!(config.name, "test_service");
414
        assert_eq!(config.environment, "test");
415
        assert_eq!(config.version, "1.0.0");
416
    }
417
418
    #[test]
419
    fn test_config_manager_new() {
420
        let config = create_test_config();
421
        let manager = ConfigManager::new(config);
422
        let retrieved_config = manager.get_config();
423
        assert_eq!(retrieved_config.name, "test_service");
424
    }
425
426
    #[test]
427
    fn test_config_manager_builder() {
428
        let config = create_test_config();
429
        let manager = ConfigManagerBuilder::new(config)
430
            .with_cache_timeout(std::time::Duration::from_secs(60))
431
            .build();
432
433
        let retrieved_config = manager.get_config();
434
        assert_eq!(retrieved_config.name, "test_service");
435
    }
436
437
    #[test]
438
    fn test_config_manager_cache_set_and_get() {
439
        let config = create_test_config();
440
        let manager = ConfigManager::new(config);
441
442
        let test_value = json!({"cached": "data"});
443
        manager.set_cached_config("test_key".to_string(), test_value.clone());
444
445
        let retrieved = manager.get_cached_config("test_key");
446
        assert!(retrieved.is_some());
447
        assert_eq!(retrieved.unwrap(), test_value);
448
    }
449
450
    #[test]
451
    fn test_config_manager_cache_miss() {
452
        let config = create_test_config();
453
        let manager = ConfigManager::new(config);
454
455
        let retrieved = manager.get_cached_config("nonexistent_key");
456
        assert!(retrieved.is_none());
457
    }
458
459
    #[test]
460
    fn test_config_manager_cleanup_cache() {
461
        let config = create_test_config();
462
        let manager = ConfigManager::new(config);
463
464
        let test_value = json!({"cached": "data"});
465
        manager.set_cached_config("test_key".to_string(), test_value);
466
467
        manager.cleanup_cache();
468
469
        // Cache entry should still exist since it was just created
470
        let retrieved = manager.get_cached_config("test_key");
471
        assert!(retrieved.is_some());
472
    }
473
474
    #[test]
475
    fn test_config_manager_classify_symbol_without_asset_manager() {
476
        let config = create_test_config();
477
        let manager = ConfigManager::new(config);
478
479
        let asset_class = manager.classify_symbol("AAPL");
480
        assert_eq!(
481
            asset_class,
482
            crate::asset_classification::AssetClass::Unknown
483
        );
484
    }
485
486
    #[test]
487
    fn test_config_manager_get_daily_volatility_default() {
488
        let config = create_test_config();
489
        let manager = ConfigManager::new(config);
490
491
        let volatility = manager.get_daily_volatility("AAPL");
492
        assert_eq!(volatility, 0.05); // Default value
493
    }
494
495
    #[test]
496
    fn test_config_manager_is_trading_active_default() {
497
        let config = create_test_config();
498
        let manager = ConfigManager::new(config);
499
500
        let now = chrono::Utc::now();
501
        let is_active = manager.is_trading_active("AAPL", now);
502
        assert!(is_active); // Default to always active
503
    }
504
505
    #[test]
506
    fn test_config_manager_get_trading_parameters_none() {
507
        let config = create_test_config();
508
        let manager = ConfigManager::new(config);
509
510
        let params = manager.get_trading_parameters("AAPL");
511
        assert!(params.is_none());
512
    }
513
514
    #[test]
515
    fn test_config_manager_get_volatility_profile_none() {
516
        let config = create_test_config();
517
        let manager = ConfigManager::new(config);
518
519
        let profile = manager.get_volatility_profile("AAPL");
520
        assert!(profile.is_none());
521
    }
522
523
    #[test]
524
    fn test_config_manager_get_position_size_recommendation_none() {
525
        let config = create_test_config();
526
        let manager = ConfigManager::new(config);
527
528
        let recommendation =
529
            manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0));
530
        assert!(recommendation.is_none());
531
    }
532
533
    #[test]
534
    fn test_config_manager_with_asset_classification() {
535
        let config = create_test_config();
536
        let asset_manager = crate::asset_classification::AssetClassificationManager::new();
537
        let manager = ConfigManager::with_asset_classification(config, asset_manager);
538
539
        let retrieved_config = manager.get_config();
540
        assert_eq!(retrieved_config.name, "test_service");
541
    }
542
543
    #[test]
544
    fn test_builder_with_asset_classification() {
545
        let config = create_test_config();
546
        let asset_manager = crate::asset_classification::AssetClassificationManager::new();
547
548
        let manager = ConfigManagerBuilder::new(config)
549
            .with_asset_classification(asset_manager)
550
            .build();
551
552
        let retrieved_config = manager.get_config();
553
        assert_eq!(retrieved_config.name, "test_service");
554
    }
555
556
    #[test]
557
    fn test_service_config_serialization() {
558
        let config = create_test_config();
559
        let serialized = serde_json::to_string(&config).unwrap();
560
        let deserialized: ServiceConfig = serde_json::from_str(&serialized).unwrap();
561
562
        assert_eq!(config.name, deserialized.name);
563
        assert_eq!(config.environment, deserialized.environment);
564
        assert_eq!(config.version, deserialized.version);
565
    }
566
567
    #[test]
568
    fn test_config_manager_multiple_cache_entries() {
569
        let config = create_test_config();
570
        let manager = ConfigManager::new(config);
571
572
        for i in 0..10 {
573
            manager.set_cached_config(format!("key_{}", i), json!({"value": i}));
574
        }
575
576
        for i in 0..10 {
577
            let retrieved = manager.get_cached_config(&format!("key_{}", i));
578
            assert!(retrieved.is_some());
579
        }
580
    }
581
582
    #[test]
583
    fn test_config_manager_cache_overwrite() {
584
        let config = create_test_config();
585
        let manager = ConfigManager::new(config);
586
587
        manager.set_cached_config("key".to_string(), json!({"value": 1}));
588
        manager.set_cached_config("key".to_string(), json!({"value": 2}));
589
590
        let retrieved = manager.get_cached_config("key");
591
        assert_eq!(retrieved.unwrap(), json!({"value": 2}));
592
    }
593
594
    #[test]
595
    fn test_builder_custom_cache_timeout() {
596
        let config = create_test_config();
597
        let custom_timeout = std::time::Duration::from_secs(120);
598
599
        let manager = ConfigManagerBuilder::new(config)
600
            .with_cache_timeout(custom_timeout)
601
            .build();
602
603
        // Cache timeout is set internally
604
        let retrieved_config = manager.get_config();
605
        assert_eq!(retrieved_config.name, "test_service");
606
    }
607
608
    #[test]
609
    fn test_config_manager_shared_config() {
610
        let config = create_test_config();
611
        let manager = ConfigManager::new(config);
612
613
        let config1 = manager.get_config();
614
        let config2 = manager.get_config();
615
616
        // Both should point to the same Arc
617
        assert_eq!(config1.name, config2.name);
618
    }
619
620
    #[test]
621
    fn test_service_config_clone() {
622
        let config1 = create_test_config();
623
        let config2 = config1.clone();
624
625
        assert_eq!(config1.name, config2.name);
626
        assert_eq!(config1.environment, config2.environment);
627
        assert_eq!(config1.version, config2.version);
628
    }
629
630
    #[test]
631
    fn test_config_manager_cache_timeout_configuration() {
632
        let config = create_test_config();
633
        let custom_timeout = std::time::Duration::from_millis(10);
634
        let manager = ConfigManagerBuilder::new(config)
635
            .with_cache_timeout(custom_timeout)
636
            .build();
637
638
        // Cache timeout is configured internally
639
        assert_eq!(manager.cache_timeout, custom_timeout);
640
641
        // Test that cache still works normally
642
        manager.set_cached_config("test_key".to_string(), json!({"value": 42}));
643
        assert!(manager.get_cached_config("test_key").is_some());
644
    }
645
646
    #[test]
647
    fn test_config_manager_concurrent_access() {
648
        use std::sync::Arc;
649
        use std::thread;
650
651
        let config = create_test_config();
652
        let manager = Arc::new(ConfigManager::new(config));
653
654
        let mut handles = vec![];
655
656
        for i in 0..10 {
657
            let manager_clone = Arc::clone(&manager);
658
            let handle = thread::spawn(move || {
659
                manager_clone
660
                    .set_cached_config(format!("concurrent_key_{}", i), json!({"thread_id": i}));
661
                manager_clone.get_cached_config(&format!("concurrent_key_{}", i))
662
            });
663
            handles.push(handle);
664
        }
665
666
        for handle in handles {
667
            assert!(handle.join().unwrap().is_some());
668
        }
669
    }
670
671
    #[test]
672
    fn test_config_manager_daily_volatility_fallback() {
673
        let config = create_test_config();
674
        let manager = ConfigManager::new(config);
675
676
        // Should return default 5% for unknown symbols
677
        let vol = manager.get_daily_volatility("UNKNOWN_SYMBOL");
678
        assert_eq!(vol, 0.05);
679
    }
680
681
    #[test]
682
    fn test_config_manager_position_size_none() {
683
        let config = create_test_config();
684
        let manager = ConfigManager::new(config);
685
686
        // Should return None without asset classification
687
        let size =
688
            manager.get_position_size_recommendation("AAPL", rust_decimal::Decimal::new(100000, 0));
689
        assert!(size.is_none());
690
    }
691
692
    #[test]
693
    fn test_service_config_validation() {
694
        let mut config = create_test_config();
695
696
        // Valid config
697
        assert!(!config.name.is_empty());
698
        assert!(!config.environment.is_empty());
699
700
        // Test with empty name
701
        config.name = String::new();
702
        assert!(config.name.is_empty());
703
    }
704
705
    #[test]
706
    fn test_config_manager_cache_clear() {
707
        let config = create_test_config();
708
        let manager = ConfigManager::new(config);
709
710
        // Add some cache entries
711
        manager.set_cached_config("key1".to_string(), json!({"value": 1}));
712
        manager.set_cached_config("key2".to_string(), json!({"value": 2}));
713
714
        assert!(manager.get_cached_config("key1").is_some());
715
        assert!(manager.get_cached_config("key2").is_some());
716
717
        // Manual clear
718
        if let Ok(mut cache) = manager.cache.write() {
719
            cache.clear();
720
        }
721
722
        assert!(manager.get_cached_config("key1").is_none());
723
        assert!(manager.get_cached_config("key2").is_none());
724
    }
725
726
    #[test]
727
    fn test_builder_default_values() {
728
        let config = create_test_config();
729
        let manager = ConfigManagerBuilder::new(config.clone()).build();
730
731
        let retrieved = manager.get_config();
732
        assert_eq!(retrieved.name, config.name);
733
        assert_eq!(retrieved.environment, config.environment);
734
    }
735
736
    #[test]
737
    fn test_config_manager_arc_cloning() {
738
        let config = create_test_config();
739
        let manager = ConfigManager::new(config);
740
741
        let config1 = Arc::clone(&manager.config);
742
        let config2 = Arc::clone(&manager.config);
743
744
        assert_eq!(config1.name, config2.name);
745
        assert_eq!(Arc::strong_count(&manager.config), 3); // Original + 2 clones
746
    }
747
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html deleted file mode 100644 index 100665739..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/ml_config.rs
Line
Count
Source
1
//! Machine learning configuration
2
3
use serde::{Deserialize, Serialize};
4
use std::collections::HashMap;
5
6
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7
pub struct MLConfig {
8
    pub model_config: ModelArchitectureConfig,
9
    pub training_config: TrainingConfig,
10
    pub simulation_config: SimulationConfig,
11
}
12
13
/// Configuration for market data simulation and stress testing
14
#[derive(Debug, Clone, Serialize, Deserialize)]
15
pub struct SimulationConfig {
16
    /// Initial market state with configurable symbol prices
17
    pub initial_market_state: MarketState,
18
    /// Simulation parameters
19
    pub parameters: SimulationParameters,
20
    /// Test symbol configuration for generic testing
21
    pub test_symbols: TestSymbolConfig,
22
}
23
24
/// Initial market state configuration
25
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct MarketState {
27
    /// Symbol-specific initial prices and configuration
28
    pub symbols: HashMap<String, SymbolConfig>,
29
    /// Default configuration for unlisted symbols
30
    pub default_symbol: SymbolConfig,
31
}
32
33
/// Configuration for individual symbols
34
#[derive(Debug, Clone, Serialize, Deserialize)]
35
pub struct SymbolConfig {
36
    /// Initial price for the symbol
37
    pub initial_price: f64,
38
    /// Base volatility for the symbol
39
    pub volatility: f64,
40
    /// Base trading volume
41
    pub base_volume: f64,
42
    /// Minimum spread in basis points
43
    pub min_spread_bps: f64,
44
    /// Maximum spread in basis points
45
    pub max_spread_bps: f64,
46
    /// Market capitalization tier (affects behavior)
47
    pub market_cap_tier: MarketCapTier,
48
}
49
50
/// Market capitalization tiers for different symbol behaviors
51
#[derive(Debug, Clone, Serialize, Deserialize)]
52
pub enum MarketCapTier {
53
    /// Large cap stocks (>$10B)
54
    LargeCap,
55
    /// Mid cap stocks ($2B-$10B)
56
    MidCap,
57
    /// Small cap stocks (<$2B)
58
    SmallCap,
59
    /// Generic test symbol
60
    Test,
61
}
62
63
/// Simulation parameters
64
#[derive(Debug, Clone, Serialize, Deserialize)]
65
pub struct SimulationParameters {
66
    /// Update rate in Hz
67
    pub update_rate_hz: u32,
68
    /// Base market volatility
69
    pub base_volatility: f64,
70
    /// Market trend direction (-1.0 to 1.0)
71
    pub trend: f64,
72
    /// Enable realistic market microstructure
73
    pub enable_microstructure: bool,
74
    /// Enable correlated movements between symbols
75
    pub enable_correlation: bool,
76
}
77
78
/// Test symbol configuration for generic testing
79
#[derive(Debug, Clone, Serialize, Deserialize)]
80
pub struct TestSymbolConfig {
81
    /// Prefix for test symbols (e.g., "TEST")
82
    pub symbol_prefix: String,
83
    /// Number of test symbols to generate
84
    pub count: usize,
85
    /// Price range for test symbols
86
    pub price_range: (f64, f64),
87
    /// Volume range for test symbols
88
    pub volume_range: (f64, f64),
89
}
90
91
/// Default simulation configuration
92
impl Default for SimulationConfig {
93
0
    fn default() -> Self {
94
0
        let mut symbols = HashMap::new();
95
96
        // Production-ready major symbols with realistic configurations
97
0
        symbols.insert(
98
0
            "AAPL".to_string(),
99
0
            SymbolConfig {
100
0
                initial_price: 150.0,
101
0
                volatility: 0.25,
102
0
                base_volume: 50000000.0,
103
0
                min_spread_bps: 1.0,
104
0
                max_spread_bps: 5.0,
105
0
                market_cap_tier: MarketCapTier::LargeCap,
106
0
            },
107
        );
108
109
0
        symbols.insert(
110
0
            "MSFT".to_string(),
111
0
            SymbolConfig {
112
0
                initial_price: 300.0,
113
0
                volatility: 0.22,
114
0
                base_volume: 30000000.0,
115
0
                min_spread_bps: 1.0,
116
0
                max_spread_bps: 5.0,
117
0
                market_cap_tier: MarketCapTier::LargeCap,
118
0
            },
119
        );
120
121
0
        symbols.insert(
122
0
            "GOOGL".to_string(),
123
0
            SymbolConfig {
124
0
                initial_price: 2500.0,
125
0
                volatility: 0.28,
126
0
                base_volume: 20000000.0,
127
0
                min_spread_bps: 2.0,
128
0
                max_spread_bps: 8.0,
129
0
                market_cap_tier: MarketCapTier::LargeCap,
130
0
            },
131
        );
132
133
0
        symbols.insert(
134
0
            "TSLA".to_string(),
135
0
            SymbolConfig {
136
0
                initial_price: 800.0,
137
0
                volatility: 0.45,
138
0
                base_volume: 80000000.0,
139
0
                min_spread_bps: 2.0,
140
0
                max_spread_bps: 10.0,
141
0
                market_cap_tier: MarketCapTier::LargeCap,
142
0
            },
143
        );
144
145
0
        symbols.insert(
146
0
            "AMZN".to_string(),
147
0
            SymbolConfig {
148
0
                initial_price: 3200.0,
149
0
                volatility: 0.30,
150
0
                base_volume: 25000000.0,
151
0
                min_spread_bps: 2.0,
152
0
                max_spread_bps: 8.0,
153
0
                market_cap_tier: MarketCapTier::LargeCap,
154
0
            },
155
        );
156
157
0
        symbols.insert(
158
0
            "NVDA".to_string(),
159
0
            SymbolConfig {
160
0
                initial_price: 500.0,
161
0
                volatility: 0.40,
162
0
                base_volume: 40000000.0,
163
0
                min_spread_bps: 2.0,
164
0
                max_spread_bps: 8.0,
165
0
                market_cap_tier: MarketCapTier::LargeCap,
166
0
            },
167
        );
168
169
0
        Self {
170
0
            initial_market_state: MarketState {
171
0
                symbols,
172
0
                default_symbol: SymbolConfig {
173
0
                    initial_price: 100.0,
174
0
                    volatility: 0.30,
175
0
                    base_volume: 1000000.0,
176
0
                    min_spread_bps: 5.0,
177
0
                    max_spread_bps: 20.0,
178
0
                    market_cap_tier: MarketCapTier::Test,
179
0
                },
180
0
            },
181
0
            parameters: SimulationParameters {
182
0
                update_rate_hz: 1000,
183
0
                base_volatility: 0.02,
184
0
                trend: 0.0,
185
0
                enable_microstructure: true,
186
0
                enable_correlation: false,
187
0
            },
188
0
            test_symbols: TestSymbolConfig {
189
0
                symbol_prefix: "TEST".to_string(),
190
0
                count: 10,
191
0
                price_range: (50.0, 500.0),
192
0
                volume_range: (100000.0, 10000000.0),
193
0
            },
194
0
        }
195
0
    }
196
}
197
198
#[derive(Debug, Clone, Serialize, Deserialize)]
199
pub struct ModelArchitectureConfig {
200
    pub model_type: String,
201
    pub hidden_dims: Vec<usize>,
202
    pub dropout_rate: f64,
203
    pub activation: String,
204
}
205
206
impl Default for ModelArchitectureConfig {
207
0
    fn default() -> Self {
208
0
        Self {
209
0
            model_type: "transformer".to_string(),
210
0
            hidden_dims: vec![256, 128, 64],
211
0
            dropout_rate: 0.1,
212
0
            activation: "relu".to_string(),
213
0
        }
214
0
    }
215
}
216
217
#[derive(Debug, Clone, Serialize, Deserialize)]
218
pub struct TrainingConfig {
219
    pub batch_size: usize,
220
    pub learning_rate: f64,
221
    pub epochs: u32,
222
    pub early_stopping_patience: u32,
223
}
224
225
impl Default for TrainingConfig {
226
0
    fn default() -> Self {
227
0
        Self {
228
0
            batch_size: 32,
229
0
            learning_rate: 0.001,
230
0
            epochs: 100,
231
0
            early_stopping_patience: 10,
232
0
        }
233
0
    }
234
}
235
236
#[derive(Debug, Clone, Serialize, Deserialize)]
237
pub struct Mamba2Config {
238
    pub d_model: usize,
239
    pub d_state: usize,
240
    pub d_conv: usize,
241
    pub expand: usize,
242
    pub dt_rank: Option<usize>,
243
    pub dt_min: f64,
244
    pub dt_max: f64,
245
    pub dt_init: String,
246
    pub dt_scale: f64,
247
    pub dt_init_floor: f64,
248
    pub conv_bias: bool,
249
    pub bias: bool,
250
    pub use_fast_path: bool,
251
    pub layer_idx: Option<usize>,
252
    pub device: Option<String>,
253
    pub dtype: Option<String>,
254
    pub d_head: usize,
255
    pub num_heads: usize,
256
    pub num_layers: usize,
257
    pub target_latency_us: u64,
258
    pub hardware_aware: bool,
259
    pub use_ssd: bool,
260
    pub use_selective_state: bool,
261
    pub max_seq_len: usize,
262
    pub batch_size: usize,
263
    pub seq_len: usize,
264
    pub dropout: f64,
265
}
266
267
impl Default for Mamba2Config {
268
0
    fn default() -> Self {
269
0
        Self {
270
0
            d_model: 768,
271
0
            d_state: 128,
272
0
            d_conv: 4,
273
0
            expand: 2,
274
0
            dt_rank: None, // Auto-calculated as ceil(d_model / 16)
275
0
            dt_min: 0.001,
276
0
            dt_max: 0.1,
277
0
            dt_init: "random".to_string(),
278
0
            dt_scale: 1.0,
279
0
            dt_init_floor: 1e-4,
280
0
            conv_bias: true,
281
0
            bias: false,
282
0
            use_fast_path: true,
283
0
            layer_idx: None,
284
0
            device: None,
285
0
            dtype: None,
286
0
            d_head: 32,
287
0
            num_heads: 8,
288
0
            num_layers: 4,
289
0
            target_latency_us: 3,
290
0
            hardware_aware: true,
291
0
            use_ssd: true,
292
0
            use_selective_state: true,
293
0
            max_seq_len: 1024,
294
0
            batch_size: 1,
295
0
            seq_len: 256,
296
0
            dropout: 0.0,
297
0
        }
298
0
    }
299
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html deleted file mode 100644 index 1918bebee..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/risk_config.rs
Line
Count
Source
1
//! Risk management configuration structures
2
//!
3
//! Provides configuration types for risk management components including
4
//! stress testing scenarios, asset class definitions, and market shock parameters.
5
6
use serde::{Deserialize, Serialize};
7
use std::collections::HashMap;
8
9
/// Configuration for stress testing scenarios
10
///
11
/// Defines how stress scenarios are configured and applied to portfolios.
12
/// Supports both individual instrument shocks and asset class-based shocks
13
/// for more flexible and maintainable stress testing.
14
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15
pub struct StressScenarioConfig {
16
    /// Unique identifier for this stress test scenario
17
    pub id: String,
18
    /// Human-readable name describing the scenario
19
    pub name: String,
20
    /// Description of the stress scenario and its historical context
21
    pub description: String,
22
    /// Individual instrument-specific shocks (symbol -> shock percentage)
23
    pub instrument_shocks: HashMap<String, f64>,
24
    /// Asset class-based shocks that apply to all instruments in a class
25
    pub asset_class_shocks: HashMap<AssetClass, f64>,
26
    /// Global volatility multiplier to apply across all instruments
27
    pub volatility_multiplier: f64,
28
    /// Asset class-specific volatility multipliers
29
    pub volatility_multipliers: HashMap<AssetClass, f64>,
30
    /// Correlation adjustments between asset classes
31
    pub correlation_adjustments: HashMap<String, f64>,
32
    /// Liquidity haircuts to apply per asset class
33
    pub liquidity_haircuts: HashMap<AssetClass, f64>,
34
    /// Whether this scenario is active and available for use
35
    pub is_active: bool,
36
}
37
38
/// Asset class definitions for grouping instruments
39
///
40
/// Provides a hierarchical way to apply stress shocks to groups
41
/// of related instruments rather than hardcoding individual symbols.
42
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
43
pub enum AssetClass {
44
    /// Large-cap US equities (S&P 500 companies)
45
    LargeCapEquity,
46
    /// Small-cap US equities
47
    SmallCapEquity,
48
    /// Technology sector equities
49
    Technology,
50
    /// Financial sector equities
51
    Financials,
52
    /// Healthcare sector equities
53
    Healthcare,
54
    /// Energy sector equities
55
    Energy,
56
    /// Consumer discretionary equities
57
    ConsumerDiscretionary,
58
    /// Consumer staples equities
59
    ConsumerStaples,
60
    /// Industrial sector equities
61
    Industrials,
62
    /// Materials sector equities
63
    Materials,
64
    /// Real estate sector equities
65
    RealEstate,
66
    /// Utilities sector equities
67
    Utilities,
68
    /// Communication services sector equities
69
    CommunicationServices,
70
    /// US Treasury bonds
71
    USBonds,
72
    /// Corporate bonds
73
    CorporateBonds,
74
    /// High-yield bonds
75
    HighYieldBonds,
76
    /// International developed market equities
77
    InternationalEquity,
78
    /// Emerging market equities
79
    EmergingMarkets,
80
    /// Commodities
81
    Commodities,
82
    /// Foreign exchange
83
    ForeignExchange,
84
    /// Cryptocurrencies
85
    Crypto,
86
    /// Alternative investments
87
    Alternatives,
88
}
89
90
/// Asset class mapping configuration
91
///
92
/// Maps individual instrument symbols to their asset classes for
93
/// applying class-based stress shocks and risk calculations.
94
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95
pub struct AssetClassMapping {
96
    /// Symbol to asset class mappings
97
    pub mappings: HashMap<String, AssetClass>,
98
    /// Default asset class for unmapped symbols
99
    pub default_class: AssetClass,
100
}
101
102
/// Complete risk configuration containing all risk-related settings
103
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104
pub struct RiskConfig {
105
    /// Available stress test scenarios
106
    pub stress_scenarios: Vec<StressScenarioConfig>,
107
    /// Asset class mappings for instruments
108
    pub asset_class_mapping: AssetClassMapping,
109
    /// Default volatility settings
110
    pub default_volatility_multiplier: f64,
111
    /// Maximum allowed portfolio loss percentage
112
    pub max_portfolio_loss_pct: f64,
113
    /// VaR confidence level (e.g., 0.95 for 95% confidence)
114
    pub var_confidence_level: f64,
115
    /// Time horizon for VaR calculations in days
116
    pub var_time_horizon_days: u32,
117
}
118
119
impl Default for RiskConfig {
120
4
    fn default() -> Self {
121
4
        Self {
122
4
            stress_scenarios: create_default_stress_scenarios(),
123
4
            asset_class_mapping: create_default_asset_class_mapping(),
124
4
            default_volatility_multiplier: 1.0,
125
4
            max_portfolio_loss_pct: 20.0,
126
4
            var_confidence_level: 0.95,
127
4
            var_time_horizon_days: 1,
128
4
        }
129
4
    }
130
}
131
132
impl StressScenarioConfig {
133
    /// Get the effective shock for a given instrument symbol
134
    ///
135
    /// Returns the instrument-specific shock if available, otherwise
136
    /// returns the asset class shock based on the symbol's asset class mapping.
137
0
    pub fn get_shock_for_symbol(
138
0
        &self,
139
0
        symbol: &str,
140
0
        asset_mapping: &AssetClassMapping,
141
0
    ) -> Option<f64> {
142
        // First check for instrument-specific shock
143
0
        if let Some(shock) = self.instrument_shocks.get(symbol) {
144
0
            return Some(*shock);
145
0
        }
146
147
        // Then check for asset class shock
148
0
        if let Some(asset_class) = asset_mapping.mappings.get(symbol) {
149
0
            return self.asset_class_shocks.get(asset_class).copied();
150
0
        }
151
152
        // Fall back to default asset class shock
153
0
        self.asset_class_shocks
154
0
            .get(&asset_mapping.default_class)
155
0
            .copied()
156
0
    }
157
158
    /// Get volatility multiplier for a given instrument symbol
159
0
    pub fn get_volatility_multiplier_for_symbol(
160
0
        &self,
161
0
        symbol: &str,
162
0
        asset_mapping: &AssetClassMapping,
163
0
    ) -> f64 {
164
        // Check for asset class-specific volatility multiplier
165
0
        if let Some(asset_class) = asset_mapping.mappings.get(symbol) {
166
0
            if let Some(multiplier) = self.volatility_multipliers.get(asset_class) {
167
0
                return *multiplier;
168
0
            }
169
0
        }
170
171
        // Fall back to default asset class
172
0
        if let Some(multiplier) = self
173
0
            .volatility_multipliers
174
0
            .get(&asset_mapping.default_class)
175
        {
176
0
            return *multiplier;
177
0
        }
178
179
        // Fall back to global multiplier
180
0
        self.volatility_multiplier
181
0
    }
182
}
183
184
/// Create default stress test scenarios based on historical events
185
4
fn create_default_stress_scenarios() -> Vec<StressScenarioConfig> {
186
4
    vec![
187
4
        StressScenarioConfig {
188
4
            id: "market_crash_2008".to_string(),
189
4
            name: "2008 Financial Crisis".to_string(),
190
4
            description: "Simulates the market conditions during the 2008 financial crisis with severe equity declines and financial sector stress".to_string(),
191
4
            instrument_shocks: HashMap::new(),
192
4
            asset_class_shocks: {
193
4
                let mut shocks = HashMap::new();
194
4
                shocks.insert(AssetClass::LargeCapEquity, -37.0);
195
4
                shocks.insert(AssetClass::SmallCapEquity, -45.0);
196
4
                shocks.insert(AssetClass::Financials, -55.0);
197
4
                shocks.insert(AssetClass::Technology, -40.0);
198
4
                shocks.insert(AssetClass::RealEstate, -60.0);
199
4
                shocks.insert(AssetClass::EmergingMarkets, -50.0);
200
4
                shocks.insert(AssetClass::HighYieldBonds, -25.0);
201
4
                shocks
202
4
            },
203
4
            volatility_multiplier: 2.5,
204
4
            volatility_multipliers: HashMap::new(),
205
4
            correlation_adjustments: HashMap::new(),
206
4
            liquidity_haircuts: {
207
4
                let mut haircuts = HashMap::new();
208
4
                haircuts.insert(AssetClass::SmallCapEquity, 0.15);
209
4
                haircuts.insert(AssetClass::EmergingMarkets, 0.20);
210
4
                haircuts.insert(AssetClass::HighYieldBonds, 0.10);
211
4
                haircuts
212
4
            },
213
4
            is_active: true,
214
4
        },
215
4
        StressScenarioConfig {
216
4
            id: "covid_crash_2020".to_string(),
217
4
            name: "COVID-19 Market Crash".to_string(),
218
4
            description: "Simulates the market crash of March 2020 due to COVID-19 pandemic with broad-based equity declines".to_string(),
219
4
            instrument_shocks: HashMap::new(),
220
4
            asset_class_shocks: {
221
4
                let mut shocks = HashMap::new();
222
4
                shocks.insert(AssetClass::LargeCapEquity, -34.0);
223
4
                shocks.insert(AssetClass::SmallCapEquity, -40.0);
224
4
                shocks.insert(AssetClass::Energy, -50.0);
225
4
                shocks.insert(AssetClass::Financials, -45.0);
226
4
                shocks.insert(AssetClass::RealEstate, -35.0);
227
4
                shocks.insert(AssetClass::Technology, -25.0);
228
4
                shocks.insert(AssetClass::EmergingMarkets, -45.0);
229
4
                shocks
230
4
            },
231
4
            volatility_multiplier: 3.0,
232
4
            volatility_multipliers: HashMap::new(),
233
4
            correlation_adjustments: HashMap::new(),
234
4
            liquidity_haircuts: HashMap::new(),
235
4
            is_active: true,
236
4
        },
237
4
        StressScenarioConfig {
238
4
            id: "flash_crash_2010".to_string(),
239
4
            name: "Flash Crash 2010".to_string(),
240
4
            description: "Simulates the May 6, 2010 flash crash with rapid market decline and liquidity issues".to_string(),
241
4
            instrument_shocks: HashMap::new(),
242
4
            asset_class_shocks: {
243
4
                let mut shocks = HashMap::new();
244
4
                shocks.insert(AssetClass::LargeCapEquity, -9.0);
245
4
                shocks.insert(AssetClass::SmallCapEquity, -15.0);
246
4
                shocks.insert(AssetClass::Technology, -12.0);
247
4
                shocks
248
4
            },
249
4
            volatility_multiplier: 5.0,
250
4
            volatility_multipliers: HashMap::new(),
251
4
            correlation_adjustments: HashMap::new(),
252
4
            liquidity_haircuts: {
253
4
                let mut haircuts = HashMap::new();
254
4
                haircuts.insert(AssetClass::LargeCapEquity, 0.05);
255
4
                haircuts.insert(AssetClass::SmallCapEquity, 0.20);
256
4
                haircuts.insert(AssetClass::Technology, 0.10);
257
4
                haircuts
258
4
            },
259
4
            is_active: true,
260
4
        },
261
4
        StressScenarioConfig {
262
4
            id: "volatility_spike".to_string(),
263
4
            name: "Volatility Spike".to_string(),
264
4
            description: "Simulates a sudden spike in market volatility without significant price moves".to_string(),
265
4
            instrument_shocks: HashMap::new(),
266
4
            asset_class_shocks: HashMap::new(),
267
4
            volatility_multiplier: 3.0,
268
4
            volatility_multipliers: {
269
4
                let mut multipliers = HashMap::new();
270
4
                multipliers.insert(AssetClass::SmallCapEquity, 4.0);
271
4
                multipliers.insert(AssetClass::EmergingMarkets, 3.5);
272
4
                multipliers.insert(AssetClass::HighYieldBonds, 2.5);
273
4
                multipliers
274
4
            },
275
4
            correlation_adjustments: HashMap::new(),
276
4
            liquidity_haircuts: HashMap::new(),
277
4
            is_active: true,
278
4
        },
279
4
        StressScenarioConfig {
280
4
            id: "interest_rate_shock".to_string(),
281
4
            name: "Interest Rate Shock".to_string(),
282
4
            description: "Simulates a sudden rise in interest rates affecting bonds and rate-sensitive sectors".to_string(),
283
4
            instrument_shocks: HashMap::new(),
284
4
            asset_class_shocks: {
285
4
                let mut shocks = HashMap::new();
286
4
                shocks.insert(AssetClass::USBonds, -8.0);
287
4
                shocks.insert(AssetClass::CorporateBonds, -12.0);
288
4
                shocks.insert(AssetClass::RealEstate, -15.0);
289
4
                shocks.insert(AssetClass::Utilities, -10.0);
290
4
                shocks.insert(AssetClass::Financials, 5.0); // Banks benefit from higher rates
291
4
                shocks
292
4
            },
293
4
            volatility_multiplier: 1.5,
294
4
            volatility_multipliers: HashMap::new(),
295
4
            correlation_adjustments: HashMap::new(),
296
4
            liquidity_haircuts: HashMap::new(),
297
4
            is_active: true,
298
4
        },
299
    ]
300
4
}
301
302
/// Create default asset class mapping for common symbols
303
4
fn create_default_asset_class_mapping() -> AssetClassMapping {
304
4
    let mut mappings = HashMap::new();
305
306
    // Large Cap Technology
307
4
    mappings.insert("AAPL".to_string(), AssetClass::Technology);
308
4
    mappings.insert("MSFT".to_string(), AssetClass::Technology);
309
4
    mappings.insert("GOOGL".to_string(), AssetClass::Technology);
310
4
    mappings.insert("GOOG".to_string(), AssetClass::Technology);
311
4
    mappings.insert("AMZN".to_string(), AssetClass::Technology);
312
4
    mappings.insert("META".to_string(), AssetClass::Technology);
313
4
    mappings.insert("TSLA".to_string(), AssetClass::Technology);
314
4
    mappings.insert("NVDA".to_string(), AssetClass::Technology);
315
316
    // Large Cap Financials
317
4
    mappings.insert("JPM".to_string(), AssetClass::Financials);
318
4
    mappings.insert("BAC".to_string(), AssetClass::Financials);
319
4
    mappings.insert("WFC".to_string(), AssetClass::Financials);
320
4
    mappings.insert("GS".to_string(), AssetClass::Financials);
321
4
    mappings.insert("MS".to_string(), AssetClass::Financials);
322
323
    // ETFs
324
4
    mappings.insert("SPY".to_string(), AssetClass::LargeCapEquity);
325
4
    mappings.insert("QQQ".to_string(), AssetClass::Technology);
326
4
    mappings.insert("IWM".to_string(), AssetClass::SmallCapEquity);
327
4
    mappings.insert("VTI".to_string(), AssetClass::LargeCapEquity);
328
4
    mappings.insert("EEM".to_string(), AssetClass::EmergingMarkets);
329
4
    mappings.insert("VEA".to_string(), AssetClass::InternationalEquity);
330
4
    mappings.insert("TLT".to_string(), AssetClass::USBonds);
331
4
    mappings.insert("HYG".to_string(), AssetClass::HighYieldBonds);
332
333
    // Healthcare
334
4
    mappings.insert("JNJ".to_string(), AssetClass::Healthcare);
335
4
    mappings.insert("PFE".to_string(), AssetClass::Healthcare);
336
4
    mappings.insert("UNH".to_string(), AssetClass::Healthcare);
337
338
    // Energy
339
4
    mappings.insert("XOM".to_string(), AssetClass::Energy);
340
4
    mappings.insert("CVX".to_string(), AssetClass::Energy);
341
342
4
    AssetClassMapping {
343
4
        mappings,
344
4
        default_class: AssetClass::LargeCapEquity,
345
4
    }
346
4
}
347
348
#[cfg(test)]
349
mod tests {
350
    use super::*;
351
352
    #[test]
353
    fn test_stress_scenario_config_creation() {
354
        let config = StressScenarioConfig {
355
            id: "test".to_string(),
356
            name: "Test Scenario".to_string(),
357
            description: "Test description".to_string(),
358
            instrument_shocks: HashMap::new(),
359
            asset_class_shocks: {
360
                let mut shocks = HashMap::new();
361
                shocks.insert(AssetClass::Technology, -10.0);
362
                shocks
363
            },
364
            volatility_multiplier: 2.0,
365
            volatility_multipliers: HashMap::new(),
366
            correlation_adjustments: HashMap::new(),
367
            liquidity_haircuts: HashMap::new(),
368
            is_active: true,
369
        };
370
371
        assert_eq!(config.id, "test");
372
        assert_eq!(config.volatility_multiplier, 2.0);
373
    }
374
375
    #[test]
376
    fn test_asset_class_mapping() {
377
        let mapping = create_default_asset_class_mapping();
378
379
        assert_eq!(mapping.mappings.get("AAPL"), Some(&AssetClass::Technology));
380
        assert_eq!(
381
            mapping.mappings.get("SPY"),
382
            Some(&AssetClass::LargeCapEquity)
383
        );
384
        assert_eq!(mapping.default_class, AssetClass::LargeCapEquity);
385
    }
386
387
    #[test]
388
    fn test_get_shock_for_symbol() {
389
        let config = StressScenarioConfig {
390
            id: "test".to_string(),
391
            name: "Test".to_string(),
392
            description: "Test".to_string(),
393
            instrument_shocks: {
394
                let mut shocks = HashMap::new();
395
                shocks.insert("AAPL".to_string(), -15.0);
396
                shocks
397
            },
398
            asset_class_shocks: {
399
                let mut shocks = HashMap::new();
400
                shocks.insert(AssetClass::Technology, -10.0);
401
                shocks.insert(AssetClass::LargeCapEquity, -5.0);
402
                shocks
403
            },
404
            volatility_multiplier: 1.0,
405
            volatility_multipliers: HashMap::new(),
406
            correlation_adjustments: HashMap::new(),
407
            liquidity_haircuts: HashMap::new(),
408
            is_active: true,
409
        };
410
411
        let mapping = create_default_asset_class_mapping();
412
413
        // Should get instrument-specific shock
414
        assert_eq!(config.get_shock_for_symbol("AAPL", &mapping), Some(-15.0));
415
416
        // Should get asset class shock for GOOGL (Technology)
417
        assert_eq!(config.get_shock_for_symbol("GOOGL", &mapping), Some(-10.0));
418
419
        // Should get default class shock for unknown symbol
420
        assert_eq!(config.get_shock_for_symbol("UNKNOWN", &mapping), Some(-5.0));
421
    }
422
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html deleted file mode 100644 index 661d5c3b0..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/runtime.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/runtime.rs
Line
Count
Source
1
//! Runtime configuration layer for environment-aware defaults.
2
//!
3
//! This module provides Tier 2 runtime configuration that complements the
4
//! compile-time constants in `common::thresholds`. Values here can be overridden
5
//! via environment variables to support different deployment environments
6
//! (development, staging, production) without recompilation.
7
//!
8
//! # Architecture
9
//!
10
//! - Tier 1 (Compile-time): `common::thresholds` - Performance-critical constants
11
//! - Tier 2 (Runtime): This module - Environment-aware operational parameters
12
//! - Tier 3 (Database): Hot-reload via PostgreSQL NOTIFY/LISTEN
13
//!
14
//! # Environment Variables
15
//!
16
//! ## Database Configuration
17
//! - `DATABASE_QUERY_TIMEOUT_MS` - Query timeout in milliseconds (default: environment-aware)
18
//! - `DATABASE_CONNECTION_TIMEOUT_MS` - Connection timeout in milliseconds
19
//! - `DATABASE_POOL_SIZE` - Connection pool size
20
//! - `DATABASE_MAX_POOL_SIZE` - Maximum pool size
21
//! - `DATABASE_ACQUIRE_TIMEOUT_MS` - Pool acquire timeout in milliseconds
22
//!
23
//! ## Cache Configuration
24
//! - `CACHE_POSITION_TTL_SECS` - Position cache TTL in seconds
25
//! - `CACHE_VAR_TTL_SECS` - VaR calculation cache TTL in seconds
26
//! - `CACHE_COMPLIANCE_TTL_SECS` - Compliance check cache TTL in seconds
27
//! - `CACHE_MARKET_DATA_TTL_SECS` - Market data cache TTL in seconds
28
//! - `CACHE_MODEL_PREDICTION_TTL_SECS` - Model prediction cache TTL in seconds
29
//!
30
//! ## Network Configuration
31
//! - `NETWORK_GRPC_CONNECT_TIMEOUT_SECS` - gRPC connect timeout in seconds
32
//! - `NETWORK_GRPC_REQUEST_TIMEOUT_SECS` - gRPC request timeout in seconds
33
//! - `NETWORK_KEEP_ALIVE_INTERVAL_SECS` - Keep-alive interval in seconds
34
//! - `NETWORK_KEEP_ALIVE_TIMEOUT_SECS` - Keep-alive timeout in seconds
35
//! - `NETWORK_MAX_CONCURRENT_CONNECTIONS` - Maximum concurrent connections
36
//!
37
//! ## Retry Configuration
38
//! - `RETRY_INITIAL_DELAY_MS` - Initial retry delay in milliseconds
39
//! - `RETRY_MAX_DELAY_SECS` - Maximum retry delay in seconds
40
//! - `RETRY_MAX_ATTEMPTS` - Maximum retry attempts
41
//! - `RETRY_BACKOFF_MULTIPLIER` - Backoff multiplier for exponential backoff
42
//!
43
//! ## Safety Configuration
44
//! - `SAFETY_CHECK_TIMEOUT_MS` - Safety check timeout in milliseconds
45
//! - `SAFETY_AUTO_RECOVERY_DELAY_SECS` - Auto-recovery delay in seconds
46
//! - `SAFETY_LOSS_CHECK_INTERVAL_SECS` - Loss check interval in seconds
47
//! - `SAFETY_POSITION_CHECK_INTERVAL_SECS` - Position check interval in seconds
48
//!
49
//! ## ML Configuration
50
//! - `ML_MAX_BATCH_SIZE` - Maximum batch size for ML inference
51
//! - `ML_INFERENCE_TIMEOUT_MS` - ML inference timeout in milliseconds
52
//! - `ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS` - Model cache cleanup interval
53
//! - `ML_DRIFT_CHECK_INTERVAL_SECS` - Drift detection check interval
54
//!
55
//! ## Risk Configuration
56
//! - `RISK_VAR_LOOKBACK_DAYS` - VaR lookback period in trading days
57
//! - `RISK_VAR_CONFIDENCE` - VaR confidence level (0.0-1.0)
58
//! - `RISK_MAX_DRAWDOWN_WARNING_PCT` - Max drawdown warning threshold
59
//!
60
//! # Example
61
//!
62
//! ```rust,no_run
63
//! use config::runtime::{RuntimeConfig, Environment};
64
//!
65
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
66
//! // Auto-detect environment and load from env vars
67
//! let config = RuntimeConfig::from_env()?;
68
//!
69
//! // Or specify environment explicitly
70
//! let prod_config = RuntimeConfig::from_env_with_environment(Environment::Production)?;
71
//!
72
//! // Or use defaults for specific environment
73
//! let dev_config = RuntimeConfig::with_defaults(Environment::Development);
74
//!
75
//! println!("Database query timeout: {:?}", config.database.query_timeout);
76
//! println!("Position cache TTL: {:?}", config.cache.position_ttl);
77
//! # Ok(())
78
//! # }
79
//! ```
80
81
use crate::error::{ConfigError, ConfigResult};
82
use serde::{Deserialize, Serialize};
83
use std::time::Duration;
84
85
/// Deployment environment enumeration.
86
///
87
/// Determines default values for runtime configuration parameters.
88
/// Different environments have different performance vs safety trade-offs.
89
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90
pub enum Environment {
91
    /// Development environment - Relaxed timeouts, verbose logging
92
    Development,
93
    /// Staging environment - Production-like settings with some debug features
94
    Staging,
95
    /// Production environment - Optimized for performance and reliability
96
    Production,
97
}
98
99
impl Environment {
100
    /// Detects the environment from the ENVIRONMENT environment variable.
101
    ///
102
    /// Falls back to Development if not set or invalid.
103
0
    pub fn detect() -> Self {
104
0
        match std::env::var("ENVIRONMENT")
105
0
            .unwrap_or_else(|_| "development".to_string())
106
0
            .to_lowercase()
107
0
            .as_str()
108
        {
109
0
            "production" | "prod" => Environment::Production,
110
0
            "staging" | "stage" => Environment::Staging,
111
0
            _ => Environment::Development,
112
        }
113
0
    }
114
115
    /// Returns true if this is a production environment.
116
0
    pub fn is_production(&self) -> bool {
117
0
        matches!(self, Environment::Production)
118
0
    }
119
120
    /// Returns true if this is a development environment.
121
0
    pub fn is_development(&self) -> bool {
122
0
        matches!(self, Environment::Development)
123
0
    }
124
}
125
126
/// Database runtime configuration.
127
///
128
/// Controls database connection pooling, timeouts, and query execution limits.
129
#[derive(Debug, Clone, Serialize, Deserialize)]
130
pub struct DatabaseRuntimeConfig {
131
    /// Query timeout for standard operations
132
    pub query_timeout: Duration,
133
    /// Connection establishment timeout
134
    pub connection_timeout: Duration,
135
    /// Pool acquire timeout
136
    pub acquire_timeout: Duration,
137
    /// Default pool size
138
    pub pool_size: u32,
139
    /// Maximum pool size
140
    pub max_pool_size: u32,
141
    /// Connection lifetime
142
    pub connection_lifetime: Duration,
143
    /// Idle timeout
144
    pub idle_timeout: Duration,
145
}
146
147
impl DatabaseRuntimeConfig {
148
    /// Creates configuration with environment-aware defaults.
149
0
    pub fn with_defaults(env: Environment) -> Self {
150
0
        match env {
151
0
            Environment::Development => Self {
152
0
                query_timeout: Duration::from_millis(5000), // More relaxed for debugging
153
0
                connection_timeout: Duration::from_millis(500),
154
0
                acquire_timeout: Duration::from_millis(200),
155
0
                pool_size: 10,
156
0
                max_pool_size: 50,
157
0
                connection_lifetime: Duration::from_secs(1800), // 30 minutes
158
0
                idle_timeout: Duration::from_secs(600), // 10 minutes
159
0
            },
160
0
            Environment::Staging => Self {
161
0
                query_timeout: Duration::from_millis(2000),
162
0
                connection_timeout: Duration::from_millis(200),
163
0
                acquire_timeout: Duration::from_millis(100),
164
0
                pool_size: 15,
165
0
                max_pool_size: 75,
166
0
                connection_lifetime: Duration::from_secs(3600), // 1 hour
167
0
                idle_timeout: Duration::from_secs(300), // 5 minutes
168
0
            },
169
0
            Environment::Production => Self {
170
0
                query_timeout: Duration::from_millis(1000), // Tight timeout for HFT
171
0
                connection_timeout: Duration::from_millis(100),
172
0
                acquire_timeout: Duration::from_millis(50),
173
0
                pool_size: 20,
174
0
                max_pool_size: 100,
175
0
                connection_lifetime: Duration::from_secs(3600), // 1 hour
176
0
                idle_timeout: Duration::from_secs(300), // 5 minutes
177
0
            },
178
        }
179
0
    }
180
181
    /// Loads from environment variables with fallback to defaults.
182
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
183
0
        let defaults = Self::with_defaults(env);
184
185
        Ok(Self {
186
0
            query_timeout: parse_env_duration_ms("DATABASE_QUERY_TIMEOUT_MS", defaults.query_timeout)?,
187
0
            connection_timeout: parse_env_duration_ms("DATABASE_CONNECTION_TIMEOUT_MS", defaults.connection_timeout)?,
188
0
            acquire_timeout: parse_env_duration_ms("DATABASE_ACQUIRE_TIMEOUT_MS", defaults.acquire_timeout)?,
189
0
            pool_size: parse_env_u32("DATABASE_POOL_SIZE", defaults.pool_size)?,
190
0
            max_pool_size: parse_env_u32("DATABASE_MAX_POOL_SIZE", defaults.max_pool_size)?,
191
0
            connection_lifetime: parse_env_duration_secs("DATABASE_CONNECTION_LIFETIME_SECS", defaults.connection_lifetime)?,
192
0
            idle_timeout: parse_env_duration_secs("DATABASE_IDLE_TIMEOUT_SECS", defaults.idle_timeout)?,
193
        })
194
0
    }
195
196
    /// Validates the configuration.
197
0
    pub fn validate(&self) -> ConfigResult<()> {
198
0
        if self.query_timeout.as_millis() == 0 {
199
0
            return Err(ConfigError::Invalid("Query timeout must be positive".into()));
200
0
        }
201
0
        if self.pool_size == 0 {
202
0
            return Err(ConfigError::Invalid("Pool size must be positive".into()));
203
0
        }
204
0
        if self.pool_size > self.max_pool_size {
205
0
            return Err(ConfigError::Invalid("Pool size cannot exceed max pool size".into()));
206
0
        }
207
0
        Ok(())
208
0
    }
209
}
210
211
/// Cache TTL runtime configuration.
212
///
213
/// Controls time-to-live values for various cache types.
214
#[derive(Debug, Clone, Serialize, Deserialize)]
215
pub struct CacheRuntimeConfig {
216
    /// Position cache TTL
217
    pub position_ttl: Duration,
218
    /// VaR calculation cache TTL
219
    pub var_ttl: Duration,
220
    /// Compliance check cache TTL
221
    pub compliance_ttl: Duration,
222
    /// Market data cache TTL
223
    pub market_data_ttl: Duration,
224
    /// Model prediction cache TTL
225
    pub model_prediction_ttl: Duration,
226
}
227
228
impl CacheRuntimeConfig {
229
    /// Creates configuration with environment-aware defaults.
230
0
    pub fn with_defaults(env: Environment) -> Self {
231
0
        match env {
232
0
            Environment::Development => Self {
233
0
                position_ttl: Duration::from_secs(120), // Longer TTL for debugging
234
0
                var_ttl: Duration::from_secs(7200), // 2 hours
235
0
                compliance_ttl: Duration::from_secs(172800), // 48 hours
236
0
                market_data_ttl: Duration::from_secs(600), // 10 minutes
237
0
                model_prediction_ttl: Duration::from_secs(120), // 2 minutes
238
0
            },
239
0
            Environment::Staging => Self {
240
0
                position_ttl: Duration::from_secs(90),
241
0
                var_ttl: Duration::from_secs(5400), // 1.5 hours
242
0
                compliance_ttl: Duration::from_secs(129600), // 36 hours
243
0
                market_data_ttl: Duration::from_secs(450), // 7.5 minutes
244
0
                model_prediction_ttl: Duration::from_secs(90),
245
0
            },
246
0
            Environment::Production => Self {
247
0
                position_ttl: Duration::from_secs(60), // 1 minute for HFT
248
0
                var_ttl: Duration::from_secs(3600), // 1 hour
249
0
                compliance_ttl: Duration::from_secs(86400), // 24 hours
250
0
                market_data_ttl: Duration::from_secs(300), // 5 minutes
251
0
                model_prediction_ttl: Duration::from_secs(60), // 1 minute
252
0
            },
253
        }
254
0
    }
255
256
    /// Loads from environment variables with fallback to defaults.
257
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
258
0
        let defaults = Self::with_defaults(env);
259
260
        Ok(Self {
261
0
            position_ttl: parse_env_duration_secs("CACHE_POSITION_TTL_SECS", defaults.position_ttl)?,
262
0
            var_ttl: parse_env_duration_secs("CACHE_VAR_TTL_SECS", defaults.var_ttl)?,
263
0
            compliance_ttl: parse_env_duration_secs("CACHE_COMPLIANCE_TTL_SECS", defaults.compliance_ttl)?,
264
0
            market_data_ttl: parse_env_duration_secs("CACHE_MARKET_DATA_TTL_SECS", defaults.market_data_ttl)?,
265
0
            model_prediction_ttl: parse_env_duration_secs("CACHE_MODEL_PREDICTION_TTL_SECS", defaults.model_prediction_ttl)?,
266
        })
267
0
    }
268
269
    /// Validates the configuration.
270
0
    pub fn validate(&self) -> ConfigResult<()> {
271
0
        if self.position_ttl.as_secs() == 0 {
272
0
            return Err(ConfigError::Invalid("Position TTL must be positive".into()));
273
0
        }
274
0
        if self.var_ttl.as_secs() == 0 {
275
0
            return Err(ConfigError::Invalid("VaR TTL must be positive".into()));
276
0
        }
277
0
        Ok(())
278
0
    }
279
}
280
281
/// Network timeout runtime configuration.
282
///
283
/// Controls gRPC and network-related timeouts.
284
#[derive(Debug, Clone, Serialize, Deserialize)]
285
pub struct TimeoutConfig {
286
    /// gRPC connect timeout
287
    pub grpc_connect_timeout: Duration,
288
    /// gRPC request timeout
289
    pub grpc_request_timeout: Duration,
290
    /// Keep-alive interval
291
    pub keep_alive_interval: Duration,
292
    /// Keep-alive timeout
293
    pub keep_alive_timeout: Duration,
294
    /// Maximum concurrent connections
295
    pub max_concurrent_connections: u32,
296
}
297
298
impl TimeoutConfig {
299
    /// Creates configuration with environment-aware defaults.
300
0
    pub fn with_defaults(env: Environment) -> Self {
301
0
        match env {
302
0
            Environment::Development => Self {
303
0
                grpc_connect_timeout: Duration::from_secs(10),
304
0
                grpc_request_timeout: Duration::from_secs(30),
305
0
                keep_alive_interval: Duration::from_secs(60),
306
0
                keep_alive_timeout: Duration::from_secs(10),
307
0
                max_concurrent_connections: 50,
308
0
            },
309
0
            Environment::Staging => Self {
310
0
                grpc_connect_timeout: Duration::from_secs(7),
311
0
                grpc_request_timeout: Duration::from_secs(20),
312
0
                keep_alive_interval: Duration::from_secs(45),
313
0
                keep_alive_timeout: Duration::from_secs(7),
314
0
                max_concurrent_connections: 75,
315
0
            },
316
0
            Environment::Production => Self {
317
0
                grpc_connect_timeout: Duration::from_secs(5),
318
0
                grpc_request_timeout: Duration::from_secs(10),
319
0
                keep_alive_interval: Duration::from_secs(30),
320
0
                keep_alive_timeout: Duration::from_secs(5),
321
0
                max_concurrent_connections: 100,
322
0
            },
323
        }
324
0
    }
325
326
    /// Loads from environment variables with fallback to defaults.
327
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
328
0
        let defaults = Self::with_defaults(env);
329
330
        Ok(Self {
331
0
            grpc_connect_timeout: parse_env_duration_secs("NETWORK_GRPC_CONNECT_TIMEOUT_SECS", defaults.grpc_connect_timeout)?,
332
0
            grpc_request_timeout: parse_env_duration_secs("NETWORK_GRPC_REQUEST_TIMEOUT_SECS", defaults.grpc_request_timeout)?,
333
0
            keep_alive_interval: parse_env_duration_secs("NETWORK_KEEP_ALIVE_INTERVAL_SECS", defaults.keep_alive_interval)?,
334
0
            keep_alive_timeout: parse_env_duration_secs("NETWORK_KEEP_ALIVE_TIMEOUT_SECS", defaults.keep_alive_timeout)?,
335
0
            max_concurrent_connections: parse_env_u32("NETWORK_MAX_CONCURRENT_CONNECTIONS", defaults.max_concurrent_connections)?,
336
        })
337
0
    }
338
339
    /// Validates the configuration.
340
0
    pub fn validate(&self) -> ConfigResult<()> {
341
0
        if self.grpc_connect_timeout.as_secs() == 0 {
342
0
            return Err(ConfigError::Invalid("gRPC connect timeout must be positive".into()));
343
0
        }
344
0
        if self.max_concurrent_connections == 0 {
345
0
            return Err(ConfigError::Invalid("Max concurrent connections must be positive".into()));
346
0
        }
347
0
        Ok(())
348
0
    }
349
}
350
351
/// Operational limits runtime configuration.
352
///
353
/// Controls retry behavior, safety checks, ML parameters, and risk calculations.
354
#[derive(Debug, Clone, Serialize, Deserialize)]
355
pub struct LimitsConfig {
356
    // Retry configuration
357
    /// Initial retry delay
358
    pub retry_initial_delay: Duration,
359
    /// Maximum retry delay
360
    pub retry_max_delay: Duration,
361
    /// Maximum retry attempts
362
    pub retry_max_attempts: u32,
363
    /// Backoff multiplier
364
    pub retry_backoff_multiplier: f32,
365
366
    // Safety configuration
367
    /// Safety check timeout
368
    pub safety_check_timeout: Duration,
369
    /// Auto-recovery delay
370
    pub safety_auto_recovery_delay: Duration,
371
    /// Loss check interval
372
    pub safety_loss_check_interval: Duration,
373
    /// Position check interval
374
    pub safety_position_check_interval: Duration,
375
376
    // ML configuration
377
    /// Maximum batch size for ML inference
378
    pub ml_max_batch_size: usize,
379
    /// ML inference timeout
380
    pub ml_inference_timeout: Duration,
381
    /// Model cache cleanup interval
382
    pub ml_cache_cleanup_interval: Duration,
383
    /// Drift detection check interval
384
    pub ml_drift_check_interval: Duration,
385
386
    // Risk configuration
387
    /// VaR lookback period in trading days
388
    pub risk_var_lookback_days: usize,
389
    /// VaR confidence level
390
    pub risk_var_confidence: f64,
391
    /// Max drawdown warning threshold (percentage)
392
    pub risk_max_drawdown_warning_pct: u8,
393
}
394
395
impl LimitsConfig {
396
    /// Creates configuration with environment-aware defaults.
397
0
    pub fn with_defaults(env: Environment) -> Self {
398
0
        match env {
399
0
            Environment::Development => Self {
400
0
                // Retry
401
0
                retry_initial_delay: Duration::from_millis(200),
402
0
                retry_max_delay: Duration::from_secs(60),
403
0
                retry_max_attempts: 5,
404
0
                retry_backoff_multiplier: 2.0,
405
0
406
0
                // Safety
407
0
                safety_check_timeout: Duration::from_millis(50),
408
0
                safety_auto_recovery_delay: Duration::from_secs(60),
409
0
                safety_loss_check_interval: Duration::from_secs(30),
410
0
                safety_position_check_interval: Duration::from_secs(15),
411
0
412
0
                // ML
413
0
                ml_max_batch_size: 1024,
414
0
                ml_inference_timeout: Duration::from_millis(200),
415
0
                ml_cache_cleanup_interval: Duration::from_secs(7200), // 2 hours
416
0
                ml_drift_check_interval: Duration::from_secs(600), // 10 minutes
417
0
418
0
                // Risk
419
0
                risk_var_lookback_days: 252,
420
0
                risk_var_confidence: 0.95,
421
0
                risk_max_drawdown_warning_pct: 20,
422
0
            },
423
0
            Environment::Staging => Self {
424
0
                // Retry
425
0
                retry_initial_delay: Duration::from_millis(150),
426
0
                retry_max_delay: Duration::from_secs(45),
427
0
                retry_max_attempts: 4,
428
0
                retry_backoff_multiplier: 1.75,
429
0
430
0
                // Safety
431
0
                safety_check_timeout: Duration::from_millis(25),
432
0
                safety_auto_recovery_delay: Duration::from_secs(900), // 15 minutes
433
0
                safety_loss_check_interval: Duration::from_secs(15),
434
0
                safety_position_check_interval: Duration::from_secs(7),
435
0
436
0
                // ML
437
0
                ml_max_batch_size: 4096,
438
0
                ml_inference_timeout: Duration::from_millis(150),
439
0
                ml_cache_cleanup_interval: Duration::from_secs(5400), // 1.5 hours
440
0
                ml_drift_check_interval: Duration::from_secs(450), // 7.5 minutes
441
0
442
0
                // Risk
443
0
                risk_var_lookback_days: 252,
444
0
                risk_var_confidence: 0.95,
445
0
                risk_max_drawdown_warning_pct: 17,
446
0
            },
447
0
            Environment::Production => Self {
448
0
                // Retry
449
0
                retry_initial_delay: Duration::from_millis(100),
450
0
                retry_max_delay: Duration::from_secs(30),
451
0
                retry_max_attempts: 3,
452
0
                retry_backoff_multiplier: 1.5,
453
0
454
0
                // Safety
455
0
                safety_check_timeout: Duration::from_millis(5),
456
0
                safety_auto_recovery_delay: Duration::from_secs(1800), // 30 minutes
457
0
                safety_loss_check_interval: Duration::from_secs(5),
458
0
                safety_position_check_interval: Duration::from_secs(2),
459
0
460
0
                // ML
461
0
                ml_max_batch_size: 8192,
462
0
                ml_inference_timeout: Duration::from_millis(100),
463
0
                ml_cache_cleanup_interval: Duration::from_secs(3600), // 1 hour
464
0
                ml_drift_check_interval: Duration::from_secs(300), // 5 minutes
465
0
466
0
                // Risk
467
0
                risk_var_lookback_days: 252,
468
0
                risk_var_confidence: 0.95,
469
0
                risk_max_drawdown_warning_pct: 15,
470
0
            },
471
        }
472
0
    }
473
474
    /// Loads from environment variables with fallback to defaults.
475
0
    pub fn from_env(env: Environment) -> ConfigResult<Self> {
476
0
        let defaults = Self::with_defaults(env);
477
478
        Ok(Self {
479
            // Retry
480
0
            retry_initial_delay: parse_env_duration_ms("RETRY_INITIAL_DELAY_MS", defaults.retry_initial_delay)?,
481
0
            retry_max_delay: parse_env_duration_secs("RETRY_MAX_DELAY_SECS", defaults.retry_max_delay)?,
482
0
            retry_max_attempts: parse_env_u32("RETRY_MAX_ATTEMPTS", defaults.retry_max_attempts)?,
483
0
            retry_backoff_multiplier: parse_env_f32("RETRY_BACKOFF_MULTIPLIER", defaults.retry_backoff_multiplier)?,
484
485
            // Safety
486
0
            safety_check_timeout: parse_env_duration_ms("SAFETY_CHECK_TIMEOUT_MS", defaults.safety_check_timeout)?,
487
0
            safety_auto_recovery_delay: parse_env_duration_secs("SAFETY_AUTO_RECOVERY_DELAY_SECS", defaults.safety_auto_recovery_delay)?,
488
0
            safety_loss_check_interval: parse_env_duration_secs("SAFETY_LOSS_CHECK_INTERVAL_SECS", defaults.safety_loss_check_interval)?,
489
0
            safety_position_check_interval: parse_env_duration_secs("SAFETY_POSITION_CHECK_INTERVAL_SECS", defaults.safety_position_check_interval)?,
490
491
            // ML
492
0
            ml_max_batch_size: parse_env_usize("ML_MAX_BATCH_SIZE", defaults.ml_max_batch_size)?,
493
0
            ml_inference_timeout: parse_env_duration_ms("ML_INFERENCE_TIMEOUT_MS", defaults.ml_inference_timeout)?,
494
0
            ml_cache_cleanup_interval: parse_env_duration_secs("ML_MODEL_CACHE_CLEANUP_INTERVAL_SECS", defaults.ml_cache_cleanup_interval)?,
495
0
            ml_drift_check_interval: parse_env_duration_secs("ML_DRIFT_CHECK_INTERVAL_SECS", defaults.ml_drift_check_interval)?,
496
497
            // Risk
498
0
            risk_var_lookback_days: parse_env_usize("RISK_VAR_LOOKBACK_DAYS", defaults.risk_var_lookback_days)?,
499
0
            risk_var_confidence: parse_env_f64("RISK_VAR_CONFIDENCE", defaults.risk_var_confidence)?,
500
0
            risk_max_drawdown_warning_pct: parse_env_u8("RISK_MAX_DRAWDOWN_WARNING_PCT", defaults.risk_max_drawdown_warning_pct)?,
501
        })
502
0
    }
503
504
    /// Validates the configuration.
505
0
    pub fn validate(&self) -> ConfigResult<()> {
506
0
        if self.retry_max_attempts == 0 {
507
0
            return Err(ConfigError::Invalid("Retry max attempts must be positive".into()));
508
0
        }
509
0
        if self.retry_backoff_multiplier <= 1.0 {
510
0
            return Err(ConfigError::Invalid("Backoff multiplier must be > 1.0".into()));
511
0
        }
512
0
        if self.ml_max_batch_size == 0 {
513
0
            return Err(ConfigError::Invalid("ML max batch size must be positive".into()));
514
0
        }
515
0
        if self.risk_var_confidence < 0.0 || self.risk_var_confidence > 1.0 {
516
0
            return Err(ConfigError::Invalid("VaR confidence must be between 0.0 and 1.0".into()));
517
0
        }
518
0
        if self.risk_var_lookback_days == 0 {
519
0
            return Err(ConfigError::Invalid("VaR lookback days must be positive".into()));
520
0
        }
521
0
        Ok(())
522
0
    }
523
}
524
525
/// Complete runtime configuration for the Foxhunt trading system.
526
///
527
/// Aggregates all runtime configuration categories with environment-aware defaults
528
/// and environment variable overrides.
529
#[derive(Debug, Clone, Serialize, Deserialize)]
530
pub struct RuntimeConfig {
531
    /// Detected or specified environment
532
    pub environment: Environment,
533
    /// Database configuration
534
    pub database: DatabaseRuntimeConfig,
535
    /// Cache configuration
536
    pub cache: CacheRuntimeConfig,
537
    /// Timeout configuration
538
    pub timeouts: TimeoutConfig,
539
    /// Limits and operational parameters
540
    pub limits: LimitsConfig,
541
}
542
543
impl RuntimeConfig {
544
    /// Creates runtime configuration by auto-detecting environment and loading from env vars.
545
    ///
546
    /// # Errors
547
    ///
548
    /// Returns ConfigError if environment variables contain invalid values or
549
    /// if validation fails.
550
0
    pub fn from_env() -> ConfigResult<Self> {
551
0
        let environment = Environment::detect();
552
0
        Self::from_env_with_environment(environment)
553
0
    }
554
555
    /// Creates runtime configuration with specified environment and loads from env vars.
556
    ///
557
    /// # Arguments
558
    ///
559
    /// * `environment` - The deployment environment to use for defaults
560
    ///
561
    /// # Errors
562
    ///
563
    /// Returns ConfigError if environment variables contain invalid values or
564
    /// if validation fails.
565
0
    pub fn from_env_with_environment(environment: Environment) -> ConfigResult<Self> {
566
0
        let config = Self {
567
0
            environment,
568
0
            database: DatabaseRuntimeConfig::from_env(environment)?,
569
0
            cache: CacheRuntimeConfig::from_env(environment)?,
570
0
            timeouts: TimeoutConfig::from_env(environment)?,
571
0
            limits: LimitsConfig::from_env(environment)?,
572
        };
573
574
0
        config.validate()?;
575
0
        Ok(config)
576
0
    }
577
578
    /// Creates runtime configuration with environment-specific defaults.
579
    ///
580
    /// Does not read from environment variables. Useful for testing or
581
    /// when you want pure default values.
582
    ///
583
    /// # Arguments
584
    ///
585
    /// * `environment` - The deployment environment to use for defaults
586
0
    pub fn with_defaults(environment: Environment) -> Self {
587
0
        Self {
588
0
            environment,
589
0
            database: DatabaseRuntimeConfig::with_defaults(environment),
590
0
            cache: CacheRuntimeConfig::with_defaults(environment),
591
0
            timeouts: TimeoutConfig::with_defaults(environment),
592
0
            limits: LimitsConfig::with_defaults(environment),
593
0
        }
594
0
    }
595
596
    /// Validates the entire runtime configuration.
597
    ///
598
    /// # Errors
599
    ///
600
    /// Returns ConfigError if any configuration values are invalid.
601
0
    pub fn validate(&self) -> ConfigResult<()> {
602
0
        self.database.validate()?;
603
0
        self.cache.validate()?;
604
0
        self.timeouts.validate()?;
605
0
        self.limits.validate()?;
606
0
        Ok(())
607
0
    }
608
}
609
610
// Helper functions for parsing environment variables
611
612
0
fn parse_env_duration_ms(key: &str, default: Duration) -> ConfigResult<Duration> {
613
0
    match std::env::var(key) {
614
0
        Ok(val) => {
615
0
            let ms = val.parse::<u64>()
616
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
617
0
            Ok(Duration::from_millis(ms))
618
        }
619
0
        Err(_) => Ok(default),
620
    }
621
0
}
622
623
0
fn parse_env_duration_secs(key: &str, default: Duration) -> ConfigResult<Duration> {
624
0
    match std::env::var(key) {
625
0
        Ok(val) => {
626
0
            let secs = val.parse::<u64>()
627
0
                .map_err(|e| ConfigError::Invalid(format!("Invalid duration for {}: {}", key, e)))?;
628
0
            Ok(Duration::from_secs(secs))
629
        }
630
0
        Err(_) => Ok(default),
631
    }
632
0
}
633
634
0
fn parse_env_u32(key: &str, default: u32) -> ConfigResult<u32> {
635
0
    match std::env::var(key) {
636
0
        Ok(val) => val.parse::<u32>()
637
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u32 for {}: {}", key, e))),
638
0
        Err(_) => Ok(default),
639
    }
640
0
}
641
642
0
fn parse_env_u8(key: &str, default: u8) -> ConfigResult<u8> {
643
0
    match std::env::var(key) {
644
0
        Ok(val) => val.parse::<u8>()
645
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid u8 for {}: {}", key, e))),
646
0
        Err(_) => Ok(default),
647
    }
648
0
}
649
650
0
fn parse_env_usize(key: &str, default: usize) -> ConfigResult<usize> {
651
0
    match std::env::var(key) {
652
0
        Ok(val) => val.parse::<usize>()
653
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid usize for {}: {}", key, e))),
654
0
        Err(_) => Ok(default),
655
    }
656
0
}
657
658
0
fn parse_env_f32(key: &str, default: f32) -> ConfigResult<f32> {
659
0
    match std::env::var(key) {
660
0
        Ok(val) => val.parse::<f32>()
661
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f32 for {}: {}", key, e))),
662
0
        Err(_) => Ok(default),
663
    }
664
0
}
665
666
0
fn parse_env_f64(key: &str, default: f64) -> ConfigResult<f64> {
667
0
    match std::env::var(key) {
668
0
        Ok(val) => val.parse::<f64>()
669
0
            .map_err(|e| ConfigError::Invalid(format!("Invalid f64 for {}: {}", key, e))),
670
0
        Err(_) => Ok(default),
671
    }
672
0
}
673
674
#[cfg(test)]
675
mod tests {
676
    use super::*;
677
678
    #[test]
679
    fn test_environment_detection() {
680
        // Should default to Development
681
        let env = Environment::detect();
682
        assert!(matches!(env, Environment::Development | Environment::Production | Environment::Staging));
683
    }
684
685
    #[test]
686
    fn test_environment_is_production() {
687
        assert!(Environment::Production.is_production());
688
        assert!(!Environment::Development.is_production());
689
        assert!(!Environment::Staging.is_production());
690
    }
691
692
    #[test]
693
    fn test_environment_is_development() {
694
        assert!(Environment::Development.is_development());
695
        assert!(!Environment::Production.is_development());
696
        assert!(!Environment::Staging.is_development());
697
    }
698
699
    #[test]
700
    fn test_runtime_config_with_defaults() {
701
        let config = RuntimeConfig::with_defaults(Environment::Production);
702
        assert_eq!(config.environment, Environment::Production);
703
        assert!(config.database.query_timeout.as_millis() > 0);
704
        assert!(config.cache.position_ttl.as_secs() > 0);
705
    }
706
707
    #[test]
708
    fn test_runtime_config_validation() {
709
        let config = RuntimeConfig::with_defaults(Environment::Development);
710
        assert!(config.validate().is_ok());
711
    }
712
713
    #[test]
714
    fn test_database_config_defaults() {
715
        let dev_config = DatabaseRuntimeConfig::with_defaults(Environment::Development);
716
        let prod_config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
717
718
        // Production should have tighter timeouts
719
        assert!(prod_config.query_timeout < dev_config.query_timeout);
720
        assert!(prod_config.connection_timeout < dev_config.connection_timeout);
721
    }
722
723
    #[test]
724
    fn test_cache_config_defaults() {
725
        let dev_config = CacheRuntimeConfig::with_defaults(Environment::Development);
726
        let prod_config = CacheRuntimeConfig::with_defaults(Environment::Production);
727
728
        // Production should have shorter TTLs for HFT
729
        assert!(prod_config.position_ttl < dev_config.position_ttl);
730
        assert!(prod_config.var_ttl < dev_config.var_ttl);
731
    }
732
733
    #[test]
734
    fn test_timeout_config_defaults() {
735
        let dev_config = TimeoutConfig::with_defaults(Environment::Development);
736
        let prod_config = TimeoutConfig::with_defaults(Environment::Production);
737
738
        // Production should have tighter timeouts
739
        assert!(prod_config.grpc_request_timeout < dev_config.grpc_request_timeout);
740
        assert!(prod_config.grpc_connect_timeout < dev_config.grpc_connect_timeout);
741
    }
742
743
    #[test]
744
    fn test_limits_config_defaults() {
745
        let dev_config = LimitsConfig::with_defaults(Environment::Development);
746
        let prod_config = LimitsConfig::with_defaults(Environment::Production);
747
748
        // Production should have more aggressive settings
749
        assert!(prod_config.safety_check_timeout < dev_config.safety_check_timeout);
750
        assert!(prod_config.ml_inference_timeout < dev_config.ml_inference_timeout);
751
    }
752
753
    #[test]
754
    fn test_database_config_validation() {
755
        let mut config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
756
        assert!(config.validate().is_ok());
757
758
        config.query_timeout = Duration::from_millis(0);
759
        assert!(config.validate().is_err());
760
761
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
762
        config.pool_size = 0;
763
        assert!(config.validate().is_err());
764
765
        config = DatabaseRuntimeConfig::with_defaults(Environment::Production);
766
        config.pool_size = 200;
767
        config.max_pool_size = 100;
768
        assert!(config.validate().is_err());
769
    }
770
771
    #[test]
772
    fn test_cache_config_validation() {
773
        let mut config = CacheRuntimeConfig::with_defaults(Environment::Production);
774
        assert!(config.validate().is_ok());
775
776
        config.position_ttl = Duration::from_secs(0);
777
        assert!(config.validate().is_err());
778
    }
779
780
    #[test]
781
    fn test_limits_config_validation() {
782
        let mut config = LimitsConfig::with_defaults(Environment::Production);
783
        assert!(config.validate().is_ok());
784
785
        config.retry_max_attempts = 0;
786
        assert!(config.validate().is_err());
787
788
        config = LimitsConfig::with_defaults(Environment::Production);
789
        config.retry_backoff_multiplier = 0.5;
790
        assert!(config.validate().is_err());
791
792
        config = LimitsConfig::with_defaults(Environment::Production);
793
        config.risk_var_confidence = 1.5;
794
        assert!(config.validate().is_err());
795
    }
796
797
    #[test]
798
    fn test_staging_environment_defaults() {
799
        let config = RuntimeConfig::with_defaults(Environment::Staging);
800
801
        // Staging should be between dev and prod
802
        let dev_config = RuntimeConfig::with_defaults(Environment::Development);
803
        let prod_config = RuntimeConfig::with_defaults(Environment::Production);
804
805
        assert!(config.database.query_timeout > prod_config.database.query_timeout);
806
        assert!(config.database.query_timeout < dev_config.database.query_timeout);
807
    }
808
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html deleted file mode 100644 index 129841769..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/schemas.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/schemas.rs
Line
Count
Source
1
//! Configuration schemas and cloud storage configurations.
2
//!
3
//! This module defines configuration schemas for various cloud storage backends
4
//! and configuration versioning. Primarily focused on S3-compatible storage
5
//! for model artifacts and configuration management in the Foxhunt trading system.
6
7
use chrono::{DateTime, Utc};
8
use serde::{Deserialize, Serialize};
9
use std::collections::HashMap;
10
use std::time::Duration;
11
use uuid::Uuid;
12
13
/// Configuration schema metadata for versioning and tracking.
14
///
15
/// Provides versioning and audit trail information for configuration schemas.
16
/// Used to track configuration changes over time and maintain compatibility
17
/// across different versions of the trading system.
18
#[derive(Debug, Clone, Serialize, Deserialize)]
19
pub struct ConfigSchema {
20
    /// Unique identifier for this configuration schema
21
    pub id: Uuid,
22
    /// Semantic version string (e.g., "1.2.3")
23
    pub version: String,
24
    /// Timestamp when this schema was created
25
    pub created_at: DateTime<Utc>,
26
    /// Timestamp when this schema was last updated
27
    pub updated_at: DateTime<Utc>,
28
}
29
30
/// Amazon S3 and S3-compatible storage configuration.
31
///
32
/// Configures access to S3 or S3-compatible storage services for storing
33
/// ML model artifacts, configuration backups, and other binary data.
34
/// Supports various authentication methods and connection options.
35
#[derive(Debug, Clone, Serialize, Deserialize)]
36
pub struct S3Config {
37
    /// S3 bucket name for storing model artifacts and data
38
    pub bucket_name: String,
39
    /// AWS region or S3-compatible service region
40
    pub region: String,
41
    /// AWS access key ID (optional, can use IAM roles or environment variables)
42
    pub access_key_id: Option<String>,
43
    /// AWS secret access key (optional, can use IAM roles or environment variables)
44
    pub secret_access_key: Option<String>,
45
    /// AWS session token for temporary credentials (optional)
46
    pub session_token: Option<String>,
47
    /// Custom S3-compatible endpoint URL (e.g., MinIO, DigitalOcean Spaces)
48
    pub endpoint_url: Option<String>,
49
    /// Force path-style URLs instead of virtual-hosted-style URLs
50
    pub force_path_style: bool,
51
    /// Request timeout duration for S3 operations
52
    pub timeout: Duration,
53
    /// Maximum number of retry attempts for failed requests
54
    pub max_retry_attempts: u32,
55
    /// Enable SSL/TLS for S3 connections
56
    pub use_ssl: bool,
57
}
58
59
impl S3Config {
60
    /// Validates the S3 configuration for correctness.
61
    ///
62
    /// Performs validation checks on the S3 configuration to ensure all
63
    /// required fields are present and have valid values before attempting
64
    /// to establish connections to S3 services.
65
    ///
66
    /// # Errors
67
    ///
68
    /// Returns an error string if the configuration is invalid:
69
    /// - Empty bucket name
70
    /// - Empty region
71
    /// - Invalid endpoint URL format
72
0
    pub fn validate(&self) -> Result<(), String> {
73
0
        if self.bucket_name.is_empty() {
74
0
            return Err("S3 bucket name cannot be empty".to_string());
75
0
        }
76
0
        if self.region.is_empty() {
77
0
            return Err("S3 region cannot be empty".to_string());
78
0
        }
79
0
        Ok(())
80
0
    }
81
}
82
83
/// Schema-level asset classification configuration for sector and type categorization.
84
///
85
/// Provides configuration-driven asset classification that replaces hardcoded
86
/// symbol-based classification logic. Supports flexible categorization rules
87
/// based on instrument properties rather than specific symbol names.
88
///
89
/// **Note**: This is a simpler schema-level config. For full asset classification
90
/// with volatility profiles and pattern rules, use `structures::AssetClassificationConfig`.
91
#[derive(Debug, Clone, Serialize, Deserialize)]
92
pub struct AssetClassificationSchema {
93
    /// Classification rules based on asset type patterns
94
    pub asset_type_rules: HashMap<String, String>,
95
    /// Default classifications for different asset categories
96
    pub default_sectors: HashMap<String, String>,
97
    /// Regex patterns for currency pair detection
98
    pub currency_patterns: Vec<String>,
99
    /// Regex patterns for cryptocurrency detection
100
    pub crypto_patterns: Vec<String>,
101
}
102
103
impl AssetClassificationSchema {
104
    /// Creates a new asset classification schema with default rules.
105
0
    pub fn new() -> Self {
106
0
        let mut asset_type_rules = HashMap::new();
107
0
        asset_type_rules.insert("EQUITY".to_string(), "Equity".to_string());
108
0
        asset_type_rules.insert("FOREX".to_string(), "Currencies".to_string());
109
0
        asset_type_rules.insert("CRYPTO".to_string(), "Cryptocurrency".to_string());
110
0
        asset_type_rules.insert("COMMODITY".to_string(), "Commodities".to_string());
111
0
        asset_type_rules.insert("BOND".to_string(), "Fixed Income".to_string());
112
113
0
        let mut default_sectors = HashMap::new();
114
0
        default_sectors.insert("Equity".to_string(), "Other".to_string());
115
0
        default_sectors.insert("Currencies".to_string(), "Currencies".to_string());
116
0
        default_sectors.insert("Cryptocurrency".to_string(), "Cryptocurrency".to_string());
117
0
        default_sectors.insert("Commodities".to_string(), "Commodities".to_string());
118
0
        default_sectors.insert("Fixed Income".to_string(), "Fixed Income".to_string());
119
120
0
        Self {
121
0
            asset_type_rules,
122
0
            default_sectors,
123
0
            currency_patterns: vec![
124
0
                r"^[A-Z]{3}[A-Z]{3}$".to_string(), // USDEUR format
125
0
                r".*USD.*".to_string(),
126
0
                r".*EUR.*".to_string(),
127
0
                r".*GBP.*".to_string(),
128
0
                r".*JPY.*".to_string(),
129
0
            ],
130
0
            crypto_patterns: vec![
131
0
                r".*BTC.*".to_string(),
132
0
                r".*ETH.*".to_string(),
133
0
                r".*CRYPTO.*".to_string(),
134
0
            ],
135
0
        }
136
0
    }
137
138
    /// Classifies an instrument based on configuration rules.
139
0
    pub fn classify_sector(&self, instrument_id: &str, asset_type: Option<&str>) -> String {
140
        // First try to classify based on asset type if provided
141
0
        if let Some(asset_type) = asset_type {
142
0
            if let Some(sector) = self.asset_type_rules.get(asset_type) {
143
0
                return sector.clone();
144
0
            }
145
0
        }
146
147
        // Check for currency patterns
148
0
        for pattern in &self.currency_patterns {
149
0
            if let Ok(regex) = regex::Regex::new(pattern) {
150
0
                if regex.is_match(instrument_id) {
151
0
                    return "Currencies".to_string();
152
0
                }
153
0
            }
154
        }
155
156
        // Check for crypto patterns
157
0
        for pattern in &self.crypto_patterns {
158
0
            if let Ok(regex) = regex::Regex::new(pattern) {
159
0
                if regex.is_match(instrument_id) {
160
0
                    return "Cryptocurrency".to_string();
161
0
                }
162
0
            }
163
        }
164
165
        // Default classification
166
0
        "Other".to_string()
167
0
    }
168
}
169
170
impl Default for AssetClassificationSchema {
171
0
    fn default() -> Self {
172
0
        Self::new()
173
0
    }
174
}
175
176
impl Default for S3Config {
177
0
    fn default() -> Self {
178
0
        Self {
179
0
            bucket_name: "foxhunt-models".to_string(),
180
0
            region: "us-east-1".to_string(),
181
0
            access_key_id: None,
182
0
            secret_access_key: None,
183
0
            session_token: None,
184
0
            endpoint_url: None,
185
0
            force_path_style: false,
186
0
            timeout: Duration::from_secs(30),
187
0
            max_retry_attempts: 3,
188
0
            use_ssl: true,
189
0
        }
190
0
    }
191
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html deleted file mode 100644 index 58ab87688..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/storage_config.rs
Line
Count
Source
1
//! Model storage and metadata configuration structures.
2
//!
3
//! This module defines configuration structures for managing ML model metadata,
4
//! training metrics, and architectural information. Used for model versioning,
5
//! performance tracking, and deployment management in the Foxhunt trading system.
6
7
use chrono::{DateTime, Utc};
8
use serde::{Deserialize, Serialize};
9
use std::path::PathBuf;
10
use uuid::Uuid;
11
12
/// Comprehensive metadata for ML model storage and tracking.
13
///
14
/// Contains all information necessary for model identification, versioning,
15
/// and performance tracking. Used for model lifecycle management and
16
/// deployment coordination across the trading system.
17
#[derive(Debug, Clone, Serialize, Deserialize)]
18
pub struct ModelMetadata {
19
    /// Unique identifier for this model instance
20
    pub id: Uuid,
21
    /// Human-readable model name (e.g., "mamba2-price-prediction")
22
    pub name: String,
23
    /// Semantic version string (e.g., "1.2.3")
24
    pub version: String,
25
    /// Timestamp when this model was created/trained
26
    pub created_at: DateTime<Utc>,
27
    /// Timestamp when this model metadata was last updated
28
    pub updated_at: DateTime<Utc>,
29
    /// Training performance metrics for model evaluation
30
    pub training_metrics: TrainingMetrics,
31
    /// Model architecture and hyperparameter configuration
32
    pub architecture: ModelArchitecture,
33
}
34
35
/// Training performance metrics for model evaluation.
36
///
37
/// Captures key performance indicators from model training to enable
38
/// comparison between different model versions and architectures.
39
/// Essential for model selection and performance monitoring.
40
#[derive(Debug, Clone, Serialize, Deserialize)]
41
pub struct TrainingMetrics {
42
    /// Final training accuracy (0.0 to 1.0)
43
    pub accuracy: f64,
44
    /// Final training loss value
45
    pub loss: f64,
46
    /// Final validation accuracy (0.0 to 1.0)
47
    pub validation_accuracy: f64,
48
    /// Final validation loss value
49
    pub validation_loss: f64,
50
    /// Number of training epochs completed
51
    pub epochs: u32,
52
    /// Total training time in seconds
53
    pub training_time_seconds: f64,
54
}
55
56
/// Model architecture and hyperparameter specification.
57
///
58
/// Defines the structural configuration of ML models including layer
59
/// dimensions, activation functions, and optimization parameters.
60
/// Used for model reconstruction and hyperparameter tracking.
61
#[derive(Debug, Clone, Serialize, Deserialize)]
62
pub struct ModelArchitecture {
63
    /// Model type identifier (e.g., "mamba2", "transformer", "dqn")
64
    pub model_type: String,
65
    /// Input feature dimension size
66
    pub input_dim: usize,
67
    /// Output prediction dimension size
68
    pub output_dim: usize,
69
    /// Hidden layer sizes in order from input to output
70
    pub hidden_layers: Vec<usize>,
71
    /// Activation function name (e.g., "relu", "gelu", "swish")
72
    pub activation: String,
73
    /// Optimizer type (e.g., "adam", "sgd", "adamw")
74
    pub optimizer: String,
75
    /// Learning rate used during training
76
    pub learning_rate: f64,
77
}
78
79
/// Storage configuration for model artifacts
80
#[derive(Debug, Clone, Serialize, Deserialize)]
81
pub struct StorageConfig {
82
    /// Storage type (e.g., "local", "s3")
83
    pub storage_type: String,
84
    /// Local base path for file storage (required for "local" storage type)
85
    pub local_base_path: Option<PathBuf>,
86
    /// Enable compression for stored models
87
    pub enable_compression: bool,
88
}
89
90
impl Default for StorageConfig {
91
0
    fn default() -> Self {
92
0
        Self {
93
0
            storage_type: "local".to_string(),
94
0
            local_base_path: Some(PathBuf::from("/tmp/foxhunt/models")),
95
0
            enable_compression: false,
96
0
        }
97
0
    }
98
}
99
100
impl StorageConfig {
101
    /// Create StorageConfig from environment variables
102
0
    pub fn from_env() -> Result<Self, Box<dyn std::error::Error>> {
103
0
        let storage_type = std::env::var("STORAGE_TYPE").unwrap_or_else(|_| "local".to_string());
104
0
        let local_base_path = std::env::var("STORAGE_LOCAL_PATH")
105
0
            .ok()
106
0
            .map(PathBuf::from)
107
0
            .or_else(|| Some(PathBuf::from("/tmp/foxhunt/models")));
108
0
        let enable_compression = std::env::var("STORAGE_ENABLE_COMPRESSION")
109
0
            .ok()
110
0
            .and_then(|v| v.parse().ok())
111
0
            .unwrap_or(false);
112
113
0
        Ok(Self {
114
0
            storage_type,
115
0
            local_base_path,
116
0
            enable_compression,
117
0
        })
118
0
    }
119
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html deleted file mode 100644 index bb24db6d2..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/structures.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/structures.rs
Line
Count
Source
1
//! Configuration structures
2
3
use rust_decimal::Decimal;
4
use serde::{Deserialize, Serialize};
5
use std::collections::HashMap;
6
7
#[derive(Debug, Clone, Serialize, Deserialize)]
8
pub struct RiskConfig {
9
    /// Maximum single position size in base currency
10
    pub max_position_size: Decimal,
11
    /// Maximum total portfolio exposure in base currency
12
    pub max_portfolio_exposure: Decimal,
13
    /// Maximum concentration percentage for a single position (0.0-1.0)
14
    pub max_concentration_pct: Decimal,
15
    /// Maximum daily loss threshold in base currency
16
    pub max_daily_loss: Decimal,
17
    /// Maximum drawdown percentage allowed (0.0-1.0)
18
    pub max_drawdown_pct: Decimal,
19
    /// Stop loss threshold in base currency
20
    pub stop_loss_threshold: Decimal,
21
    /// VaR confidence level (e.g., 0.95 for 95%)
22
    pub var_confidence_level: f64,
23
    /// VaR time horizon in days
24
    pub var_time_horizon: u32,
25
    /// 1-day VaR limit in base currency
26
    pub var_limit_1d: Decimal,
27
    /// 10-day VaR limit in base currency
28
    pub var_limit_10d: Decimal,
29
    /// Maximum single order size in base currency
30
    pub max_order_size: Decimal,
31
    /// Maximum orders per second (rate limiting)
32
    pub max_orders_per_second: u64,
33
    /// Maximum notional value per hour in base currency
34
    pub max_notional_per_hour: Decimal,
35
    /// Kelly criterion fraction limit (0.0-1.0)
36
    pub kelly_fraction_limit: f64,
37
    /// Maximum Kelly criterion position size (0.0-1.0)
38
    pub max_kelly_position_size: f64,
39
    /// Emergency stop threshold as fraction of capital (0.0-1.0)
40
    pub emergency_stop_threshold: f64,
41
    /// VaR configuration
42
    pub var_config: VarConfig,
43
    /// Circuit breaker configuration
44
    pub circuit_breaker: CircuitBreakerConfig,
45
    /// Position limits configuration
46
    pub position_limits: PositionLimitsConfig,
47
    /// Asset classification configuration
48
    pub asset_classification: crate::schemas::AssetClassificationSchema,
49
}
50
51
impl Default for RiskConfig {
52
0
    fn default() -> Self {
53
0
        Self {
54
0
            // Position and exposure limits
55
0
            max_position_size: Decimal::new(1_000_000, 0), // $1M max single position
56
0
            max_portfolio_exposure: Decimal::new(10_000_000, 0), // $10M total portfolio exposure
57
0
            max_concentration_pct: Decimal::new(25, 2), // 25% max concentration
58
0
            
59
0
            // Loss and drawdown limits
60
0
            max_daily_loss: Decimal::new(100_000, 0), // $100K max daily loss
61
0
            max_drawdown_pct: Decimal::new(15, 2), // 15% max drawdown
62
0
            stop_loss_threshold: Decimal::new(50_000, 0), // $50K stop loss threshold
63
0
            
64
0
            // VaR configuration
65
0
            var_confidence_level: 0.95, // 95% confidence
66
0
            var_time_horizon: 1, // 1-day horizon
67
0
            var_limit_1d: Decimal::new(50_000, 0), // $50K 1-day VaR limit
68
0
            var_limit_10d: Decimal::new(150_000, 0), // $150K 10-day VaR limit
69
0
            
70
0
            // Order limits and rate limiting
71
0
            max_order_size: Decimal::new(100_000, 0), // $100K max order size
72
0
            max_orders_per_second: 100, // 100 orders/sec
73
0
            max_notional_per_hour: Decimal::new(10_000_000, 0), // $10M hourly notional
74
0
            
75
0
            // Kelly criterion parameters
76
0
            kelly_fraction_limit: 0.25, // 25% Kelly fraction limit
77
0
            max_kelly_position_size: 0.20, // 20% max Kelly position
78
0
            
79
0
            // Emergency stop
80
0
            emergency_stop_threshold: 0.10, // 10% loss triggers emergency stop
81
0
            
82
0
            // Nested configurations
83
0
            var_config: VarConfig::default(),
84
0
            circuit_breaker: CircuitBreakerConfig::default(),
85
0
            position_limits: PositionLimitsConfig::default(),
86
0
            asset_classification: crate::schemas::AssetClassificationSchema::default(),
87
0
        }
88
0
    }
89
}
90
91
#[derive(Debug, Clone, Serialize, Deserialize)]
92
pub struct VarConfig {
93
    /// VaR confidence level (0.0-1.0)
94
    pub confidence_level: f64,
95
    /// Time horizon in days
96
    pub time_horizon_days: u32,
97
    /// Historical lookback period in days
98
    pub lookback_period_days: u32,
99
    /// Calculation method (e.g., "historical", "monte_carlo")
100
    pub calculation_method: String,
101
    /// Maximum VaR limit
102
    pub max_var_limit: f64,
103
}
104
105
impl Default for VarConfig {
106
0
    fn default() -> Self {
107
0
        Self {
108
0
            confidence_level: 0.95,
109
0
            time_horizon_days: 1,
110
0
            lookback_period_days: 252,
111
0
            calculation_method: "historical".to_string(),
112
0
            max_var_limit: 100_000.0,
113
0
        }
114
0
    }
115
}
116
117
#[derive(Debug, Clone, Serialize, Deserialize)]
118
pub struct KellyConfig {
119
    pub kelly_fraction: f64,
120
    pub max_kelly_leverage: f64,
121
    pub min_kelly_leverage: f64,
122
    pub confidence_threshold: f64,
123
    pub lookback_periods: usize,
124
    pub default_position_fraction: f64,
125
    pub enabled: bool,
126
    pub fractional_kelly: f64,
127
    pub min_kelly_fraction: f64,
128
    pub max_kelly_fraction: f64,
129
}
130
131
impl Default for KellyConfig {
132
37
    fn default() -> Self {
133
37
        Self {
134
37
            kelly_fraction: 0.25,
135
37
            max_kelly_leverage: 2.0,
136
37
            min_kelly_leverage: 0.1,
137
37
            confidence_threshold: 0.95,
138
37
            lookback_periods: 252,
139
37
            default_position_fraction: 0.02,
140
37
            enabled: true,
141
37
            fractional_kelly: 0.5,
142
37
            min_kelly_fraction: 0.01,
143
37
            max_kelly_fraction: 0.5,
144
37
        }
145
37
    }
146
}
147
148
#[derive(Debug, Clone, Serialize, Deserialize)]
149
pub struct CircuitBreakerConfig {
150
    /// Enable circuit breaker
151
    pub enabled: bool,
152
    /// Price movement threshold to trigger halt (0.0-1.0)
153
    pub price_move_threshold: f64,
154
    /// Duration to halt trading in seconds
155
    pub halt_duration_seconds: u64,
156
}
157
158
impl Default for CircuitBreakerConfig {
159
0
    fn default() -> Self {
160
0
        Self {
161
0
            enabled: true,
162
0
            price_move_threshold: 0.05, // 5% price move
163
0
            halt_duration_seconds: 300, // 5 minutes
164
0
        }
165
0
    }
166
}
167
168
#[derive(Debug, Clone, Serialize, Deserialize)]
169
pub struct PositionLimitsConfig {
170
    /// Global position limit
171
    pub global_limit: f64,
172
    /// Maximum leverage allowed
173
    pub max_leverage: f64,
174
    /// Maximum VaR limit
175
    pub max_var_limit: f64,
176
}
177
178
impl Default for PositionLimitsConfig {
179
0
    fn default() -> Self {
180
0
        Self {
181
0
            global_limit: 10_000_000.0,
182
0
            max_leverage: 3.0,
183
0
            max_var_limit: 100_000.0,
184
0
        }
185
0
    }
186
}
187
188
/// Broker configuration for order routing and execution
189
#[derive(Debug, Clone, Serialize, Deserialize)]
190
pub struct BrokerConfig {
191
    /// Broker routing rules based on symbol patterns and sizes
192
    pub routing_rules: Vec<BrokerRoutingRule>,
193
    /// Default broker when no rules match
194
    pub default_broker: String,
195
    /// Commission rates by broker
196
    pub commission_rates: HashMap<String, CommissionConfig>,
197
}
198
199
/// Rule for routing orders to specific brokers
200
#[derive(Debug, Clone, Serialize, Deserialize)]
201
pub struct BrokerRoutingRule {
202
    /// Priority (higher numbers take precedence)
203
    pub priority: u32,
204
    /// Symbol pattern (regex)
205
    pub symbol_pattern: String,
206
    /// Minimum quantity for this rule
207
    pub min_quantity: Option<f64>,
208
    /// Maximum quantity for this rule
209
    pub max_quantity: Option<f64>,
210
    /// Target broker ID
211
    pub broker_id: String,
212
    /// Rule description for debugging
213
    pub description: String,
214
}
215
216
/// Commission configuration per broker
217
#[derive(Debug, Clone, Serialize, Deserialize)]
218
pub struct CommissionConfig {
219
    /// Commission rate (basis points, e.g., 0.00007 = 0.7 bps)
220
    pub rate_bps: f64,
221
    /// Minimum commission per trade
222
    pub min_commission: f64,
223
}
224
225
impl Default for BrokerConfig {
226
0
    fn default() -> Self {
227
0
        let mut commission_rates = HashMap::new();
228
229
0
        commission_rates.insert(
230
0
            "ICMARKETS".to_string(),
231
0
            CommissionConfig {
232
0
                rate_bps: 0.00007, // 0.7 bps
233
0
                min_commission: 0.0,
234
0
            },
235
        );
236
237
0
        commission_rates.insert(
238
0
            "IBKR".to_string(),
239
0
            CommissionConfig {
240
0
                rate_bps: 0.00005, // 0.5 bps
241
0
                min_commission: 1.0,
242
0
            },
243
        );
244
245
0
        let routing_rules = vec![
246
0
            BrokerRoutingRule {
247
0
                priority: 100,
248
0
                symbol_pattern: r"^(BTC|ETH).*".to_string(),
249
0
                min_quantity: None,
250
0
                max_quantity: None,
251
0
                broker_id: "ICMARKETS".to_string(),
252
0
                description: "Route all crypto symbols to ICMarkets".to_string(),
253
0
            },
254
0
            BrokerRoutingRule {
255
0
                priority: 90,
256
0
                symbol_pattern: r".*USD$".to_string(),
257
0
                min_quantity: None,
258
0
                max_quantity: Some(1_000_000.0),
259
0
                broker_id: "ICMARKETS".to_string(),
260
0
                description: "Route smaller USD pairs to ICMarkets".to_string(),
261
0
            },
262
0
            BrokerRoutingRule {
263
0
                priority: 50,
264
0
                symbol_pattern: r".*".to_string(), // Catch-all
265
0
                min_quantity: None,
266
0
                max_quantity: None,
267
0
                broker_id: "IBKR".to_string(),
268
0
                description: "Default routing to IBKR".to_string(),
269
0
            },
270
        ];
271
272
0
        Self {
273
0
            routing_rules,
274
0
            default_broker: "IBKR".to_string(),
275
0
            commission_rates,
276
0
        }
277
0
    }
278
}
279
280
impl BrokerConfig {
281
    /// Select optimal broker based on symbol and quantity using routing rules
282
0
    pub fn select_broker(&self, symbol: &str, quantity: f64) -> String {
283
0
        let symbol_upper = symbol.to_uppercase();
284
285
        // Sort rules by priority (highest first)
286
0
        let mut applicable_rules: Vec<_> = self
287
0
            .routing_rules
288
0
            .iter()
289
0
            .filter(|rule| {
290
                // Check symbol pattern
291
0
                let symbol_matches = if let Ok(regex) = regex::Regex::new(&rule.symbol_pattern) {
292
0
                    regex.is_match(&symbol_upper)
293
                } else {
294
0
                    false
295
                };
296
297
                // Check quantity bounds
298
0
                let quantity_matches = {
299
0
                    let min_ok = rule.min_quantity.map_or(true, |min| quantity >= min);
300
0
                    let max_ok = rule.max_quantity.map_or(true, |max| quantity <= max);
301
0
                    min_ok && max_ok
302
                };
303
304
0
                symbol_matches && quantity_matches
305
0
            })
306
0
            .collect();
307
308
0
        applicable_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
309
310
0
        if let Some(rule) = applicable_rules.first() {
311
0
            rule.broker_id.clone()
312
        } else {
313
0
            self.default_broker.clone()
314
        }
315
0
    }
316
317
    /// Calculate commission for a given broker and notional value
318
0
    pub fn calculate_commission(&self, broker_id: &str, notional: f64) -> f64 {
319
0
        if let Some(config) = self.commission_rates.get(broker_id) {
320
0
            (notional * config.rate_bps).max(config.min_commission)
321
        } else {
322
            // Default commission if broker not found
323
0
            notional * 0.0001 // 1 bps
324
        }
325
0
    }
326
}
327
328
/// Asset classification for risk management and volatility profiling
329
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
330
pub enum AssetClass {
331
    /// Equity securities and stocks
332
    Equities,
333
    /// Bonds and fixed income securities
334
    FixedIncome,
335
    /// Physical and financial commodities
336
    Commodities,
337
    /// Foreign exchange and currencies
338
    Currencies,
339
    /// Alternative investments
340
    Alternatives,
341
    /// Derivative instruments
342
    Derivatives,
343
    /// Cash and cash equivalents
344
    Cash,
345
}
346
347
/// Volatility and risk profile for an asset class
348
#[derive(Debug, Clone, Serialize, Deserialize)]
349
pub struct VolatilityProfile {
350
    /// Annual volatility (0.0 to 1.0, e.g., 0.25 = 25%)
351
    pub annual_volatility: f64,
352
    /// Maximum position size as fraction of portfolio (0.0 to 1.0)
353
    pub max_position_fraction: f64,
354
    /// Volatility threshold for risk alerts (0.0 to 1.0)
355
    pub volatility_threshold: f64,
356
    /// Maximum daily loss threshold (0.0 to 1.0)
357
    pub daily_loss_threshold: f64,
358
}
359
360
/// Asset classification configuration with symbol mappings and volatility profiles
361
#[derive(Debug, Clone, Serialize, Deserialize)]
362
pub struct AssetClassificationConfig {
363
    /// Explicit symbol to asset class mappings
364
    pub symbol_mappings: HashMap<String, AssetClass>,
365
    /// Volatility profiles for each asset class
366
    pub volatility_profiles: HashMap<AssetClass, VolatilityProfile>,
367
    /// Pattern-based classification rules (regex patterns)
368
    pub pattern_rules: Vec<PatternRule>,
369
}
370
371
/// Pattern-based rule for asset classification
372
#[derive(Debug, Clone, Serialize, Deserialize)]
373
pub struct PatternRule {
374
    /// Regex pattern to match against symbol
375
    pub pattern: String,
376
    /// Asset class to assign if pattern matches
377
    pub asset_class: AssetClass,
378
    /// Priority (higher numbers take precedence)
379
    pub priority: u32,
380
}
381
382
/// Encryption configuration for secure model storage
383
#[derive(Debug, Clone, Serialize, Deserialize)]
384
pub struct EncryptionConfig {
385
    /// Enable/disable encryption for model storage
386
    pub enable_encryption: bool,
387
    /// Encryption algorithm (e.g., "AES-256-GCM")
388
    pub algorithm: String,
389
    /// Key rotation period in days
390
    pub key_rotation_days: u64,
391
    /// Vault path for encryption keys (optional, can use local keys)
392
    pub encryption_keys_vault_path: Option<String>,
393
    /// Local key file path for development/testing
394
    pub local_key_file: Option<String>,
395
}
396
397
impl Default for EncryptionConfig {
398
0
    fn default() -> Self {
399
0
        Self {
400
0
            enable_encryption: false,
401
0
            algorithm: "AES-256-GCM".to_string(),
402
0
            key_rotation_days: 90,
403
0
            encryption_keys_vault_path: None,
404
0
            local_key_file: None,
405
0
        }
406
0
    }
407
}
408
409
impl Default for AssetClassificationConfig {
410
35
    fn default() -> Self {
411
35
        let mut symbol_mappings = HashMap::new();
412
413
        // Equity stocks
414
350
        for symbol in [
415
35
            "AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "JPM", "JNJ", "V",
416
350
        ] {
417
350
            symbol_mappings.insert(symbol.to_string(), AssetClass::Equities);
418
350
        }
419
420
        // Major cryptocurrencies
421
210
        for symbol in ["BTC", 
"ETH"35
,
"BTCUSD"35
,
"ETHUSD"35
,
"BTCUSDT"35
,
"ETHUSDT"35
] {
422
210
            symbol_mappings.insert(symbol.to_string(), AssetClass::Alternatives);
423
210
        }
424
425
35
        let mut volatility_profiles = HashMap::new();
426
427
35
        volatility_profiles.insert(
428
35
            AssetClass::Equities,
429
35
            VolatilityProfile {
430
35
                annual_volatility: 0.25,
431
35
                max_position_fraction: 0.20,
432
35
                volatility_threshold: 0.025,
433
35
                daily_loss_threshold: 0.03,
434
35
            },
435
        );
436
437
35
        volatility_profiles.insert(
438
35
            AssetClass::Alternatives,
439
35
            VolatilityProfile {
440
35
                annual_volatility: 0.80,
441
35
                max_position_fraction: 0.08,
442
35
                volatility_threshold: 0.15,
443
35
                daily_loss_threshold: 0.05,
444
35
            },
445
        );
446
447
35
        volatility_profiles.insert(
448
35
            AssetClass::Currencies,
449
35
            VolatilityProfile {
450
35
                annual_volatility: 0.15,
451
35
                max_position_fraction: 0.30,
452
35
                volatility_threshold: 0.02,
453
35
                daily_loss_threshold: 0.02,
454
35
            },
455
        );
456
457
35
        volatility_profiles.insert(
458
35
            AssetClass::Cash,
459
35
            VolatilityProfile {
460
35
                annual_volatility: 0.01,
461
35
                max_position_fraction: 1.00,
462
35
                volatility_threshold: 0.001,
463
35
                daily_loss_threshold: 0.001,
464
35
            },
465
        );
466
467
35
        volatility_profiles.insert(
468
35
            AssetClass::FixedIncome,
469
35
            VolatilityProfile {
470
35
                annual_volatility: 0.25,
471
35
                max_position_fraction: 0.15,
472
35
                volatility_threshold: 0.03,
473
35
                daily_loss_threshold: 0.025,
474
35
            },
475
        );
476
477
35
        volatility_profiles.insert(
478
35
            AssetClass::Derivatives,
479
35
            VolatilityProfile {
480
35
                annual_volatility: 0.40,
481
35
                max_position_fraction: 0.10,
482
35
                volatility_threshold: 0.05,
483
35
                daily_loss_threshold: 0.04,
484
35
            },
485
        );
486
487
35
        volatility_profiles.insert(
488
35
            AssetClass::Commodities,
489
35
            VolatilityProfile {
490
35
                annual_volatility: 0.30,
491
35
                max_position_fraction: 0.15,
492
35
                volatility_threshold: 0.04,
493
35
                daily_loss_threshold: 0.03,
494
35
            },
495
        );
496
497
35
        let pattern_rules = vec![
498
35
            PatternRule {
499
35
                pattern: r"^(BTC|ETH).*".to_string(),
500
35
                asset_class: AssetClass::Alternatives,
501
35
                priority: 100,
502
35
            },
503
35
            PatternRule {
504
35
                pattern: r".*USD$".to_string(),
505
35
                asset_class: AssetClass::Currencies,
506
35
                priority: 80,
507
35
            },
508
35
            PatternRule {
509
35
                pattern: r".*JPY$".to_string(),
510
35
                asset_class: AssetClass::Currencies,
511
35
                priority: 90,
512
35
            },
513
35
            PatternRule {
514
35
                pattern: r"^[A-Z]{3,6}$".to_string(), // 3-6 letter symbols (likely equities)
515
35
                asset_class: AssetClass::Equities,
516
35
                priority: 50,
517
35
            },
518
        ];
519
520
35
        Self {
521
35
            symbol_mappings,
522
35
            volatility_profiles,
523
35
            pattern_rules,
524
35
        }
525
35
    }
526
}
527
528
impl AssetClassificationConfig {
529
    /// Classify a symbol based on explicit mappings and pattern rules
530
0
    pub fn classify_symbol(&self, symbol: &str) -> AssetClass {
531
0
        let symbol_upper = symbol.to_uppercase();
532
533
        // First check explicit mappings
534
0
        if let Some(asset_class) = self.symbol_mappings.get(&symbol_upper) {
535
0
            return asset_class.clone();
536
0
        }
537
538
        // Then check pattern rules (sorted by priority, highest first)
539
0
        let mut applicable_rules: Vec<_> = self
540
0
            .pattern_rules
541
0
            .iter()
542
0
            .filter(|rule| {
543
0
                if let Ok(regex) = regex::Regex::new(&rule.pattern) {
544
0
                    regex.is_match(&symbol_upper)
545
                } else {
546
0
                    false
547
                }
548
0
            })
549
0
            .collect();
550
551
0
        applicable_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
552
553
0
        if let Some(rule) = applicable_rules.first() {
554
0
            rule.asset_class.clone()
555
        } else {
556
0
            AssetClass::Cash // Default fallback for unknown symbols
557
        }
558
0
    }
559
560
    /// Get volatility profile for a symbol
561
0
    pub fn get_volatility_profile(&self, symbol: &str) -> VolatilityProfile {
562
0
        let asset_class = self.classify_symbol(symbol);
563
0
        self.volatility_profiles
564
0
            .get(&asset_class)
565
0
            .cloned()
566
0
            .unwrap_or(VolatilityProfile {
567
0
                annual_volatility: 0.20,
568
0
                max_position_fraction: 0.05,
569
0
                volatility_threshold: 0.02,
570
0
                daily_loss_threshold: 0.01,
571
0
            })
572
0
    }
573
574
    /// Get daily volatility for a symbol
575
0
    pub fn get_daily_volatility(&self, symbol: &str) -> f64 {
576
0
        let profile = self.get_volatility_profile(symbol);
577
0
        profile.annual_volatility / 252.0_f64.sqrt()
578
0
    }
579
580
    /// Get risk configuration tuple (position_fraction, volatility_threshold, daily_loss_threshold)
581
0
    pub fn get_risk_config(&self, symbol: &str) -> (f64, f64, f64) {
582
0
        let profile = self.get_volatility_profile(symbol);
583
0
        (
584
0
            profile.max_position_fraction,
585
0
            profile.volatility_threshold,
586
0
            profile.daily_loss_threshold,
587
0
        )
588
0
    }
589
}
590
591
/// Configuration for backtesting database connections
592
#[derive(Debug, Clone, Serialize, Deserialize)]
593
pub struct BacktestingDatabaseConfig {
594
    /// Database connection URL
595
    pub database_url: String,
596
    /// Maximum number of database connections in the pool
597
    pub max_connections: Option<u32>,
598
    /// Minimum number of database connections in the pool
599
    pub min_connections: Option<u32>,
600
    /// Timeout in milliseconds for acquiring a connection
601
    pub acquire_timeout_ms: Option<u64>,
602
    /// Statement cache capacity
603
    pub statement_cache_capacity: Option<usize>,
604
    /// Enable SQL query logging
605
    pub enable_logging: Option<bool>,
606
}
607
608
/// Configuration for backtesting strategy execution
609
#[derive(Debug, Clone, Serialize, Deserialize)]
610
pub struct BacktestingStrategyConfig {
611
    /// Commission rate for trades (e.g., 0.001 = 0.1%)
612
    pub commission_rate: f64,
613
    /// Slippage rate for trades (e.g., 0.0005 = 0.05%)
614
    pub slippage_rate: f64,
615
    /// Maximum position size as fraction of portfolio
616
    pub max_position_size: Option<f64>,
617
    /// Enable short selling
618
    pub allow_short_selling: Option<bool>,
619
}
620
621
impl Default for BacktestingStrategyConfig {
622
0
    fn default() -> Self {
623
0
        Self {
624
0
            commission_rate: 0.0007,      // 0.07% = 7 bps
625
0
            slippage_rate: 0.0002,        // 0.02% = 2 bps
626
0
            max_position_size: Some(0.2), // 20% max position
627
0
            allow_short_selling: Some(false),
628
0
        }
629
0
    }
630
}
631
632
/// Configuration for backtesting performance analysis
633
#[derive(Debug, Clone, Serialize, Deserialize)]
634
pub struct BacktestingPerformanceConfig {
635
    /// Risk-free rate for Sharpe ratio calculations (annual rate)
636
    pub risk_free_rate: f64,
637
    /// Resolution for equity curve (number of points)
638
    pub equity_curve_resolution: usize,
639
    /// Enable advanced performance metrics
640
    pub enable_advanced_metrics: Option<bool>,
641
}
642
643
impl Default for BacktestingPerformanceConfig {
644
0
    fn default() -> Self {
645
0
        Self {
646
0
            risk_free_rate: 0.04, // 4% annual risk-free rate
647
0
            equity_curve_resolution: 1000,
648
0
            enable_advanced_metrics: Some(true),
649
0
        }
650
0
    }
651
}
652
653
/// TLS/SSL configuration for secure gRPC connections
654
#[derive(Debug, Clone, Serialize, Deserialize)]
655
pub struct TlsConfig {
656
    /// Enable/disable TLS for gRPC connections
657
    pub enabled: bool,
658
    /// Path to server certificate file
659
    pub cert_path: String,
660
    /// Path to server private key file
661
    pub key_path: String,
662
    /// Path to CA certificate for client verification (optional)
663
    pub ca_cert_path: Option<String>,
664
    /// Require client certificate verification
665
    pub require_client_cert: bool,
666
    /// TLS protocol versions to support (e.g., ["TLSv1.2", "TLSv1.3"])
667
    pub protocol_versions: Vec<String>,
668
    /// Cipher suites to use (empty means default)
669
    pub cipher_suites: Vec<String>,
670
}
671
672
impl Default for TlsConfig {
673
0
    fn default() -> Self {
674
        // Wave 75 Fix: Use environment variables with fallback to /tmp instead of /etc
675
0
        let cert_path = std::env::var("TLS_CERT_PATH")
676
0
            .unwrap_or_else(|_| "/tmp/foxhunt/certs/server.crt".to_string());
677
0
        let key_path = std::env::var("TLS_KEY_PATH")
678
0
            .unwrap_or_else(|_| "/tmp/foxhunt/certs/server.key".to_string());
679
0
        let ca_cert_path = std::env::var("TLS_CA_PATH").ok();
680
681
0
        Self {
682
0
            enabled: false,
683
0
            cert_path,
684
0
            key_path,
685
0
            ca_cert_path,
686
0
            require_client_cert: false,
687
0
            protocol_versions: vec!["TLSv1.3".to_string()],
688
0
            cipher_suites: Vec::new(),
689
0
        }
690
0
    }
691
}
692
693
/// Trading system configuration
694
#[derive(Debug, Clone, Serialize, Deserialize)]
695
pub struct TradingConfig {
696
    /// Maximum order size (in base units)
697
    pub max_order_size: f64,
698
    /// Minimum order size (in base units)
699
    pub min_order_size: f64,
700
    /// Maximum price deviation from market (as fraction, e.g., 0.05 = 5%)
701
    pub max_price_deviation: f64,
702
    /// Enable symbol validation
703
    pub enable_symbol_validation: bool,
704
    /// Maximum batch notional value (total value of orders in a batch)
705
    pub max_batch_notional: f64,
706
    /// Maximum position VaR (Value at Risk) limit
707
    pub max_position_var: f64,
708
}
709
710
impl Default for TradingConfig {
711
0
    fn default() -> Self {
712
0
        Self {
713
0
            max_order_size: 1_000_000.0,
714
0
            min_order_size: 0.001,
715
0
            max_price_deviation: 0.05,
716
0
            enable_symbol_validation: false,
717
0
            max_batch_notional: 10_000_000.0, // $10M batch limit
718
0
            max_position_var: 50_000.0,        // $50K VaR limit
719
0
        }
720
0
    }
721
}
722
723
/// Market data ingestion configuration
724
#[derive(Debug, Clone, Serialize, Deserialize)]
725
pub struct MarketDataConfig {
726
    /// Market data server host
727
    pub host: String,
728
    /// WebSocket port for streaming data
729
    pub websocket_port: u16,
730
    /// API key for authentication
731
    pub api_key: String,
732
    /// Use SSL/TLS for connections
733
    pub use_ssl: bool,
734
    /// Connection timeout in seconds
735
    pub timeout_seconds: u64,
736
}
737
738
impl Default for MarketDataConfig {
739
0
    fn default() -> Self {
740
0
        Self {
741
0
            host: "localhost".to_string(),
742
0
            websocket_port: 8080,
743
0
            api_key: String::new(),
744
0
            use_ssl: false,
745
0
            timeout_seconds: 30,
746
0
        }
747
0
    }
748
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html deleted file mode 100644 index 54dc1c09a..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/symbol_config.rs
Line
Count
Source
1
//! Symbol classification and configuration management for trading instruments.
2
//!
3
//! This module provides comprehensive symbol classification and configuration
4
//! management for various financial instruments in the Foxhunt HFT trading system.
5
//! It handles asset classification, volatility profiles, trading hours, and
6
//! market-specific parameters for optimal trading execution.
7
8
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc, Weekday};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashMap;
11
use std::time::Duration;
12
use uuid::Uuid;
13
14
/// Asset classification enumeration for different financial instrument types.
15
///
16
/// Provides standardized classification for all tradeable instruments,
17
/// enabling type-specific risk management, execution logic, and regulatory
18
/// compliance across different asset classes.
19
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20
pub enum AssetClassification {
21
    /// Equity securities (stocks, ADRs, REITs)
22
    Equity,
23
    /// Futures contracts (commodities, financials, indices)
24
    Future,
25
    /// Foreign exchange pairs (major, minor, exotic)
26
    Forex,
27
    /// Cryptocurrency and digital assets
28
    Crypto,
29
    /// Physical commodities (metals, energy, agriculture)
30
    Commodity,
31
    /// Fixed income securities (bonds, notes, bills)
32
    FixedIncome,
33
    /// Options contracts (equity, index, commodity options)
34
    Option,
35
    /// Exchange-traded funds and products
36
    Etf,
37
    /// Indices and benchmark instruments
38
    Index,
39
    /// Structured products and derivatives
40
    Derivative,
41
}
42
43
impl AssetClassification {
44
    /// Returns the regulatory classification for compliance purposes.
45
0
    pub fn regulatory_class(&self) -> &'static str {
46
0
        match self {
47
0
            AssetClassification::Equity => "EQUITY",
48
0
            AssetClassification::Future => "FUTURE",
49
0
            AssetClassification::Forex => "FX",
50
0
            AssetClassification::Crypto => "CRYPTO",
51
0
            AssetClassification::Commodity => "COMMODITY",
52
0
            AssetClassification::FixedIncome => "FIXED_INCOME",
53
0
            AssetClassification::Option => "OPTION",
54
0
            AssetClassification::Etf => "ETF",
55
0
            AssetClassification::Index => "INDEX",
56
0
            AssetClassification::Derivative => "DERIVATIVE",
57
        }
58
0
    }
59
60
    /// Returns whether this asset class requires T+1 settlement.
61
0
    pub fn requires_t_plus_one_settlement(&self) -> bool {
62
0
        matches!(self, AssetClassification::Equity | AssetClassification::Etf)
63
0
    }
64
65
    /// Returns whether this asset class supports after-hours trading.
66
0
    pub fn supports_extended_hours(&self) -> bool {
67
0
        matches!(
68
0
            self,
69
            AssetClassification::Equity
70
                | AssetClassification::Etf
71
                | AssetClassification::Forex
72
                | AssetClassification::Crypto
73
        )
74
0
    }
75
}
76
77
/// Volatility profile configuration for risk management and position sizing.
78
///
79
/// Defines volatility characteristics and risk parameters for different
80
/// instruments, enabling dynamic position sizing and risk-adjusted execution.
81
#[derive(Debug, Clone, Serialize, Deserialize)]
82
pub struct VolatilityProfile {
83
    /// Historical average volatility (annualized)
84
    pub average_volatility: f64,
85
    /// Maximum observed volatility (99th percentile)
86
    pub max_volatility: f64,
87
    /// Minimum observed volatility (1st percentile)
88
    pub min_volatility: f64,
89
    /// Beta coefficient relative to market index
90
    pub beta: f64,
91
    /// Average True Range (ATR) for recent period
92
    pub atr: f64,
93
    /// Correlation with market benchmark
94
    pub market_correlation: f64,
95
    /// Volatility regime classification
96
    pub volatility_regime: VolatilityRegime,
97
    /// Last updated timestamp for volatility metrics
98
    pub last_updated: DateTime<Utc>,
99
    /// Number of observations used for calculation
100
    pub sample_size: u32,
101
}
102
103
impl VolatilityProfile {
104
    /// Creates a new volatility profile with default values.
105
0
    pub fn new() -> Self {
106
0
        Self {
107
0
            average_volatility: 0.20,
108
0
            max_volatility: 1.00,
109
0
            min_volatility: 0.05,
110
0
            beta: 1.0,
111
0
            atr: 0.0,
112
0
            market_correlation: 0.0,
113
0
            volatility_regime: VolatilityRegime::Normal,
114
0
            last_updated: Utc::now(),
115
0
            sample_size: 0,
116
0
        }
117
0
    }
118
119
    /// Updates volatility metrics with new data point.
120
0
    pub fn update_metrics(&mut self, new_volatility: f64, new_atr: f64) {
121
        // Update exponential moving average
122
0
        let alpha = 0.1; // Smoothing factor
123
0
        self.average_volatility = alpha * new_volatility + (1.0 - alpha) * self.average_volatility;
124
0
        self.atr = alpha * new_atr + (1.0 - alpha) * self.atr;
125
0
        self.last_updated = Utc::now();
126
0
        self.sample_size += 1;
127
128
        // Update volatility regime
129
0
        self.volatility_regime = self.classify_regime();
130
0
    }
131
132
    /// Classifies current volatility regime based on metrics.
133
0
    fn classify_regime(&self) -> VolatilityRegime {
134
0
        let volatility_ratio = self.average_volatility / 0.20; // Relative to 20% baseline
135
136
0
        if volatility_ratio > 2.0 {
137
0
            VolatilityRegime::High
138
0
        } else if volatility_ratio > 1.5 {
139
0
            VolatilityRegime::Elevated
140
0
        } else if volatility_ratio < 0.5 {
141
0
            VolatilityRegime::Low
142
        } else {
143
0
            VolatilityRegime::Normal
144
        }
145
0
    }
146
147
    /// Returns risk-adjusted position size multiplier.
148
0
    pub fn position_size_multiplier(&self) -> f64 {
149
0
        match self.volatility_regime {
150
0
            VolatilityRegime::Low => 1.5,
151
0
            VolatilityRegime::Normal => 1.0,
152
0
            VolatilityRegime::Elevated => 0.7,
153
0
            VolatilityRegime::High => 0.4,
154
        }
155
0
    }
156
}
157
158
impl Default for VolatilityProfile {
159
0
    fn default() -> Self {
160
0
        Self::new()
161
0
    }
162
}
163
164
/// Volatility regime classification for risk management.
165
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166
pub enum VolatilityRegime {
167
    /// Low volatility environment (< 50% of normal)
168
    Low,
169
    /// Normal volatility environment
170
    Normal,
171
    /// Elevated volatility (50-100% above normal)
172
    Elevated,
173
    /// High volatility environment (> 100% above normal)
174
    High,
175
}
176
177
/// Trading hours configuration for different markets and sessions.
178
///
179
/// Defines market operating hours, pre-market and after-hours sessions,
180
/// and holiday schedules for accurate trade timing and execution.
181
#[derive(Debug, Clone, Serialize, Deserialize)]
182
pub struct TradingHours {
183
    /// Primary market timezone identifier (e.g., "America/New_York")
184
    pub timezone: String,
185
    /// Regular trading session start time
186
    pub market_open: NaiveTime,
187
    /// Regular trading session end time
188
    pub market_close: NaiveTime,
189
    /// Pre-market session start time (optional)
190
    pub pre_market_open: Option<NaiveTime>,
191
    /// After-hours session end time (optional)
192
    pub after_hours_close: Option<NaiveTime>,
193
    /// Trading days of the week
194
    pub trading_days: Vec<Weekday>,
195
    /// Market holidays (dates when market is closed)
196
    pub holidays: Vec<NaiveDate>,
197
    /// Half-day sessions with early close times
198
    pub half_days: HashMap<NaiveDate, NaiveTime>,
199
}
200
201
impl TradingHours {
202
    /// Creates US equity market trading hours configuration.
203
0
    pub fn us_equity() -> Self {
204
0
        Self {
205
0
            timezone: "America/New_York".to_string(),
206
0
            market_open: NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
207
0
            market_close: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
208
0
            pre_market_open: Some(NaiveTime::from_hms_opt(4, 0, 0).unwrap()),
209
0
            after_hours_close: Some(NaiveTime::from_hms_opt(20, 0, 0).unwrap()),
210
0
            trading_days: vec![
211
0
                Weekday::Mon,
212
0
                Weekday::Tue,
213
0
                Weekday::Wed,
214
0
                Weekday::Thu,
215
0
                Weekday::Fri,
216
0
            ],
217
0
            holidays: vec![],
218
0
            half_days: HashMap::new(),
219
0
        }
220
0
    }
221
222
    /// Creates 24/7 trading hours for crypto markets.
223
0
    pub fn crypto_24_7() -> Self {
224
0
        Self {
225
0
            timezone: "UTC".to_string(),
226
0
            market_open: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
227
0
            market_close: NaiveTime::from_hms_opt(23, 59, 59).unwrap(),
228
0
            pre_market_open: None,
229
0
            after_hours_close: None,
230
0
            trading_days: vec![
231
0
                Weekday::Mon,
232
0
                Weekday::Tue,
233
0
                Weekday::Wed,
234
0
                Weekday::Thu,
235
0
                Weekday::Fri,
236
0
                Weekday::Sat,
237
0
                Weekday::Sun,
238
0
            ],
239
0
            holidays: vec![],
240
0
            half_days: HashMap::new(),
241
0
        }
242
0
    }
243
244
    /// Creates forex market trading hours (Sunday 5 PM to Friday 5 PM EST).
245
0
    pub fn forex() -> Self {
246
0
        Self {
247
0
            timezone: "America/New_York".to_string(),
248
0
            market_open: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
249
0
            market_close: NaiveTime::from_hms_opt(17, 0, 0).unwrap(),
250
0
            pre_market_open: None,
251
0
            after_hours_close: None,
252
0
            trading_days: vec![
253
0
                Weekday::Sun,
254
0
                Weekday::Mon,
255
0
                Weekday::Tue,
256
0
                Weekday::Wed,
257
0
                Weekday::Thu,
258
0
                Weekday::Fri,
259
0
            ],
260
0
            holidays: vec![],
261
0
            half_days: HashMap::new(),
262
0
        }
263
0
    }
264
265
    /// Checks if market is currently open.
266
0
    pub fn is_market_open(&self, current_time: DateTime<Utc>) -> bool {
267
        // Convert to market timezone and check if within trading hours
268
        // This is a simplified implementation - production would use proper timezone handling
269
0
        let current_date = current_time.date_naive();
270
0
        let current_time = current_time.time();
271
0
        let current_weekday = current_date.weekday();
272
273
        // Check if it's a trading day
274
0
        if !self.trading_days.contains(&current_weekday) {
275
0
            return false;
276
0
        }
277
278
        // Check if it's a holiday
279
0
        if self.holidays.contains(&current_date) {
280
0
            return false;
281
0
        }
282
283
        // Check if within trading hours
284
0
        current_time >= self.market_open && current_time <= self.market_close
285
0
    }
286
287
    /// Checks if extended hours trading is active.
288
0
    pub fn is_extended_hours_open(&self, current_time: DateTime<Utc>) -> bool {
289
0
        let current_time = current_time.time();
290
291
        // Check pre-market
292
0
        if let Some(pre_open) = self.pre_market_open {
293
0
            if current_time >= pre_open && current_time < self.market_open {
294
0
                return true;
295
0
            }
296
0
        }
297
298
        // Check after-hours
299
0
        if let Some(after_close) = self.after_hours_close {
300
0
            if current_time > self.market_close && current_time <= after_close {
301
0
                return true;
302
0
            }
303
0
        }
304
305
0
        false
306
0
    }
307
}
308
309
impl Default for TradingHours {
310
0
    fn default() -> Self {
311
0
        Self::us_equity()
312
0
    }
313
}
314
315
/// Comprehensive symbol configuration containing all trading parameters.
316
///
317
/// Central configuration structure for each tradeable symbol, containing
318
/// classification, market parameters, risk settings, and execution rules.
319
#[derive(Debug, Clone, Serialize, Deserialize)]
320
pub struct SymbolConfig {
321
    /// Unique symbol identifier
322
    pub symbol: String,
323
    /// Symbol description or company name
324
    pub description: String,
325
    /// Asset classification
326
    pub classification: AssetClassification,
327
    /// Volatility and risk profile
328
    pub volatility_profile: VolatilityProfile,
329
    /// Market operating hours
330
    pub trading_hours: TradingHours,
331
    /// Minimum price increment (tick size)
332
    pub tick_size: f64,
333
    /// Standard trading unit size
334
    pub lot_size: f64,
335
    /// Minimum order quantity
336
    pub min_order_size: f64,
337
    /// Maximum order quantity
338
    pub max_order_size: f64,
339
    /// Primary exchange or venue
340
    pub primary_exchange: String,
341
    /// Currency denomination
342
    pub currency: String,
343
    /// Sector classification (for equities)
344
    pub sector: Option<String>,
345
    /// Industry classification (for equities)
346
    pub industry: Option<String>,
347
    /// Market capitalization (for equities)
348
    pub market_cap: Option<f64>,
349
    /// Average daily volume
350
    pub avg_daily_volume: f64,
351
    /// Margin requirements
352
    pub margin_requirement: f64,
353
    /// Position limits
354
    pub position_limit: Option<f64>,
355
    /// Risk multiplier for position sizing
356
    pub risk_multiplier: f64,
357
    /// Configuration metadata
358
    pub metadata: SymbolMetadata,
359
}
360
361
impl SymbolConfig {
362
    /// Creates a new symbol configuration with default values.
363
0
    pub fn new(symbol: String, classification: AssetClassification) -> Self {
364
0
        let trading_hours = match classification {
365
0
            AssetClassification::Crypto => TradingHours::crypto_24_7(),
366
0
            AssetClassification::Forex => TradingHours::forex(),
367
0
            _ => TradingHours::us_equity(),
368
        };
369
370
0
        Self {
371
0
            symbol: symbol.clone(),
372
0
            description: format!("{} - Auto-generated", symbol),
373
0
            classification,
374
0
            volatility_profile: VolatilityProfile::new(),
375
0
            trading_hours,
376
0
            tick_size: 0.01,
377
0
            lot_size: 1.0,
378
0
            min_order_size: 1.0,
379
0
            max_order_size: 1_000_000.0,
380
0
            primary_exchange: "".to_string(),
381
0
            currency: "USD".to_string(),
382
0
            sector: None,
383
0
            industry: None,
384
0
            market_cap: None,
385
0
            avg_daily_volume: 0.0,
386
0
            margin_requirement: 0.25,
387
0
            position_limit: None,
388
0
            risk_multiplier: 1.0,
389
0
            metadata: SymbolMetadata::new(),
390
0
        }
391
0
    }
392
393
    /// Validates the symbol configuration for correctness.
394
0
    pub fn validate(&self) -> Result<(), String> {
395
0
        if self.symbol.is_empty() {
396
0
            return Err("Symbol cannot be empty".to_string());
397
0
        }
398
399
0
        if self.tick_size <= 0.0 {
400
0
            return Err("Tick size must be positive".to_string());
401
0
        }
402
403
0
        if self.lot_size <= 0.0 {
404
0
            return Err("Lot size must be positive".to_string());
405
0
        }
406
407
0
        if self.min_order_size <= 0.0 {
408
0
            return Err("Minimum order size must be positive".to_string());
409
0
        }
410
411
0
        if self.max_order_size <= self.min_order_size {
412
0
            return Err("Maximum order size must be greater than minimum".to_string());
413
0
        }
414
415
0
        if self.margin_requirement < 0.0 || self.margin_requirement > 1.0 {
416
0
            return Err("Margin requirement must be between 0 and 1".to_string());
417
0
        }
418
419
0
        Ok(())
420
0
    }
421
422
    /// Calculates the effective position size based on risk parameters.
423
0
    pub fn calculate_position_size(&self, base_size: f64, _account_value: f64) -> f64 {
424
0
        let volatility_multiplier = self.volatility_profile.position_size_multiplier();
425
0
        let risk_adjusted_size = base_size * volatility_multiplier * self.risk_multiplier;
426
427
        // Apply position limits
428
0
        if let Some(limit) = self.position_limit {
429
0
            risk_adjusted_size.min(limit)
430
        } else {
431
0
            risk_adjusted_size
432
        }
433
0
    }
434
435
    /// Returns the appropriate tick size for a given price level.
436
0
    pub fn get_tick_size_for_price(&self, _price: f64) -> f64 {
437
        // Some markets have variable tick sizes based on price
438
        // This is a simplified implementation
439
0
        self.tick_size
440
0
    }
441
442
    /// Rounds price to the nearest valid tick.
443
0
    pub fn round_to_tick(&self, price: f64) -> f64 {
444
0
        let tick = self.get_tick_size_for_price(price);
445
0
        (price / tick).round() * tick
446
0
    }
447
448
    /// Checks if the symbol is currently tradeable.
449
0
    pub fn is_tradeable(&self, current_time: DateTime<Utc>) -> bool {
450
0
        self.trading_hours.is_market_open(current_time) && self.metadata.is_active
451
0
    }
452
453
    /// Checks if extended hours trading is available.
454
0
    pub fn supports_extended_hours(&self) -> bool {
455
0
        self.classification.supports_extended_hours()
456
0
    }
457
}
458
459
/// Symbol configuration metadata for versioning and tracking.
460
#[derive(Debug, Clone, Serialize, Deserialize)]
461
pub struct SymbolMetadata {
462
    /// Unique configuration ID
463
    pub id: Uuid,
464
    /// Configuration version
465
    pub version: u32,
466
    /// Creation timestamp
467
    pub created_at: DateTime<Utc>,
468
    /// Last update timestamp
469
    pub updated_at: DateTime<Utc>,
470
    /// Active status
471
    pub is_active: bool,
472
    /// Data source for configuration
473
    pub data_source: String,
474
    /// Last validation timestamp
475
    pub last_validated: Option<DateTime<Utc>>,
476
    /// Configuration tags for organization
477
    pub tags: Vec<String>,
478
}
479
480
impl SymbolMetadata {
481
    /// Creates new metadata with default values.
482
0
    pub fn new() -> Self {
483
0
        let now = Utc::now();
484
0
        Self {
485
0
            id: Uuid::new_v4(),
486
0
            version: 1,
487
0
            created_at: now,
488
0
            updated_at: now,
489
0
            is_active: true,
490
0
            data_source: "manual".to_string(),
491
0
            last_validated: None,
492
0
            tags: vec![],
493
0
        }
494
0
    }
495
496
    /// Updates the metadata timestamp and version.
497
0
    pub fn update(&mut self) {
498
0
        self.updated_at = Utc::now();
499
0
        self.version += 1;
500
0
    }
501
502
    /// Marks the configuration as validated.
503
0
    pub fn mark_validated(&mut self) {
504
0
        self.last_validated = Some(Utc::now());
505
0
    }
506
}
507
508
impl Default for SymbolMetadata {
509
0
    fn default() -> Self {
510
0
        Self::new()
511
0
    }
512
}
513
514
/// Symbol configuration manager for loading and caching symbol configurations.
515
///
516
/// Provides high-performance access to symbol configurations with caching,
517
/// hot-reload capabilities, and configuration validation.
518
#[derive(Debug)]
519
pub struct SymbolConfigManager {
520
    /// In-memory cache of symbol configurations
521
    symbol_cache: HashMap<String, SymbolConfig>,
522
    /// Last cache update timestamp
523
    last_updated: DateTime<Utc>,
524
    /// Cache timeout duration
525
    cache_timeout: Duration,
526
}
527
528
impl SymbolConfigManager {
529
    /// Creates a new symbol configuration manager.
530
0
    pub fn new() -> Self {
531
0
        Self {
532
0
            symbol_cache: HashMap::new(),
533
0
            last_updated: Utc::now(),
534
0
            cache_timeout: Duration::from_secs(300), // 5 minutes
535
0
        }
536
0
    }
537
538
    /// Loads symbol configuration from cache or source.
539
0
    pub async fn get_symbol_config(
540
0
        &mut self,
541
0
        symbol: &str,
542
0
    ) -> Result<Option<SymbolConfig>, String> {
543
        // Check cache first
544
0
        if let Some(config) = self.symbol_cache.get(symbol) {
545
0
            if !self.is_cache_expired() {
546
0
                return Ok(Some(config.clone()));
547
0
            }
548
0
        }
549
550
        // Load from source (this would integrate with database/external source)
551
0
        self.load_symbol_from_source(symbol).await
552
0
    }
553
554
    /// Loads all symbol configurations into cache.
555
0
    pub async fn load_all_symbols(&mut self) -> Result<usize, String> {
556
        // This would integrate with the database or external configuration source
557
0
        self.refresh_cache().await
558
0
    }
559
560
    /// Adds or updates a symbol configuration.
561
0
    pub fn upsert_symbol_config(&mut self, config: SymbolConfig) -> Result<(), String> {
562
        // Validate configuration
563
0
        config.validate()?;
564
565
        // Update cache
566
0
        self.symbol_cache.insert(config.symbol.clone(), config);
567
0
        self.last_updated = Utc::now();
568
569
0
        Ok(())
570
0
    }
571
572
    /// Removes a symbol configuration.
573
0
    pub fn remove_symbol_config(&mut self, symbol: &str) -> Option<SymbolConfig> {
574
0
        self.symbol_cache.remove(symbol)
575
0
    }
576
577
    /// Returns all cached symbol configurations.
578
0
    pub fn get_all_symbols(&self) -> Vec<&SymbolConfig> {
579
0
        self.symbol_cache.values().collect()
580
0
    }
581
582
    /// Returns symbols filtered by asset classification.
583
0
    pub fn get_symbols_by_classification(
584
0
        &self,
585
0
        classification: &AssetClassification,
586
0
    ) -> Vec<&SymbolConfig> {
587
0
        self.symbol_cache
588
0
            .values()
589
0
            .filter(|config| &config.classification == classification)
590
0
            .collect()
591
0
    }
592
593
    /// Checks if cache has expired.
594
0
    fn is_cache_expired(&self) -> bool {
595
0
        Utc::now()
596
0
            .signed_duration_since(self.last_updated)
597
0
            .to_std()
598
0
            .unwrap_or(Duration::MAX)
599
0
            > self.cache_timeout
600
0
    }
601
602
    /// Loads symbol configuration from external source.
603
0
    async fn load_symbol_from_source(
604
0
        &mut self,
605
0
        _symbol: &str,
606
0
    ) -> Result<Option<SymbolConfig>, String> {
607
        // This would integrate with database or external configuration API
608
        // For now, return None to indicate symbol not found
609
610
        // Example of creating a default config if needed:
611
        // let config = SymbolConfig::new(symbol.to_string(), AssetClassification::Equity);
612
        // self.symbol_cache.insert(symbol.to_string(), config.clone());
613
        // Ok(Some(config))
614
615
0
        Ok(None)
616
0
    }
617
618
    /// Refreshes the entire symbol cache from source.
619
0
    async fn refresh_cache(&mut self) -> Result<usize, String> {
620
        // This would integrate with database to load all active symbols
621
        // For now, return the current cache size
622
0
        Ok(self.symbol_cache.len())
623
0
    }
624
625
    /// Sets cache timeout duration.
626
0
    pub fn set_cache_timeout(&mut self, timeout: Duration) {
627
0
        self.cache_timeout = timeout;
628
0
    }
629
630
    /// Forces cache refresh on next access.
631
0
    pub fn invalidate_cache(&mut self) {
632
0
        self.last_updated = DateTime::<Utc>::MIN_UTC;
633
0
    }
634
635
    /// Returns cache statistics.
636
0
    pub fn cache_stats(&self) -> (usize, DateTime<Utc>, bool) {
637
0
        (
638
0
            self.symbol_cache.len(),
639
0
            self.last_updated,
640
0
            self.is_cache_expired(),
641
0
        )
642
0
    }
643
}
644
645
impl Default for SymbolConfigManager {
646
0
    fn default() -> Self {
647
0
        Self::new()
648
0
    }
649
}
650
651
#[cfg(test)]
652
mod tests {
653
    use super::*;
654
655
    #[test]
656
    fn test_asset_classification_regulatory_class() {
657
        assert_eq!(AssetClassification::Equity.regulatory_class(), "EQUITY");
658
        assert_eq!(AssetClassification::Forex.regulatory_class(), "FX");
659
        assert_eq!(AssetClassification::Crypto.regulatory_class(), "CRYPTO");
660
    }
661
662
    #[test]
663
    fn test_volatility_profile_update() {
664
        let mut profile = VolatilityProfile::new();
665
        profile.update_metrics(0.40, 2.5);
666
667
        // With exponential smoothing: 0.1 * 0.40 + 0.9 * 0.20 = 0.22
668
        assert!(profile.average_volatility > 0.20 && profile.average_volatility < 0.25);
669
        // With exponential smoothing: 0.1 * 2.5 + 0.9 * 0.0 = 0.25
670
        assert!((profile.atr - 0.25).abs() < 0.01);
671
        assert_eq!(profile.volatility_regime, VolatilityRegime::Normal);
672
    }
673
674
    #[test]
675
    fn test_symbol_config_validation() {
676
        let mut config = SymbolConfig::new("AAPL".to_string(), AssetClassification::Equity);
677
        assert!(config.validate().is_ok());
678
679
        config.tick_size = -0.01;
680
        assert!(config.validate().is_err());
681
    }
682
683
    #[test]
684
    fn test_trading_hours_us_equity() {
685
        let hours = TradingHours::us_equity();
686
        assert_eq!(hours.timezone, "America/New_York");
687
        assert_eq!(
688
            hours.market_open,
689
            NaiveTime::from_hms_opt(9, 30, 0).unwrap()
690
        );
691
        assert_eq!(
692
            hours.market_close,
693
            NaiveTime::from_hms_opt(16, 0, 0).unwrap()
694
        );
695
    }
696
697
    #[test]
698
    fn test_symbol_config_manager() {
699
        let mut manager = SymbolConfigManager::new();
700
        let config = SymbolConfig::new("TEST".to_string(), AssetClassification::Equity);
701
702
        assert!(manager.upsert_symbol_config(config).is_ok());
703
        assert_eq!(manager.get_all_symbols().len(), 1);
704
    }
705
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html deleted file mode 100644 index 69a59e2f4..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/config/src/vault.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/vault.rs
Line
Count
Source
1
//! HashiCorp Vault configuration for secure secret management.
2
//!
3
//! This module provides configuration structures for integrating with HashiCorp Vault
4
//! to securely manage secrets, API keys, and sensitive configuration data in the
5
//! Foxhunt trading system. Supports token-based authentication and namespace isolation.
6
7
use serde::{Deserialize, Serialize};
8
use secrecy::{ExposeSecret, SecretString};
9
use std::fmt;
10
11
/// HashiCorp Vault configuration for secure secret storage.
12
///
13
/// Configures connection to HashiCorp Vault for retrieving sensitive
14
/// configuration data such as API keys, database passwords, and other
15
/// secrets. Supports Vault Enterprise features like namespaces.
16
///
17
/// # Security
18
///
19
/// The Vault token is wrapped in `SecretString` to prevent accidental
20
/// exposure in logs, debug output, or memory dumps. The token is automatically
21
/// zeroized when the config is dropped.
22
#[derive(Clone, Serialize, Deserialize)]
23
pub struct VaultConfig {
24
    /// Vault server URL (e.g., "<https://vault.example.com:8200>")
25
    pub url: String,
26
    /// Vault authentication token for API access (securely stored)
27
    #[serde(serialize_with = "serialize_secret", deserialize_with = "deserialize_secret")]
28
    pub token: SecretString,
29
    /// Mount path for the secrets engine (e.g., "secret/")
30
    pub mount_path: String,
31
    /// Vault namespace for multi-tenant deployments (Enterprise feature)
32
    pub namespace: Option<String>,
33
}
34
35
/// Custom serializer for SecretString that prevents token exposure
36
0
fn serialize_secret<S>(_secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
37
0
where
38
0
    S: serde::Serializer,
39
{
40
    // Serialize as redacted placeholder to prevent token exposure
41
0
    serializer.serialize_str("***REDACTED***")
42
0
}
43
44
/// Custom deserializer for SecretString
45
0
fn deserialize_secret<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
46
0
where
47
0
    D: serde::Deserializer<'de>,
48
{
49
0
    let s = String::deserialize(deserializer)?;
50
0
    Ok(SecretString::from(s))
51
0
}
52
53
impl fmt::Debug for VaultConfig {
54
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55
0
        f.debug_struct("VaultConfig")
56
0
            .field("url", &self.url)
57
0
            .field("token", &"***REDACTED***")
58
0
            .field("mount_path", &self.mount_path)
59
0
            .field("namespace", &self.namespace)
60
0
            .finish()
61
0
    }
62
}
63
64
impl Drop for VaultConfig {
65
0
    fn drop(&mut self) {
66
        // Explicitly zeroize the token when VaultConfig is dropped
67
        // This ensures the secret is cleared from memory
68
        // Note: SecretString already implements ZeroizeOnDrop, but we make it explicit
69
        // for documentation purposes
70
0
    }
71
}
72
73
impl VaultConfig {
74
    /// Creates a new VaultConfig with the specified parameters.
75
    ///
76
    /// # Security
77
    ///
78
    /// The token is immediately wrapped in a `SecretString` to prevent exposure.
79
    /// Consider using `from_env()` or loading from secure configuration
80
    /// sources instead of passing plain strings.
81
0
    pub fn new(url: String, token: String, mount_path: String) -> Self {
82
0
        Self {
83
0
            url,
84
0
            token: SecretString::from(token),
85
0
            mount_path,
86
0
            namespace: None,
87
0
        }
88
0
    }
89
90
    /// Sets the namespace for multi-tenant Vault deployments.
91
0
    pub fn with_namespace(mut self, namespace: String) -> Self {
92
0
        self.namespace = Some(namespace);
93
0
        self
94
0
    }
95
96
    /// Gets a reference to the secret token (requires explicit exposure)
97
    ///
98
    /// # Security
99
    ///
100
    /// This method requires the caller to explicitly acknowledge they are
101
    /// exposing the secret. Use only when necessary (e.g., when making
102
    /// API calls to Vault) and ensure the exposed value is not logged
103
    /// or stored in insecure locations.
104
0
    pub fn token(&self) -> &SecretString {
105
0
        &self.token
106
0
    }
107
108
    /// Validates the vault configuration.
109
    ///
110
    /// # Security
111
    ///
112
    /// Validation checks length without exposing the token value.
113
0
    pub fn validate(&self) -> Result<(), String> {
114
0
        if self.url.is_empty() {
115
0
            return Err("Vault URL cannot be empty".to_string());
116
0
        }
117
0
        if self.token.expose_secret().is_empty() {
118
0
            return Err("Vault token cannot be empty".to_string());
119
0
        }
120
0
        if self.mount_path.is_empty() {
121
0
            return Err("Vault mount path cannot be empty".to_string());
122
0
        }
123
0
        Ok(())
124
0
    }
125
}
126
127
#[cfg(test)]
128
mod tests {
129
    use super::*;
130
131
    fn create_test_config() -> VaultConfig {
132
        VaultConfig::new(
133
            "https://vault.example.com:8200".to_string(),
134
            "test-token-12345".to_string(),
135
            "secret/".to_string(),
136
        )
137
    }
138
139
    #[test]
140
    fn test_vault_config_creation() {
141
        let config = create_test_config();
142
        assert_eq!(config.url, "https://vault.example.com:8200");
143
        assert_eq!(config.mount_path, "secret/");
144
        assert!(config.namespace.is_none());
145
    }
146
147
    #[test]
148
    fn test_vault_config_with_namespace() {
149
        let config = create_test_config().with_namespace("production".to_string());
150
        assert_eq!(config.namespace.as_deref(), Some("production"));
151
    }
152
153
    #[test]
154
    fn test_vault_config_validation_success() {
155
        let config = create_test_config();
156
        assert!(config.validate().is_ok());
157
    }
158
159
    #[test]
160
    fn test_vault_config_validation_empty_url() {
161
        let mut config = create_test_config();
162
        config.url = String::new();
163
        assert!(config.validate().is_err());
164
        assert_eq!(config.validate().unwrap_err(), "Vault URL cannot be empty");
165
    }
166
167
    #[test]
168
    fn test_vault_config_validation_empty_token() {
169
        let mut config = create_test_config();
170
        config.token = SecretString::from(String::new());
171
        assert!(config.validate().is_err());
172
        assert_eq!(
173
            config.validate().unwrap_err(),
174
            "Vault token cannot be empty"
175
        );
176
    }
177
178
    #[test]
179
    fn test_vault_config_validation_empty_mount_path() {
180
        let mut config = create_test_config();
181
        config.mount_path = String::new();
182
        assert!(config.validate().is_err());
183
        assert_eq!(
184
            config.validate().unwrap_err(),
185
            "Vault mount path cannot be empty"
186
        );
187
    }
188
189
    #[test]
190
    fn test_vault_config_serialization() {
191
        let config = create_test_config();
192
        let serialized = serde_json::to_string(&config).unwrap();
193
        // Token should be redacted in serialization
194
        assert!(serialized.contains("***REDACTED***"));
195
        assert!(!serialized.contains("test-token-12345"));
196
    }
197
198
    #[test]
199
    fn test_vault_config_deserialization() {
200
        let config = create_test_config();
201
        let serialized = serde_json::to_string(&config).unwrap();
202
        let deserialized: VaultConfig = serde_json::from_str(&serialized).unwrap();
203
        assert_eq!(config.url, deserialized.url);
204
        assert_eq!(config.mount_path, deserialized.mount_path);
205
    }
206
207
    #[test]
208
    fn test_vault_config_clone() {
209
        let config1 = create_test_config();
210
        let config2 = config1.clone();
211
        assert_eq!(config1.url, config2.url);
212
    }
213
214
    #[test]
215
    fn test_vault_config_debug() {
216
        let config = create_test_config();
217
        let debug_output = format!("{:?}", config);
218
        assert!(debug_output.contains("VaultConfig"));
219
        assert!(debug_output.contains("***REDACTED***"));
220
        assert!(!debug_output.contains("test-token-12345"));
221
    }
222
223
    #[test]
224
    fn test_vault_config_namespace_none() {
225
        let config = create_test_config();
226
        assert!(config.namespace.is_none());
227
    }
228
229
    #[test]
230
    fn test_vault_config_namespace_some() {
231
        let config = create_test_config().with_namespace("dev".to_string());
232
        assert!(config.namespace.is_some());
233
        assert_eq!(config.namespace.as_deref(), Some("dev"));
234
    }
235
236
    #[test]
237
    fn test_vault_config_token_not_exposed() {
238
        let config = create_test_config();
239
        // Verify token accessor works
240
        assert_eq!(config.token().expose_secret(), "test-token-12345");
241
    }
242
243
    #[test]
244
    fn test_vault_config_token_redacted_in_display() {
245
        let config = create_test_config();
246
        let debug_str = format!("{:?}", config);
247
        assert!(!debug_str.contains("test-token-12345"));
248
    }
249
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/circuit_breaker.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/circuit_breaker.rs.html deleted file mode 100644 index 343b0aa7d..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/circuit_breaker.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/circuit_breaker.rs
Line
Count
Source
1
//! Circuit Breaker for Risk Management Service
2
//! Circuit Breaker Module
3
//!
4
//! Implements dynamic portfolio-based circuit breakers with distributed Redis coordination.
5
//! Eliminates fixed $1M daily loss limits in favor of dynamic 2% portfolio-based limits.
6
7
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
8
#![warn(clippy::indexing_slicing)]
9
10
use std::collections::HashMap;
11
use std::marker::Send;
12
use std::sync::{
13
    atomic::{AtomicU32, Ordering},
14
    Arc,
15
};
16
// Removed foxhunt_infrastructure - not available in this simplified risk crate
17
18
use async_trait::async_trait;
19
use chrono::{DateTime, Utc};
20
use common::{Position, Price, Quantity, Symbol};
21
use redis::{AsyncCommands, RedisResult};
22
use rust_decimal::Decimal;
23
// REMOVED: Direct Decimal usage - use canonical types
24
use serde::{Deserialize, Serialize};
25
use tokio::sync::RwLock;
26
use tracing::{debug, error, info, warn};
27
28
// Import types using established patterns
29
use crate::error::{
30
    decimal_to_f64_safe, f64_to_decimal_safe, f64_to_price_safe, RiskError, RiskResult,
31
};
32
33
/// Circuit breaker state with Redis coordination
34
#[derive(Debug, Clone, Serialize, Deserialize)]
35
pub struct CircuitBreakerState {
36
    /// Whether circuit breaker is currently active
37
    pub is_active: bool,
38
    /// Current portfolio value
39
    pub portfolio_value: Price,
40
    /// Dynamic daily loss limit (percentage of portfolio)
41
    pub daily_loss_limit: Price,
42
    /// Current realized daily loss
43
    pub current_daily_loss: Price,
44
    /// Reason for activation
45
    pub activation_reason: Option<String>,
46
    /// When circuit breaker was activated
47
    pub activated_at: Option<DateTime<Utc>>,
48
    /// Last state update timestamp
49
    pub last_updated: DateTime<Utc>,
50
    /// Associated account ID
51
    pub account_id: String,
52
    /// Number of consecutive violations
53
    pub consecutive_violations: u32,
54
}
55
56
impl Default for CircuitBreakerState {
57
0
    fn default() -> Self {
58
0
        Self {
59
0
            is_active: false,
60
0
            portfolio_value: Price::ZERO,
61
0
            daily_loss_limit: Price::ZERO,
62
0
            current_daily_loss: Price::ZERO,
63
0
            activation_reason: None,
64
0
            activated_at: None,
65
0
            last_updated: Utc::now(),
66
0
            account_id: "default".to_owned(),
67
0
            consecutive_violations: 0,
68
0
        }
69
0
    }
70
}
71
72
/// Circuit breaker configuration with dynamic limits
73
#[derive(Debug, Clone)]
74
pub struct CircuitBreakerConfig {
75
    /// Enable circuit breaker functionality
76
    pub enabled: bool,
77
    /// Daily loss limit as percentage of portfolio (e.g., 2.0 = 2%)
78
    pub daily_loss_percentage: Price,
79
    /// Position size limit as percentage of portfolio (e.g., 5.0 = 5%)
80
    pub position_limit_percentage: Price,
81
    /// Maximum consecutive violations before emergency stop
82
    pub max_consecutive_violations: u32,
83
    /// Redis connection URL for distributed coordination
84
    pub redis_url: String,
85
    /// Redis key prefix for namespacing
86
    pub redis_key_prefix: String,
87
    /// Enable automatic recovery from circuit breaker state
88
    pub auto_recovery_enabled: bool,
89
    /// Interval for refreshing portfolio values (seconds)
90
    pub portfolio_refresh_interval_secs: u64,
91
    /// Cooldown period before allowing new positions after breach (seconds)
92
    pub cooldown_period_secs: u64,
93
}
94
95
impl Default for CircuitBreakerConfig {
96
7
    fn default() -> Self {
97
        Self {
98
            enabled: true,
99
7
            daily_loss_percentage: f64_to_price_safe(2.0, "default daily loss percentage")
100
7
                .unwrap_or_else(|_| 
{0
101
0
                    warn!("Failed to create default daily loss percentage, using ZERO");
102
0
                    Price::ZERO
103
0
                }), // 2.00%
104
7
            position_limit_percentage: f64_to_price_safe(5.0, "default position limit percentage")
105
7
                .unwrap_or_else(|_| 
{0
106
0
                    warn!("Failed to create default position limit percentage, using ZERO");
107
0
                    Price::ZERO
108
0
                }), // 5.00%
109
            max_consecutive_violations: 5,
110
7
            redis_url: std::env::var("REDIS_URL").unwrap_or_else(|_| 
{0
111
0
                std::env::var("FOXHUNT_REDIS_URL").unwrap_or_else(|_| {
112
0
                    let redis_host =
113
0
                        std::env::var("REDIS_HOST").unwrap_or_else(|_| "localhost".to_owned());
114
0
                    let redis_port =
115
0
                        std::env::var("REDIS_PORT").unwrap_or_else(|_| "6379".to_owned());
116
0
                    format!("redis://{redis_host}:{redis_port}")
117
0
                })
118
0
            }),
119
7
            redis_key_prefix: "foxhunt:circuit_breaker".to_owned(),
120
            auto_recovery_enabled: false, // Manual recovery for safety
121
            portfolio_refresh_interval_secs: 60, // 1 minute
122
            cooldown_period_secs: 300,    // 5 minutes
123
        }
124
7
    }
125
}
126
127
/// Trait for broker account services
128
#[async_trait]
129
pub trait BrokerAccountService: Send + Sync {
130
    /// Get current portfolio value
131
    async fn get_portfolio_value(&self, account_id: &str) -> RiskResult<Decimal>;
132
133
    /// Get daily `PnL` for account
134
    async fn get_daily_pnl(&self, account_id: &str) -> RiskResult<Decimal>;
135
136
    /// Get current positions for account
137
    async fn get_positions(&self, account_id: &str) -> RiskResult<Vec<Position>>;
138
}
139
140
/// `PnL` metrics for risk calculations
141
#[derive(Debug, Clone, Default)]
142
pub struct PnLMetrics {
143
    /// Unrealized profit/loss from open positions
144
    pub unrealized_pnl: Decimal,
145
    /// Realized profit/loss from closed positions
146
    pub realized_pnl: Decimal,
147
    /// Total profit/loss (realized + unrealized)
148
    pub total_pnl: Decimal,
149
    /// Daily profit/loss for the current trading day
150
    pub daily_pnl: Decimal,
151
}
152
153
/// Real circuit breaker with dynamic portfolio-based limits
154
pub struct RealCircuitBreaker {
155
    config: CircuitBreakerConfig,
156
    broker_service: Arc<dyn BrokerAccountService>,
157
    state: Arc<RwLock<HashMap<String, CircuitBreakerState>>>,
158
    redis_client: Option<redis::Client>,
159
    consecutive_violations: AtomicU32,
160
    last_portfolio_refresh: Arc<RwLock<HashMap<String, DateTime<Utc>>>>,
161
}
162
163
impl RealCircuitBreaker {
164
    /// Create new circuit breaker with real broker integration
165
21
    pub async fn new(
166
21
        config: CircuitBreakerConfig,
167
21
        broker_service: Arc<dyn BrokerAccountService>,
168
21
    ) -> RiskResult<Self> {
169
21
        info!(
"\u{1f512} Initializing REAL Circuit Breaker"0
);
170
21
        info!(
171
0
            "   Daily Loss Limit: {}% of portfolio value",
172
            config.daily_loss_percentage
173
        );
174
21
        info!(
" Redis Coordination: {}"0
, config.redis_url);
175
176
        // Initialize Redis connection
177
21
        let 
redis_client15
= if config.enabled {
178
20
            match redis::Client::open(config.redis_url.as_str()) {
179
14
                Ok(client) => {
180
                    // Test Redis connection
181
14
                    match client.get_multiplexed_async_connection().await {
182
14
                        Ok(mut conn) => {
183
14
                            match redis::cmd("PING").query_async::<String>(&mut conn).await {
184
                                Ok(_) => {
185
14
                                    info!(
"\u{2705} Redis connection established for circuit breaker coordination"0
);
186
14
                                    Some(client)
187
                                },
188
0
                                Err(e) => {
189
0
                                    warn!("\u{26a0}\u{fe0f} Redis connection test failed: {}", e);
190
0
                                    Some(client) // Still store client for retry attempts
191
                                },
192
                            }
193
                        },
194
0
                        Err(e) => {
195
0
                            warn!(
196
0
                                "\u{26a0}\u{fe0f} Could not establish initial Redis connection: {}",
197
                                e
198
                            );
199
0
                            Some(client) // Still store client for retry attempts
200
                        },
201
                    }
202
                },
203
6
                Err(e) => {
204
6
                    error!(
"\u{274c} Failed to create Redis client: {}"0
, e);
205
6
                    return Err(RiskError::Network(format!(
206
6
                        "Failed to create Redis client: {e}"
207
6
                    )));
208
                },
209
            }
210
        } else {
211
1
            None
212
        };
213
214
15
        Ok(Self {
215
15
            config,
216
15
            broker_service,
217
15
            state: Arc::new(RwLock::new(HashMap::new())),
218
15
            redis_client,
219
15
            consecutive_violations: AtomicU32::new(0),
220
15
            last_portfolio_refresh: Arc::new(RwLock::new(HashMap::new())),
221
15
        })
222
21
    }
223
224
    /// Check if circuit breaker should be triggered
225
1
    pub async fn check_circuit_breaker(&self, account_id: &str) -> RiskResult<bool> {
226
1
        if !self.config.enabled {
227
1
            return Ok(false);
228
0
        }
229
230
0
        debug!("Checking circuit breaker for account: {}", account_id);
231
232
        // Get or create state for account
233
0
        let mut state = self.get_or_create_state(account_id).await?;
234
235
        // Refresh portfolio value if needed
236
0
        if self.should_refresh_portfolio(&state).await? {
237
0
            self.refresh_portfolio_value(&mut state).await?;
238
0
        }
239
240
        // Check daily loss against dynamic limit - use safe conversions
241
0
        let loss_percentage =
242
0
            if state.portfolio_value > Price::ZERO {
243
0
                let current_loss_decimal = state.current_daily_loss.to_decimal().map_err(|_| {
244
0
                    RiskError::TypeConversion {
245
0
                        from_type: "Price".to_owned(),
246
0
                        to_type: "Decimal".to_owned(),
247
0
                        reason: "current daily loss conversion failed".to_owned(),
248
0
                    }
249
0
                })?;
250
0
                let portfolio_decimal =
251
0
                    state
252
0
                        .portfolio_value
253
0
                        .to_decimal()
254
0
                        .map_err(|_| RiskError::TypeConversion {
255
0
                            from_type: "Price".to_owned(),
256
0
                            to_type: "Decimal".to_owned(),
257
0
                            reason: "portfolio value conversion failed".to_owned(),
258
0
                        })?;
259
0
                let ratio = current_loss_decimal / portfolio_decimal;
260
0
                let ratio_f64 = decimal_to_f64_safe(ratio, "loss ratio calculation")?;
261
0
                f64_to_decimal_safe(ratio_f64 * 100.0, "loss percentage calculation")?
262
            } else {
263
0
                Decimal::ZERO
264
            };
265
266
0
        let daily_loss_limit_decimal =
267
0
            self.config
268
0
                .daily_loss_percentage
269
0
                .to_decimal()
270
0
                .map_err(|_| RiskError::TypeConversion {
271
0
                    from_type: "Price".to_owned(),
272
0
                    to_type: "Decimal".to_owned(),
273
0
                    reason: "daily loss limit conversion failed".to_owned(),
274
0
                })?;
275
0
        let should_activate = loss_percentage >= daily_loss_limit_decimal;
276
277
0
        if should_activate && !state.is_active {
278
0
            self.activate_circuit_breaker(
279
0
                &mut state,
280
0
                format!(
281
0
                    "Daily loss {}% exceeds limit {}%",
282
0
                    loss_percentage, self.config.daily_loss_percentage
283
0
                ),
284
0
            )
285
0
            .await?;
286
0
        }
287
288
0
        Ok(state.is_active)
289
1
    }
290
291
    /// Get current circuit breaker state
292
0
    pub async fn get_state(&self, account_id: &str) -> RiskResult<CircuitBreakerState> {
293
0
        let state_map = self.state.read().await;
294
0
        Ok(state_map.get(account_id).cloned().unwrap_or_else(|| {
295
0
            warn!(
296
0
                "No circuit breaker state found for account {}, using default",
297
                account_id
298
            );
299
0
            CircuitBreakerState::default()
300
0
        }))
301
0
    }
302
303
    /// Check if circuit breaker is active for an account
304
16
    pub async fn is_active(&self, account_id: &str) -> bool {
305
16
        let state_map = self.state.read().await;
306
16
        state_map
307
16
            .get(account_id)
308
16
            .is_some_and(|state| state.is_active)
309
16
    }
310
311
    /// Record a violation and potentially activate circuit breaker
312
0
    pub async fn record_violation(&self, violation_type: &str) {
313
0
        warn!(
314
0
            "\u{1f6a8} Circuit breaker violation recorded: {}",
315
            violation_type
316
        );
317
0
        self.consecutive_violations.fetch_add(1, Ordering::SeqCst);
318
0
    }
319
320
    /// Manually reset circuit breaker
321
0
    pub async fn reset_circuit_breaker(&self, account_id: &str, reason: String) -> RiskResult<()> {
322
0
        info!(
323
0
            "\u{1f513} Manually resetting circuit breaker for account {}: {}",
324
            account_id, reason
325
        );
326
327
0
        let mut state_map = self.state.write().await;
328
0
        if let Some(state) = state_map.get_mut(account_id) {
329
0
            state.is_active = false;
330
0
            state.activation_reason = None;
331
0
            state.activated_at = None;
332
0
            state.consecutive_violations = 0;
333
0
            state.last_updated = Utc::now();
334
335
            // Persist to Redis
336
0
            if let Err(e) = self.persist_state_to_redis(state).await {
337
0
                warn!("Failed to persist reset state to Redis: {}", e);
338
0
            }
339
0
        }
340
341
0
        self.consecutive_violations.store(0, Ordering::SeqCst);
342
0
        info!(
343
0
            "\u{2705} Circuit breaker reset completed for account {}",
344
            account_id
345
        );
346
0
        Ok(())
347
0
    }
348
349
    /// Check position size limits
350
0
    pub async fn check_position_limit(
351
0
        &self,
352
0
        account_id: &str,
353
0
        symbol: &Symbol,
354
0
        quantity: Quantity,
355
0
    ) -> RiskResult<bool> {
356
0
        if !self.config.enabled {
357
0
            return Ok(true); // Allow all positions if circuit breaker disabled
358
0
        }
359
360
0
        let state = self.get_or_create_state(account_id).await?;
361
362
0
        if state.portfolio_value <= Price::ZERO {
363
0
            return Ok(false); // Block if no portfolio value
364
0
        }
365
366
        // Calculate position value (simplified - would need current price in real implementation)
367
0
        let estimated_position_value = quantity.to_f64(); // Convert to f64 for calculation
368
0
        let estimated_decimal =
369
0
            f64_to_decimal_safe(estimated_position_value, "position value calculation")
370
0
                .unwrap_or_else(|_| {
371
0
                    warn!("Failed to convert position value to decimal, using ZERO");
372
0
                    Decimal::ZERO
373
0
                });
374
0
        let portfolio_decimal = state
375
0
            .portfolio_value
376
0
            .to_decimal()
377
0
            .map_err(|_| RiskError::TypeConversion {
378
0
                from_type: "Price".to_owned(),
379
0
                to_type: "Decimal".to_owned(),
380
0
                reason: "portfolio value conversion for position limit failed".to_owned(),
381
0
            })
382
0
            .unwrap_or_else(|e| {
383
0
                warn!("Portfolio value conversion failed: {}, using default", e);
384
0
                Decimal::from(1) // Use 1 to avoid division by zero
385
0
            });
386
0
        let position_percentage = estimated_decimal / portfolio_decimal * Decimal::from(100);
387
388
0
        let position_limit_decimal = self
389
0
            .config
390
0
            .position_limit_percentage
391
0
            .to_decimal()
392
0
            .map_err(|_| RiskError::TypeConversion {
393
0
                from_type: "Price".to_owned(),
394
0
                to_type: "Decimal".to_owned(),
395
0
                reason: "position limit percentage conversion failed".to_owned(),
396
0
            })
397
0
            .unwrap_or_else(|e| {
398
0
                warn!("Position limit conversion failed: {}, using default 5%", e);
399
0
                Decimal::from(5) // 5% default
400
0
            });
401
0
        let within_limit = position_percentage <= position_limit_decimal;
402
403
0
        if !within_limit {
404
0
            warn!(
405
0
                "Position size limit exceeded: {}% > {}% for {} in account {}",
406
                position_percentage, self.config.position_limit_percentage, symbol, account_id
407
            );
408
0
        }
409
410
0
        Ok(within_limit)
411
0
    }
412
413
    /// Get or create state for account
414
0
    async fn get_or_create_state(&self, account_id: &str) -> RiskResult<CircuitBreakerState> {
415
0
        let mut state_map = self.state.write().await;
416
417
0
        if let Some(existing_state) = state_map.get(account_id) {
418
0
            return Ok(existing_state.clone());
419
0
        }
420
421
        // Try to load from Redis first
422
0
        if let Some(redis_state) = self.load_state_from_redis(account_id).await? {
423
0
            state_map.insert(account_id.to_owned(), redis_state.clone());
424
0
            return Ok(redis_state);
425
0
        }
426
427
        // Create new state
428
0
        let mut new_state = CircuitBreakerState::default();
429
0
        new_state.account_id = account_id.to_owned();
430
431
        // Initialize portfolio value
432
0
        self.refresh_portfolio_value(&mut new_state).await?;
433
434
0
        state_map.insert(account_id.to_owned(), new_state.clone());
435
0
        Ok(new_state)
436
0
    }
437
438
    /// Check if portfolio should be refreshed
439
0
    async fn should_refresh_portfolio(&self, state: &CircuitBreakerState) -> RiskResult<bool> {
440
0
        let refresh_map = self.last_portfolio_refresh.read().await;
441
442
0
        if let Some(last_refresh) = refresh_map.get(&state.account_id) {
443
0
            let elapsed = Utc::now().signed_duration_since(*last_refresh);
444
0
            Ok(elapsed.num_seconds() >= self.config.portfolio_refresh_interval_secs as i64)
445
        } else {
446
0
            Ok(true) // Refresh if never refreshed
447
        }
448
0
    }
449
450
    /// Refresh portfolio value from broker
451
0
    async fn refresh_portfolio_value(&self, state: &mut CircuitBreakerState) -> RiskResult<()> {
452
0
        debug!(
453
0
            "Refreshing portfolio value for account: {}",
454
            state.account_id
455
        );
456
457
        // Get portfolio value from broker
458
0
        let portfolio_value = self
459
0
            .broker_service
460
0
            .get_portfolio_value(&state.account_id)
461
0
            .await?;
462
0
        let daily_pnl = self.broker_service.get_daily_pnl(&state.account_id).await?;
463
464
0
        state.portfolio_value = portfolio_value.into();
465
0
        state.current_daily_loss = if daily_pnl < Decimal::ZERO {
466
0
            daily_pnl.abs().into()
467
        } else {
468
0
            Price::ZERO
469
        };
470
471
0
        let daily_loss_percentage_decimal = self
472
0
            .config
473
0
            .daily_loss_percentage
474
0
            .to_decimal()
475
0
            .map_err(|_| RiskError::TypeConversion {
476
0
                from_type: "Price".to_owned(),
477
0
                to_type: "Decimal".to_owned(),
478
0
                reason: "daily loss percentage conversion failed".to_owned(),
479
0
            })
480
0
            .unwrap_or_else(|e| {
481
0
                warn!(
482
0
                    "Daily loss percentage conversion failed: {}, using default 2%",
483
                    e
484
                );
485
0
                Decimal::from(2) // 2% default
486
0
            });
487
488
0
        state.daily_loss_limit =
489
0
            ((portfolio_value * daily_loss_percentage_decimal) / Decimal::from(100)).into();
490
0
        state.last_updated = Utc::now();
491
492
        // Update refresh timestamp
493
        {
494
0
            let mut refresh_map = self.last_portfolio_refresh.write().await;
495
0
            refresh_map.insert(state.account_id.clone(), Utc::now());
496
        }
497
498
0
        debug!(
499
0
            "Portfolio refreshed - Value: {}, Daily Loss: {}, Limit: {}",
500
            state.portfolio_value, state.current_daily_loss, state.daily_loss_limit
501
        );
502
503
0
        Ok(())
504
0
    }
505
506
    /// Activate circuit breaker
507
0
    async fn activate_circuit_breaker(
508
0
        &self,
509
0
        state: &mut CircuitBreakerState,
510
0
        reason: String,
511
0
    ) -> RiskResult<()> {
512
0
        warn!(
513
0
            "\u{1f6a8} ACTIVATING CIRCUIT BREAKER for account {}: {}",
514
            state.account_id, reason
515
        );
516
517
0
        state.is_active = true;
518
0
        state.activation_reason = Some(reason.clone());
519
0
        state.activated_at = Some(Utc::now());
520
0
        state.consecutive_violations += 1;
521
0
        state.last_updated = Utc::now();
522
523
        // Update global violation counter
524
0
        self.consecutive_violations.fetch_add(1, Ordering::SeqCst);
525
526
        // Persist to Redis
527
0
        if let Err(e) = self.persist_state_to_redis(state).await {
528
0
            error!("Failed to persist circuit breaker state to Redis: {}", e);
529
0
        }
530
531
        // Update in-memory state
532
        {
533
0
            let mut state_map = self.state.write().await;
534
0
            state_map.insert(state.account_id.clone(), state.clone());
535
        }
536
537
0
        warn!(
538
0
            "\u{26d4} Circuit breaker ACTIVE - Trading halted for account {}",
539
            state.account_id
540
        );
541
0
        Ok(())
542
0
    }
543
544
    /// Load state from Redis
545
0
    async fn load_state_from_redis(
546
0
        &self,
547
0
        account_id: &str,
548
0
    ) -> RiskResult<Option<CircuitBreakerState>> {
549
0
        let Some(ref client) = self.redis_client else {
550
0
            return Ok(None);
551
        };
552
553
0
        match client.get_multiplexed_async_connection().await {
554
0
            Ok(mut conn) => {
555
0
                let key = format!("{}:{}", self.config.redis_key_prefix, account_id);
556
0
                match conn.get::<_, Option<String>>(&key).await {
557
0
                    Ok(Some(json_data)) => {
558
0
                        match serde_json::from_str::<CircuitBreakerState>(&json_data) {
559
0
                            Ok(state) => Ok(Some(state)),
560
0
                            Err(e) => {
561
0
                                warn!(
562
0
                                    "Failed to deserialize circuit breaker state from Redis: {}",
563
                                    e
564
                                );
565
0
                                Ok(None)
566
                            },
567
                        }
568
                    },
569
0
                    Ok(None) => Ok(None),
570
0
                    Err(e) => {
571
0
                        warn!("Failed to load circuit breaker state from Redis: {}", e);
572
0
                        Ok(None)
573
                    },
574
                }
575
            },
576
0
            Err(e) => {
577
0
                warn!("Failed to connect to Redis for state loading: {}", e);
578
0
                Ok(None)
579
            },
580
        }
581
0
    }
582
583
    /// Persist state to Redis
584
0
    async fn persist_state_to_redis(&self, state: &CircuitBreakerState) -> RiskResult<()> {
585
0
        let Some(ref client) = self.redis_client else {
586
0
            return Ok(()); // No Redis client, skip persistence
587
        };
588
589
0
        match client.get_multiplexed_async_connection().await {
590
0
            Ok(mut conn) => {
591
0
                let key = format!("{}:{}", self.config.redis_key_prefix, state.account_id);
592
0
                let json_data = serde_json::to_string(state)?;
593
594
                // Set with expiration (24 hours)
595
0
                let _: RedisResult<()> = conn.set_ex(&key, json_data, 86400).await;
596
597
0
                debug!(
598
0
                    "Persisted circuit breaker state to Redis for account {}",
599
                    state.account_id
600
                );
601
0
                Ok(())
602
            },
603
0
            Err(e) => {
604
0
                warn!("Failed to connect to Redis for state persistence: {}", e);
605
0
                Ok(()) // Don't fail the operation if Redis is unavailable
606
            },
607
        }
608
0
    }
609
610
    /// Get circuit breaker metrics
611
0
    pub async fn get_metrics(&self) -> HashMap<String, f64> {
612
0
        let mut metrics = HashMap::new();
613
614
0
        let state_map = self.state.read().await;
615
0
        let active_count = state_map.values().filter(|s| s.is_active).count();
616
0
        let total_violations = self.consecutive_violations.load(Ordering::SeqCst);
617
618
0
        metrics.insert("active_circuit_breakers".to_owned(), active_count as f64);
619
0
        metrics.insert("total_violations".to_owned(), f64::from(total_violations));
620
0
        metrics.insert("accounts_monitored".to_owned(), state_map.len() as f64);
621
622
0
        metrics
623
0
    }
624
625
    /// Health check for circuit breaker
626
0
    pub async fn health_check(&self) -> bool {
627
        // Check Redis connectivity if enabled
628
0
        if let Some(ref client) = self.redis_client {
629
0
            match client.get_multiplexed_async_connection().await {
630
0
                Ok(mut conn) => (redis::cmd("PING").query_async::<String>(&mut conn).await).is_ok(),
631
0
                Err(_) => false,
632
            }
633
        } else {
634
0
            true // Always healthy if Redis not configured
635
        }
636
0
    }
637
}
638
639
// REAL BROKER CLIENT - NO MOCKS IN PRODUCTION CODE
640
/// Real broker client implementation for production use
641
pub struct RealBrokerClient {
642
    /// HTTP endpoint URL for the broker service
643
    endpoint: String,
644
}
645
646
impl RealBrokerClient {
647
    #[must_use]
648
7
    pub const fn new(endpoint: String) -> Self {
649
7
        Self { endpoint }
650
7
    }
651
}
652
653
#[async_trait]
654
impl BrokerAccountService for RealBrokerClient {
655
0
    async fn get_portfolio_value(&self, account_id: &str) -> RiskResult<Decimal> {
656
        // Real HTTP call to broker service
657
        let client = reqwest::Client::new();
658
        let response = client
659
            .get(format!(
660
                "{}/accounts/{}/portfolio/value",
661
                self.endpoint, account_id
662
            ))
663
            .send()
664
            .await
665
0
            .map_err(|e| RiskError::BrokerError(format!("Portfolio value request failed: {e}")))?;
666
667
        if !response.status().is_success() {
668
            return Err(RiskError::BrokerError(format!(
669
                "Broker returned error: {}",
670
                response.status()
671
            )));
672
        }
673
674
        let data: serde_json::Value = response
675
            .json()
676
            .await
677
0
            .map_err(|e| RiskError::BrokerError(format!("Invalid response format: {e}")))?;
678
679
0
        let portfolio_value = data["portfolio_value"].as_f64().ok_or_else(|| {
680
0
            RiskError::BrokerError("Missing portfolio_value in response".to_owned())
681
0
        })?;
682
683
        Decimal::try_from(portfolio_value).map_err(|e| RiskError::TypeConversion {
684
0
            from_type: "f64".to_owned(),
685
0
            to_type: "Decimal".to_owned(),
686
0
            reason: format!("portfolio value conversion failed: {e}"),
687
0
        })
688
0
    }
689
690
0
    async fn get_daily_pnl(&self, account_id: &str) -> RiskResult<Decimal> {
691
        // Real HTTP call to broker service
692
        let client = reqwest::Client::new();
693
        let response = client
694
            .get(format!(
695
                "{}/accounts/{}/pnl/daily",
696
                self.endpoint, account_id
697
            ))
698
            .send()
699
            .await
700
0
            .map_err(|e| RiskError::BrokerError(format!("Daily PnL request failed: {e}")))?;
701
702
        if !response.status().is_success() {
703
            return Err(RiskError::BrokerError(format!(
704
                "Broker returned error: {}",
705
                response.status()
706
            )));
707
        }
708
709
        let data: serde_json::Value = response
710
            .json()
711
            .await
712
0
            .map_err(|e| RiskError::BrokerError(format!("Invalid response format: {e}")))?;
713
714
        let daily_pnl = data["daily_pnl"]
715
            .as_f64()
716
0
            .ok_or_else(|| RiskError::BrokerError("Missing daily_pnl in response".to_owned()))?;
717
718
        Decimal::try_from(daily_pnl).map_err(|e| RiskError::TypeConversion {
719
0
            from_type: "f64".to_owned(),
720
0
            to_type: "Decimal".to_owned(),
721
0
            reason: format!("daily PnL conversion failed: {e}"),
722
0
        })
723
0
    }
724
725
0
    async fn get_positions(&self, account_id: &str) -> RiskResult<Vec<Position>> {
726
        // Real HTTP call to broker service
727
        let client = reqwest::Client::new();
728
        let response = client
729
            .get(format!(
730
                "{}/accounts/{}/positions",
731
                self.endpoint, account_id
732
            ))
733
            .send()
734
            .await
735
0
            .map_err(|e| RiskError::BrokerError(format!("Positions request failed: {e}")))?;
736
737
        if !response.status().is_success() {
738
            return Err(RiskError::BrokerError(format!(
739
                "Broker returned error: {}",
740
                response.status()
741
            )));
742
        }
743
744
        let data: serde_json::Value = response
745
            .json()
746
            .await
747
0
            .map_err(|e| RiskError::BrokerError(format!("Invalid response format: {e}")))?;
748
749
        // Parse positions array from real broker response
750
0
        let positions_array = data["positions"].as_array().ok_or_else(|| {
751
0
            RiskError::BrokerError("Missing positions array in response".to_owned())
752
0
        })?;
753
754
        let mut positions = Vec::new();
755
        for pos_data in positions_array {
756
            let symbol = pos_data["symbol"]
757
                .as_str()
758
0
                .ok_or_else(|| RiskError::BrokerError("Missing symbol in position".to_owned()))?;
759
760
            let quantity_raw = pos_data["quantity"]
761
                .as_f64()
762
0
                .ok_or_else(|| RiskError::BrokerError("Missing quantity in position".to_owned()))?;
763
764
0
            let market_value_raw = pos_data["market_value"].as_f64().ok_or_else(|| {
765
0
                RiskError::BrokerError("Missing market_value in position".to_owned())
766
0
            })?;
767
768
0
            let quantity = Decimal::try_from(quantity_raw).map_err(|_| {
769
0
                RiskError::CalculationError("Failed to convert quantity_raw to decimal".to_owned())
770
0
            })?;
771
0
            let market_value = Decimal::try_from(market_value_raw).map_err(|_| {
772
0
                RiskError::CalculationError(
773
0
                    "Failed to convert market_value_raw to decimal".to_owned(),
774
0
                )
775
0
            })?;
776
777
            // Use Position::new constructor for consistency
778
            let mut position = Position::new(
779
                symbol.to_owned(),
780
                quantity,
781
                market_value / quantity.abs().max(Decimal::ONE), // Derive avg_price from market_value
782
            );
783
784
            // Update market_value to match the actual market value from broker
785
            position.market_value = market_value;
786
            position.last_updated = Utc::now();
787
            positions.push(position);
788
        }
789
790
        Ok(positions)
791
0
    }
792
}
793
794
#[cfg(test)]
795
mod tests {
796
    use super::*;
797
    use std::sync::Arc;
798
    use tokio;
799
    // CANONICAL TYPE IMPORTS - Use types::prelude for dec! macro
800
801
7
    fn create_test_config() -> Result<CircuitBreakerConfig, Box<dyn std::error::Error>> {
802
        Ok(CircuitBreakerConfig {
803
            enabled: true,
804
7
            daily_loss_percentage: Price::from_f64(2.00)
?0
, // 2%
805
7
            position_limit_percentage: Price::from_f64(5.00)
?0
, // 5%
806
7
            redis_url: "redis://${REDIS_HOST:-localhost}:6379".to_string(), // Different port for tests
807
7
            ..Default::default()
808
        })
809
7
    }
810
811
    #[tokio::test]
812
1
    async fn test_circuit_breaker_creation() -> Result<(), Box<dyn std::error::Error>> {
813
1
        let config = create_test_config()
?0
;
814
1
        let broker_service = Arc::new(RealBrokerClient::new(
815
1
            std::env::var("FOXHUNT_BROKER_SERVICE_ENDPOINT").unwrap_or_else(|_| {
816
1
                let service_host =
817
1
                    std::env::var("SERVICE_HOST").unwrap_or_else(|_| "localhost".to_string());
818
1
                format!("http://{}:50054", service_host)
819
1
            }), // Real broker service endpoint
820
        ));
821
822
        // Circuit breaker creation might fail if Redis is not available, which is fine for tests
823
1
        let _result = RealCircuitBreaker::new(config, broker_service).await;
824
        // Don't assert success since Redis might not be available in test environment
825
        // Debug output removed for production
826
2
        Ok(())
827
1
    }
828
829
    #[tokio::test]
830
1
    async fn test_circuit_breaker_daily_loss_check() -> Result<(), Box<dyn std::error::Error>> {
831
1
        let config = create_test_config()
?0
;
832
1
        let broker_service = Arc::new(RealBrokerClient::new(
833
1
            "http://${SERVICE_HOST:-localhost}:50054".to_string(), // Real broker service endpoint
834
        ));
835
836
        // Skip test if broker service is not available
837
1
        if let Ok(
circuit_breaker0
) = RealCircuitBreaker::new(config, broker_service).await {
838
1
            let 
account_id0
=
"TEST_ACCOUNT"0
;
839
1
            let 
result0
=
circuit_breaker0
.
check_circuit_breaker0
(account_id).await;
840
1
841
1
            match 
result0
{
842
1
                Ok(
_should_trigger0
) => {
843
0
                    // Debug output removed for production
844
0
                },
845
1
                Err(
_e0
) => {
846
0
                    // Debug output removed for production
847
0
                },
848
1
            }
849
1
        } else {
850
1
            // Debug output removed for production
851
1
        }
852
1
        Ok(())
853
1
    }
854
855
    #[tokio::test]
856
1
    async fn test_circuit_breaker_disabled() -> Result<(), Box<dyn std::error::Error>> {
857
1
        let mut config = create_test_config()
?0
;
858
1
        config.enabled = false;
859
1
        let broker_service = Arc::new(RealBrokerClient::new("http://localhost:50054".to_string()));
860
861
1
        if let Ok(circuit_breaker) = RealCircuitBreaker::new(config, broker_service).await {
862
1
            let account_id = "TEST_ACCOUNT";
863
1
            let result = circuit_breaker.check_circuit_breaker(account_id).await
?0
;
864
1
            assert!(!result, 
"Circuit breaker should not trigger when disabled"0
);
865
1
        
}0
866
1
        Ok(())
867
1
    }
868
869
    #[tokio::test]
870
1
    async fn test_circuit_breaker_position_limit_zero_portfolio(
871
1
    ) -> Result<(), Box<dyn std::error::Error>> {
872
1
        let config = create_test_config()
?0
;
873
1
        let broker_service = Arc::new(RealBrokerClient::new("http://localhost:50054".to_string()));
874
875
1
        if let Ok(
circuit_breaker0
) = RealCircuitBreaker::new(config, broker_service).await {
876
1
            let 
account_id0
=
"TEST_ACCOUNT"0
;
877
1
            let 
symbol0
=
Symbol::from0
("AAPL");
878
1
            let 
quantity0
=
Quantity::from_f640
(100.0)
?0
;
879
1
880
1
            let 
result0
=
circuit_breaker0
881
0
                .check_position_limit(account_id, &symbol, quantity)
882
0
                .await?;
883
1
            
assert!0
(
884
1
                
!result0
,
885
1
                
"Should block positions when portfolio value is zero"0
886
1
            );
887
1
        }
888
1
        Ok(())
889
1
    }
890
891
    #[tokio::test]
892
1
    async fn test_circuit_breaker_consecutive_violations() -> Result<(), Box<dyn std::error::Error>>
893
1
    {
894
1
        let config = create_test_config()
?0
;
895
1
        let broker_service = Arc::new(RealBrokerClient::new("http://localhost:50054".to_string()));
896
897
1
        if let Ok(
circuit_breaker0
) = RealCircuitBreaker::new(config, broker_service).await {
898
1
            
circuit_breaker0
.
record_violation0
("Test violation 1").await;
899
1
            
circuit_breaker0
.
record_violation0
("Test violation 2").await;
900
1
            
circuit_breaker0
.
record_violation0
("Test violation 3").await;
901
1
902
1
            let 
metrics0
=
circuit_breaker0
.get_metrics().await;
903
1
            
assert_eq!0
(
metrics0
.
get0
(
"total_violations"0
).
copied0
(), Some(3.0));
904
1
        }
905
1
        Ok(())
906
1
    }
907
908
    #[tokio::test]
909
1
    async fn test_circuit_breaker_reset() -> Result<(), Box<dyn std::error::Error>> {
910
1
        let config = create_test_config()
?0
;
911
1
        let broker_service = Arc::new(RealBrokerClient::new("http://localhost:50054".to_string()));
912
913
1
        if let Ok(
circuit_breaker0
) = RealCircuitBreaker::new(config, broker_service).await {
914
1
            let 
account_id0
=
"TEST_ACCOUNT"0
;
915
1
916
1
            // Manually activate by setting state (would normally be done through check_circuit_breaker)
917
1
            let 
mut state0
=
CircuitBreakerState::default0
();
918
1
            
state.account_id0
=
account_id0
.
to_string0
();
919
1
            
state.is_active = true0
;
920
1
            
state.consecutive_violations = 30
;
921
1
922
1
            // Reset the circuit breaker
923
1
            
circuit_breaker0
924
0
                .reset_circuit_breaker(account_id, "Manual reset for testing".to_string())
925
0
                .await?;
926
1
927
1
            // Verify it was reset
928
1
            
assert!0
(
!0
circuit_breaker0
.
is_active0
(account_id).await);
929
1
        }
930
1
        Ok(())
931
1
    }
932
933
    #[tokio::test]
934
1
    async fn test_circuit_breaker_health_check() -> Result<(), Box<dyn std::error::Error>> {
935
1
        let config = create_test_config()
?0
;
936
1
        let broker_service = Arc::new(RealBrokerClient::new("http://localhost:50054".to_string()));
937
938
1
        if let Ok(
circuit_breaker0
) = RealCircuitBreaker::new(config, broker_service).await {
939
1
            // Health check might pass or fail depending on Redis availability
940
1
            let _ = 
circuit_breaker0
.
health_check0
().
await0
;
941
1
        }
942
1
        Ok(())
943
1
    }
944
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/compliance.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/compliance.rs.html deleted file mode 100644 index ac11bb451..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/compliance.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/compliance.rs
Line
Count
Source
1
//! Compliance validation and reporting module
2
// #![deny(clippy::unwrap_used, clippy::expect_used)] // COMMENTED: Crate-level allows applied
3
4
//! ENTERPRISE-GRADE Compliance validation and comprehensive audit trail system
5
//! Implements regulatory compliance including `MiFID` II, Dodd-Frank, and Basel III requirements
6
//! Provides real-time violation detection, audit logging, and regulatory reporting
7
8
use chrono::{DateTime, Duration, Utc};
9
use std::collections::HashMap;
10
use std::sync::Arc;
11
// REMOVED: Direct Decimal usage - use canonical types
12
use common::types::Price;
13
use num::FromPrimitive;
14
use rust_decimal::Decimal;
15
use serde::{Deserialize, Serialize};
16
use tokio::sync::{broadcast, RwLock};
17
use tracing::{error, info, warn};
18
use uuid::Uuid;
19
20
// Removed config module - not available in this simplified risk crate
21
use crate::error::{decimal_to_f64_safe, f64_to_price_safe, parse_env_var, RiskError, RiskResult};
22
use crate::operations::price_to_f64_safe;
23
use crate::risk_types::{
24
    AuditEntry, ComplianceConfig, ComplianceRule, OrderInfo, RiskViolation, ViolationType,
25
};
26
// Position comes from common::types::prelude::* - removed from risk_types
27
use crate::risk_types::{
28
    ComplianceWarning, ComplianceWarningType, InstrumentId, RegulatoryFlag, RegulatoryFlagType,
29
    RiskSeverity, WarningSeverity,
30
};
31
// CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
32
33
/// **Comprehensive Compliance Validation Result**
34
///
35
/// Contains the complete results of regulatory compliance validation,
36
/// including violations, warnings, and regulatory flags for audit purposes.
37
/// Provides detailed compliance assessment supporting multiple regulatory
38
/// frameworks including `MiFID` II, Dodd-Frank, and Basel III.
39
///
40
/// # Compliance Assessment Components
41
/// - **Binary Compliance Status**: Overall pass/fail determination
42
/// - **Violation Tracking**: Serious breaches requiring immediate action
43
/// - **Warning System**: Minor concerns requiring monitoring
44
/// - **Regulatory Flags**: Special handling requirements
45
/// - **Audit Trail**: Complete validation timestamp and source tracking
46
///
47
/// # Usage in Trading Workflow
48
/// ```rust
49
/// let validation_result = compliance_engine.validate_order(&order).await?;
50
///
51
/// if !validation_result.is_compliant {
52
///     for violation in &validation_result.violations {
53
///         compliance_logger.log_violation(violation).await?;
54
///     }
55
///     return Err(ComplianceError::OrderRejected);
56
/// }
57
///
58
/// // Process warnings without blocking execution
59
/// for warning in &validation_result.warnings {
60
///     compliance_monitor.track_warning(warning).await?;
61
/// }
62
/// ```
63
#[derive(Debug, Clone, Serialize, Deserialize)]
64
pub struct ComplianceValidationResult {
65
    /// Whether the validation passed all compliance checks without violations
66
    pub is_compliant: bool,
67
    /// List of serious compliance violations that prevent execution
68
    pub violations: Vec<RiskViolation>,
69
    /// List of compliance warnings that require attention but don't block execution
70
    pub warnings: Vec<ComplianceWarning>,
71
    /// Regulatory flags for special handling requirements or enhanced monitoring
72
    pub regulatory_flags: Vec<RegulatoryFlag>,
73
    /// UTC timestamp when validation was performed for audit trail
74
    pub validation_timestamp: DateTime<Utc>,
75
    /// Unique identifier of the validator instance for traceability
76
    pub validator_id: String,
77
    /// Optional additional compliance metadata and regulatory context
78
    pub metadata: Option<serde_json::Value>,
79
}
80
81
/// Compliance warning for regulatory attention
82
// ComplianceWarning is imported from crate::risk_types
83
84
// ComplianceWarningType and WarningSeverity are imported from crate::risk_types
85
86
// RegulatoryFlag is imported from crate::risk_types
87
88
// RegulatoryFlagType is imported from crate::risk_types
89
90
/// **Enhanced Audit Trail Entry with Regulatory Compliance Data**
91
///
92
/// Comprehensive audit entry that extends the base audit functionality
93
/// with regulatory compliance information required for `MiFID` II, Dodd-Frank,
94
/// and Basel III reporting requirements.
95
///
96
/// # Purpose
97
/// - Provides complete audit trail for regulatory reporting
98
/// - Tracks compliance status and regulatory references
99
/// - Includes best execution analysis for `MiFID` II
100
/// - Maintains client classification for appropriate treatment
101
/// - Records execution venue for transparency requirements
102
///
103
/// # Regulatory Framework
104
/// - **`MiFID` II**: Best execution reporting and client protection
105
/// - **Dodd-Frank**: Systematic risk monitoring and reporting
106
/// - **Basel III**: Risk scoring and capital adequacy assessment
107
///
108
/// # Usage
109
/// ```rust
110
/// use risk::compliance::EnhancedAuditEntry;
111
///
112
/// let audit_entry = EnhancedAuditEntry {
113
///     base_entry: audit_entry_base,
114
///     compliance_status: ComplianceStatus::Compliant,
115
///     regulatory_references: vec!["MiFID-II-27.1".to_string()],
116
///     risk_score: Some(Price::from(0.15)), // 15 basis points
117
///     client_classification: Some("Professional".to_string()),
118
///     execution_venue: Some("XLON".to_string()), // London Stock Exchange
119
///     best_execution_analysis: Some(best_exec_analysis),
120
/// };
121
/// ```
122
#[derive(Debug, Clone, Serialize, Deserialize)]
123
pub struct EnhancedAuditEntry {
124
    /// Base audit entry containing core transaction information
125
    pub base_entry: AuditEntry,
126
    /// Current compliance status of this transaction
127
    pub compliance_status: ComplianceStatus,
128
    /// List of regulatory rule references that apply to this transaction
129
    pub regulatory_references: Vec<String>,
130
    /// Risk score for this transaction (optional, in basis points)
131
    pub risk_score: Option<Price>,
132
    /// Client classification (Professional, Retail, Eligible Counterparty)
133
    pub client_classification: Option<String>,
134
    /// Execution venue identifier (MIC code or venue name)
135
    pub execution_venue: Option<String>,
136
    /// Best execution analysis for `MiFID` II compliance (when applicable)
137
    pub best_execution_analysis: Option<BestExecutionAnalysis>,
138
}
139
140
/// **Compliance Status Classification for Audit Entries**
141
///
142
/// Represents the current regulatory compliance status of a transaction
143
/// or audit entry. Used for real-time compliance monitoring and
144
/// regulatory reporting workflows.
145
///
146
/// # Status Hierarchy
147
/// - **Compliant**: Fully compliant with all applicable regulations
148
/// - **Warning**: Minor compliance concerns requiring attention
149
/// - **Violation**: Serious compliance breach requiring immediate action
150
/// - **`UnderReview`**: Pending compliance review by compliance team
151
///
152
/// # Usage in Workflows
153
/// ```rust
154
/// match audit_entry.compliance_status {
155
///     ComplianceStatus::Compliant => proceed_with_execution(),
156
///     ComplianceStatus::Warning => log_warning_and_proceed(),
157
///     ComplianceStatus::Violation => halt_execution_and_escalate(),
158
///     ComplianceStatus::UnderReview => queue_for_manual_review(),
159
/// }
160
/// ```
161
#[derive(Debug, Clone, Serialize, Deserialize)]
162
pub enum ComplianceStatus {
163
    /// Transaction is fully compliant with all applicable regulations
164
    Compliant,
165
    /// Minor compliance concerns detected, requires attention but not blocking
166
    Warning,
167
    /// Serious compliance violation detected, execution should be halted
168
    Violation,
169
    /// Transaction is pending compliance review by compliance team
170
    UnderReview,
171
}
172
173
/// **Best Execution Analysis for `MiFID` II Compliance**
174
///
175
/// Comprehensive analysis of execution quality required under `MiFID` II
176
/// Article 27 (Best Execution) and RTS 28 (Execution Quality Reports).
177
/// Evaluates execution venues against multiple criteria to demonstrate
178
/// best execution compliance.
179
///
180
/// # `MiFID` II Requirements
181
/// - **Article 27**: Best execution obligation for investment firms
182
/// - **RTS 28**: Annual execution quality reports
183
/// - **Execution Factors**: Price, costs, speed, likelihood of execution
184
/// - **Venue Analysis**: Systematic comparison of execution venues
185
///
186
/// # Analysis Components
187
/// - Venue-by-venue performance comparison
188
/// - Price improvement measurement vs. market
189
/// - Execution speed analysis
190
/// - Fill probability assessment
191
/// - Comprehensive cost breakdown
192
///
193
/// # Usage
194
/// ```rust
195
/// let analysis = BestExecutionAnalysis {
196
///     venue_analysis: venue_metrics_map,
197
///     price_improvement: Some(Price::from(0.0025)), // 2.5 bps improvement
198
///     speed_of_execution: Duration::from_millis(150),
199
///     likelihood_of_execution: Price::from(0.98), // 98% fill probability
200
///     cost_analysis: total_cost_breakdown,
201
/// };
202
/// ```
203
#[derive(Debug, Clone, Serialize, Deserialize)]
204
pub struct BestExecutionAnalysis {
205
    /// Performance metrics for each available execution venue
206
    pub venue_analysis: HashMap<String, VenueMetrics>,
207
    /// Price improvement achieved vs. market benchmark (in basis points)
208
    pub price_improvement: Option<Price>,
209
    /// Total time from order submission to complete execution
210
    pub speed_of_execution: Duration,
211
    /// Probability of complete execution at this venue (0.0 to 1.0)
212
    pub likelihood_of_execution: Price,
213
    /// Comprehensive breakdown of all execution costs
214
    pub cost_analysis: CostAnalysis,
215
}
216
217
/// **Execution Venue Performance Metrics**
218
///
219
/// Detailed performance statistics for an execution venue used in
220
/// best execution analysis under `MiFID` II. Tracks key execution
221
/// quality indicators required for regulatory reporting.
222
///
223
/// # Key Performance Indicators
224
/// - **Spread Analysis**: Average bid-ask spread characteristics
225
/// - **Fill Rate**: Percentage of orders successfully executed
226
/// - **Execution Speed**: Average time to complete execution
227
/// - **Market Impact**: Price impact measurement for executed orders
228
///
229
/// # Regulatory Context
230
/// These metrics support `MiFID` II RTS 28 reporting requirements
231
/// for annual execution quality reports and best execution
232
/// compliance demonstration.
233
///
234
/// # Usage
235
/// ```rust
236
/// let venue_metrics = VenueMetrics {
237
///     venue_name: "XLON".to_string(), // London Stock Exchange
238
///     average_spread: Price::from(0.0015), // 1.5 bps average spread
239
///     fill_rate: Price::from(0.985), // 98.5% fill rate
240
///     average_execution_time: Duration::from_millis(120),
241
///     market_impact: Price::from(0.0008), // 0.8 bps market impact
242
/// };
243
/// ```
244
#[derive(Debug, Clone, Serialize, Deserialize)]
245
pub struct VenueMetrics {
246
    /// Official venue name or MIC (Market Identifier Code)
247
    pub venue_name: String,
248
    /// Average bid-ask spread observed at this venue (in basis points)
249
    pub average_spread: Price,
250
    /// Percentage of orders successfully filled (0.0 to 1.0)
251
    pub fill_rate: Price,
252
    /// Average time from order submission to execution completion
253
    pub average_execution_time: Duration,
254
    /// Average market impact of executed orders (in basis points)
255
    pub market_impact: Price,
256
    /// Volume-weighted average price quality at this venue
257
    pub vwap_quality: Option<Price>,
258
    /// Percentage of time this venue provides best bid/offer (0.0 to 1.0)
259
    pub top_of_book_percentage: Option<Price>,
260
}
261
262
/// **Comprehensive Cost Analysis for Best Execution**
263
///
264
/// Detailed breakdown of all execution costs required for `MiFID` II
265
/// best execution analysis and RTS 28 reporting. Categorizes costs
266
/// into explicit, implicit, and market impact components.
267
///
268
/// # Cost Categories (`MiFID` II Framework)
269
/// - **Explicit Costs**: Direct fees, commissions, taxes, and charges
270
/// - **Implicit Costs**: Bid-ask spread costs and timing costs
271
/// - **Market Impact**: Price movement caused by order execution
272
/// - **Total Costs**: Comprehensive cost including all components
273
///
274
/// # Regulatory Requirements
275
/// - `MiFID` II Article 27: Best execution cost analysis
276
/// - RTS 28: Annual execution quality reports
277
/// - Commission Delegated Directive: Cost disclosure requirements
278
///
279
/// # Usage
280
/// ```rust
281
/// let cost_analysis = CostAnalysis {
282
///     explicit_costs: Price::from(0.0015), // 1.5 bps commission
283
///     implicit_costs: Price::from(0.0008), // 0.8 bps spread cost
284
///     market_impact_costs: Price::from(0.0012), // 1.2 bps impact
285
///     total_costs: Price::from(0.0035), // 3.5 bps total
286
/// };
287
/// ```
288
#[derive(Debug, Clone, Serialize, Deserialize)]
289
pub struct CostAnalysis {
290
    /// Direct costs including commissions, fees, taxes (in basis points)
291
    pub explicit_costs: Price,
292
    /// Indirect costs including spread and timing costs (in basis points)
293
    pub implicit_costs: Price,
294
    /// Market impact costs from order execution (in basis points)
295
    pub market_impact_costs: Price,
296
    /// Total execution costs across all categories (in basis points)
297
    pub total_costs: Price,
298
}
299
300
/// **Regulatory Reporting Configuration**
301
///
302
/// Configuration for multiple regulatory frameworks and their
303
/// reporting requirements. Manages endpoints, intervals, and
304
/// feature flags for various regulatory compliance systems.
305
///
306
/// # Supported Regulatory Frameworks
307
/// - **`MiFID` II**: Markets in Financial Instruments Directive
308
/// - **Dodd-Frank**: US Financial Reform Act
309
/// - **Basel III**: International Banking Regulations
310
/// - **EMIR**: European Market Infrastructure Regulation
311
///
312
/// # Configuration Components
313
/// - Feature flags to enable/disable specific frameworks
314
/// - Reporting endpoints for regulatory submissions
315
/// - Configurable reporting intervals per framework
316
/// - Extensible design for additional regulations
317
///
318
/// # Usage
319
/// ```rust
320
/// let config = RegulatoryReportingConfig {
321
///     mifid2_enabled: true,
322
///     mifid2_reporting_endpoint: Some("https://esma.europa.eu/api".to_string()),
323
///     dodd_frank_enabled: true,
324
///     basel_iii_enabled: true,
325
///     emir_enabled: true,
326
///     reporting_intervals: HashMap::from([
327
///         ("mifid2_best_execution".to_string(), Duration::from_secs(86400)), // Daily
328
///         ("dodd_frank_swap_data".to_string(), Duration::from_secs(3600)),   // Hourly
329
///     ]),
330
/// };
331
/// ```
332
#[derive(Debug, Clone)]
333
pub struct RegulatoryReportingConfig {
334
    /// Enable `MiFID` II compliance and reporting features
335
    pub mifid2_enabled: bool,
336
    /// API endpoint for `MiFID` II regulatory submissions
337
    pub mifid2_reporting_endpoint: Option<String>,
338
    /// Enable Dodd-Frank compliance and reporting features
339
    pub dodd_frank_enabled: bool,
340
    /// Enable Basel III compliance and reporting features
341
    pub basel_iii_enabled: bool,
342
    /// Enable European Market Infrastructure Regulation compliance
343
    pub emir_enabled: bool,
344
    /// Configurable reporting intervals for each regulatory framework
345
    pub reporting_intervals: HashMap<String, Duration>,
346
}
347
348
/// **Enterprise-Grade Compliance Validator**
349
///
350
/// Comprehensive regulatory compliance validation engine supporting
351
/// multiple regulatory frameworks including `MiFID` II, Dodd-Frank,
352
/// Basel III, and EMIR. Provides real-time compliance checking,
353
/// audit trail management, and regulatory reporting.
354
///
355
/// # Core Capabilities
356
/// - **Multi-Regulatory Support**: `MiFID` II, Dodd-Frank, Basel III, EMIR
357
/// - **Real-Time Validation**: Sub-microsecond compliance checking
358
/// - **Audit Trail Management**: Complete transaction audit logging
359
/// - **Position Limit Monitoring**: Dynamic limit enforcement
360
/// - **Best Execution Analysis**: `MiFID` II Article 27 compliance
361
/// - **Client Classification**: Regulatory client categorization
362
/// - **Violation Broadcasting**: Real-time compliance alerts
363
///
364
/// # Thread Safety
365
/// All internal state is protected by `Arc<RwLock<>>` for safe
366
/// concurrent access across multiple trading threads.
367
///
368
/// # Usage
369
/// ```rust
370
/// let validator = ComplianceValidator::new(
371
///     compliance_config,
372
///     regulatory_config,
373
/// ).await?;
374
///
375
/// let result = validator.validate_order(&order_info).await?;
376
/// if !result.is_compliant {
377
///     // Handle compliance violations
378
///     for violation in result.violations {
379
///         compliance_handler.escalate_violation(violation).await?;
380
///     }
381
/// }
382
/// ```
383
#[derive(Debug)]
384
pub struct ComplianceValidator {
385
    /// Core compliance configuration and rules
386
    config: ComplianceConfig,
387
    /// Regulatory framework configuration and endpoints
388
    regulatory_config: RegulatoryReportingConfig,
389
    /// Thread-safe audit trail storage for regulatory reporting
390
    audit_trail: Arc<RwLock<Vec<EnhancedAuditEntry>>>,
391
    /// Dynamic compliance rules loaded from configuration
392
    // Infrastructure - will be used for dynamic compliance rule evaluation
393
    #[allow(dead_code)]
394
    compliance_rules: Arc<RwLock<HashMap<String, ComplianceRule>>>,
395
    /// Position limits per instrument for risk management
396
    position_limits: Arc<RwLock<HashMap<String, PositionLimit>>>,
397
    /// Client regulatory classifications (Professional, Retail, etc.)
398
    client_classifications: Arc<RwLock<HashMap<String, ClientClassification>>>,
399
    /// Best execution venue metrics for `MiFID` II compliance
400
    best_execution_venues: Arc<RwLock<HashMap<String, VenueMetrics>>>,
401
    /// Broadcast channel for real-time violation notifications
402
    violation_broadcast: broadcast::Sender<RiskViolation>,
403
    /// Broadcast channel for compliance warning notifications
404
    warning_broadcast: broadcast::Sender<ComplianceWarning>,
405
    /// Unique identifier for this validator instance
406
    validator_id: String,
407
}
408
409
/// **Position Limit Configuration for Regulatory Compliance**
410
///
411
/// Defines position limits and risk constraints for individual
412
/// instruments as required by various regulatory frameworks.
413
/// Supports Basel III capital requirements, `MiFID` II position
414
/// limits, and internal risk management policies.
415
///
416
/// # Regulatory Framework Support
417
/// - **Basel III**: Capital adequacy and leverage ratio requirements
418
/// - **`MiFID` II**: Position limit requirements for commodity derivatives
419
/// - **EMIR**: Risk mitigation techniques for OTC derivatives
420
/// - **Internal Risk**: Firm-specific risk management policies
421
///
422
/// # Limit Types
423
/// - **Position Size**: Maximum allowed position in this instrument
424
/// - **Daily Turnover**: Maximum daily trading volume limit
425
/// - **Concentration**: Maximum percentage of portfolio in this instrument
426
/// - **Regulatory Basis**: The regulation requiring this limit
427
///
428
/// # Usage
429
/// ```rust
430
/// let position_limit = PositionLimit {
431
///     instrument_id: InstrumentId::from("TEST_INSTRUMENT_001"),
432
///     max_position_size: Price::from(1000000.0), // $1M max position
433
///     max_daily_turnover: Price::from(5000000.0), // $5M daily volume
434
///     concentration_limit: Price::from(0.05), // 5% of portfolio max
435
///     regulatory_basis: "Basel III".to_string(),
436
///     limit_currency: "USD".to_string(),
437
///     effective_date: Utc::now(),
438
///     expiry_date: None, // Permanent limit
439
/// };
440
/// ```
441
#[derive(Debug, Clone, Serialize, Deserialize)]
442
pub struct PositionLimit {
443
    /// Unique identifier for the instrument this limit applies to
444
    pub instrument_id: InstrumentId,
445
    /// Maximum allowed position size in base currency
446
    pub max_position_size: Price,
447
    /// Maximum daily trading volume allowed in base currency
448
    pub max_daily_turnover: Price,
449
    /// Maximum concentration as percentage of total portfolio (0.0 to 1.0)
450
    pub concentration_limit: Price,
451
    /// Regulatory framework requiring this limit (e.g., "Basel III", "`MiFID` II")
452
    pub regulatory_basis: String,
453
}
454
455
/// **Client Classification for Regulatory Purposes**
456
///
457
/// Comprehensive client categorization system required under `MiFID` II
458
/// and other regulatory frameworks. Determines appropriate treatment,
459
/// risk limits, and regulatory protections for each client type.
460
///
461
/// # Regulatory Context
462
/// - **`MiFID` II**: Client categorization and protection levels
463
/// - **ESMA Guidelines**: Investment advice and portfolio management
464
/// - **FCA Handbook**: Client classification requirements
465
/// - **Basel III**: Counterparty risk assessment
466
///
467
/// # Classification Impact
468
/// - **Protection Level**: Regulatory protections based on classification
469
/// - **Leverage Limits**: Maximum allowable leverage per client type
470
/// - **Risk Tolerance**: Investment suitability assessment
471
/// - **Product Access**: Eligible products and services
472
/// - **Disclosure Requirements**: Information that must be provided
473
///
474
/// # Usage
475
/// ```rust
476
/// let client_classification = ClientClassification {
477
///     client_id: "CLIENT-12345".to_string(),
478
///     classification: ClientType::ProfessionalClient,
479
///     leverage_limit: Price::from(30.0), // 30:1 leverage
480
///     risk_tolerance: RiskTolerance::Moderate,
481
///     regulatory_restrictions: vec![
482
///         "NO_COMPLEX_DERIVATIVES".to_string(),
483
///         "ENHANCED_DUE_DILIGENCE".to_string(),
484
///     ],
485
/// };
486
/// ```
487
#[derive(Debug, Clone, Serialize, Deserialize)]
488
pub struct ClientClassification {
489
    /// Unique identifier for the client in the system
490
    pub client_id: String,
491
    /// Regulatory classification determining protection level
492
    pub classification: ClientType,
493
    /// Maximum leverage ratio allowed for this client
494
    pub leverage_limit: Price,
495
    /// Assessed risk tolerance level for investment suitability
496
    pub risk_tolerance: RiskTolerance,
497
    /// List of regulatory restrictions applicable to this client
498
    pub regulatory_restrictions: Vec<String>,
499
}
500
501
/// **Client Types for Regulatory Compliance**
502
///
503
/// `MiFID` II client categorization determining the level of regulatory
504
/// protection and the range of services that can be provided.
505
/// Each category has different requirements for disclosure, suitability,
506
/// and investor protection.
507
///
508
/// # Regulatory Framework
509
/// - **Article 4**: `MiFID` II client categorization definitions
510
/// - **Annex II**: Professional client criteria
511
/// - **Article 30**: Information requirements per client type
512
///
513
/// # Protection Levels (Highest to Lowest)
514
/// 1. **Retail Client**: Maximum regulatory protection
515
/// 2. **Professional Client**: Reduced protection, increased access
516
/// 3. **Eligible Counterparty**: Minimal protection, full market access
517
/// 4. **Institutional Investor**: Specialized category with custom rules
518
///
519
/// # Usage in Compliance
520
/// ```rust
521
/// match client.classification {
522
///     ClientType::RetailClient => apply_full_protection(),
523
///     ClientType::ProfessionalClient => apply_reduced_protection(),
524
///     ClientType::EligibleCounterparty => apply_minimal_protection(),
525
///     ClientType::InstitutionalInvestor => apply_institutional_rules(),
526
/// }
527
/// ```
528
#[derive(Debug, Clone, Serialize, Deserialize)]
529
pub enum ClientType {
530
    /// Retail client requiring maximum regulatory protection under `MiFID` II
531
    RetailClient,
532
    /// Professional client with reduced protection but increased market access
533
    ProfessionalClient,
534
    /// Eligible counterparty with minimal protection and full market access
535
    EligibleCounterparty,
536
    /// Institutional investor with specialized regulatory treatment
537
    InstitutionalInvestor,
538
}
539
540
/// **Risk Tolerance Levels for Investment Suitability**
541
///
542
/// Client risk tolerance assessment required under `MiFID` II for
543
/// investment advice and portfolio management services. Determines
544
/// appropriate investment products and strategies.
545
///
546
/// # Regulatory Requirements
547
/// - **`MiFID` II Article 25**: Suitability assessment requirements
548
/// - **ESMA Guidelines**: Investment advice and portfolio management
549
/// - **Risk Questionnaire**: Standardized risk assessment process
550
///
551
/// # Risk Level Characteristics
552
/// - **Conservative**: Capital preservation, minimal volatility tolerance
553
/// - **Moderate**: Balanced approach, moderate volatility acceptance
554
/// - **Aggressive**: Growth focused, high volatility tolerance
555
/// - **Speculative**: Maximum risk, complex product eligibility
556
///
557
/// # Impact on Product Access
558
/// ```rust
559
/// let eligible_products = match client.risk_tolerance {
560
///     RiskTolerance::Conservative => conservative_product_universe(),
561
///     RiskTolerance::Moderate => balanced_product_universe(),
562
///     RiskTolerance::Aggressive => growth_product_universe(),
563
///     RiskTolerance::Speculative => full_product_universe(),
564
/// };
565
/// ```
566
#[derive(Debug, Clone, Serialize, Deserialize)]
567
pub enum RiskTolerance {
568
    /// Conservative risk profile - capital preservation focused
569
    Conservative,
570
    /// Moderate risk profile - balanced growth and preservation
571
    Moderate,
572
    /// Aggressive risk profile - growth focused with volatility tolerance
573
    Aggressive,
574
    /// Speculative risk profile - maximum risk tolerance for complex products
575
    Speculative,
576
}
577
578
impl Default for RegulatoryReportingConfig {
579
14
    fn default() -> Self {
580
14
        Self {
581
14
            mifid2_enabled: true,
582
14
            mifid2_reporting_endpoint: None,
583
14
            dodd_frank_enabled: true,
584
14
            basel_iii_enabled: true,
585
14
            emir_enabled: true,
586
14
            reporting_intervals: HashMap::new(),
587
14
        }
588
14
    }
589
}
590
591
impl ComplianceValidator {
592
    /// **Create a New Enterprise-Grade Compliance Validator**
593
    ///
594
    /// Initializes a comprehensive compliance validation engine with
595
    /// support for multiple regulatory frameworks. Sets up internal
596
    /// state management, broadcast channels, and regulatory configuration.
597
    ///
598
    /// # Parameters
599
    /// - `config`: Core compliance configuration and rules
600
    /// - `regulatory_config`: Multi-regulatory framework settings
601
    ///
602
    /// # Returns
603
    /// Fully initialized `ComplianceValidator` ready for real-time
604
    /// compliance checking across trading operations.
605
    ///
606
    /// # Thread Safety
607
    /// Creates thread-safe internal state using `Arc<RwLock<>>`
608
    /// for concurrent access across multiple trading threads.
609
    ///
610
    /// # Usage
611
    /// ```rust
612
    /// let validator = ComplianceValidator::new(
613
    ///     ComplianceConfig::default(),
614
    ///     RegulatoryReportingConfig::default(),
615
    /// );
616
    /// ```
617
    #[must_use]
618
14
    pub fn new(config: ComplianceConfig, regulatory_config: RegulatoryReportingConfig) -> Self {
619
14
        let (violation_broadcast, _) = broadcast::channel(1000);
620
14
        let (warning_broadcast, _) = broadcast::channel(1000);
621
622
14
        Self {
623
14
            config,
624
14
            regulatory_config,
625
14
            audit_trail: Arc::new(RwLock::new(Vec::new())),
626
14
            compliance_rules: Arc::new(RwLock::new(HashMap::new())),
627
14
            position_limits: Arc::new(RwLock::new(HashMap::new())),
628
14
            client_classifications: Arc::new(RwLock::new(HashMap::new())),
629
14
            best_execution_venues: Arc::new(RwLock::new(HashMap::new())),
630
14
            violation_broadcast,
631
14
            warning_broadcast,
632
14
            validator_id: Uuid::new_v4().to_string(),
633
14
        }
634
14
    }
635
636
    /// **Comprehensive Order Validation with Full Regulatory Compliance**
637
    ///
638
    /// Performs complete regulatory compliance validation for trading orders
639
    /// across multiple regulatory frameworks including `MiFID` II, Dodd-Frank,
640
    /// Basel III, and EMIR. Returns detailed compliance assessment.
641
    ///
642
    /// # Validation Components
643
    /// - **Position Limits**: Basel III and internal risk limit validation
644
    /// - **Client Suitability**: `MiFID` II Article 25 suitability assessment
645
    /// - **Market Abuse Detection**: MAR compliance and suspicious activity
646
    /// - **Best Execution**: `MiFID` II Article 27 best execution analysis
647
    /// - **Regulatory Flags**: Special handling requirements identification
648
    ///
649
    /// # Parameters
650
    /// - `order`: Order information to validate
651
    /// - `client_id`: Optional client identifier for suitability checks
652
    ///
653
    /// # Returns
654
    /// `ComplianceValidationResult` containing:
655
    /// - Overall compliance status (pass/fail)
656
    /// - List of compliance violations (blocking)
657
    /// - List of compliance warnings (non-blocking)
658
    /// - Regulatory flags for special handling
659
    /// - Complete audit trail information
660
    ///
661
    /// # Errors
662
    /// Returns `RiskError` for:
663
    /// - Database connectivity issues
664
    /// - Configuration loading failures
665
    /// - Internal compliance engine errors
666
    ///
667
    /// # Usage
668
    /// ```rust
669
    /// let result = validator.validate_order(&order_info, Some("CLIENT-123")).await?;
670
    ///
671
    /// if !result.is_compliant {
672
    ///     for violation in &result.violations {
673
    ///         log::error!("Compliance violation: {:?}", violation);
674
    ///     }
675
    ///     return Err(ComplianceError::OrderRejected);
676
    /// }
677
    ///
678
    /// // Process warnings without blocking execution
679
    /// for warning in &result.warnings {
680
    ///     compliance_monitor.track_warning(warning).await?;
681
    /// }
682
    /// ```
683
10
    pub async fn validate_order(
684
10
        &self,
685
10
        order: &OrderInfo,
686
10
        client_id: Option<&str>,
687
10
    ) -> RiskResult<ComplianceValidationResult> {
688
10
        info!(
689
0
            "\u{1f50d} Validating order {} for comprehensive regulatory compliance",
690
            order.order_id
691
        );
692
693
10
        let mut violations = Vec::new();
694
10
        let mut warnings = Vec::new();
695
10
        let mut regulatory_flags = Vec::new();
696
697
        // 1. Position limit validation
698
10
        if let Some(
position_violations0
) = self.validate_position_limits(order).await
?0
{
699
0
            violations.extend(position_violations);
700
10
        }
701
702
        // 2. Client suitability validation (MiFID II requirement)
703
10
        if let Some(
client_id1
) = client_id {
704
1
            if let Some(suitability_warnings) =
705
1
                self.validate_client_suitability(order, client_id).await
?0
706
1
            {
707
1
                warnings.extend(suitability_warnings);
708
1
            
}0
709
9
        }
710
711
        // 3. Market abuse detection
712
10
        if let Some(
market_abuse_flags2
) = self.detect_market_abuse_risk(order).await
?0
{
713
2
            regulatory_flags.extend(market_abuse_flags);
714
8
        }
715
716
        // 4. Best execution analysis (MiFID II requirement)
717
10
        if self.regulatory_config.mifid2_enabled {
718
10
            if let Some(execution_warnings) = self.analyze_best_execution(order).await
?0
{
719
10
                warnings.extend(execution_warnings);
720
10
            
}0
721
0
        }
722
723
        // 5. Transaction reporting requirements
724
10
        let reporting_flags = self.check_transaction_reporting_requirements(order).await
?0
;
725
10
        regulatory_flags.extend(reporting_flags);
726
727
        // 6. Leverage and concentration risk validation (Basel III)
728
10
        if self.regulatory_config.basel_iii_enabled {
729
10
            if let Some(
basel_warnings6
) = self.validate_basel_iii_requirements(order).await
?0
{
730
6
                warnings.extend(basel_warnings);
731
6
            
}4
732
0
        }
733
734
        // Create comprehensive audit entry
735
10
        let audit_entry = EnhancedAuditEntry {
736
10
            base_entry: AuditEntry {
737
10
                id: format!("order_validation_{}", order.order_id),
738
10
                timestamp: Utc::now().timestamp(),
739
10
                event_type: "COMPREHENSIVE_ORDER_VALIDATION".to_owned(),
740
10
                description: format!(
741
10
                    "Full regulatory compliance validation for order {}",
742
10
                    order.order_id
743
10
                ),
744
10
                actor: "ComplianceValidator".to_owned(),
745
10
                user_id: client_id.map(ToOwned::to_owned),
746
10
                instrument_id: Some(order.instrument_id.clone()),
747
10
                portfolio_id: order.portfolio_id.clone(),
748
10
                data: {
749
10
                    let mut data = HashMap::new();
750
10
                    data.insert("order_type".to_owned(), format!("{:?}", order.order_type));
751
10
                    data.insert("side".to_owned(), format!("{:?}", order.side));
752
10
                    data.insert("quantity".to_owned(), order.quantity.to_string());
753
10
                    data.insert("price".to_owned(), order.price.to_string());
754
10
                    data
755
10
                },
756
10
                metadata: HashMap::new(),
757
10
            },
758
10
            compliance_status: if violations.is_empty() {
759
10
                if warnings.is_empty() {
760
0
                    ComplianceStatus::Compliant
761
                } else {
762
10
                    ComplianceStatus::Warning
763
                }
764
            } else {
765
0
                ComplianceStatus::Violation
766
            },
767
10
            regulatory_references: vec![
768
10
                "MiFID II Article 27".to_owned(),
769
10
                "Basel III Capital Requirements".to_owned(),
770
10
                "Dodd-Frank Section 165".to_owned(),
771
            ],
772
            risk_score: Some(
773
10
                self.calculate_order_risk_score(order, &violations, &warnings)
774
10
                    .await
775
10
                    .unwrap_or_else(|e| 
{0
776
0
                        error!("Failed to calculate order risk score: {}", e);
777
0
                        Price::ZERO
778
0
                    }),
779
            ),
780
10
            client_classification: client_id.map(|_id| 
"ProfessionalClient"1
.
to_owned1
()),
781
10
            execution_venue: Some("PRIMARY_EXCHANGE".to_owned()),
782
10
            best_execution_analysis: None, // Would be populated with actual analysis
783
        };
784
785
10
        self.log_enhanced_audit_entry(audit_entry).await
?0
;
786
787
        // Broadcast violations and warnings
788
10
        for 
violation0
in &violations {
789
0
            let _ = self.violation_broadcast.send(violation.clone());
790
0
        }
791
28
        for 
warning18
in &warnings {
792
18
            let _ = self.warning_broadcast.send(warning.clone());
793
18
        }
794
795
10
        let result = ComplianceValidationResult {
796
10
            is_compliant: violations.is_empty(),
797
10
            violations,
798
10
            warnings,
799
10
            regulatory_flags,
800
10
            validation_timestamp: Utc::now(),
801
10
            validator_id: self.validator_id.clone(),
802
10
            metadata: None,
803
10
        };
804
805
10
        if !result.is_compliant {
806
0
            error!(
807
0
                "\u{274c} Order {} FAILED compliance validation with {} violations",
808
                order.order_id,
809
0
                result.violations.len()
810
            );
811
10
        } else if !result.warnings.is_empty() {
812
10
            warn!(
813
0
                "\u{26a0}\u{fe0f} Order {} has {} compliance warnings",
814
                order.order_id,
815
0
                result.warnings.len()
816
            );
817
        } else {
818
0
            info!(
819
0
                "\u{2705} Order {} PASSED comprehensive compliance validation",
820
                order.order_id
821
            );
822
        }
823
824
10
        Ok(result)
825
10
    }
826
827
    /// **Validate Position Limits Against Regulatory Requirements**
828
    ///
829
    /// Validates trading orders against position limits as required by
830
    /// Basel III capital requirements and internal risk management policies.
831
    /// Checks maximum position size, daily turnover limits, and concentration limits.
832
    ///
833
    /// # Regulatory Framework
834
    /// - **Basel III**: Capital adequacy and leverage ratio requirements
835
    /// - **`MiFID` II**: Position limit requirements for commodity derivatives
836
    /// - **Internal Risk**: Firm-specific risk management policies
837
    ///
838
    /// # Validation Checks
839
    /// - **Position Size**: Order value vs. maximum allowed position
840
    /// - **Daily Turnover**: Cumulative daily trading vs. daily limits
841
    /// - **Concentration**: Position percentage vs. portfolio concentration limits
842
    ///
843
    /// # Parameters
844
    /// - `order`: Order information to validate against position limits
845
    ///
846
    /// # Returns
847
    /// - `None`: No position limit violations found
848
    /// - `Some(Vec<RiskViolation>)`: List of position limit violations
849
    ///
850
    /// # Errors
851
    /// Returns `RiskError` for type conversion failures or calculation errors.
852
10
    async fn validate_position_limits(
853
10
        &self,
854
10
        order: &OrderInfo,
855
10
    ) -> RiskResult<Option<Vec<RiskViolation>>> {
856
10
        let position_limits = self.position_limits.read().await;
857
858
10
        if let Some(
limit1
) = position_limits.get(&order.instrument_id) {
859
            // Calculate order market value for comparison with price-based limit
860
1
            let order_price = order.price;
861
1
            let quantity_f64 = decimal_to_f64_safe(
862
1
                order
863
1
                    .quantity
864
1
                    .to_decimal()
865
1
                    .map_err(|_| RiskError::TypeConversion {
866
0
                        from_type: "Quantity".to_owned(),
867
0
                        to_type: "Decimal".to_owned(),
868
0
                        reason: "quantity conversion failed".to_owned(),
869
0
                    })?,
870
1
                "order quantity conversion",
871
0
            )?;
872
1
            let price_f64 = decimal_to_f64_safe(
873
1
                order_price
874
1
                    .to_decimal()
875
1
                    .map_err(|_| RiskError::TypeConversion {
876
0
                        from_type: "Price".to_owned(),
877
0
                        to_type: "Decimal".to_owned(),
878
0
                        reason: "price conversion failed".to_owned(),
879
0
                    })?,
880
1
                "order price conversion",
881
0
            )?;
882
1
            let order_market_value =
883
1
                f64_to_price_safe(quantity_f64 * price_f64, "order market value calculation")
?0
;
884
885
1
            if order_market_value > limit.max_position_size {
886
0
                let violation = RiskViolation {
887
0
                    id: Uuid::new_v4().to_string(),
888
0
                    violation_type: ViolationType::PositionLimit,
889
0
                    severity: RiskSeverity::High,
890
0
                    message: format!("Position limit exceeded for {}", order.instrument_id),
891
0
                    description: format!(
892
0
                        "Position limit exceeded for {}: current {} exceeds limit {}",
893
0
                        order.instrument_id, order_market_value, limit.max_position_size
894
0
                    ),
895
0
                    instrument_id: Some(order.instrument_id.clone()),
896
0
                    portfolio_id: order.portfolio_id.clone(),
897
0
                    strategy_id: order.strategy_id.clone(),
898
0
                    current_value: Some(order_market_value),
899
0
                    limit_value: Some(limit.max_position_size),
900
0
                    breach_amount: Some(order_market_value - limit.max_position_size),
901
0
                    timestamp: Some(Utc::now().timestamp()),
902
0
                    resolved: false,
903
0
                };
904
0
                return Ok(Some(vec![violation]));
905
1
            }
906
9
        }
907
908
10
        Ok(None)
909
10
    }
910
911
    /// **Validate Client Suitability (`MiFID` II Article 25)**
912
    ///
913
    /// Validates trading orders against client suitability requirements
914
    /// as mandated by `MiFID` II Article 25. Ensures orders are appropriate
915
    /// for the client's risk profile, experience, and investment objectives.
916
    ///
917
    /// # Regulatory Requirements
918
    /// - **`MiFID` II Article 25**: Suitability assessment for investment advice
919
    /// - **ESMA Guidelines**: Investment advice and portfolio management
920
    /// - **Know Your Customer (KYC)**: Client due diligence requirements
921
    ///
922
    /// # Suitability Checks
923
    /// - **Risk Tolerance**: Order size vs. client risk profile
924
    /// - **Investment Experience**: Product complexity vs. client experience
925
    /// - **Financial Capacity**: Order value vs. client financial situation
926
    /// - **Investment Objectives**: Order type vs. stated investment goals
927
    ///
928
    /// # Parameters
929
    /// - `order`: Order information to validate for suitability
930
    /// - `client_id`: Client identifier for classification lookup
931
    ///
932
    /// # Returns
933
    /// - `None`: Order is suitable for client
934
    /// - `Some(Vec<ComplianceWarning>)`: List of suitability concerns
935
    ///
936
    /// # Errors
937
    /// Returns `RiskError` for type conversion or calculation failures.
938
1
    async fn validate_client_suitability(
939
1
        &self,
940
1
        order: &OrderInfo,
941
1
        client_id: &str,
942
1
    ) -> RiskResult<Option<Vec<ComplianceWarning>>> {
943
1
        let client_classifications = self.client_classifications.read().await;
944
945
1
        if let Some(client) = client_classifications.get(client_id) {
946
1
            let mut warnings = Vec::new();
947
948
            // Check if order size is appropriate for client risk tolerance - use safe conversions
949
1
            let order_price = order.price;
950
951
1
            let quantity_f64 = decimal_to_f64_safe(
952
1
                order
953
1
                    .quantity
954
1
                    .to_decimal()
955
1
                    .map_err(|_| RiskError::TypeConversion {
956
0
                        from_type: "Quantity".to_owned(),
957
0
                        to_type: "Decimal".to_owned(),
958
0
                        reason: "quantity conversion failed".to_owned(),
959
0
                    })?,
960
1
                "order quantity conversion for suitability",
961
0
            )?;
962
1
            let price_f64 = decimal_to_f64_safe(
963
1
                order_price
964
1
                    .to_decimal()
965
1
                    .map_err(|_| RiskError::TypeConversion {
966
0
                        from_type: "Price".to_owned(),
967
0
                        to_type: "Decimal".to_owned(),
968
0
                        reason: "price conversion failed".to_owned(),
969
0
                    })?,
970
1
                "order price conversion for suitability",
971
0
            )?;
972
1
            let order_value =
973
1
                FromPrimitive::from_f64(quantity_f64 * price_f64).unwrap_or_else(|| 
{0
974
0
                    warn!(
975
0
                        "Failed to calculate order value for client suitability check, using ZERO"
976
                    );
977
0
                    Decimal::ZERO
978
0
                });
979
980
1
            match client.risk_tolerance {
981
1
                RiskTolerance::Conservative if order_value > Decimal::from(1000) => {
982
1
                    warnings.push(ComplianceWarning {
983
1
                        id: Uuid::new_v4().to_string(),
984
1
                        warning_type: ComplianceWarningType::NearLimit,
985
1
                        severity: WarningSeverity::Medium,
986
1
                        message: format!("Position approaching regulatory limit for {}", order.instrument_id),
987
1
                        description: format!(
988
1
                            "Order value exceeds conservative client risk tolerance for {}: order value {}",
989
1
                            order.instrument_id, order_value
990
1
                        ),
991
1
                        instrument_id: Some(order.instrument_id.clone()),
992
1
                        portfolio_id: order.portfolio_id.clone(),                        regulatory_reference: "MiFID II Article 25 - Client Suitability".to_owned(),
993
1
                        recommended_action: "Review client suitability assessment".to_owned(),
994
1
                        timestamp: Utc::now(),
995
1
                    });
996
1
                },
997
0
                _ => {}, // Other risk tolerances handled similarly
998
            }
999
1000
1
            if !warnings.is_empty() {
1001
1
                return Ok(Some(warnings));
1002
0
            }
1003
0
        }
1004
1005
0
        Ok(None)
1006
1
    }
1007
1008
    /// Detect potential market abuse risks
1009
10
    async fn detect_market_abuse_risk(
1010
10
        &self,
1011
10
        order: &OrderInfo,
1012
10
    ) -> RiskResult<Option<Vec<RegulatoryFlag>>> {
1013
10
        let mut flags = Vec::new();
1014
1015
        // Check for unusually large orders that might indicate market manipulation
1016
        // Calculate order value safely for market abuse check
1017
10
        let _price = order.price;
1018
        // Convert to Decimal for safe calculation
1019
10
        let quantity_decimal = match order.quantity.to_decimal() {
1020
10
            Ok(decimal) => decimal,
1021
0
            Err(e) => {
1022
0
                warn!(
1023
0
                    "Failed to convert quantity to decimal for market abuse check: {}",
1024
                    e
1025
                );
1026
0
                return Ok(None);
1027
            },
1028
        };
1029
1030
10
        let price = order.price;
1031
10
        let price_f64 = match price_to_f64_safe(price, "market abuse check price conversion") {
1032
10
            Ok(p) => p,
1033
            Err(_) => {
1034
0
                return Err(RiskError::TypeConversion {
1035
0
                    from_type: "Price".to_owned(),
1036
0
                    to_type: "f64".to_owned(),
1037
0
                    reason: "Failed to convert price to f64 for market abuse check".to_owned(),
1038
0
                })
1039
            },
1040
        };
1041
10
        let price_decimal = Decimal::try_from(price_f64).unwrap_or_else(|_| 
{0
1042
0
            warn!("Failed to convert f64 price to decimal for market abuse check, using ZERO");
1043
0
            Decimal::ZERO
1044
0
        });
1045
10
        let order_value = quantity_decimal * price_decimal;
1046
        // CRITICAL: Market abuse thresholds must be configurable, not hardcoded
1047
        // Different markets have different reporting thresholds - hardcoding could cause regulatory violations
1048
10
        let threshold =
1049
10
            self.config
1050
10
                .market_abuse_threshold
1051
10
                .ok_or_else(|| RiskError::Configuration {
1052
                    message:
1053
0
                        "Market abuse threshold not configured - required for regulatory compliance"
1054
0
                            .to_owned(),
1055
0
                })?;
1056
10
        let threshold_decimal = threshold.to_decimal().unwrap_or_else(|e| 
{0
1057
0
            warn!("Failed to convert threshold to decimal: {}", e);
1058
0
            Decimal::from(1_000_000) // Default $1M threshold
1059
0
        });
1060
10
        if order_value > threshold_decimal {
1061
2
            // $1M threshold
1062
2
            flags.push(RegulatoryFlag {
1063
2
                flag_type: RegulatoryFlagType::MarketRisk,
1064
2
                regulation: "Market Abuse Regulation (MAR)".to_owned(),
1065
2
                description: format!(
1066
2
                    "Large order value ${order_value} requires enhanced monitoring for market impact"
1067
2
                ),
1068
2
                action_required: true,
1069
2
                deadline: Some(Utc::now() + Duration::hours(1)),
1070
2
            });
1071
8
        }
1072
1073
10
        if flags.is_empty() {
1074
8
            Ok(None)
1075
        } else {
1076
2
            Ok(Some(flags))
1077
        }
1078
10
    }
1079
1080
    /// Analyze best execution requirements (`MiFID` II Article 27)
1081
10
    async fn analyze_best_execution(
1082
10
        &self,
1083
10
        order: &OrderInfo,
1084
10
    ) -> RiskResult<Option<Vec<ComplianceWarning>>> {
1085
10
        let best_execution_venues = self.best_execution_venues.read().await;
1086
1087
        // In a real implementation, this would analyze multiple execution venues
1088
        // and determine the best execution strategy
1089
1090
10
        let mut warnings = Vec::new();
1091
1092
        // Check if we have venue analysis for this instrument type
1093
10
        if best_execution_venues.is_empty() {
1094
10
            warnings.push(ComplianceWarning {
1095
10
                id: Uuid::new_v4().to_string(),
1096
10
                warning_type: ComplianceWarningType::BestExecutionRisk,
1097
10
                severity: WarningSeverity::High,
1098
10
                message: "Best execution analysis required".to_owned(),
1099
10
                description: "No best execution venue analysis available".to_owned(),
1100
10
                instrument_id: Some(order.instrument_id.clone()),
1101
10
                portfolio_id: order.portfolio_id.clone(),
1102
10
                regulatory_reference: "MiFID II Article 27 - Best Execution".to_owned(),
1103
10
                recommended_action: "Configure best execution venue analysis".to_owned(),
1104
10
                timestamp: Utc::now(),
1105
10
            });
1106
10
        
}0
1107
1108
10
        if warnings.is_empty() {
1109
0
            Ok(None)
1110
        } else {
1111
10
            Ok(Some(warnings))
1112
        }
1113
10
    }
1114
1115
    /// Check transaction reporting requirements
1116
10
    async fn check_transaction_reporting_requirements(
1117
10
        &self,
1118
10
        _order: &OrderInfo,
1119
10
    ) -> RiskResult<Vec<RegulatoryFlag>> {
1120
10
        let mut flags = Vec::new();
1121
1122
        // MiFID II transaction reporting
1123
10
        if self.regulatory_config.mifid2_enabled {
1124
10
            flags.push(RegulatoryFlag {
1125
10
                flag_type: RegulatoryFlagType::ReportingRequired,
1126
10
                regulation: "MiFID II Article 26".to_owned(),
1127
10
                description: "Transaction reporting required within 1 business day".to_owned(),
1128
10
                action_required: true,
1129
10
                deadline: Some(Utc::now() + Duration::days(1)),
1130
10
            });
1131
10
        
}0
1132
1133
10
        Ok(flags)
1134
10
    }
1135
1136
    /// Validate Basel III capital requirements
1137
10
    async fn validate_basel_iii_requirements(
1138
10
        &self,
1139
10
        order: &OrderInfo,
1140
10
    ) -> RiskResult<Option<Vec<ComplianceWarning>>> {
1141
        // REAL Basel III capital ratio calculations implementation
1142
        // Based on Basel III framework for capital adequacy
1143
1144
        // Calculate order value safely for Basel III compliance check
1145
10
        let price = order.price;
1146
1147
        // Convert to Decimal for calculation
1148
10
        let quantity_decimal = match order.quantity.to_decimal() {
1149
10
            Ok(decimal) => decimal,
1150
0
            Err(e) => {
1151
0
                warn!(
1152
0
                    "Failed to convert quantity to decimal for Basel III check: {}",
1153
                    e
1154
                );
1155
0
                return Ok(None);
1156
            },
1157
        };
1158
1159
10
        let price_decimal = match price.to_decimal() {
1160
10
            Ok(decimal) => decimal,
1161
0
            Err(e) => {
1162
0
                warn!(
1163
0
                    "Failed to convert price to decimal for Basel III check: {}",
1164
                    e
1165
                );
1166
0
                return Ok(None);
1167
            },
1168
        };
1169
1170
10
        let order_value = quantity_decimal * price_decimal;
1171
10
        let mut warnings = Vec::new();
1172
1173
        // Calculate Basel III capital ratios - use safe environment variable parsing
1174
10
        let tier1_capital = parse_env_var::<f64>("TIER1_CAPITAL", "Basel III tier 1 capital")
1175
10
            .unwrap_or_else(|_| 
{4
1176
4
                warn!(
"Failed to parse TIER1_CAPITAL from environment, using default $10M"0
);
1177
4
                10_000_000.0
1178
4
            });
1179
1180
10
        let risk_weighted_assets =
1181
10
            parse_env_var::<f64>("RISK_WEIGHTED_ASSETS", "Basel III risk weighted assets")
1182
10
                .unwrap_or_else(|_| 
{4
1183
4
                    warn!(
1184
0
                        "Failed to parse RISK_WEIGHTED_ASSETS from environment, using default $50M"
1185
                    );
1186
4
                    50_000_000.0
1187
4
                });
1188
1189
10
        let total_exposure = parse_env_var::<f64>("TOTAL_EXPOSURE", "Basel III total exposure")
1190
10
            .unwrap_or_else(|_| 
{4
1191
4
                warn!(
"Failed to parse TOTAL_EXPOSURE from environment, using default $100M"0
);
1192
4
                100_000_000.0
1193
4
            });
1194
1195
        // Basel III Capital Adequacy Ratio (minimum 8%)
1196
10
        let capital_adequacy_ratio = tier1_capital / risk_weighted_assets;
1197
10
        if capital_adequacy_ratio < 0.08 {
1198
6
            warnings.push(ComplianceWarning {
1199
6
                id: Uuid::new_v4().to_string(),
1200
6
                warning_type: ComplianceWarningType::CapitalAdequacyLow,
1201
6
                severity: WarningSeverity::High,
1202
6
                message: "Capital adequacy ratio below Basel III minimum".to_owned(),
1203
6
                description: format!(
1204
6
                    "Capital adequacy ratio {:.2}% below Basel III minimum 8%",
1205
6
                    capital_adequacy_ratio * 100.0
1206
6
                ),
1207
6
                instrument_id: Some(order.instrument_id.clone()),
1208
6
                portfolio_id: order.portfolio_id.clone(),
1209
6
                regulatory_reference: "Basel III Capital Adequacy Ratio".to_owned(),
1210
6
                recommended_action: "Increase Tier 1 capital or reduce risk-weighted assets"
1211
6
                    .to_owned(),
1212
6
                timestamp: Utc::now(),
1213
6
            });
1214
6
        
}4
1215
1216
        // Basel III Leverage Ratio (minimum 3%)
1217
10
        let leverage_ratio = tier1_capital / total_exposure;
1218
10
        if leverage_ratio < 0.03 {
1219
0
            warnings.push(ComplianceWarning {
1220
0
                id: Uuid::new_v4().to_string(),
1221
0
                warning_type: ComplianceWarningType::LeverageRatioHigh,
1222
0
                severity: WarningSeverity::High,
1223
0
                message: "Leverage ratio below Basel III minimum".to_owned(),
1224
0
                description: format!(
1225
0
                    "Leverage ratio {:.2}% below Basel III minimum 3%",
1226
0
                    leverage_ratio * 100.0
1227
0
                ),
1228
0
                instrument_id: Some(order.instrument_id.clone()),
1229
0
                portfolio_id: order.portfolio_id.clone(),
1230
0
                regulatory_reference: "Basel III Leverage Ratio".to_owned(),
1231
0
                recommended_action: "Reduce total exposure or increase Tier 1 capital".to_owned(),
1232
0
                timestamp: Utc::now(),
1233
0
            });
1234
10
        }
1235
1236
        // Large exposure check - configurable threshold
1237
10
        let large_exposure_threshold = self
1238
10
            .config
1239
10
            .large_exposure_threshold
1240
10
            .to_decimal()
1241
10
            .unwrap_or_else(|e| 
{0
1242
0
                warn!(
1243
0
                    "Failed to convert large exposure threshold to decimal: {}",
1244
                    e
1245
                );
1246
0
                Decimal::from(500000) // Fallback for backward compatibility
1247
0
            });
1248
10
        if order_value > large_exposure_threshold {
1249
1
            warnings.push(ComplianceWarning {
1250
1
                id: Uuid::new_v4().to_string(),
1251
1
                warning_type: ComplianceWarningType::LargeExposure,
1252
1
                severity: WarningSeverity::Medium,
1253
1
                message: "Large position may impact compliance ratios".to_owned(),
1254
1
                description: format!(
1255
1
                    "Large position ${order_value} may impact Basel III compliance ratios"
1256
1
                ),
1257
1
                instrument_id: Some(order.instrument_id.clone()),
1258
1
                portfolio_id: order.portfolio_id.clone(),
1259
1
                regulatory_reference: "Basel III Large Exposure Limits".to_owned(),
1260
1
                recommended_action: "Monitor impact on capital and leverage ratios".to_owned(),
1261
1
                timestamp: Utc::now(),
1262
1
            });
1263
9
        }
1264
1265
10
        if warnings.is_empty() {
1266
4
            Ok(None)
1267
        } else {
1268
6
            Ok(Some(warnings))
1269
        }
1270
10
    }
1271
1272
    /// Calculate comprehensive risk score for an order
1273
10
    async fn calculate_order_risk_score(
1274
10
        &self,
1275
10
        order: &OrderInfo,
1276
10
        violations: &[RiskViolation],
1277
10
        warnings: &[ComplianceWarning],
1278
10
    ) -> Result<Price, RiskError> {
1279
10
        let mut risk_score = Price::ZERO;
1280
1281
        // Base risk from order size - use safe conversion helpers
1282
10
        let quantity_decimal = order.quantity.to_decimal().unwrap_or_else(|_| 
{0
1283
0
            warn!("Failed to convert quantity to decimal for risk score calculation, using ZERO");
1284
0
            Decimal::ZERO
1285
0
        });
1286
10
        let price_value = order.price;
1287
10
        let price_decimal = price_value.to_decimal().unwrap_or_else(|_| 
{0
1288
0
            warn!(
1289
0
                "Failed to convert price to decimal for risk score calculation, using fallback 100"
1290
            );
1291
0
            Decimal::from(100)
1292
0
        });
1293
10
        let order_value = quantity_decimal * price_decimal;
1294
10
        let order_value_f64 = decimal_to_f64_safe(order_value, "order value for risk score")
1295
10
            .unwrap_or_else(|_| 
{0
1296
0
                warn!("Failed to convert order value to f64 for risk score calculation, using 0.0");
1297
0
                0.0
1298
0
            });
1299
10
        let order_risk = f64_to_price_safe(order_value_f64 / 100_000.0, "order risk calculation")
1300
10
            .map_err(|e| 
{0
1301
0
                error!("CRITICAL: Failed to calculate order risk - this could hide compliance violations: {}", e);
1302
0
                RiskError::Calculation { operation: "order_risk_calculation".to_owned(), reason: format!("Failed to calculate order risk: {e}") }
1303
0
            })?;
1304
10
        let current_risk_f64 = decimal_to_f64_safe(
1305
10
            risk_score.to_decimal().unwrap_or(Decimal::ZERO),
1306
10
            "current risk score",
1307
        )
1308
10
        .unwrap_or_else(|_| 
{0
1309
0
            warn!("Failed to convert current risk score to f64, using 0.0");
1310
0
            0.0
1311
0
        });
1312
10
        let order_risk_f64 = decimal_to_f64_safe(
1313
10
            order_risk.to_decimal().unwrap_or(Decimal::ZERO),
1314
10
            "order risk value",
1315
        )
1316
10
        .unwrap_or_else(|_| 
{0
1317
0
            warn!("Failed to convert order risk to f64, using 0.0");
1318
0
            0.0
1319
0
        });
1320
10
        risk_score = f64_to_price_safe(current_risk_f64 + order_risk_f64, "updated risk score")
1321
10
            .unwrap_or_else(|_| 
{0
1322
0
                warn!("Failed to update risk score with order risk, keeping original");
1323
0
                risk_score
1324
0
            });
1325
1326
        // Risk from violations - use safe conversion
1327
10
        let violation_risk =
1328
10
            f64_to_price_safe((violations.len() * 10) as f64, "violation risk calculation")
1329
10
                .map_err(|e| 
{0
1330
0
                    error!("CRITICAL: Failed to calculate violation risk - this could hide compliance issues: {}", e);
1331
0
                    RiskError::Calculation { operation: "violation_risk_calculation".to_owned(), reason: format!("Failed to calculate violation risk: {e}") }
1332
0
                })?;
1333
10
        let violation_risk_f64 = decimal_to_f64_safe(
1334
10
            violation_risk.to_decimal().unwrap_or(Decimal::ZERO),
1335
10
            "violation risk value",
1336
        )
1337
10
        .unwrap_or_else(|_| 
{0
1338
0
            warn!("Failed to convert violation risk to f64, using 0.0");
1339
0
            0.0
1340
0
        });
1341
10
        let current_risk_with_violations_f64 = decimal_to_f64_safe(
1342
10
            risk_score.to_decimal().unwrap_or(Decimal::ZERO),
1343
10
            "current risk with violations",
1344
        )
1345
10
        .unwrap_or_else(|_| 
{0
1346
0
            warn!("Failed to convert current risk score to f64, using 0.0");
1347
0
            0.0
1348
0
        });
1349
10
        risk_score = f64_to_price_safe(
1350
10
            current_risk_with_violations_f64 + violation_risk_f64,
1351
10
            "updated risk score with violations",
1352
        )
1353
10
        .unwrap_or_else(|_| 
{0
1354
0
            warn!("Failed to update risk score with violation risk, keeping original");
1355
0
            risk_score
1356
0
        });
1357
1358
        // Risk from warnings - use safe conversion
1359
10
        let warning_risk: f64 = warnings
1360
10
            .iter()
1361
18
            .
map10
(|w| match w.severity {
1362
0
                WarningSeverity::Info => 0.05,
1363
0
                WarningSeverity::Low => 0.1,
1364
0
                WarningSeverity::Warning => 0.3,
1365
2
                WarningSeverity::Medium => 0.5,
1366
16
                WarningSeverity::High => 1.0,
1367
0
                WarningSeverity::Error => 1.5,
1368
0
                WarningSeverity::Critical => 2.0,
1369
18
            })
1370
10
            .sum();
1371
10
        let warning_risk_price = f64_to_price_safe(warning_risk, "warning risk calculation")
1372
10
            .unwrap_or_else(|_| 
{0
1373
0
                warn!("Failed to create warning risk price, using ZERO");
1374
0
                Price::ZERO
1375
0
            });
1376
10
        let warning_risk_f64 = decimal_to_f64_safe(
1377
10
            warning_risk_price.to_decimal().unwrap_or(Decimal::ZERO),
1378
10
            "warning risk value",
1379
        )
1380
10
        .unwrap_or_else(|_| 
{0
1381
0
            warn!("Failed to convert warning risk to f64, using 0.0");
1382
0
            0.0
1383
0
        });
1384
10
        let current_risk_with_warnings_f64 = decimal_to_f64_safe(
1385
10
            risk_score.to_decimal().unwrap_or(Decimal::ZERO),
1386
10
            "current risk with warnings",
1387
        )
1388
10
        .unwrap_or_else(|_| 
{0
1389
0
            warn!("Failed to convert current risk score to f64, using 0.0");
1390
0
            0.0
1391
0
        });
1392
10
        risk_score = f64_to_price_safe(
1393
10
            current_risk_with_warnings_f64 + warning_risk_f64,
1394
10
            "final risk score calculation",
1395
        )
1396
10
        .unwrap_or_else(|_| 
{0
1397
0
            warn!("Failed to update risk score with warning risk, keeping original");
1398
0
            risk_score
1399
0
        });
1400
1401
        // Cap risk score at 100 - use safe conversion
1402
10
        let max_risk = f64_to_price_safe(100.0, "max risk score limit").unwrap_or_else(|_| 
{0
1403
0
            warn!("Failed to create max risk price, using ZERO as fallback");
1404
0
            Price::ZERO
1405
0
        });
1406
10
        let current_risk_f64 = decimal_to_f64_safe(
1407
10
            risk_score.to_decimal().unwrap_or(Decimal::ZERO),
1408
10
            "final risk score for capping",
1409
        )
1410
10
        .unwrap_or_else(|_| 
{0
1411
0
            warn!("Failed to convert final risk score to f64, using 0.0");
1412
0
            0.0
1413
0
        });
1414
10
        let max_risk_f64 = decimal_to_f64_safe(
1415
10
            max_risk.to_decimal().unwrap_or(Decimal::ZERO),
1416
10
            "max risk value for comparison",
1417
        )
1418
10
        .unwrap_or_else(|_| 
{0
1419
0
            warn!("Failed to convert max risk to f64, using 0.0");
1420
0
            0.0
1421
0
        });
1422
10
        if current_risk_f64 > max_risk_f64 {
1423
0
            Ok(max_risk)
1424
        } else {
1425
10
            Ok(risk_score)
1426
        }
1427
10
    }
1428
1429
    /// Log enhanced audit entry with regulatory data
1430
15
    async fn log_enhanced_audit_entry(&self, entry: EnhancedAuditEntry) -> RiskResult<()> {
1431
15
        let mut audit_trail = self.audit_trail.write().await;
1432
15
        audit_trail.push(entry);
1433
15
        Ok(())
1434
15
    }
1435
1436
    /// Report a risk violation with full audit trail
1437
4
    pub async fn report_violation(&self, violation: &RiskViolation) -> RiskResult<()> {
1438
4
        error!(
1439
0
            "\u{1f6a8} Risk violation reported: {} - {}",
1440
            violation.violation_type, violation.description
1441
        );
1442
1443
4
        let audit_entry = EnhancedAuditEntry {
1444
            base_entry: AuditEntry {
1445
4
                id: format!("risk_violation_{}", violation.id),
1446
4
                timestamp: Utc::now().timestamp(),
1447
4
                event_type: "RISK_VIOLATION".to_owned(),
1448
4
                description: format!("Risk violation: {}", violation.description),
1449
4
                actor: "RiskEngine".to_owned(),
1450
4
                user_id: None,
1451
4
                instrument_id: violation.instrument_id.clone(),
1452
4
                portfolio_id: violation.portfolio_id.clone(),
1453
                data: {
1454
4
                    let mut data = HashMap::new();
1455
4
                    data.insert(
1456
4
                        "violation_type".to_owned(),
1457
4
                        format!("{:?}", violation.violation_type),
1458
                    );
1459
4
                    data.insert("severity".to_owned(), format!("{:?}", violation.severity));
1460
4
                    if let Some(current_value) = violation.current_value {
1461
4
                        data.insert("current_value".to_owned(), current_value.to_string());
1462
4
                    
}0
1463
4
                    if let Some(limit_value) = violation.limit_value {
1464
4
                        data.insert("limit_value".to_owned(), limit_value.to_string());
1465
4
                    
}0
1466
4
                    if let Some(breach_amount) = violation.breach_amount {
1467
4
                        data.insert("breach_amount".to_owned(), breach_amount.to_string());
1468
4
                    
}0
1469
4
                    data
1470
                },
1471
4
                metadata: HashMap::new(),
1472
            },
1473
4
            compliance_status: ComplianceStatus::Violation,
1474
4
            regulatory_references: vec![
1475
4
                "Internal Risk Management Policy".to_owned(),
1476
4
                "Regulatory Capital Requirements".to_owned(),
1477
            ],
1478
            risk_score: Some(
1479
4
                f64_to_price_safe(8.0, "violation risk score").unwrap_or_else(|_| 
{0
1480
0
                    warn!("Failed to create risk score price for violation reporting, using ZERO");
1481
0
                    Price::ZERO
1482
0
                }),
1483
            ), // High risk score for violations
1484
4
            client_classification: None,
1485
4
            execution_venue: None,
1486
4
            best_execution_analysis: None,
1487
        };
1488
1489
4
        self.log_enhanced_audit_entry(audit_entry).await
?0
;
1490
4
        let _ = self.violation_broadcast.send(violation.clone());
1491
1492
4
        Ok(())
1493
4
    }
1494
1495
    /// Set position limits with regulatory basis
1496
1
    pub async fn set_position_limit(
1497
1
        &self,
1498
1
        instrument_id: String,
1499
1
        limit: PositionLimit,
1500
1
    ) -> RiskResult<()> {
1501
1
        let mut position_limits = self.position_limits.write().await;
1502
1
        position_limits.insert(instrument_id.clone(), limit.clone());
1503
1504
1
        info!(
1505
0
            "\u{1f4ca} Position limit set for {}: {} under {}",
1506
            instrument_id, limit.max_position_size, limit.regulatory_basis
1507
        );
1508
1
        Ok(())
1509
1
    }
1510
1511
    /// Set client classification for regulatory purposes
1512
1
    pub async fn set_client_classification(
1513
1
        &self,
1514
1
        client_id: String,
1515
1
        classification: ClientClassification,
1516
1
    ) -> RiskResult<()> {
1517
1
        let mut client_classifications = self.client_classifications.write().await;
1518
1
        client_classifications.insert(client_id.clone(), classification.clone());
1519
1520
1
        info!(
1521
0
            "\u{1f464} Client classification set for {}: {:?}",
1522
            client_id, classification.classification
1523
        );
1524
1
        Ok(())
1525
1
    }
1526
1527
    /// Generate comprehensive regulatory report
1528
1
    pub async fn generate_regulatory_report(
1529
1
        &self,
1530
1
        start_date: DateTime<Utc>,
1531
1
        end_date: DateTime<Utc>,
1532
1
    ) -> RiskResult<String> {
1533
1
        let audit_trail = self.audit_trail.read().await;
1534
1535
1
        let relevant_entries: Vec<_> = audit_trail
1536
1
            .iter()
1537
2
            .
filter1
(|entry| {
1538
2
                entry.base_entry.timestamp >= start_date.timestamp()
1539
2
                    && entry.base_entry.timestamp <= end_date.timestamp()
1540
2
            })
1541
1
            .collect();
1542
1543
1
        let total_validations = relevant_entries
1544
1
            .iter()
1545
2
            .
filter1
(|entry| entry.base_entry.event_type.contains("VALIDATION"))
1546
1
            .count();
1547
1548
1
        let violations = relevant_entries
1549
1
            .iter()
1550
2
            .
filter1
(|entry| matches!(entry.compliance_status, ComplianceStatus::Violation))
1551
1
            .count();
1552
1553
1
        let warnings = relevant_entries
1554
1
            .iter()
1555
2
            .
filter1
(|entry| matches!(entry.compliance_status, ComplianceStatus::Warning))
1556
1
            .count();
1557
1558
1
        let average_risk_score = if relevant_entries.is_empty() {
1559
0
            Price::ZERO
1560
        } else {
1561
1
            let total_risk_decimal: Decimal = relevant_entries
1562
1
                .iter()
1563
2
                .
filter_map1
(|entry| {
1564
2
                    entry
1565
2
                        .risk_score
1566
2
                        .map(|p| p.to_decimal().unwrap_or(Decimal::ZERO))
1567
2
                })
1568
1
                .sum();
1569
1
            let total_risk = Price::from_decimal(total_risk_decimal);
1570
1
            let count = relevant_entries.len() as f64;
1571
1
            let total_risk_f64 = decimal_to_f64_safe(
1572
1
                total_risk.to_decimal().unwrap_or(Decimal::ZERO),
1573
1
                "total risk for average calculation",
1574
            )
1575
1
            .unwrap_or_else(|_| 
{0
1576
0
                warn!("Failed to convert total risk to f64 for average calculation, using 0.0");
1577
0
                0.0
1578
0
            });
1579
1
            f64_to_price_safe(total_risk_f64 / count, "average risk score calculation")
1580
1
                .unwrap_or_else(|_| 
{0
1581
0
                    warn!("Failed to calculate average risk score, using ZERO");
1582
0
                    Price::ZERO
1583
0
                })
1584
        };
1585
1586
1
        let report = format!(
1587
1
            "COMPREHENSIVE REGULATORY COMPLIANCE REPORT\n\
1588
1
             ==========================================\n\
1589
1
             \n\
1590
1
             Report Period: {} to {}\n\
1591
1
             Generated: {}\n\
1592
1
             Validator ID: {}\n\
1593
1
             \n\
1594
1
             SUMMARY STATISTICS:\n\
1595
1
             - Total Compliance Validations: {}\n\
1596
1
             - Regulatory Violations: {}\n\
1597
1
             - Compliance Warnings: {}\n\
1598
1
             - Average Risk Score: {:.2}\n\
1599
1
             - Total Audit Entries: {}\n\
1600
1
             \n\
1601
1
             REGULATORY FRAMEWORK COVERAGE:\n\
1602
1
             - MiFID II: {}\n\
1603
1
             - Basel III: {}\n\
1604
1
             - Dodd-Frank: {}\n\
1605
1
             - EMIR: {}\n\
1606
1
             \n\
1607
1
             COMPLIANCE STATUS: {}\n\
1608
1
             \n\
1609
1
             This report demonstrates adherence to regulatory requirements\n\
1610
1
             and provides comprehensive audit trail for regulatory examination.\n",
1611
            start_date,
1612
            end_date,
1613
1
            Utc::now(),
1614
            self.validator_id,
1615
            total_validations,
1616
            violations,
1617
            warnings,
1618
            average_risk_score,
1619
1
            relevant_entries.len(),
1620
1
            if self.regulatory_config.mifid2_enabled {
1621
1
                "ACTIVE"
1622
            } else {
1623
0
                "INACTIVE"
1624
            },
1625
1
            if self.regulatory_config.basel_iii_enabled {
1626
1
                "ACTIVE"
1627
            } else {
1628
0
                "INACTIVE"
1629
            },
1630
1
            if self.regulatory_config.dodd_frank_enabled {
1631
1
                "ACTIVE"
1632
            } else {
1633
0
                "INACTIVE"
1634
            },
1635
1
            if self.regulatory_config.emir_enabled {
1636
1
                "ACTIVE"
1637
            } else {
1638
0
                "INACTIVE"
1639
            },
1640
1
            if violations == 0 {
1641
0
                "COMPLIANT"
1642
            } else {
1643
1
                "NON-COMPLIANT - REQUIRES ATTENTION"
1644
            }
1645
        );
1646
1647
1
        Ok(report)
1648
1
    }
1649
1650
    /// Subscribe to violation events
1651
    #[must_use]
1652
1
    pub fn subscribe_to_violations(&self) -> broadcast::Receiver<RiskViolation> {
1653
1
        self.violation_broadcast.subscribe()
1654
1
    }
1655
1656
    /// Subscribe to warning events  
1657
    #[must_use]
1658
1
    pub fn subscribe_to_warnings(&self) -> broadcast::Receiver<ComplianceWarning> {
1659
1
        self.warning_broadcast.subscribe()
1660
1
    }
1661
1662
    /// Get comprehensive audit trail
1663
4
    pub async fn get_enhanced_audit_trail(&self, limit: Option<usize>) -> Vec<EnhancedAuditEntry> {
1664
4
        let audit_trail = self.audit_trail.read().await;
1665
1666
4
        if let Some(
limit0
) = limit {
1667
0
            audit_trail.iter().rev().take(limit).cloned().collect()
1668
        } else {
1669
4
            audit_trail.clone()
1670
        }
1671
4
    }
1672
1673
    /// Clean up old audit trail entries based on regulatory retention requirements
1674
1
    pub async fn cleanup_audit_trail(&self) -> RiskResult<()> {
1675
1
        let retention_days = self.config.audit_retention_days.min(2555); // Use config value or 7 years max
1676
1
        let cutoff_date = Utc::now() - Duration::days(i64::from(retention_days));
1677
1678
1
        let mut audit_trail = self.audit_trail.write().await;
1679
1
        let initial_count = audit_trail.len();
1680
1681
1
        audit_trail.retain(|entry| entry.base_entry.timestamp > cutoff_date.timestamp());
1682
1683
1
        let final_count = audit_trail.len();
1684
1
        let removed_count = initial_count - final_count;
1685
1686
1
        if removed_count > 0 {
1687
1
            info!(
1688
0
                "\u{1f9f9} Cleaned up {} old audit trail entries (retention: {} days)",
1689
                removed_count, retention_days
1690
            );
1691
0
        }
1692
1693
1
        Ok(())
1694
1
    }
1695
1696
    /// Get compliance metrics for monitoring
1697
1
    pub async fn get_compliance_metrics(&self) -> HashMap<String, f64> {
1698
1
        let audit_trail = self.audit_trail.read().await;
1699
1700
1
        let total_entries = audit_trail.len() as f64;
1701
1
        let violations = audit_trail
1702
1
            .iter()
1703
2
            .
filter1
(|entry| matches!(entry.compliance_status, ComplianceStatus::Violation))
1704
1
            .count() as f64;
1705
1
        let warnings = audit_trail
1706
1
            .iter()
1707
2
            .
filter1
(|entry| matches!(entry.compliance_status, ComplianceStatus::Warning))
1708
1
            .count() as f64;
1709
1710
1
        let mut metrics = HashMap::new();
1711
1
        metrics.insert("total_audit_entries".to_owned(), total_entries);
1712
1
        metrics.insert("compliance_violations".to_owned(), violations);
1713
1
        metrics.insert("compliance_warnings".to_owned(), warnings);
1714
1
        metrics.insert(
1715
1
            "compliance_rate".to_owned(),
1716
1
            if total_entries > 0.0 {
1717
1
                (total_entries - violations) / total_entries * 100.0
1718
            } else {
1719
0
                100.0
1720
            },
1721
        );
1722
1723
1
        metrics
1724
1
    }
1725
1726
    /// **Load Compliance Rules from Database (Dynamic Configuration)**
1727
    ///
1728
    /// Loads compliance rules from the PostgreSQL database using the
1729
    /// config crate's PostgresComplianceRuleLoader. Enables hot-reload
1730
    /// of compliance rules without service restarts.
1731
    ///
1732
    /// # Arguments
1733
    ///
1734
    /// * `rule_loader` - Reference to PostgresComplianceRuleLoader
1735
    ///
1736
    /// # Returns
1737
    ///
1738
    /// Result indicating success or error with count of loaded rules
1739
    ///
1740
    /// # Example
1741
    ///
1742
    /// ```rust,no_run
1743
    /// use config::PostgresComplianceRuleLoader;
1744
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1745
    /// let loader = PostgresComplianceRuleLoader::new("postgresql://localhost/foxhunt").await?;
1746
    /// let validator = ComplianceValidator::new(config, regulatory_config);
1747
    ///
1748
    /// // Load all active rules from database
1749
    /// let count = validator.load_compliance_rules(&loader).await?;
1750
    /// println!("Loaded {} compliance rules", count);
1751
    /// # Ok(())
1752
    /// # }
1753
    /// ```
1754
    #[cfg(feature = "postgres")]
1755
    pub async fn load_compliance_rules(
1756
        &self,
1757
        rule_loader: &config::PostgresComplianceRuleLoader,
1758
    ) -> Result<usize, Box<dyn std::error::Error>> {
1759
        use crate::risk_types::ComplianceRuleType;
1760
1761
        let rules = rule_loader.load_all_active_rules().await?;
1762
        let mut compliance_rules = self.compliance_rules.write().await;
1763
1764
        for rule_config in &rules {
1765
            // Convert config::ComplianceRuleConfig to risk::ComplianceRule
1766
            let rule_type = match rule_config.rule_type.as_str() {
1767
                "POSITION_LIMIT" => ComplianceRuleType::PositionLimit,
1768
                "MARKET_ABUSE" => ComplianceRuleType::MarketAbuse,
1769
                "CLIENT_SUITABILITY" => ComplianceRuleType::ClientSuitability,
1770
                "BEST_EXECUTION" => ComplianceRuleType::BestExecution,
1771
                "CONCENTRATION_RISK" => ComplianceRuleType::ConcentrationRisk,
1772
                "LEVERAGE_LIMIT" => ComplianceRuleType::LeverageLimit,
1773
                "CAPITAL_ADEQUACY" => ComplianceRuleType::CapitalAdequacy,
1774
                "REGULATORY_REPORTING" => ComplianceRuleType::RegulatoryReporting,
1775
                _ => ComplianceRuleType::Custom,
1776
            };
1777
1778
            let severity = match rule_config.severity.as_str() {
1779
                "Low" | "Info" => RiskSeverity::Low,
1780
                "Medium" => RiskSeverity::Medium,
1781
                "High" => RiskSeverity::High,
1782
                "Critical" => RiskSeverity::Critical,
1783
                _ => RiskSeverity::Medium,
1784
            };
1785
1786
            let rule = ComplianceRule {
1787
                id: rule_config.rule_id.clone(),
1788
                name: rule_config.name.clone(),
1789
                description: rule_config.description.clone(),
1790
                rule_type,
1791
                active: rule_config.active,
1792
                version: rule_config.version,
1793
                severity,
1794
                priority: rule_config.priority,
1795
                parameters: rule_config.parameters.clone(),
1796
                regulatory_framework: rule_config.regulatory_framework.clone(),
1797
                regulatory_reference: rule_config.regulatory_reference.clone(),
1798
            };
1799
1800
            compliance_rules.insert(rule.id.clone(), rule);
1801
        }
1802
1803
        let count = rules.len();
1804
        drop(compliance_rules);
1805
1806
        tracing::info!("Loaded {} compliance rules from database", count);
1807
        Ok(count)
1808
    }
1809
1810
    /// **Reload Specific Compliance Rule (Hot-Reload)**
1811
    ///
1812
    /// Reloads a specific compliance rule from the database.
1813
    /// Used for hot-reload when rules are updated in the database.
1814
    ///
1815
    /// # Arguments
1816
    ///
1817
    /// * `rule_loader` - Reference to PostgresComplianceRuleLoader
1818
    /// * `rule_id` - ID of rule to reload
1819
    ///
1820
    /// # Returns
1821
    ///
1822
    /// Result indicating success or error
1823
    #[cfg(feature = "postgres")]
1824
    pub async fn reload_compliance_rule(
1825
        &self,
1826
        rule_loader: &config::PostgresComplianceRuleLoader,
1827
        rule_id: &str,
1828
    ) -> Result<(), Box<dyn std::error::Error>> {
1829
        use crate::risk_types::ComplianceRuleType;
1830
1831
        if let Some(rule_config) = rule_loader.get_rule(rule_id).await? {
1832
            let rule_type = match rule_config.rule_type.as_str() {
1833
                "POSITION_LIMIT" => ComplianceRuleType::PositionLimit,
1834
                "MARKET_ABUSE" => ComplianceRuleType::MarketAbuse,
1835
                "CLIENT_SUITABILITY" => ComplianceRuleType::ClientSuitability,
1836
                "BEST_EXECUTION" => ComplianceRuleType::BestExecution,
1837
                "CONCENTRATION_RISK" => ComplianceRuleType::ConcentrationRisk,
1838
                "LEVERAGE_LIMIT" => ComplianceRuleType::LeverageLimit,
1839
                "CAPITAL_ADEQUACY" => ComplianceRuleType::CapitalAdequacy,
1840
                "REGULATORY_REPORTING" => ComplianceRuleType::RegulatoryReporting,
1841
                _ => ComplianceRuleType::Custom,
1842
            };
1843
1844
            let severity = match rule_config.severity.as_str() {
1845
                "Low" | "Info" => RiskSeverity::Low,
1846
                "Medium" => RiskSeverity::Medium,
1847
                "High" => RiskSeverity::High,
1848
                "Critical" => RiskSeverity::Critical,
1849
                _ => RiskSeverity::Medium,
1850
            };
1851
1852
            let rule = ComplianceRule {
1853
                id: rule_config.rule_id.clone(),
1854
                name: rule_config.name.clone(),
1855
                description: rule_config.description.clone(),
1856
                rule_type,
1857
                active: rule_config.active,
1858
                version: rule_config.version,
1859
                severity,
1860
                priority: rule_config.priority,
1861
                parameters: rule_config.parameters.clone(),
1862
                regulatory_framework: rule_config.regulatory_framework.clone(),
1863
                regulatory_reference: rule_config.regulatory_reference.clone(),
1864
            };
1865
1866
            self.compliance_rules
1867
                .write()
1868
                .await
1869
                .insert(rule.id.clone(), rule);
1870
1871
            tracing::info!("Reloaded compliance rule: {}", rule_id);
1872
        } else {
1873
            // Rule was deleted or deactivated, remove from cache
1874
            self.compliance_rules.write().await.remove(rule_id);
1875
            tracing::info!("Removed deactivated compliance rule: {}", rule_id);
1876
        }
1877
1878
        Ok(())
1879
    }
1880
1881
    /// **Get Active Compliance Rule by ID**
1882
    ///
1883
    /// Retrieves a specific compliance rule from the loaded rules.
1884
    ///
1885
    /// # Arguments
1886
    ///
1887
    /// * `rule_id` - ID of rule to retrieve
1888
    ///
1889
    /// # Returns
1890
    ///
1891
    /// Optional `ComplianceRule` if found and active
1892
0
    pub async fn get_compliance_rule(&self, rule_id: &str) -> Option<ComplianceRule> {
1893
0
        self.compliance_rules
1894
0
            .read()
1895
0
            .await
1896
0
            .get(rule_id)
1897
0
            .filter(|r| r.active)
1898
0
            .cloned()
1899
0
    }
1900
1901
    /// **Get All Active Compliance Rules**
1902
    ///
1903
    /// Returns all currently loaded and active compliance rules.
1904
    ///
1905
    /// # Returns
1906
    ///
1907
    /// Vector of active compliance rules sorted by priority
1908
0
    pub async fn get_all_compliance_rules(&self) -> Vec<ComplianceRule> {
1909
0
        let rules = self.compliance_rules.read().await;
1910
0
        let mut active_rules: Vec<_> = rules
1911
0
            .values()
1912
0
            .filter(|r| r.active)
1913
0
            .cloned()
1914
0
            .collect();
1915
1916
        // Sort by priority (descending) then by name
1917
0
        active_rules.sort_by(|a, b| {
1918
0
            b.priority
1919
0
                .cmp(&a.priority)
1920
0
                .then_with(|| a.name.cmp(&b.name))
1921
0
        });
1922
1923
0
        active_rules
1924
0
    }
1925
1926
    /// **Get Compliance Rules by Type**
1927
    ///
1928
    /// Returns all active compliance rules of a specific type.
1929
    ///
1930
    /// # Arguments
1931
    ///
1932
    /// * `rule_type` - Type of rules to retrieve
1933
    ///
1934
    /// # Returns
1935
    ///
1936
    /// Vector of compliance rules matching the specified type
1937
0
    pub async fn get_compliance_rules_by_type(
1938
0
        &self,
1939
0
        rule_type: &crate::risk_types::ComplianceRuleType,
1940
0
    ) -> Vec<ComplianceRule> {
1941
0
        let rules = self.compliance_rules.read().await;
1942
0
        let mut matching_rules: Vec<_> = rules
1943
0
            .values()
1944
0
            .filter(|r| r.active && &r.rule_type == rule_type)
1945
0
            .cloned()
1946
0
            .collect();
1947
1948
0
        matching_rules.sort_by(|a, b| {
1949
0
            b.priority
1950
0
                .cmp(&a.priority)
1951
0
                .then_with(|| a.name.cmp(&b.name))
1952
0
        });
1953
1954
0
        matching_rules
1955
0
    }
1956
1957
    /// **Clear Compliance Rule Cache**
1958
    ///
1959
    /// Clears all loaded compliance rules. Used for testing or
1960
    /// when forcing a complete reload from database.
1961
0
    pub async fn clear_compliance_rules(&self) {
1962
0
        self.compliance_rules.write().await.clear();
1963
0
        tracing::info!("Cleared compliance rule cache");
1964
0
    }
1965
1966
    /// **Get Compliance Rule Count**
1967
    ///
1968
    /// Returns the number of loaded compliance rules.
1969
    ///
1970
    /// # Returns
1971
    ///
1972
    /// Total count of loaded rules (active and inactive)
1973
0
    pub async fn compliance_rule_count(&self) -> usize {
1974
0
        self.compliance_rules.read().await.len()
1975
0
    }
1976
}
1977
1978
#[cfg(test)]
1979
mod tests {
1980
    use super::*;
1981
    use common::{OrderSide, OrderType, Quantity, Symbol};
1982
    // operations module removed - use direct imports from common
1983
1984
14
    fn create_test_config() -> Result<ComplianceConfig, Box<dyn std::error::Error>> {
1985
        use crate::risk_types::PositionLimits;
1986
        use std::collections::HashMap;
1987
14
        Ok(ComplianceConfig {
1988
14
            rules: vec![], // Empty rules for test
1989
14
            position_limits: PositionLimits {
1990
14
                max_position_per_instrument: HashMap::new(),
1991
14
                max_portfolio_value: Price::from_f64(1000000.0).unwrap_or(Price::ZERO),
1992
14
                max_leverage: 10.0,
1993
14
                max_concentration_pct: 0.1,
1994
14
                global_limit: Price::from_f64(10000000.0).unwrap_or(Price::ZERO),
1995
14
            },
1996
14
            audit_retention_days: 2555,
1997
14
            market_abuse_threshold: Some(Price::from_f64(100000.0).unwrap_or(Price::ZERO)),
1998
14
            large_exposure_threshold: Price::from_f64(500000.0).unwrap_or(Price::ZERO),
1999
14
        })
2000
14
    }
2001
2002
14
    fn create_test_regulatory_config(
2003
14
    ) -> Result<RegulatoryReportingConfig, Box<dyn std::error::Error>> {
2004
14
        Ok(RegulatoryReportingConfig::default())
2005
14
    }
2006
2007
10
    fn create_test_order() -> Result<OrderInfo, Box<dyn std::error::Error>> {
2008
10
        Ok(OrderInfo {
2009
10
            order_id: "test_order_1".to_string(),
2010
10
            symbol: Symbol::from("TEST_INSTRUMENT_001".to_string()),
2011
10
            instrument_id: "TEST_INSTRUMENT_001".to_string(),
2012
10
            side: OrderSide::Buy,
2013
10
            quantity: Quantity::from_f64(100.0).unwrap_or(Quantity::ZERO),
2014
10
            price: Price::from_f64(150.0).unwrap_or(Price::ZERO),
2015
10
            order_type: Some(OrderType::Limit),
2016
10
            portfolio_id: Some("test_portfolio".to_string()),
2017
10
            strategy_id: Some("test_strategy".to_string()),
2018
10
        })
2019
10
    }
2020
2021
4
    fn create_test_violation() -> Result<RiskViolation, Box<dyn std::error::Error>> {
2022
4
        Ok(RiskViolation {
2023
4
            id: Uuid::new_v4().to_string(),
2024
4
            violation_type: ViolationType::RiskModelBreach,
2025
4
            severity: RiskSeverity::High,
2026
4
            message: "Test compliance violation".to_string(),
2027
4
            description: "Test compliance violation".to_string(),
2028
4
            instrument_id: Some("TEST_INSTRUMENT_001".to_string()),
2029
4
            portfolio_id: Some("test_portfolio".to_string()),
2030
4
            strategy_id: Some("test_strategy".to_string()),
2031
4
            current_value: Some(Price::from_f64(1000.0).unwrap_or(Price::ZERO)),
2032
4
            limit_value: Some(Price::from_f64(500.0).unwrap_or(Price::ZERO)),
2033
4
            breach_amount: Some(Price::from_f64(500.0).unwrap_or(Price::ZERO)),
2034
4
            timestamp: Some(Utc::now().timestamp()),
2035
4
            resolved: false,
2036
4
        })
2037
4
    }
2038
2039
    #[tokio::test]
2040
1
    async fn test_compliance_validator_creation() -> Result<(), Box<dyn std::error::Error>> {
2041
1
        let config = create_test_config()
?0
;
2042
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2043
1
        let _validator = ComplianceValidator::new(config, regulatory_config);
2044
        // Test passes if no panic
2045
2
        Ok(())
2046
1
    }
2047
2048
    #[tokio::test]
2049
1
    async fn test_order_validation() -> Result<(), Box<dyn std::error::Error>> {
2050
1
        let config = create_test_config()
?0
;
2051
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2052
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2053
1
        let order = create_test_order()
?0
;
2054
2055
1
        let result = validator.validate_order(&order, None).await
?0
;
2056
2057
        // Order should be compliant by default
2058
1
        assert!(result.is_compliant);
2059
1
        assert!(result.violations.is_empty());
2060
2061
        // Should have logged an audit entry
2062
1
        assert!(!validator.get_enhanced_audit_trail(None).await.is_empty());
2063
2
        Ok(())
2064
1
    }
2065
2066
    #[tokio::test]
2067
1
    async fn test_position_size_violation() -> Result<(), Box<dyn std::error::Error>> {
2068
1
        let config = create_test_config()
?0
;
2069
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2070
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2071
2072
        // Known limitation: Dynamic rule configuration not implemented
2073
        // Production should implement set_compliance_rule() for runtime rule updates
2074
        // Current implementation uses static rules from config file
2075
        //
2076
        // validator.set_compliance_rule(
2077
        //     "position_size_limit".to_string(),
2078
        //     ComplianceRule::PositionSizeLimit {
2079
        //         instrument_id: "TEST_INSTRUMENT_001".to_string(),
2080
        //         max_position: Quantity::from_f64(50.0).unwrap_or(Quantity::ZERO),
2081
        //     },
2082
        // );
2083
2084
1
        let order = create_test_order()
?0
;
2085
1
        let result = validator.validate_order(&order, None).await
?0
;
2086
2087
        // Without dynamic compliance rules, order should be compliant by default
2088
        // Test validates baseline behavior with static configuration
2089
1
        assert!(result.is_compliant);
2090
1
        assert!(result.violations.is_empty());
2091
2
        Ok(())
2092
1
    }
2093
2094
    #[tokio::test]
2095
1
    async fn test_violation_reporting() -> Result<(), Box<dyn std::error::Error>> {
2096
1
        let config = create_test_config()
?0
;
2097
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2098
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2099
1
        let violation = create_test_violation()
?0
;
2100
2101
1
        let result = validator.report_violation(&violation).await;
2102
1
        assert!(result.is_ok());
2103
2104
        // Should have logged the violation
2105
1
        let audit_entries = validator.get_enhanced_audit_trail(None).await;
2106
1
        assert!(audit_entries
2107
1
            .iter()
2108
1
            .any(|e| e.base_entry.event_type == "RISK_VIOLATION"));
2109
2
        Ok(())
2110
1
    }
2111
2112
    #[tokio::test]
2113
1
    async fn test_compliance_report_generation() -> Result<(), Box<dyn std::error::Error>> {
2114
1
        let config = create_test_config()
?0
;
2115
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2116
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2117
2118
        // Add some test data
2119
1
        let order = create_test_order()
?0
;
2120
1
        let violation = create_test_violation()
?0
;
2121
2122
1
        validator.validate_order(&order, None).await
?0
;
2123
1
        validator.report_violation(&violation).await
?0
;
2124
2125
        // Generate report
2126
1
        let start_date = Utc::now() - Duration::hours(1);
2127
1
        let end_date = Utc::now() + Duration::hours(1);
2128
2129
1
        let report = validator
2130
1
            .generate_regulatory_report(start_date, end_date)
2131
1
            .await
?0
;
2132
2133
1
        assert!(report.contains("REGULATORY COMPLIANCE REPORT"));
2134
1
        assert!(report.contains("Total Compliance Validations"));
2135
1
        assert!(report.contains("Regulatory Violations"));
2136
2
        Ok(())
2137
1
    }
2138
2139
    #[tokio::test]
2140
1
    async fn test_audit_trail_cleanup() -> Result<(), Box<dyn std::error::Error>> {
2141
1
        let config = create_test_config()
?0
;
2142
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2143
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2144
2145
        // Add a test entry with old timestamp using enhanced audit entry
2146
1
        let old_entry = EnhancedAuditEntry {
2147
1
            base_entry: AuditEntry {
2148
1
                id: "old_entry".to_string(),
2149
1
                timestamp: (Utc::now() - Duration::days(3000)).timestamp(),
2150
1
                event_type: "TEST".to_string(),
2151
1
                description: "Old test entry".to_string(),
2152
1
                actor: "TestSystem".to_string(),
2153
1
                user_id: None,
2154
1
                instrument_id: None,
2155
1
                portfolio_id: None,
2156
1
                data: HashMap::new(),
2157
1
                metadata: HashMap::new(),
2158
1
            },
2159
1
            compliance_status: ComplianceStatus::Compliant,
2160
1
            regulatory_references: vec![],
2161
1
            risk_score: None,
2162
1
            client_classification: None,
2163
1
            execution_venue: None,
2164
1
            best_execution_analysis: None,
2165
1
        };
2166
2167
1
        validator.log_enhanced_audit_entry(old_entry).await
?0
;
2168
2169
1
        let initial_count = validator.get_enhanced_audit_trail(None).await.len();
2170
1
        validator.cleanup_audit_trail().await
?0
;
2171
1
        let final_count = validator.get_enhanced_audit_trail(None).await.len();
2172
2173
        // Old entry should be removed
2174
1
        assert!(final_count < initial_count);
2175
2
        Ok(())
2176
1
    }
2177
2178
    #[tokio::test]
2179
1
    async fn test_position_limit_exactly_at_threshold() -> Result<(), Box<dyn std::error::Error>> {
2180
1
        let config = create_test_config()
?0
;
2181
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2182
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2183
2184
        // Set position limit exactly at order size
2185
1
        let limit = PositionLimit {
2186
1
            instrument_id: "TEST_INSTRUMENT_001".to_string(),
2187
1
            max_position_size: Price::from_f64(15000.0)
?0
, // Exactly order value
2188
1
            max_daily_turnover: Price::from_f64(50000.0)
?0
,
2189
1
            concentration_limit: Price::from_f64(0.1)
?0
,
2190
1
            regulatory_basis: "Test limit".to_string(),
2191
        };
2192
2193
1
        validator
2194
1
            .set_position_limit("TEST_INSTRUMENT_001".to_string(), limit)
2195
1
            .await
?0
;
2196
2197
1
        let order = create_test_order()
?0
;
2198
1
        let result = validator.validate_order(&order, None).await
?0
;
2199
2200
        // Order exactly at limit should be compliant
2201
1
        assert!(result.is_compliant);
2202
2
        Ok(())
2203
1
    }
2204
2205
    #[tokio::test]
2206
1
    async fn test_basel_iii_capital_adequacy_below_minimum(
2207
1
    ) -> Result<(), Box<dyn std::error::Error>> {
2208
        // Set environment variables for low capital ratio
2209
1
        std::env::set_var("TIER1_CAPITAL", "7000000");
2210
1
        std::env::set_var("RISK_WEIGHTED_ASSETS", "100000000");
2211
1
        std::env::set_var("TOTAL_EXPOSURE", "200000000");
2212
2213
1
        let config = create_test_config()
?0
;
2214
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2215
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2216
2217
1
        let order = create_test_order()
?0
;
2218
1
        let result = validator.validate_order(&order, None).await
?0
;
2219
2220
        // Should have warnings about capital adequacy
2221
1
        assert!(!result.warnings.is_empty());
2222
1
        assert!(result
2223
1
            .warnings
2224
1
            .iter()
2225
2
            .
any1
(|w| matches!(w.warning_type, ComplianceWarningType::CapitalAdequacyLow)));
2226
2227
        // Clean up
2228
1
        std::env::remove_var("TIER1_CAPITAL");
2229
1
        std::env::remove_var("RISK_WEIGHTED_ASSETS");
2230
1
        std::env::remove_var("TOTAL_EXPOSURE");
2231
2
        Ok(())
2232
1
    }
2233
2234
    #[tokio::test]
2235
1
    async fn test_market_abuse_large_order_detection() -> Result<(), Box<dyn std::error::Error>> {
2236
1
        let config = create_test_config()
?0
;
2237
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2238
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2239
2240
        // Create large order to trigger market abuse detection
2241
1
        let mut order = create_test_order()
?0
;
2242
1
        order.quantity = Quantity::from_f64(10000.0)
?0
;
2243
1
        order.price = Price::from_f64(150.0)
?0
;
2244
2245
1
        let result = validator.validate_order(&order, None).await
?0
;
2246
2247
        // Should have regulatory flag for large order
2248
1
        assert!(!result.regulatory_flags.is_empty());
2249
1
        assert!(result
2250
1
            .regulatory_flags
2251
1
            .iter()
2252
1
            .any(|f| matches!(f.flag_type, RegulatoryFlagType::MarketRisk)));
2253
2
        Ok(())
2254
1
    }
2255
2256
    #[tokio::test]
2257
1
    async fn test_client_suitability_conservative_profile() -> Result<(), Box<dyn std::error::Error>>
2258
1
    {
2259
1
        let config = create_test_config()
?0
;
2260
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2261
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2262
2263
        // Set conservative client classification
2264
1
        let classification = ClientClassification {
2265
1
            client_id: "conservative_client".to_string(),
2266
1
            classification: ClientType::RetailClient,
2267
1
            leverage_limit: Price::from_f64(5.0)
?0
,
2268
1
            risk_tolerance: RiskTolerance::Conservative,
2269
1
            regulatory_restrictions: vec![],
2270
        };
2271
2272
1
        validator
2273
1
            .set_client_classification("conservative_client".to_string(), classification)
2274
1
            .await
?0
;
2275
2276
        // Create large order for conservative client
2277
1
        let mut order = create_test_order()
?0
;
2278
1
        order.quantity = Quantity::from_f64(1000.0)
?0
;
2279
1
        order.price = Price::from_f64(150.0)
?0
;
2280
2281
1
        let result = validator
2282
1
            .validate_order(&order, Some("conservative_client"))
2283
1
            .await
?0
;
2284
2285
        // Should have warnings about suitability
2286
1
        assert!(!result.warnings.is_empty());
2287
2
        Ok(())
2288
1
    }
2289
2290
    #[tokio::test]
2291
1
    async fn test_best_execution_no_venues() -> Result<(), Box<dyn std::error::Error>> {
2292
1
        let config = create_test_config()
?0
;
2293
1
        let mut regulatory_config = create_test_regulatory_config()
?0
;
2294
1
        regulatory_config.mifid2_enabled = true;
2295
2296
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2297
2298
1
        let order = create_test_order()
?0
;
2299
1
        let result = validator.validate_order(&order, None).await
?0
;
2300
2301
        // Should have warning about missing best execution analysis
2302
1
        assert!(result
2303
1
            .warnings
2304
1
            .iter()
2305
1
            .any(|w| matches!(w.warning_type, ComplianceWarningType::BestExecutionRisk)));
2306
2
        Ok(())
2307
1
    }
2308
2309
    #[tokio::test]
2310
1
    async fn test_compliance_metrics() -> Result<(), Box<dyn std::error::Error>> {
2311
1
        let config = create_test_config()
?0
;
2312
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2313
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2314
2315
        // Add some test data
2316
1
        let order = create_test_order()
?0
;
2317
1
        let violation = create_test_violation()
?0
;
2318
2319
1
        validator.validate_order(&order, None).await
?0
;
2320
1
        validator.report_violation(&violation).await
?0
;
2321
2322
1
        let metrics = validator.get_compliance_metrics().await;
2323
2324
1
        assert!(metrics.contains_key("total_audit_entries"));
2325
1
        assert!(metrics.contains_key("compliance_violations"));
2326
1
        assert!(metrics.contains_key("compliance_rate"));
2327
2
        Ok(())
2328
1
    }
2329
2330
    #[tokio::test]
2331
1
    async fn test_subscribe_to_violations() -> Result<(), Box<dyn std::error::Error>> {
2332
1
        let config = create_test_config()
?0
;
2333
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2334
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2335
2336
1
        let mut violation_receiver = validator.subscribe_to_violations();
2337
1
        let violation = create_test_violation()
?0
;
2338
2339
1
        validator.report_violation(&violation).await
?0
;
2340
2341
        // Should receive the violation through broadcast
2342
1
        let received = violation_receiver.try_recv();
2343
1
        assert!(received.is_ok());
2344
2
        Ok(())
2345
1
    }
2346
2347
    #[tokio::test]
2348
1
    async fn test_subscribe_to_warnings() -> Result<(), Box<dyn std::error::Error>> {
2349
1
        let config = create_test_config()
?0
;
2350
1
        let regulatory_config = create_test_regulatory_config()
?0
;
2351
1
        let validator = ComplianceValidator::new(config, regulatory_config);
2352
2353
1
        let mut warning_receiver = validator.subscribe_to_warnings();
2354
2355
        // Create order that triggers warnings
2356
1
        let order = create_test_order()
?0
;
2357
1
        validator.validate_order(&order, None).await
?0
;
2358
2359
        // Try to receive any warnings (might be empty)
2360
1
        let _ = warning_receiver.try_recv();
2361
2
        Ok(())
2362
1
    }
2363
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/drawdown_monitor.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/drawdown_monitor.rs.html deleted file mode 100644 index 751b4b524..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/drawdown_monitor.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/drawdown_monitor.rs
Line
Count
Source
1
//! Drawdown monitoring system for real-time risk tracking
2
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
3
4
use std::collections::HashMap;
5
6
use chrono::{DateTime, Utc};
7
// REMOVED: Direct Decimal usage - use canonical types
8
use tokio::sync::{broadcast, RwLock};
9
use tracing::warn;
10
11
use crate::error::{RiskError, RiskResult};
12
use crate::risk_types::{DrawdownAlertConfig, PnLMetrics, PortfolioId, RiskSeverity};
13
// Import canonical types
14
15
/// Drawdown alert event
16
#[derive(Debug, Clone)]
17
pub struct DrawdownAlert {
18
    /// Portfolio identifier that triggered the alert
19
    pub portfolio_id: PortfolioId,
20
    /// Severity level of the drawdown alert
21
    pub severity: RiskSeverity,
22
    /// Current drawdown percentage from high water mark
23
    pub current_drawdown_pct: f64,
24
    /// Threshold percentage that was breached
25
    pub threshold_pct: f64,
26
    /// Human-readable alert message
27
    pub message: String,
28
    /// Timestamp when the alert was generated
29
    pub timestamp: DateTime<Utc>,
30
}
31
32
/// Drawdown statistics for a portfolio
33
#[derive(Debug, Clone)]
34
pub struct DrawdownStats {
35
    /// Current drawdown percentage from peak
36
    pub current_drawdown_pct: f64,
37
    /// Maximum drawdown percentage ever recorded
38
    pub max_drawdown_pct: f64,
39
    /// Highest portfolio value achieved (high water mark)
40
    pub high_water_mark: f64,
41
    /// Number of consecutive days in drawdown
42
    pub days_in_drawdown: i32,
43
}
44
45
/// Drawdown monitor for tracking portfolio drawdowns
46
#[derive(Debug)]
47
pub struct DrawdownMonitor {
48
    /// Configuration for drawdown alerts per portfolio
49
    alert_configs: RwLock<HashMap<PortfolioId, DrawdownAlertConfig>>,
50
    /// Broadcast channel for alerts
51
    alert_sender: broadcast::Sender<DrawdownAlert>,
52
    /// Historical P&L tracking for drawdown calculation
53
    pnl_history: RwLock<HashMap<PortfolioId, Vec<PnLMetrics>>>,
54
}
55
56
impl Default for DrawdownMonitor {
57
10
    fn default() -> Self {
58
10
        let (alert_sender, _) = broadcast::channel(1000);
59
10
        Self {
60
10
            alert_configs: RwLock::new(HashMap::new()),
61
10
            alert_sender,
62
10
            pnl_history: RwLock::new(HashMap::new()),
63
10
        }
64
10
    }
65
}
66
67
impl DrawdownMonitor {
68
    /// Create a new `DrawdownMonitor`
69
    #[must_use]
70
0
    pub fn new() -> Self {
71
0
        Self::default()
72
0
    }
73
74
    /// Configure alerts for a portfolio
75
8
    pub async fn configure_alerts(&self, config: DrawdownAlertConfig) -> RiskResult<()> {
76
8
        let mut configs = self.alert_configs.write().await;
77
8
        configs.insert(config.portfolio_id.clone().unwrap_or_default(), config);
78
8
        Ok(())
79
8
    }
80
81
    /// Update P&L and return any alerts triggered
82
8
    pub async fn update_pnl(&self, metrics: &PnLMetrics) -> RiskResult<Vec<DrawdownAlert>> {
83
8
        let mut alerts = Vec::new();
84
85
        // Get current alert subscriber to catch any alerts
86
8
        let mut alert_receiver = self.subscribe_alerts();
87
88
        // Process the P&L
89
8
        self.process_pnl(metrics).await
?0
;
90
91
        // Try to receive any alerts that were sent
92
13
        while let Ok(
alert5
) = alert_receiver.try_recv() {
93
5
            alerts.push(alert);
94
5
        }
95
96
8
        Ok(alerts)
97
8
    }
98
99
    /// Process P&L metrics and check for drawdown alerts
100
1.10k
    pub async fn process_pnl(&self, metrics: &PnLMetrics) -> RiskResult<()> {
101
        // Store P&L history
102
        {
103
1.10k
            let mut history = self.pnl_history.write().await;
104
1.10k
            let portfolio_history = history
105
1.10k
                .entry(metrics.portfolio_id.clone())
106
1.10k
                .or_insert_with(Vec::new);
107
1.10k
            portfolio_history.push(metrics.clone());
108
109
            // Keep only recent history (last 1000 entries)
110
1.10k
            if portfolio_history.len() > 1000 {
111
100
                portfolio_history.drain(0..portfolio_history.len() - 1000);
112
1.00k
            }
113
        }
114
115
        // Check for drawdown alerts
116
1.10k
        let configs = self.alert_configs.read().await;
117
1.10k
        if let Some(
config7
) = configs.get(&metrics.portfolio_id) {
118
7
            if config.enabled {
119
6
                self.check_drawdown_thresholds(metrics, config).await
?0
;
120
1
            }
121
1.10k
        }
122
123
1.10k
        Ok(())
124
1.10k
    }
125
126
    /// Check if drawdown has exceeded configured thresholds
127
6
    async fn check_drawdown_thresholds(
128
6
        &self,
129
6
        metrics: &PnLMetrics,
130
6
        config: &DrawdownAlertConfig,
131
6
    ) -> RiskResult<()> {
132
        // Calculate drawdown percentage properly
133
        // Drawdown = (HWM - Current P&L) / HWM * 100
134
6
        let hwm = metrics.high_water_mark.to_f64();
135
6
        let current_pnl = metrics.total_pnl.to_f64();
136
137
6
        let current_drawdown_pct = if hwm > 0.0 {
138
6
            ((hwm - current_pnl) / hwm) * 100.0
139
        } else {
140
0
            0.0
141
        };
142
143
6
        let current_drawdown_pct = current_drawdown_pct.abs();
144
145
        // Check thresholds in order of severity
146
6
        if current_drawdown_pct >= config.emergency_threshold {
147
1
            self.send_alert(
148
1
                &metrics.portfolio_id,
149
1
                RiskSeverity::Critical,
150
1
                current_drawdown_pct,
151
1
                config.emergency_threshold,
152
1
                "Emergency drawdown threshold exceeded",
153
1
            )
154
1
            .await;
155
5
        } else if current_drawdown_pct >= config.critical_threshold {
156
3
            self.send_alert(
157
3
                &metrics.portfolio_id,
158
3
                RiskSeverity::High,
159
3
                current_drawdown_pct,
160
3
                config.critical_threshold,
161
3
                "Critical drawdown threshold exceeded",
162
3
            )
163
3
            .await;
164
2
        } else if current_drawdown_pct >= config.warning_threshold {
165
1
            self.send_alert(
166
1
                &metrics.portfolio_id,
167
1
                RiskSeverity::Medium,
168
1
                current_drawdown_pct,
169
1
                config.warning_threshold,
170
1
                "Warning drawdown threshold exceeded",
171
1
            )
172
1
            .await;
173
1
        }
174
175
6
        Ok(())
176
6
    }
177
178
    /// Send a drawdown alert
179
5
    async fn send_alert(
180
5
        &self,
181
5
        portfolio_id: &str,
182
5
        severity: RiskSeverity,
183
5
        current_pct: f64,
184
5
        threshold_pct: f64,
185
5
        message: &str,
186
5
    ) {
187
5
        let alert = DrawdownAlert {
188
5
            portfolio_id: portfolio_id.to_owned(),
189
5
            severity,
190
5
            current_drawdown_pct: current_pct,
191
5
            threshold_pct,
192
5
            message: message.to_owned(),
193
5
            timestamp: Utc::now(),
194
5
        };
195
196
5
        if let Err(
e0
) = self.alert_sender.send(alert) {
197
0
            warn!("Failed to send drawdown alert: {}", e);
198
5
        }
199
5
    }
200
201
    /// Subscribe to drawdown alerts
202
8
    pub fn subscribe_alerts(&self) -> broadcast::Receiver<DrawdownAlert> {
203
8
        self.alert_sender.subscribe()
204
8
    }
205
206
    /// Get current alert configuration for a portfolio
207
1
    pub async fn get_alert_config(&self, portfolio_id: &str) -> Option<DrawdownAlertConfig> {
208
1
        let configs = self.alert_configs.read().await;
209
1
        configs.get(portfolio_id).cloned()
210
1
    }
211
212
    /// Get P&L history for a portfolio
213
1
    pub async fn get_pnl_history(&self, portfolio_id: &str) -> Vec<PnLMetrics> {
214
1
        let history = self.pnl_history.read().await;
215
1
        history.get(portfolio_id).cloned().unwrap_or_default()
216
1
    }
217
218
    /// Get drawdown statistics for a portfolio
219
6
    pub async fn get_drawdown_stats(&self, portfolio_id: &str) -> RiskResult<DrawdownStats> {
220
6
        let history = self.pnl_history.read().await;
221
6
        let empty_vec = Vec::new();
222
6
        let portfolio_history = history.get(portfolio_id).unwrap_or(&empty_vec);
223
224
6
        if portfolio_history.is_empty() {
225
1
            return Ok(DrawdownStats {
226
1
                current_drawdown_pct: 0.0,
227
1
                max_drawdown_pct: 0.0,
228
1
                high_water_mark: 0.0,
229
1
                days_in_drawdown: 0,
230
1
            });
231
5
        }
232
233
5
        let latest = portfolio_history
234
5
            .last()
235
5
            .ok_or_else(|| RiskError::CalculationError(
"Portfolio history is empty"0
.
to_owned0
()))
?0
;
236
237
        // Calculate drawdown percentage properly
238
        // Drawdown = (HWM - Current P&L) / HWM * 100
239
5
        let hwm = latest.high_water_mark.to_f64();
240
5
        let current_pnl = latest.total_pnl.to_f64();
241
242
5
        let current_drawdown_pct = if hwm > 0.0 {
243
4
            ((hwm - current_pnl) / hwm) * 100.0
244
        } else {
245
1
            0.0
246
        };
247
248
        // For max drawdown, use the max_drawdown value from metrics
249
5
        let max_dd = latest.max_drawdown.to_f64();
250
5
        let max_drawdown_pct = if hwm > 0.0 {
251
4
            (max_dd.abs() / hwm) * 100.0
252
        } else {
253
1
            0.0
254
        };
255
256
5
        Ok(DrawdownStats {
257
5
            current_drawdown_pct,
258
5
            max_drawdown_pct,
259
5
            high_water_mark: latest.high_water_mark.to_f64(),
260
5
            days_in_drawdown: 0, // Would need more complex calculation based on history
261
5
        })
262
6
    }
263
}
264
265
#[cfg(test)]
266
mod tests {
267
    use super::*;
268
    use common::Price;
269
    // operations module removed - use direct imports from common
270
271
1.10k
    fn create_test_pnl_metrics(portfolio_id: &str, pnl: i64) -> PnLMetrics {
272
1.10k
        PnLMetrics {
273
1.10k
            portfolio_id: portfolio_id.to_string(),
274
1.10k
            realized_pnl: Price::from_f64(pnl as f64 * 0.6).unwrap_or(Price::ZERO),
275
1.10k
            unrealized_pnl: Price::from_f64(pnl as f64 * 0.4).unwrap_or(Price::ZERO),
276
1.10k
            total_unrealized_pnl: Price::from_f64(pnl as f64 * 0.4).unwrap_or(Price::ZERO),
277
1.10k
            total_pnl: Price::from_f64(pnl as f64).unwrap_or(Price::ZERO),
278
1.10k
            daily_pnl: Price::from_f64(pnl as f64 * 0.1).unwrap_or(Price::ZERO),
279
1.10k
            inception_pnl: Price::from_f64(pnl as f64).unwrap_or(Price::ZERO),
280
1.10k
            max_drawdown: Price::ZERO,
281
1.10k
            current_drawdown_pct: 0.0,
282
1.10k
            high_water_mark: Price::from_f64(1000000.0).unwrap_or(Price::ZERO),
283
1.10k
            roi_pct: 0.0,
284
1.10k
            timestamp: Utc::now().timestamp(),
285
1.10k
        }
286
1.10k
    }
287
288
    #[tokio::test]
289
1
    async fn test_drawdown_monitor_creation() {
290
1
        let _monitor = DrawdownMonitor::default();
291
        // Test passes if no panic
292
1
    }
293
294
    #[tokio::test]
295
1
    async fn test_alert_configuration() {
296
1
        let monitor = DrawdownMonitor::default();
297
298
1
        let config = DrawdownAlertConfig {
299
1
            portfolio_id: Some("test_portfolio".to_string()),
300
1
            warning_threshold: 5.0,
301
1
            critical_threshold: 10.0,
302
1
            emergency_threshold: 20.0,
303
1
            enabled: true,
304
1
        };
305
306
1
        let _ = monitor.configure_alerts(config).await;
307
308
1
        let configs = monitor.alert_configs.read().await;
309
1
        assert!(configs.contains_key("test_portfolio"));
310
1
    }
311
312
    #[tokio::test]
313
1
    async fn test_drawdown_calculation() -> Result<(), Box<dyn std::error::Error>> {
314
1
        let monitor = DrawdownMonitor::default();
315
316
        // Configure alerts
317
1
        let config = DrawdownAlertConfig {
318
1
            portfolio_id: Some("test_portfolio".to_string()),
319
1
            warning_threshold: 5.0,
320
1
            critical_threshold: 10.0,
321
1
            emergency_threshold: 20.0,
322
1
            enabled: true,
323
1
        };
324
325
1
        let _ = monitor.configure_alerts(config).await;
326
327
        // Simulate P&L progression with drawdown
328
1
        let mut pnl_metrics = create_test_pnl_metrics("test_portfolio", 1000000);
329
1
        monitor.update_pnl(&pnl_metrics).await
?0
;
330
331
        // Simulate drawdown
332
1
        pnl_metrics.total_pnl = Price::from_f64(900000.0).unwrap_or(Price::ZERO); // 10% drawdown
333
1
        let alerts = monitor.update_pnl(&pnl_metrics).await
?0
;
334
335
1
        assert!(!alerts.is_empty());
336
1
        assert_eq!(
337
1
            alerts.get(0).map(|a| &a.severity),
338
            Some(&RiskSeverity::High)
339
        ); // Should trigger critical alert
340
341
1
        let stats = monitor.get_drawdown_stats("test_portfolio").await
?0
;
342
1
        assert!(stats.current_drawdown_pct >= 10.0);
343
2
        Ok(())
344
1
    }
345
346
    #[tokio::test]
347
1
    async fn test_drawdown_emergency_threshold() -> Result<(), Box<dyn std::error::Error>> {
348
1
        let monitor = DrawdownMonitor::default();
349
350
1
        let config = DrawdownAlertConfig {
351
1
            portfolio_id: Some("test_portfolio".to_string()),
352
1
            warning_threshold: 5.0,
353
1
            critical_threshold: 10.0,
354
1
            emergency_threshold: 20.0,
355
1
            enabled: true,
356
1
        };
357
358
1
        let _ = monitor.configure_alerts(config).await;
359
360
        // Simulate large drawdown
361
1
        let mut pnl_metrics = create_test_pnl_metrics("test_portfolio", 1000000);
362
1
        pnl_metrics.total_pnl = Price::from_f64(750000.0).unwrap_or(Price::ZERO); // 25% drawdown
363
364
1
        let alerts = monitor.update_pnl(&pnl_metrics).await
?0
;
365
366
1
        assert!(!alerts.is_empty());
367
1
        assert_eq!(
368
1
            alerts.get(0).map(|a| &a.severity),
369
            Some(&RiskSeverity::Critical)
370
        ); // Should trigger emergency alert
371
2
        Ok(())
372
1
    }
373
374
    #[tokio::test]
375
1
    async fn test_drawdown_disabled_alerts() -> Result<(), Box<dyn std::error::Error>> {
376
1
        let monitor = DrawdownMonitor::default();
377
378
1
        let config = DrawdownAlertConfig {
379
1
            portfolio_id: Some("test_portfolio".to_string()),
380
1
            warning_threshold: 5.0,
381
1
            critical_threshold: 10.0,
382
1
            emergency_threshold: 20.0,
383
1
            enabled: false, // Disabled
384
1
        };
385
386
1
        let _ = monitor.configure_alerts(config).await;
387
388
        // Simulate drawdown
389
1
        let mut pnl_metrics = create_test_pnl_metrics("test_portfolio", 1000000);
390
1
        pnl_metrics.total_pnl = Price::from_f64(900000.0).unwrap_or(Price::ZERO);
391
392
1
        let alerts = monitor.update_pnl(&pnl_metrics).await
?0
;
393
394
1
        assert!(alerts.is_empty()); // No alerts when disabled
395
2
        Ok(())
396
1
    }
397
398
    #[tokio::test]
399
1
    async fn test_drawdown_zero_hwm() -> Result<(), Box<dyn std::error::Error>> {
400
1
        let monitor = DrawdownMonitor::default();
401
402
        // Metrics with zero high water mark
403
1
        let mut pnl_metrics = create_test_pnl_metrics("test_portfolio", 1000000);
404
1
        pnl_metrics.high_water_mark = Price::ZERO;
405
406
1
        monitor.update_pnl(&pnl_metrics).await
?0
;
407
408
1
        let stats = monitor.get_drawdown_stats("test_portfolio").await
?0
;
409
1
        assert_eq!(stats.current_drawdown_pct, 0.0); // Should be 0 when HWM is 0
410
2
        Ok(())
411
1
    }
412
413
    #[tokio::test]
414
1
    async fn test_drawdown_multiple_portfolios() -> Result<(), Box<dyn std::error::Error>> {
415
1
        let monitor = DrawdownMonitor::default();
416
417
        // Configure alerts for multiple portfolios
418
4
        
for 1
i3
in 1..=3 {
419
3
            let config = DrawdownAlertConfig {
420
3
                portfolio_id: Some(format!("portfolio_{}", i)),
421
3
                warning_threshold: 5.0,
422
3
                critical_threshold: 10.0,
423
3
                emergency_threshold: 20.0,
424
3
                enabled: true,
425
3
            };
426
3
            let _ = monitor.configure_alerts(config).await;
427
1
        }
428
1
429
1
        // Update P&L for each portfolio
430
4
        for 
i3
in 1..=3 {
431
3
            let pnl_metrics =
432
3
                create_test_pnl_metrics(&format!("portfolio_{}", i), 1000000 - (i * 50000));
433
3
            monitor.update_pnl(&pnl_metrics).await
?0
;
434
1
        }
435
1
436
1
        // Verify each portfolio has its own stats
437
4
        for 
i3
in 1..=3 {
438
3
            let stats = monitor
439
3
                .get_drawdown_stats(&format!("portfolio_{}", i))
440
3
                .await
?0
;
441
3
            assert!(stats.high_water_mark > 0.0);
442
1
        }
443
1
        Ok(())
444
1
    }
445
446
    #[tokio::test]
447
1
    async fn test_drawdown_history_limit() -> Result<(), Box<dyn std::error::Error>> {
448
1
        let monitor = DrawdownMonitor::default();
449
450
        // Add more than 1000 P&L entries
451
1.10k
        for 
i1.10k
in 0..1100 {
452
1.10k
            let pnl_metrics = create_test_pnl_metrics("test_portfolio", 1000000 - i);
453
1.10k
            monitor.process_pnl(&pnl_metrics).await
?0
;
454
        }
455
456
1
        let history = monitor.get_pnl_history("test_portfolio").await;
457
1
        assert!(history.len() <= 1000); // Should be capped at 1000
458
2
        Ok(())
459
1
    }
460
461
    #[tokio::test]
462
1
    async fn test_get_alert_config() -> Result<(), Box<dyn std::error::Error>> {
463
1
        let monitor = DrawdownMonitor::default();
464
465
1
        let config = DrawdownAlertConfig {
466
1
            portfolio_id: Some("test_portfolio".to_string()),
467
1
            warning_threshold: 5.0,
468
1
            critical_threshold: 10.0,
469
1
            emergency_threshold: 20.0,
470
1
            enabled: true,
471
1
        };
472
473
1
        let _ = monitor.configure_alerts(config.clone()).await;
474
475
1
        let retrieved_config = monitor.get_alert_config("test_portfolio").await;
476
1
        assert!(retrieved_config.is_some());
477
1
        assert_eq!(retrieved_config.unwrap().warning_threshold, 5.0);
478
2
        Ok(())
479
1
    }
480
481
    #[tokio::test]
482
1
    async fn test_drawdown_stats_empty_portfolio() -> Result<(), Box<dyn std::error::Error>> {
483
1
        let monitor = DrawdownMonitor::default();
484
485
1
        let stats = monitor.get_drawdown_stats("nonexistent_portfolio").await
?0
;
486
1
        assert_eq!(stats.current_drawdown_pct, 0.0);
487
1
        assert_eq!(stats.max_drawdown_pct, 0.0);
488
1
        assert_eq!(stats.high_water_mark, 0.0);
489
2
        Ok(())
490
1
    }
491
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/error.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/error.rs.html deleted file mode 100644 index 3bd4b7e38..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/error.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/error.rs
Line
Count
Source
1
//! Error types for the risk management system
2
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
3
4
use thiserror::Error;
5
6
use common::error::CommonError;
7
use common::types::Price;
8
9
use crate::risk_types::RiskSeverity;
10
11
/// Comprehensive error types for the risk management system
12
///
13
/// This enum covers all possible error conditions that can occur during risk
14
/// management operations, from configuration issues to critical safety violations.
15
/// Each error variant includes context-specific information to aid in debugging
16
/// and incident response.
17
#[derive(Debug, Error)]
18
#[error(transparent)]
19
pub enum RiskError {
20
    /// Configuration error occurred during system initialization or validation
21
    #[error("Configuration error: {0}")]
22
    Config(String),
23
    /// Database operation failed (connection, query, transaction, etc.)
24
    #[error("Database error: {0}")]
25
    Database(String),
26
    /// Position limit exceeded for a specific instrument
27
    #[error("Position limit exceeded: {instrument} has {current} but limit is {limit}")]
28
    PositionLimitExceeded {
29
        /// The financial instrument that exceeded its limit
30
        instrument: String,
31
        /// Current position size
32
        current: Price,
33
        /// Maximum allowed position size
34
        limit: Price,
35
    },
36
    /// Value at Risk limit exceeded, indicating portfolio risk is too high
37
    #[error("VaR limit exceeded: {var} exceeds limit of {limit}")]
38
    VarLimitExceeded {
39
        /// Current `VaR` value
40
        var: Price,
41
        /// Maximum allowed `VaR`
42
        limit: Price,
43
    },
44
    /// Drawdown limit exceeded, indicating excessive portfolio losses
45
    #[error("Drawdown limit exceeded: {drawdown}% exceeds limit of {limit}%")]
46
    DrawdownLimitExceeded {
47
        /// Current drawdown percentage
48
        drawdown: Price,
49
        /// Maximum allowed drawdown percentage
50
        limit: Price,
51
    },
52
    /// Daily loss limit exceeded, triggering risk controls
53
    #[error("Daily loss limit exceeded: {loss} exceeds limit of {limit}")]
54
    DailyLossLimitExceeded {
55
        /// Current daily loss amount
56
        loss: Price,
57
        /// Maximum allowed daily loss
58
        limit: Price,
59
    },
60
    /// Circuit breaker is active, preventing trading on an instrument
61
    #[error("Circuit breaker active for {instrument}: {reason}")]
62
    CircuitBreakerActive {
63
        /// The instrument with an active circuit breaker
64
        instrument: String,
65
        /// Reason why the circuit breaker was triggered
66
        reason: String,
67
    },
68
    /// Kill switch is active, immediately halting operations
69
    #[error("Kill switch active for {scope:?}: {message}")]
70
    KillSwitchActive {
71
        /// Scope of the kill switch (global, strategy, symbol)
72
        scope: crate::risk_types::KillSwitchScope,
73
        /// Detailed message about why the kill switch was activated
74
        message: String,
75
    },
76
    /// Market data is unavailable for required calculations
77
    #[error("Market data unavailable for {instrument}")]
78
    MarketDataUnavailable {
79
        /// The instrument for which market data is missing
80
        instrument: String,
81
    },
82
    /// Insufficient historical data for reliable risk calculations
83
    #[error("Insufficient historical data: need {required} but have {available}")]
84
    InsufficientHistoricalData {
85
        /// Number of data points required
86
        required: usize,
87
        /// Number of data points available
88
        available: usize,
89
    },
90
    /// Correlation calculation failed between instruments
91
    #[error("Correlation calculation failed: {reason}")]
92
    CorrelationCalculationFailed {
93
        /// Detailed reason for the correlation calculation failure
94
        reason: String,
95
    },
96
    /// Stress test scenario failed, indicating portfolio vulnerability
97
    #[error("Stress test failed: {scenario}")]
98
    StressTestFailed {
99
        /// The stress test scenario that failed
100
        scenario: String,
101
    },
102
    /// Performance metric violated its threshold
103
    #[error("Performance violation: {metric} = {value}, threshold = {threshold}")]
104
    PerformanceViolation {
105
        /// The performance metric that was violated
106
        metric: String,
107
        /// Current value of the metric
108
        value: Price,
109
        /// Threshold value that was exceeded
110
        threshold: Price,
111
    },
112
    /// Regulatory compliance rule was violated
113
    #[error("Compliance violation: {rule}")]
114
    ComplianceViolation {
115
        /// The compliance rule that was violated
116
        rule: String,
117
    },
118
    /// User authorization failed for the requested operation
119
    #[error("Authorization failed: {reason}")]
120
    AuthorizationFailed {
121
        /// Reason why authorization failed
122
        reason: String,
123
    },
124
    /// Order validation failed
125
    #[error("Invalid order: {reason}")]
126
    InvalidOrder {
127
        /// Reason why the order is invalid
128
        reason: String,
129
    },
130
    /// Required service is unavailable
131
    #[error("Service unavailable: {service}")]
132
    ServiceUnavailable {
133
        /// Name of the unavailable service
134
        service: String,
135
    },
136
    /// Operation timed out after specified duration
137
    #[error("Operation timeout: {timeout_ms}ms")]
138
    Timeout {
139
        /// Timeout duration in milliseconds
140
        timeout_ms: u64,
141
    },
142
    /// Data serialization/deserialization failed
143
    #[error("Serialization error: {0}")]
144
    Serialization(String),
145
    /// Network communication error occurred
146
    #[error("Network error: {0}")]
147
    Network(String),
148
    /// Internal system error - should not occur in normal operation
149
    #[error("Internal error: {0}")]
150
    Internal(String),
151
    /// Data validation failed for a specific field
152
    #[error("Validation error: {field} - {message}")]
153
    Validation {
154
        /// The field that failed validation
155
        field: String,
156
        /// Detailed validation error message
157
        message: String,
158
    },
159
    /// System resource was exhausted (memory, connections, etc.)
160
    #[error("Resource exhausted: {resource}")]
161
    ResourceExhausted {
162
        /// The resource that was exhausted
163
        resource: String,
164
    },
165
    /// Mathematical calculation failed
166
    #[error("Calculation error: {operation} failed - {reason}")]
167
    Calculation {
168
        /// The calculation operation that failed
169
        operation: String,
170
        /// Reason for the calculation failure
171
        reason: String,
172
    },
173
    /// Rate limit exceeded, operation must wait
174
    #[error("Rate limited: {remaining_ms}ms until reset")]
175
    RateLimited {
176
        /// Milliseconds remaining until rate limit resets
177
        remaining_ms: u64,
178
    },
179
    /// Order side (buy/sell) is invalid
180
    #[error("Invalid order side: {side}")]
181
    InvalidOrderSide {
182
        /// The invalid order side value
183
        side: String,
184
    },
185
    /// Order type is invalid or not supported
186
    #[error("Invalid order type: {order_type}")]
187
    InvalidOrderType {
188
        /// The invalid order type value
189
        order_type: String,
190
    },
191
    /// Order quantity is invalid (negative, zero, too large, etc.)
192
    #[error("Invalid quantity: {quantity}")]
193
    InvalidQuantity {
194
        /// The invalid quantity value
195
        quantity: String,
196
    },
197
    /// Order price is invalid (negative, zero, outside valid range, etc.)
198
    #[error("Invalid price: {price}")]
199
    InvalidPrice {
200
        /// The invalid price value
201
        price: String,
202
    },
203
    /// Generic connection error occurred
204
    #[error("Connection error: {message}")]
205
    ConnectionError {
206
        /// Detailed connection error message
207
        message: String,
208
    },
209
    /// Configuration-related error occurred
210
    #[error("Configuration error: {message}")]
211
    Configuration {
212
        /// Detailed configuration error message
213
        message: String,
214
    },
215
    /// Generic validation error occurred
216
    #[error("Validation error: {message}")]
217
    ValidationError {
218
        /// Detailed validation error message
219
        message: String,
220
    },
221
    /// Serialization/deserialization error occurred
222
    #[error("Serialization error: {message}")]
223
    SerializationError {
224
        /// Detailed serialization error message
225
        message: String,
226
    },
227
228
    // NEW: Enhanced error types for hardened risk management
229
    /// Type conversion failed between incompatible types
230
    #[error("Type conversion error: {from_type} to {to_type} failed - {reason}")]
231
    TypeConversion {
232
        /// Source type being converted from
233
        from_type: String,
234
        /// Target type being converted to
235
        to_type: String,
236
        /// Reason for the conversion failure
237
        reason: String,
238
    },
239
240
    /// Calculation error with simplified message
241
    #[error("Calculation error: {0}")]
242
    CalculationError(String),
243
244
    /// Required configuration parameter is missing
245
    #[error("Missing configuration: {config_key}")]
246
    MissingConfiguration {
247
        /// The configuration key that is missing
248
        config_key: String,
249
    },
250
251
    /// Environment validation failed - missing requirements
252
    #[error("Environment validation failed: {environment} requires {requirement}")]
253
    EnvironmentValidation {
254
        /// The environment being validated
255
        environment: String,
256
        /// The requirement that was not met
257
        requirement: String,
258
    },
259
260
    /// Data integrity check failed
261
    #[error("Data integrity error: {data_type} validation failed - {details}")]
262
    DataIntegrity {
263
        /// Type of data that failed integrity check
264
        data_type: String,
265
        /// Detailed information about the integrity failure
266
        details: String,
267
    },
268
269
    /// Resource is temporarily unavailable
270
    #[error("Resource unavailable: {resource} temporarily unavailable - {reason}")]
271
    ResourceUnavailable {
272
        /// The unavailable resource
273
        resource: String,
274
        /// Reason why the resource is unavailable
275
        reason: String,
276
    },
277
278
    /// Safety limit exceeded - immediate intervention required
279
    #[error("Safety limit exceeded: {limit_type} current={current} max={maximum}")]
280
    SafetyLimitExceeded {
281
        /// Type of safety limit that was exceeded
282
        limit_type: String,
283
        /// Current value that exceeded the limit
284
        current: String,
285
        /// Maximum allowed value
286
        maximum: String,
287
    },
288
289
    /// Emergency stop triggered - all operations must halt immediately
290
    #[error("Emergency stop triggered: {trigger} - immediate halt required")]
291
    EmergencyStop {
292
        /// The trigger that caused the emergency stop
293
        trigger: String,
294
    },
295
296
    /// Production safety violation detected
297
    #[error("Production safety violation: {violation} in {environment}")]
298
    ProductionSafety {
299
        /// Description of the safety violation
300
        violation: String,
301
        /// Environment where the violation occurred
302
        environment: String,
303
    },
304
305
    /// Broker system error occurred
306
    #[error("Broker connection error: {0}")]
307
    BrokerError(String),
308
309
    /// Broker connection failed
310
    #[error("Broker connection error: {message}")]
311
    BrokerConnection {
312
        /// Detailed broker connection error message
313
        message: String,
314
    },
315
316
    /// Connection to specific endpoint failed
317
    #[error("Connection failed to {endpoint}: {reason}")]
318
    Connection {
319
        /// The endpoint that failed to connect
320
        endpoint: String,
321
        /// Reason for the connection failure
322
        reason: String,
323
    },
324
325
    /// Market data system error occurred
326
    #[error("Market data error: {0}")]
327
    MarketDataError(String),
328
329
    /// Required data is unavailable for calculations
330
    #[error("Data unavailable: {resource} - {reason}")]
331
    DataUnavailable {
332
        /// The data resource that is unavailable
333
        resource: String,
334
        /// Reason why the data is unavailable
335
        reason: String,
336
    },
337
338
    /// Arithmetic overflow detected during calculation
339
    #[error("Arithmetic overflow: {operation} - {context}")]
340
    ArithmeticOverflow {
341
        /// The operation that overflowed
342
        operation: String,
343
        /// Context about the overflow
344
        context: String,
345
    },
346
}
347
348
/// Result type for risk management operations
349
pub type RiskResult<T> = Result<T, RiskError>;
350
351
/// Safe conversion helpers to eliminate `unwrap()` patterns
352
use num::ToPrimitive;
353
use rust_decimal::Decimal;
354
use std::fmt::Display;
355
356
/// Safely convert f64 to Price with context
357
99
pub fn f64_to_price_safe(value: f64, context: &str) -> RiskResult<Price> {
358
99
    Price::from_f64(value).map_err(|_| RiskError::TypeConversion {
359
0
        from_type: "f64".to_owned(),
360
0
        to_type: "Price".to_owned(),
361
0
        reason: format!("{context}: invalid f64 value {value}"),
362
0
    })
363
99
}
364
365
/// Safely convert f64 to Decimal with context
366
0
pub fn f64_to_decimal_safe(value: f64, context: &str) -> RiskResult<Decimal> {
367
0
    Decimal::try_from(value).map_err(|_| RiskError::TypeConversion {
368
0
        from_type: "f64".to_owned(),
369
0
        to_type: "Decimal".to_owned(),
370
0
        reason: format!("{context}: invalid f64 value {value}"),
371
0
    })
372
0
}
373
374
/// Safely convert Price to Decimal with context
375
0
pub fn price_to_decimal_safe(price: Price, context: &str) -> RiskResult<Decimal> {
376
0
    price.to_decimal().map_err(|_| RiskError::TypeConversion {
377
0
        from_type: "Price".to_owned(),
378
0
        to_type: "Decimal".to_owned(),
379
0
        reason: format!("{context}: price conversion failed"),
380
0
    })
381
0
}
382
383
/// Safely convert Decimal to f64 with context
384
96
pub fn decimal_to_f64_safe(decimal: Decimal, context: &str) -> RiskResult<f64> {
385
96
    decimal.to_f64().ok_or_else(|| RiskError::TypeConversion {
386
0
        from_type: "Decimal".to_owned(),
387
0
        to_type: "f64".to_owned(),
388
0
        reason: format!("{context}: decimal too large for f64"),
389
0
    })
390
96
}
391
392
/// Safely parse environment variable with context
393
30
pub fn parse_env_var<T: std::str::FromStr>(var_name: &str, context: &str) -> RiskResult<T>
394
30
where
395
30
    T::Err: Display,
396
{
397
30
    std::env::var(var_name)
398
30
        .map_err(|_| RiskError::MissingConfiguration {
399
12
            config_key: var_name.to_owned(),
400
12
        })?
401
18
        .parse()
402
18
        .map_err(|e| RiskError::EnvironmentValidation {
403
0
            environment: var_name.to_owned(),
404
0
            requirement: format!("{context}: {e}"),
405
0
        })
406
30
}
407
408
/// Safe division with zero check
409
0
pub fn safe_divide(numerator: Decimal, denominator: Decimal, context: &str) -> RiskResult<Decimal> {
410
0
    if denominator.is_zero() {
411
0
        return Err(RiskError::Calculation {
412
0
            operation: "division".to_owned(),
413
0
            reason: format!("{context}: division by zero"),
414
0
        });
415
0
    }
416
0
    Ok(numerator / denominator)
417
0
}
418
419
// ELIMINATED: Re-exports removed to force explicit imports
420
421
// Also support FoxhuntResult<T> for consistency with error-handling framework
422
// Removed core dependency - use core instead
423
424
impl RiskError {
425
    /// Get the severity level of this error
426
    #[must_use]
427
0
    pub const fn severity(&self) -> RiskSeverity {
428
0
        match self {
429
            RiskError::KillSwitchActive { .. }
430
            | RiskError::DailyLossLimitExceeded { .. }
431
            | RiskError::DrawdownLimitExceeded { .. }
432
            | RiskError::EmergencyStop { .. }
433
0
            | RiskError::ProductionSafety { .. } => RiskSeverity::Critical,
434
435
            RiskError::PositionLimitExceeded { .. }
436
            | RiskError::VarLimitExceeded { .. }
437
            | RiskError::CircuitBreakerActive { .. }
438
0
            | RiskError::ComplianceViolation { .. } => RiskSeverity::High,
439
440
            RiskError::PerformanceViolation { .. }
441
            | RiskError::MarketDataUnavailable { .. }
442
0
            | RiskError::AuthorizationFailed { .. } => RiskSeverity::Medium,
443
444
0
            _ => RiskSeverity::Low,
445
        }
446
0
    }
447
448
    /// Check if the error should trigger a kill switch
449
    #[must_use]
450
0
    pub const fn should_trigger_kill_switch(&self) -> bool {
451
0
        matches!(
452
0
            self,
453
            RiskError::DailyLossLimitExceeded { .. } | RiskError::DrawdownLimitExceeded { .. }
454
        )
455
0
    }
456
457
    /// Check if the error should trigger a circuit breaker
458
    #[must_use]
459
0
    pub const fn should_trigger_circuit_breaker(&self) -> bool {
460
0
        matches!(
461
0
            self,
462
            RiskError::PositionLimitExceeded { .. }
463
                | RiskError::VarLimitExceeded { .. }
464
                | RiskError::PerformanceViolation { .. }
465
        )
466
0
    }
467
468
    /// Get error code for logging and monitoring
469
    #[must_use]
470
0
    pub const fn error_code(&self) -> &'static str {
471
0
        match self {
472
0
            RiskError::Config(_) => "CONFIG_ERROR",
473
0
            RiskError::Database(_) => "DATABASE_ERROR",
474
0
            RiskError::PositionLimitExceeded { .. } => "POSITION_LIMIT_EXCEEDED",
475
0
            RiskError::VarLimitExceeded { .. } => "VAR_LIMIT_EXCEEDED",
476
0
            RiskError::DrawdownLimitExceeded { .. } => "DRAWDOWN_LIMIT_EXCEEDED",
477
0
            RiskError::DailyLossLimitExceeded { .. } => "DAILY_LOSS_LIMIT_EXCEEDED",
478
0
            RiskError::CircuitBreakerActive { .. } => "CIRCUIT_BREAKER_ACTIVE",
479
0
            RiskError::KillSwitchActive { .. } => "KILL_SWITCH_ACTIVE",
480
0
            RiskError::MarketDataUnavailable { .. } => "MARKET_DATA_UNAVAILABLE",
481
0
            RiskError::InsufficientHistoricalData { .. } => "INSUFFICIENT_HISTORICAL_DATA",
482
0
            RiskError::CorrelationCalculationFailed { .. } => "CORRELATION_CALCULATION_FAILED",
483
0
            RiskError::StressTestFailed { .. } => "STRESS_TEST_FAILED",
484
0
            RiskError::PerformanceViolation { .. } => "PERFORMANCE_VIOLATION",
485
0
            RiskError::ComplianceViolation { .. } => "COMPLIANCE_VIOLATION",
486
0
            RiskError::AuthorizationFailed { .. } => "AUTHORIZATION_FAILED",
487
0
            RiskError::InvalidOrder { .. } => "INVALID_ORDER",
488
0
            RiskError::ServiceUnavailable { .. } => "SERVICE_UNAVAILABLE",
489
0
            RiskError::Timeout { .. } => "TIMEOUT",
490
0
            RiskError::Serialization(_) => "SERIALIZATION_ERROR",
491
0
            RiskError::Network(_) => "NETWORK_ERROR",
492
0
            RiskError::Internal(_) => "INTERNAL_ERROR",
493
0
            RiskError::Validation { .. } => "VALIDATION_ERROR",
494
0
            RiskError::ResourceExhausted { .. } => "RESOURCE_EXHAUSTED",
495
0
            RiskError::Calculation { .. } => "CALCULATION_ERROR",
496
0
            RiskError::RateLimited { .. } => "RATE_LIMITED",
497
0
            RiskError::InvalidOrderSide { .. } => "INVALID_ORDER_SIDE",
498
0
            RiskError::InvalidOrderType { .. } => "INVALID_ORDER_TYPE",
499
0
            RiskError::InvalidQuantity { .. } => "INVALID_QUANTITY",
500
0
            RiskError::InvalidPrice { .. } => "INVALID_PRICE",
501
0
            RiskError::ConnectionError { .. } => "CONNECTION_ERROR",
502
0
            RiskError::Configuration { .. } => "CONFIGURATION_ERROR",
503
0
            RiskError::ValidationError { .. } => "VALIDATION_ERROR",
504
0
            RiskError::SerializationError { .. } => "SERIALIZATION_ERROR",
505
506
            // NEW: Enhanced error codes
507
0
            RiskError::TypeConversion { .. } => "TYPE_CONVERSION_ERROR",
508
0
            RiskError::CalculationError(_) => "CALCULATION_ERROR",
509
0
            RiskError::MissingConfiguration { .. } => "MISSING_CONFIGURATION",
510
0
            RiskError::EnvironmentValidation { .. } => "ENVIRONMENT_VALIDATION_ERROR",
511
0
            RiskError::DataIntegrity { .. } => "DATA_INTEGRITY_ERROR",
512
0
            RiskError::ResourceUnavailable { .. } => "RESOURCE_UNAVAILABLE",
513
0
            RiskError::SafetyLimitExceeded { .. } => "SAFETY_LIMIT_EXCEEDED",
514
0
            RiskError::EmergencyStop { .. } => "EMERGENCY_STOP",
515
0
            RiskError::ProductionSafety { .. } => "PRODUCTION_SAFETY_VIOLATION",
516
0
            RiskError::BrokerError(_) => "BROKER_ERROR",
517
0
            RiskError::BrokerConnection { .. } => "BROKER_CONNECTION_ERROR",
518
0
            RiskError::Connection { .. } => "CONNECTION_ERROR",
519
0
            RiskError::MarketDataError(_) => "MARKET_DATA_ERROR",
520
0
            RiskError::DataUnavailable { .. } => "DATA_UNAVAILABLE",
521
0
            RiskError::ArithmeticOverflow { .. } => "ARITHMETIC_OVERFLOW",
522
        }
523
0
    }
524
}
525
526
/// Convert from tokio timeout error
527
impl From<tokio::time::error::Elapsed> for RiskError {
528
0
    fn from(_: tokio::time::error::Elapsed) -> Self {
529
0
        RiskError::Timeout { timeout_ms: 0 }
530
0
    }
531
}
532
533
/// Convert from `anyhow::Error`
534
impl From<anyhow::Error> for RiskError {
535
0
    fn from(err: anyhow::Error) -> Self {
536
0
        RiskError::Internal(err.to_string())
537
0
    }
538
}
539
540
/// Convert from `CommonError` to `RiskError`  
541
impl From<CommonError> for RiskError {
542
0
    fn from(err: CommonError) -> Self {
543
0
        RiskError::Internal(format!("CommonError: {err}"))
544
0
    }
545
}
546
547
/// Convert from `CommonTypeError`
548
impl From<common::types::CommonTypeError> for RiskError {
549
0
    fn from(err: common::types::CommonTypeError) -> Self {
550
0
        RiskError::Internal(format!("CommonTypeError: {err}"))
551
0
    }
552
}
553
554
/// Convert from `serde_json::Error`
555
impl From<serde_json::Error> for RiskError {
556
0
    fn from(err: serde_json::Error) -> Self {
557
0
        RiskError::Serialization(err.to_string())
558
0
    }
559
}
560
561
/// Convert from `std::num::ParseFloatError`
562
impl From<std::num::ParseFloatError> for RiskError {
563
0
    fn from(err: std::num::ParseFloatError) -> Self {
564
0
        RiskError::TypeConversion {
565
0
            from_type: "string".to_owned(),
566
0
            to_type: "f64".to_owned(),
567
0
            reason: err.to_string(),
568
0
        }
569
0
    }
570
}
571
572
/// Convert from `std::num::ParseIntError`
573
impl From<std::num::ParseIntError> for RiskError {
574
0
    fn from(err: std::num::ParseIntError) -> Self {
575
0
        RiskError::TypeConversion {
576
0
            from_type: "string".to_owned(),
577
0
            to_type: "integer".to_owned(),
578
0
            reason: err.to_string(),
579
0
        }
580
0
    }
581
}
582
583
/// Convert from `std::env::VarError`
584
impl From<std::env::VarError> for RiskError {
585
0
    fn from(err: std::env::VarError) -> Self {
586
0
        match err {
587
0
            std::env::VarError::NotPresent => RiskError::MissingConfiguration {
588
0
                config_key: "unknown".to_owned(),
589
0
            },
590
0
            std::env::VarError::NotUnicode(_) => RiskError::EnvironmentValidation {
591
0
                environment: "unknown".to_owned(),
592
0
                requirement: "valid unicode".to_owned(),
593
0
            },
594
        }
595
0
    }
596
}
597
598
/// Convert from `std::io::Error`
599
impl From<std::io::Error> for RiskError {
600
0
    fn from(err: std::io::Error) -> Self {
601
0
        RiskError::Network(format!("IO error: {err}"))
602
0
    }
603
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs.html deleted file mode 100644 index 4a03f619c..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/kelly_sizing.rs
Line
Count
Source
1
//! Kelly Criterion Position Sizing Implementation
2
//!
3
//! Implements the Kelly Criterion for optimal position sizing in trading.
4
//! The Kelly Criterion determines the optimal fraction of capital to risk
5
//! on each trade based on the probability of success and the risk/reward ratio.
6
7
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
8
9
use chrono::{DateTime, Utc};
10
use rust_decimal::prelude::ToPrimitive;
11
use rust_decimal::Decimal;
12
use serde::{Deserialize, Serialize};
13
use std::collections::HashMap;
14
use std::sync::Arc;
15
use tracing::{debug, info};
16
17
use crate::error::{RiskError, RiskResult};
18
use common::types::{Price, Symbol};
19
use config::structures::KellyConfig;
20
21
// REMOVED: KellyConfig is now imported from config crate
22
// Use: config::KellyConfig instead of local definition
23
// Default implementation is provided by the config crate
24
25
/// Historical trade outcome for Kelly calculation
26
///
27
/// Records the complete details of a completed trade for use in calculating
28
/// optimal position sizes using the Kelly Criterion. Each trade outcome
29
/// contributes to the statistical analysis of win rates and profit/loss ratios.
30
#[derive(Debug, Clone, Serialize, Deserialize)]
31
pub struct TradeOutcome {
32
    /// Symbol that was traded
33
    pub symbol: Symbol,
34
    /// Strategy identifier that executed this trade
35
    pub strategy_id: String,
36
    /// Price at which the position was entered
37
    pub entry_price: Price,
38
    /// Price at which the position was exited
39
    pub exit_price: Price,
40
    /// Quantity of shares/contracts traded
41
    pub quantity: Price,
42
    /// Realized profit or loss from this trade (can be negative for losses)
43
    pub profit_loss: Decimal,
44
    /// Whether this trade was profitable (true) or a loss (false)
45
    pub win: bool,
46
    /// UTC timestamp when this trade was executed
47
    pub trade_date: DateTime<Utc>,
48
}
49
50
/// Kelly fraction calculation result
51
///
52
/// Contains the complete results of a Kelly Criterion calculation including
53
/// the raw and adjusted Kelly fractions, confidence metrics, and statistical
54
/// data used in the calculation.
55
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56
pub struct KellyResult {
57
    /// Symbol for which the Kelly fraction was calculated
58
    pub symbol: Symbol,
59
    /// Strategy identifier used in the calculation
60
    pub strategy_id: String,
61
    /// Raw Kelly fraction (can be negative)
62
    pub raw_kelly_fraction: f64,
63
    /// Adjusted Kelly fraction (capped and floored)
64
    pub adjusted_kelly_fraction: f64,
65
    /// Confidence in the calculation (0.0-1.0)
66
    pub confidence: f64,
67
    /// Win rate from historical data
68
    pub win_rate: f64,
69
    /// Average win amount
70
    pub average_win: f64,
71
    /// Average loss amount
72
    pub average_loss: f64,
73
    /// Number of trades in sample
74
    pub sample_size: usize,
75
    /// Whether to use Kelly sizing for this trade
76
    pub use_kelly: bool,
77
    /// Recommended position size as fraction of capital
78
    pub position_fraction: f64,
79
}
80
81
/// Kelly Criterion Position Sizer
82
///
83
/// Implements the Kelly Criterion for optimal position sizing based on historical
84
/// trade outcomes. Maintains a rolling history of trades and calculates optimal
85
/// position fractions for each symbol-strategy combination.
86
pub struct KellySizer {
87
    /// Configuration parameters for Kelly calculations
88
    config: KellyConfig,
89
    /// Historical trade outcomes by symbol and strategy
90
    trade_history: Arc<dashmap::DashMap<(Symbol, String), Vec<TradeOutcome>>>,
91
}
92
93
impl KellySizer {
94
    /// Create new Kelly sizer with configuration
95
    #[must_use]
96
37
    pub fn new(config: KellyConfig) -> Self {
97
37
        Self {
98
37
            config,
99
37
            trade_history: Arc::new(dashmap::DashMap::new()),
100
37
        }
101
37
    }
102
103
    /// Add a trade outcome to history for Kelly calculation
104
120
    pub fn add_trade_outcome(&self, outcome: TradeOutcome) -> RiskResult<()> {
105
120
        let key = (outcome.symbol.clone(), outcome.strategy_id.clone());
106
107
120
        let mut entry = self.trade_history.entry(key).or_default();
108
120
        entry.push(outcome.clone());
109
110
        // Keep only the last N trades for calculation
111
120
        if entry.len() > self.config.lookback_periods * 2 {
112
0
            let drain_count = entry.len() - self.config.lookback_periods;
113
0
            entry.drain(0..drain_count);
114
120
        }
115
116
120
        debug!(
117
0
            "Added trade outcome for {} ({}): P&L = {}",
118
            outcome.symbol, outcome.strategy_id, outcome.profit_loss
119
        );
120
121
120
        Ok(())
122
120
    }
123
124
    /// Calculate Kelly fraction for a given symbol and strategy
125
8
    pub fn calculate_kelly_fraction(
126
8
        &self,
127
8
        symbol: &Symbol,
128
8
        strategy_id: &str,
129
8
    ) -> RiskResult<KellyResult> {
130
8
        let key = (symbol.clone(), strategy_id.to_owned());
131
132
        // Get historical trades
133
8
        let trades = self
134
8
            .trade_history
135
8
            .get(&key)
136
8
            .map(|entry| 
entry5
.
clone5
())
137
8
            .unwrap_or_default();
138
139
8
        if trades.len() < 10 {
140
            // CRITICAL: NEVER use default sizing when insufficient data
141
            // Kelly sizing requires statistical confidence - defaults mask lack of backtesting
142
3
            return Err(RiskError::DataUnavailable {
143
3
                resource: "trade_history".to_owned(),
144
3
                reason: format!(
145
3
                    "Insufficient trade history for Kelly calculation: {} trades (minimum 10 required) for symbol {} strategy {}",
146
3
                    trades.len(), symbol, strategy_id
147
3
                ),
148
3
            });
149
5
        }
150
151
        // Calculate win rate and average win/loss
152
5
        let total_trades = trades.len();
153
5
        let wins: Vec<&TradeOutcome> = trades.iter().filter(|t| t.win).collect();
154
120
        let 
losses5
:
Vec<&TradeOutcome>5
=
trades.iter()5
.
filter5
(|t| !t.win).
collect5
();
155
156
5
        let win_rate = wins.len() as f64 / total_trades as f64;
157
5
        let loss_rate = losses.len() as f64 / total_trades as f64;
158
159
        // Calculate average win and loss amounts
160
5
        let average_win = if wins.is_empty() {
161
0
            0.0
162
        } else {
163
5
            let sum: Decimal = wins.iter().map(|t| t.profit_loss).sum();
164
5
            sum.to_f64().unwrap_or(0.0) / wins.len() as f64
165
        };
166
167
5
        let average_loss = if losses.is_empty() {
168
0
            0.0
169
        } else {
170
39
            let 
sum5
:
Decimal5
=
losses.iter()5
.
map5
(|t| t.profit_loss.abs()).
sum5
();
171
5
            sum.to_f64().unwrap_or(0.0) / losses.len() as f64
172
        };
173
174
        // Calculate Kelly fraction: f* = (bp - q) / b
175
        // where b = odds received on win (average_win / average_loss)
176
        //       p = probability of winning
177
        //       q = probability of losing (1 - p)
178
5
        let kelly_fraction = if average_loss > 0.0 && average_win > 0.0 && win_rate > 0.0 {
179
5
            let b = average_win / average_loss; // Odds ratio
180
5
            let p = win_rate;
181
5
            let q = loss_rate;
182
183
5
            debug!(
184
0
                "Kelly calculation: avg_win={}, avg_loss={}, b={}, p={}, q={}",
185
                average_win, average_loss, b, p, q
186
            );
187
188
            // Ensure Kelly fraction is positive for profitable strategies
189
5
            let raw_kelly = (b * p - q) / b;
190
5
            debug!(
"Raw Kelly before filtering: {}"0
, raw_kelly);
191
192
5
            if raw_kelly > 0.0 {
193
5
                raw_kelly
194
            } else {
195
0
                0.0 // Don't use negative Kelly fractions
196
            }
197
        } else {
198
0
            debug!(
199
0
                "Kelly calculation skipped: avg_win={}, avg_loss={}, win_rate={}",
200
                average_win, average_loss, win_rate
201
            );
202
0
            0.0
203
        };
204
205
        // Calculate confidence based on sample size and win rate consistency
206
5
        let confidence = self.calculate_confidence(total_trades, win_rate);
207
208
        // Determine if we should use Kelly sizing
209
5
        let use_kelly = self.config.enabled
210
5
            && confidence >= self.config.confidence_threshold
211
0
            && kelly_fraction > 0.0
212
0
            && total_trades >= 20;
213
214
        // Apply fractional Kelly and caps
215
5
        let adjusted_kelly = if use_kelly {
216
0
            let fractional_kelly = kelly_fraction * self.config.fractional_kelly;
217
0
            fractional_kelly
218
0
                .max(self.config.min_kelly_fraction)
219
0
                .min(self.config.max_kelly_fraction)
220
        } else {
221
5
            self.config.default_position_fraction
222
        };
223
224
5
        let result = KellyResult {
225
5
            symbol: symbol.clone(),
226
5
            strategy_id: strategy_id.to_owned(),
227
5
            raw_kelly_fraction: kelly_fraction,
228
5
            adjusted_kelly_fraction: adjusted_kelly,
229
5
            confidence,
230
5
            win_rate,
231
5
            average_win,
232
5
            average_loss,
233
5
            sample_size: total_trades,
234
5
            use_kelly,
235
5
            position_fraction: adjusted_kelly,
236
5
        };
237
238
5
        info!(
239
0
            "Kelly calculation for {} ({}): fraction={:.3}, confidence={:.2}, win_rate={:.2}",
240
            symbol, strategy_id, adjusted_kelly, confidence, win_rate
241
        );
242
243
5
        Ok(result)
244
8
    }
245
246
    /// Calculate confidence in Kelly fraction based on sample size and consistency
247
5
    fn calculate_confidence(&self, sample_size: usize, win_rate: f64) -> f64 {
248
        // Sample size confidence (larger samples = higher confidence)
249
5
        let size_confidence = (sample_size as f64 / 100.0).min(1.0);
250
251
        // Win rate confidence (avoid extreme win rates which may be overfitting)
252
5
        let rate_confidence = if (0.3..=0.7).contains(&win_rate) {
253
4
            1.0 // Reasonable win rates
254
1
        } else if (0.2..=0.8).contains(&win_rate) {
255
0
            0.8 // Slightly extreme but acceptable
256
        } else {
257
1
            0.5 // Very extreme win rates - lower confidence
258
        };
259
260
        // Combined confidence
261
5
        (size_confidence * rate_confidence).min(1.0)
262
5
    }
263
264
    /// Get recommended position size for a trade
265
5
    pub fn get_position_size(
266
5
        &self,
267
5
        symbol: &Symbol,
268
5
        strategy_id: &str,
269
5
        capital: Price,
270
5
        entry_price: Price,
271
5
    ) -> RiskResult<Price> {
272
5
        let 
kelly_result3
= self.calculate_kelly_fraction(symbol, strategy_id)
?2
;
273
274
3
        let position_fraction = Price::from_f64(kelly_result.position_fraction).map_err(|_| 
{0
275
0
            RiskError::ValidationError {
276
0
                message: "Invalid position fraction for position sizing".to_owned(),
277
0
            }
278
0
        })?;
279
280
3
        let position_value =
281
3
            (capital * position_fraction).map_err(|_| RiskError::ValidationError {
282
0
                message: "Failed to calculate position value".to_owned(),
283
0
            })?;
284
285
3
        if entry_price > Price::ZERO {
286
3
            let shares = (position_value / entry_price.to_f64()).map_err(|e| 
{0
287
0
                RiskError::ValidationError {
288
0
                    message: format!("Failed to calculate shares: {e:?}"),
289
0
                }
290
0
            })?;
291
3
            Ok(shares)
292
        } else {
293
0
            Err(RiskError::ValidationError {
294
0
                message: "Invalid entry price for position sizing".to_owned(),
295
0
            })
296
        }
297
5
    }
298
299
    /// Update configuration
300
0
    pub fn update_config(&mut self, new_config: KellyConfig) {
301
0
        self.config = new_config;
302
0
        info!("Kelly sizing configuration updated");
303
0
    }
304
305
    /// Get current configuration
306
    #[must_use]
307
0
    pub const fn get_config(&self) -> &KellyConfig {
308
0
        &self.config
309
0
    }
310
311
    /// Get trade history for a symbol and strategy
312
    #[must_use]
313
0
    pub fn get_trade_history(&self, symbol: &Symbol, strategy_id: &str) -> Vec<TradeOutcome> {
314
0
        let key = (symbol.clone(), strategy_id.to_owned());
315
0
        self.trade_history
316
0
            .get(&key)
317
0
            .map(|entry| entry.clone())
318
0
            .unwrap_or_default()
319
0
    }
320
321
    /// Clear trade history (useful for testing or reset)
322
0
    pub fn clear_history(&self) {
323
0
        self.trade_history.clear();
324
0
        info!("Kelly sizer trade history cleared");
325
0
    }
326
327
    /// Get Kelly statistics for all tracked symbols and strategies
328
    #[must_use]
329
0
    pub fn get_kelly_statistics(&self) -> HashMap<(Symbol, String), KellyResult> {
330
0
        let mut stats = HashMap::new();
331
332
0
        for entry in self.trade_history.iter() {
333
0
            let (symbol, strategy_id) = entry.key();
334
0
            if let Ok(kelly_result) = self.calculate_kelly_fraction(symbol, strategy_id) {
335
0
                stats.insert((symbol.clone(), strategy_id.clone()), kelly_result);
336
0
            }
337
        }
338
339
0
        stats
340
0
    }
341
}
342
343
#[cfg(test)]
344
mod tests {
345
    use super::*;
346
    use chrono::Utc;
347
348
90
    fn create_test_outcome(
349
90
        symbol: &str,
350
90
        strategy_id: &str,
351
90
        profit_loss: f64,
352
90
        win: bool,
353
90
    ) -> TradeOutcome {
354
        use rust_decimal::prelude::FromPrimitive;
355
356
        TradeOutcome {
357
90
            symbol: symbol.to_string().into(),
358
90
            strategy_id: strategy_id.to_string(),
359
90
            entry_price: Price::from_f64(100.0).unwrap_or(Price::ZERO),
360
90
            exit_price: Price::from_f64(if win { 
105.065
} else {
95.025
}).unwrap_or(Price::ZERO),
361
90
            quantity: Price::from_f64(10.0).unwrap_or(Price::ZERO),
362
90
            profit_loss: Decimal::from_f64(profit_loss).unwrap_or(Decimal::ZERO),
363
90
            win,
364
90
            trade_date: Utc::now(),
365
        }
366
90
    }
367
368
    #[tokio::test]
369
1
    async fn test_kelly_calculation_insufficient_data() {
370
1
        let config = KellyConfig::default();
371
1
        let sizer = KellySizer::new(config);
372
373
1
        let result =
374
1
            sizer.calculate_kelly_fraction(&Symbol::from("AAPL".to_string()), "test_strategy");
375
376
        // Should return error with insufficient data (< 10 trades)
377
1
        assert!(
378
1
            result.is_err(),
379
0
            "Kelly calculation should fail with insufficient data"
380
        );
381
382
        // Verify error message contains expected information
383
1
        if let Err(RiskError::DataUnavailable { resource, reason }) = result {
384
1
            assert_eq!(resource, "trade_history");
385
1
            assert!(reason.contains("Insufficient trade history"));
386
1
            assert!(reason.contains("0 trades"));
387
1
            assert!(reason.contains("minimum 10 required"));
388
1
        } else {
389
1
            
panic!0
(
"Expected DataUnavailable error"0
);
390
1
        }
391
1
    }
392
393
    #[tokio::test]
394
1
    async fn test_kelly_calculation_with_history() {
395
1
        let config = KellyConfig::default();
396
1
        let sizer = KellySizer::new(config);
397
398
        // Add winning trades
399
16
        for _ in 0..15 {
400
15
            let outcome = create_test_outcome("AAPL", "test_strategy", 50.0, true);
401
15
            let result = sizer.add_trade_outcome(outcome);
402
15
            assert!(
403
15
                result.is_ok(),
404
0
                "Adding trade outcome should not fail in test: {:?}",
405
0
                result.err()
406
            );
407
        }
408
409
        // Add losing trades
410
11
        for _ in 0..10 {
411
10
            let outcome = create_test_outcome("AAPL", "test_strategy", -30.0, false);
412
10
            let result = sizer.add_trade_outcome(outcome);
413
10
            assert!(
414
10
                result.is_ok(),
415
0
                "Adding trade outcome should not fail in test: {:?}",
416
0
                result.err()
417
            );
418
        }
419
420
1
        let result =
421
1
            sizer.calculate_kelly_fraction(&Symbol::from("AAPL".to_string()), "test_strategy");
422
1
        assert!(
423
1
            result.is_ok(),
424
0
            "Kelly calculation should not fail with sufficient data: {:?}",
425
0
            result.err()
426
        );
427
1
        let result = result.unwrap_or_else(|e| 
{0
428
0
            eprintln!(
429
0
                "Warning: Kelly calculation failed with sufficient data: {:?}",
430
                e
431
            );
432
0
            KellyResult::default() // Use default fallback instead of expect
433
0
        });
434
435
1
        assert_eq!(result.sample_size, 25);
436
1
        assert_eq!(result.win_rate, 0.6); // 15/25
437
1
        assert!(result.raw_kelly_fraction > 0.0);
438
1
        assert!(result.position_fraction > 0.0);
439
1
    }
440
441
    #[tokio::test]
442
1
    async fn test_position_size_calculation() {
443
1
        let config = KellyConfig::default();
444
1
        let sizer = KellySizer::new(config);
445
446
        // Add some trade history
447
21
        for _ in 0..20 {
448
20
            let outcome = create_test_outcome("AAPL", "test_strategy", 25.0, true);
449
20
            let result = sizer.add_trade_outcome(outcome);
450
20
            assert!(
451
20
                result.is_ok(),
452
0
                "Adding trade outcome should not fail in test: {:?}",
453
0
                result.err()
454
            );
455
        }
456
457
11
        for _ in 0..10 {
458
10
            let outcome = create_test_outcome("AAPL", "test_strategy", -20.0, false);
459
10
            let result = sizer.add_trade_outcome(outcome);
460
10
            assert!(
461
10
                result.is_ok(),
462
0
                "Adding trade outcome should not fail in test: {:?}",
463
0
                result.err()
464
            );
465
        }
466
467
1
        let capital = Price::from_f64(100000.0).unwrap_or(Price::ZERO); // $100k capital
468
1
        let entry_price = Price::from_f64(150.0).unwrap_or(Price::ZERO); // $150 per share
469
470
1
        let symbol = Symbol::from("AAPL");
471
1
        let position_size = sizer.get_position_size(&symbol, "test_strategy", capital, entry_price);
472
1
        assert!(
473
1
            position_size.is_ok(),
474
0
            "Position size calculation should not fail with valid inputs: {:?}",
475
0
            position_size.err()
476
        );
477
1
        let position_size = position_size.unwrap_or_else(|e| 
{0
478
0
            eprintln!("Warning: Position size calculation failed in test: {:?}", e);
479
0
            Price::ZERO // Use zero fallback instead of panic
480
0
        });
481
482
1
        assert!(position_size > Price::ZERO);
483
1
        assert!(position_size < capital); // Position should be less than total capital
484
1
    }
485
486
    #[tokio::test]
487
1
    async fn test_kelly_fraction_caps() {
488
1
        let mut config = KellyConfig::default();
489
1
        config.max_kelly_fraction = 0.1; // Cap at 10%
490
1
        config.min_kelly_fraction = 0.01; // Floor at 1%
491
492
1
        let sizer = KellySizer::new(config);
493
494
        // Add very profitable trades to generate high Kelly fraction
495
31
        for _ in 0..30 {
496
30
            let outcome = create_test_outcome("AAPL", "test_strategy", 100.0, true);
497
30
            let result = sizer.add_trade_outcome(outcome);
498
30
            assert!(
499
30
                result.is_ok(),
500
0
                "Adding trade outcome should not fail in test: {:?}",
501
0
                result.err()
502
            );
503
        }
504
505
        // Add few small losses
506
6
        for _ in 0..5 {
507
5
            let outcome = create_test_outcome("AAPL", "test_strategy", -10.0, false);
508
5
            let result = sizer.add_trade_outcome(outcome);
509
5
            assert!(
510
5
                result.is_ok(),
511
0
                "Adding trade outcome should not fail in test: {:?}",
512
0
                result.err()
513
            );
514
        }
515
516
1
        let result =
517
1
            sizer.calculate_kelly_fraction(&Symbol::from("AAPL".to_string()), "test_strategy");
518
1
        assert!(
519
1
            result.is_ok(),
520
0
            "Kelly calculation should not fail with sufficient data: {:?}",
521
0
            result.err()
522
        );
523
1
        let result = result.unwrap_or_else(|e| 
{0
524
0
            eprintln!("Warning: Kelly calculation failed in caps test: {:?}", e);
525
0
            KellyResult::default() // Use default fallback instead of expect
526
0
        });
527
528
        // Should be capped at max_kelly_fraction
529
1
        assert!(result.adjusted_kelly_fraction <= 0.1);
530
1
        assert!(result.raw_kelly_fraction > result.adjusted_kelly_fraction);
531
1
    }
532
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/lib.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/lib.rs.html deleted file mode 100644 index af51cc774..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/lib.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/lib.rs
Line
Count
Source
1
#![allow(unused_extern_crates)]
2
#![allow(unused_crate_dependencies)]
3
#![allow(missing_docs)] // Internal implementation details don't require documentation
4
#![allow(missing_debug_implementations)] // Not all types need Debug
5
// Clippy pedantic lints that are acceptable in financial/risk code
6
#![allow(clippy::default_numeric_fallback)] // Float literals are contextually typed in financial code
7
#![allow(clippy::float_arithmetic)] // Financial calculations require floating-point arithmetic
8
#![allow(clippy::arithmetic_side_effects)] // Risk calculations use saturating/checked arithmetic where needed
9
#![allow(clippy::cast_precision_loss)] // Acceptable in statistical/risk calculations with proper validation
10
#![allow(clippy::missing_errors_doc)] // Error documentation is in module-level docs
11
#![allow(clippy::cast_possible_truncation)] // Type conversions validated in context
12
#![allow(clippy::cast_sign_loss)] // Sign loss is validated in conversion functions
13
#![allow(clippy::cast_possible_wrap)] // Wrap-around is validated in context
14
#![allow(clippy::as_conversions)] // Type conversions are carefully managed in risk code
15
#![allow(clippy::map_err_ignore)] // Error context preserved in error types
16
#![allow(clippy::unused_async)] // Async needed for trait implementations and future expansion
17
#![allow(clippy::module_name_repetitions)] // Module prefixes provide clarity in risk domain
18
#![allow(clippy::similar_names)] // Financial variables often have similar names (var, cvar, etc.)
19
#![allow(clippy::shadow_reuse)] // Variable shadowing is intentional for transformations
20
#![allow(clippy::indexing_slicing)] // Bounds checked in context
21
#![allow(clippy::wildcard_enum_match_arm)] // Exhaustive matching not required for all enums
22
#![allow(clippy::cognitive_complexity)] // Risk calculations are inherently complex
23
#![allow(clippy::too_many_lines)] // Complex risk functions need many lines
24
#![allow(clippy::must_use_candidate)] // Not all functions need must_use
25
#![allow(clippy::used_underscore_binding)] // Underscore bindings used for partial pattern matches
26
#![allow(clippy::unused_self)] // Self parameter needed for trait consistency
27
#![allow(clippy::unreadable_literal)] // Large numbers are clear in financial context
28
#![allow(clippy::inline_always)] // Performance-critical code needs inlining hints
29
#![allow(clippy::expect_used)] // Expect used in validated contexts with clear messages
30
#![allow(clippy::unwrap_used)] // Unwrap used in validated contexts
31
#![allow(clippy::else_if_without_else)] // Exhaustive else not always needed
32
#![allow(clippy::partial_pub_fields)] // Intentional mixed visibility for safety
33
#![allow(clippy::unnecessary_safety_doc)] // Safety docs retained for clarity
34
#![allow(clippy::let_underscore_must_use)] // Intentional discarding of must_use
35
#![allow(clippy::redundant_clone)] // Clones needed for ownership
36
#![allow(clippy::shadow_unrelated)] // Shadowing is intentional
37
#![allow(clippy::empty_structs_with_brackets)] // Explicit struct syntax preferred
38
#![allow(clippy::to_string_trait_impl)] // String conversion is intentional
39
#![allow(clippy::infinite_loop)] // Infinite loops are intentional (servers, event loops)
40
#![allow(clippy::multiple_inherent_impl)] // Multiple impl blocks for organization
41
#![allow(clippy::unnecessary_safety_comment)] // Safety comments retained for code clarity
42
#![allow(clippy::string_to_string)] // String cloning is intentional for ownership
43
//! Risk Management Module
44
//!
45
//! This module provides comprehensive risk management functionality for HFT trading systems.
46
//! It includes Value at Risk (`VaR`) calculations, position tracking, stress testing, circuit breakers,
47
//! safety systems, and compliance monitoring.
48
//!
49
//! # Features
50
//!
51
//! - **`VaR` Calculation Engine**: Multiple methodologies (Historical Simulation, Monte Carlo, Parametric, Expected Shortfall)
52
//! - **Real-time Position Tracking**: Concentration risk monitoring with HHI calculations
53
//! - **Safety Systems**: Atomic kill switches, emergency response, position limiters
54
//! - **Circuit Breakers**: Dynamic portfolio protection with Redis coordination
55
//! - **Stress Testing**: Scenario analysis with Monte Carlo simulations
56
//! - **Compliance Monitoring**: Regulatory position limits and risk validation
57
//! - **Risk Engine**: Real-time risk validation and monitoring
58
//!
59
//! # Quick Start
60
//!
61
//! ```rust,no_run
62
//! use risk::prelude::*;
63
//!
64
//! #[tokio::main]
65
//! async fn main() -> Result<(), RiskError> {
66
//!     // Initialize risk engine
67
//!     let config = RiskConfig::default();
68
//!     let mut risk_engine = RiskEngine::new(config).await?;
69
//!     
70
//!     // Create a position tracker
71
//!     let position_tracker = PositionTracker::new();
72
//!     
73
//!     // Initialize VaR calculator
74
//!     let var_engine = RealVaREngine::new();
75
//!     
76
//!     // Perform risk check on an order
77
//!     let order_info = OrderInfo {
78
//!         symbol: Symbol::from("AAPL"),
79
//!         side: OrderSide::Buy,
80
//!         quantity: Quantity::new(100.0)?,
81
//!         price: Price::new(150.0)?,
82
//!     };
83
//!     
84
//!     let risk_result = risk_engine.validate_order(&order_info).await?;
85
//!     println!("Risk check result: {:?}", risk_result);
86
//!     
87
//!     Ok(())
88
//! }
89
//! ```
90
//!
91
//! # Architecture
92
//!
93
//! The risk module is structured around several core components:
94
//!
95
//! ## Core Components
96
//!
97
//! - **Risk Engine**: Central coordinator for all risk calculations and validations
98
//! - **Position Tracker**: Real-time position monitoring with concentration limits
99
//! - **`VaR` Calculator**: Multiple methodologies for portfolio risk assessment
100
//! - **Safety Systems**: Emergency controls and automated risk responses
101
//! - **Circuit Breakers**: Dynamic protection against market anomalies
102
//! - **Stress Tester**: Scenario analysis and portfolio stress testing
103
//! - **Compliance Monitor**: Regulatory compliance and position validation
104
//!
105
//! ## Safety Architecture
106
//!
107
//! The safety systems provide multiple layers of protection:
108
//!
109
//! 1. **Position Limits**: Hard limits on position sizes and concentrations
110
//! 2. **Kill Switches**: Atomic emergency stops with Redis broadcasting
111
//! 3. **Circuit Breakers**: Dynamic portfolio-based protection
112
//! 4. **Emergency Response**: Automated incident response and escalation
113
//! 5. **Compliance Checks**: Regulatory position limits and validation
114
//!
115
//! # Configuration
116
//!
117
//! The risk module can be configured via environment variables or configuration files:
118
//!
119
//! ```rust
120
//! use risk::RiskConfig;
121
//!
122
//! let config = RiskConfig {
123
//!     max_position_size: Price::new(1_000_000.0).unwrap(),
124
//!     max_daily_loss: Price::new(100_000.0).unwrap(),
125
//!     var_confidence_level: 0.95,
126
//!     var_lookback_days: 252,
127
//!     enable_kill_switch: true,
128
//!     enable_circuit_breakers: true,
129
//!     redis_url: "redis://localhost:6379".to_string(),
130
//! };
131
//! ```
132
133
#![warn(clippy::all)]
134
#![warn(clippy::pedantic)]
135
#![warn(clippy::cargo)]
136
// Note: unwrap/expect/panic allows are at the top of file for strategic linting
137
138
// Core modules
139
pub mod error;
140
// pub mod risk_types; // DELETED - duplicate types eliminated
141
pub mod operations;
142
143
// Risk calculation engines
144
pub mod kelly_sizing;
145
pub mod position_tracker;
146
pub mod risk_engine;
147
pub mod stress_tester;
148
pub mod var_calculator;
149
150
// Risk type definitions
151
pub mod risk_types;
152
153
// Safety and protection systems
154
pub mod circuit_breaker;
155
pub mod compliance;
156
pub mod drawdown_monitor;
157
pub mod safety;
158
159
// RE-EXPORTS FOR TEST COMPATIBILITY
160
// The following re-exports are required for test suite compilation
161
// Tests import these types directly from the risk crate
162
163
// Export key types from submodules for test compatibility
164
pub use var_calculator::var_engine::RealVaREngine;
165
pub use safety::kill_switch::AtomicKillSwitch;
166
pub use kelly_sizing::KellySizer;
167
pub use risk_engine::RiskEngine;
168
pub use stress_tester::StressTester;
169
170
// Export compliance types first
171
pub use compliance::ComplianceValidator;
172
173
// Type aliases for backward compatibility with tests
174
pub use ComplianceValidator as ComplianceEngine;
175
pub use RealVaREngine as VaRCalculator;
176
177
// ELIMINATED: Prelude module removed to force explicit imports
178
179
/// Library version
180
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
181
182
/// Library name
183
pub const NAME: &str = env!("CARGO_PKG_NAME");
184
185
/// Get risk module information
186
#[must_use]
187
1
pub fn info() -> RiskModuleInfo {
188
1
    RiskModuleInfo {
189
1
        name: NAME.to_owned(),
190
1
        version: VERSION.to_owned(),
191
1
        description: "Enterprise Risk Management for HFT Trading Systems".to_owned(),
192
1
        features: vec![
193
1
            "Value at Risk Calculations (Multiple Methodologies)".to_owned(),
194
1
            "Real-time Position Tracking".to_owned(),
195
1
            "Concentration Risk Monitoring".to_owned(),
196
1
            "Atomic Kill Switch Systems".to_owned(),
197
1
            "Dynamic Circuit Breakers".to_owned(),
198
1
            "Stress Testing and Scenario Analysis".to_owned(),
199
1
            "Compliance Monitoring".to_owned(),
200
1
            "Emergency Response Systems".to_owned(),
201
1
            "Kelly Criterion Position Sizing".to_owned(),
202
1
            "Drawdown Protection".to_owned(),
203
1
        ],
204
1
        methodologies: vec![
205
1
            "Historical Simulation VaR".to_owned(),
206
1
            "Monte Carlo VaR".to_owned(),
207
1
            "Parametric VaR".to_owned(),
208
1
            "Expected Shortfall (CVaR)".to_owned(),
209
1
        ],
210
1
    }
211
1
}
212
213
/// Risk module information structure
214
#[derive(Debug, Clone)]
215
pub struct RiskModuleInfo {
216
    /// Module name
217
    pub name: String,
218
    /// Version string
219
    pub version: String,
220
    /// Description
221
    pub description: String,
222
    /// Feature list
223
    pub features: Vec<String>,
224
    /// Risk methodologies supported
225
    pub methodologies: Vec<String>,
226
}
227
228
use std::fmt;
229
230
impl fmt::Display for RiskModuleInfo {
231
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232
0
        writeln!(f, "{} v{}", self.name, self.version)?;
233
0
        writeln!(f, "{}", self.description)?;
234
0
        writeln!(f, "\nFeatures:")?;
235
0
        for feature in &self.features {
236
0
            writeln!(f, "  \u{2022} {feature}")?;
237
        }
238
0
        writeln!(f, "\nRisk Methodologies:")?;
239
0
        for methodology in &self.methodologies {
240
0
            writeln!(f, "  \u{2022} {methodology}")?;
241
        }
242
0
        Ok(())
243
0
    }
244
}
245
246
/// Initialize the risk module with logging
247
0
pub fn init() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
248
    // Initialize tracing subscriber if not already initialized
249
0
    if std::env::var("RUST_LOG").is_err() {
250
0
        std::env::set_var("RUST_LOG", "info");
251
0
    }
252
253
0
    tracing_subscriber::fmt::try_init().map_err(|_| "Failed to initialize logging")?;
254
255
0
    tracing::info!("Initialized Risk Management Module {} v{}", NAME, VERSION);
256
0
    Ok(())
257
0
}
258
259
/// Validate risk configuration
260
5
pub fn validate_risk_config(config: &SafetyConfig) -> Result<(), String> {
261
    // Validate basic configuration
262
5
    if !config.enabled {
263
0
        tracing::warn!("Risk management is disabled - this should only be used in testing");
264
5
    }
265
266
    // Validate kill switch configuration
267
5
    if config.kill_switch.enabled {
268
5
        if config.kill_switch.global_channel.is_empty() {
269
1
            return Err("Kill switch global channel cannot be empty".to_owned());
270
4
        }
271
272
4
        if config.kill_switch.strategy_channel_prefix.is_empty() {
273
0
            return Err("Kill switch strategy channel prefix cannot be empty".to_owned());
274
4
        }
275
0
    }
276
277
    // Validate position limits
278
4
    if config.position_limits.enabled {
279
4
        if config.position_limits.max_position_per_symbol <= 0.0 {
280
1
            return Err("Maximum position per symbol must be positive".to_owned());
281
3
        }
282
283
3
        if config.position_limits.max_order_value <= 0.0 {
284
0
            return Err("Maximum order value must be positive".to_owned());
285
3
        }
286
287
3
        if config.position_limits.max_daily_loss <= 0.0 {
288
0
            return Err("Maximum daily loss must be positive".to_owned());
289
3
        }
290
0
    }
291
292
    // Validate Redis URL
293
3
    if config.redis_url.is_empty() {
294
1
        return Err("Redis URL cannot be empty".to_owned());
295
2
    }
296
297
    // Validate emergency response
298
2
    if config.emergency_response.enabled {
299
2
        if config.emergency_response.emergency_contacts.is_empty() {
300
0
            tracing::warn!("No emergency contacts configured for incident response");
301
2
        }
302
303
2
        if config.emergency_response.max_consecutive_violations == 0 {
304
0
            return Err("Max consecutive violations must be greater than 0".to_owned());
305
2
        }
306
0
    }
307
308
2
    Ok(())
309
5
}
310
311
use crate::safety::{
312
    EmergencyResponseConfig, KillSwitchConfig, PositionLimiterConfig, SafetyConfig,
313
};
314
use common::types::Price;
315
316
/// Get default configuration for development/testing
317
#[must_use]
318
4
pub fn development_config() -> SafetyConfig {
319
4
    SafetyConfig {
320
4
        enabled: true,
321
4
        kill_switch: KillSwitchConfig {
322
4
            enabled: true,
323
4
            global_channel: "foxhunt:dev:kill_switch:global".to_owned(),
324
4
            strategy_channel_prefix: "foxhunt:dev:kill_switch:strategy".to_owned(),
325
4
            symbol_channel_prefix: "foxhunt:dev:kill_switch:symbol".to_owned(),
326
4
            auto_recovery_enabled: true,
327
4
            auto_recovery_delay: std::time::Duration::from_secs(60), // 1 minute in dev
328
4
        },
329
4
        position_limits: PositionLimiterConfig {
330
4
            enabled: true,
331
4
            cache_ttl: std::time::Duration::from_secs(30),
332
4
            rpc_check_threshold_percent: 0.5, // Lower threshold for development
333
4
            max_position_per_symbol: 10_000.0, // $10K max per symbol in dev
334
4
            max_order_value: 5_000.0,         // $5K max per order in dev
335
4
            max_daily_loss: 1_000.0,          // $1K daily loss limit in dev
336
4
        },
337
4
        emergency_response: EmergencyResponseConfig {
338
4
            enabled: true,
339
4
            loss_check_interval: std::time::Duration::from_secs(30),
340
4
            position_check_interval: std::time::Duration::from_secs(15),
341
4
            max_consecutive_violations: 3,
342
4
            emergency_contacts: vec!["dev@foxhunt.com".to_owned()],
343
4
            max_daily_loss: Price::new(1000.0).unwrap_or(Price::ZERO),
344
4
            max_drawdown: Price::new(2000.0).unwrap_or(Price::ZERO),
345
4
        },
346
4
        redis_url: "redis://localhost:6379".to_owned(),
347
4
        safety_check_timeout: std::time::Duration::from_millis(50), // Longer timeout for dev
348
4
    }
349
4
}
350
351
/// Get configuration for production deployment
352
#[must_use]
353
1
pub fn production_config() -> SafetyConfig {
354
    SafetyConfig {
355
        enabled: true,
356
1
        kill_switch: KillSwitchConfig {
357
1
            enabled: true,
358
1
            global_channel: "foxhunt:prod:kill_switch:global".to_owned(),
359
1
            strategy_channel_prefix: "foxhunt:prod:kill_switch:strategy".to_owned(),
360
1
            symbol_channel_prefix: "foxhunt:prod:kill_switch:symbol".to_owned(),
361
1
            auto_recovery_enabled: false, // Manual recovery in production
362
1
            auto_recovery_delay: std::time::Duration::from_secs(1800), // 30 minutes
363
1
        },
364
1
        position_limits: PositionLimiterConfig {
365
1
            enabled: true,
366
1
            cache_ttl: std::time::Duration::from_secs(10), // Shorter cache in production
367
1
            rpc_check_threshold_percent: 0.9,              // Higher threshold for production
368
1
            max_position_per_symbol: 100_000.0,            // $100K max per symbol
369
1
            max_order_value: 50_000.0,                     // $50K max per order
370
1
            max_daily_loss: 10_000.0,                      // $10K daily loss limit
371
1
        },
372
1
        emergency_response: EmergencyResponseConfig {
373
1
            enabled: true,
374
1
            loss_check_interval: std::time::Duration::from_secs(5),
375
1
            position_check_interval: std::time::Duration::from_secs(2),
376
1
            max_consecutive_violations: 5,
377
1
            emergency_contacts: vec![
378
1
                "risk@foxhunt.com".to_owned(),
379
1
                "trading@foxhunt.com".to_owned(),
380
1
                "alerts@foxhunt.com".to_owned(),
381
1
            ],
382
1
            max_daily_loss: Price::new(10_000.0).unwrap_or(Price::ZERO),
383
1
            max_drawdown: Price::new(25_000.0).unwrap_or(Price::ZERO),
384
1
        },
385
1
        redis_url: std::env::var("REDIS_URL")
386
1
            .unwrap_or_else(|_| 
"redis://redis-cluster:6379"0
.
to_owned0
()),
387
1
        safety_check_timeout: std::time::Duration::from_millis(5), // Very tight timeout in production
388
    }
389
1
}
390
391
#[cfg(test)]
392
mod tests {
393
    use super::*;
394
395
    #[test]
396
1
    fn test_module_info() {
397
1
        let info = info();
398
1
        assert_eq!(info.name, "risk");
399
1
        assert!(!info.version.is_empty());
400
1
        assert!(!info.description.is_empty());
401
1
        assert!(!info.features.is_empty());
402
1
        assert!(!info.methodologies.is_empty());
403
1
    }
404
405
    #[test]
406
1
    fn test_development_config_validation() {
407
1
        let config = development_config();
408
1
        assert!(validate_risk_config(&config).is_ok());
409
1
        assert!(config.enabled);
410
1
        assert!(config.kill_switch.enabled);
411
1
        assert!(config.position_limits.enabled);
412
1
        assert!(config.emergency_response.enabled);
413
1
    }
414
415
    #[test]
416
1
    fn test_production_config_validation() {
417
1
        let config = production_config();
418
1
        assert!(validate_risk_config(&config).is_ok());
419
1
        assert!(config.enabled);
420
1
        assert!(!config.kill_switch.auto_recovery_enabled); // Manual recovery in prod
421
1
        assert!(config.position_limits.max_position_per_symbol > 0.0);
422
1
        assert!(!config.emergency_response.emergency_contacts.is_empty());
423
1
    }
424
425
    #[test]
426
1
    fn test_invalid_config_validation() {
427
1
        let mut config = development_config();
428
429
        // Test empty kill switch channel
430
1
        config.kill_switch.global_channel = String::new();
431
1
        assert!(validate_risk_config(&config).is_err());
432
433
        // Reset and test invalid position limits
434
1
        config = development_config();
435
1
        config.position_limits.max_position_per_symbol = 0.0;
436
1
        assert!(validate_risk_config(&config).is_err());
437
438
        // Reset and test empty Redis URL
439
1
        config = development_config();
440
1
        config.redis_url = String::new();
441
1
        assert!(validate_risk_config(&config).is_err());
442
1
    }
443
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/operations.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/operations.rs.html deleted file mode 100644 index a88be81f5..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/operations.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/operations.rs
Line
Count
Source
1
//! Safe financial operations with comprehensive error handling
2
//!
3
//! This module provides enterprise-grade financial calculations with:
4
//! - Zero-panic operations (all unwrap/expect eliminated)
5
//! - Precision-preserving decimal arithmetic
6
//! - Comprehensive validation and error reporting
7
//! - Production-ready type conversions
8
//! - Unified financial type system
9
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
10
11
// CANONICAL TYPE IMPORTS - Use unified types from core
12
use crate::error::{RiskError, RiskResult};
13
use common::types::{Price, Quantity, Volume};
14
use num::{FromPrimitive, ToPrimitive};
15
use rust_decimal::Decimal;
16
use tracing::{debug, warn};
17
18
/// Safely converts an f64 value to Decimal with comprehensive validation
19
///
20
/// This function performs financial-grade conversion with validation for:
21
/// - Finite number checking (no NaN or infinity)
22
/// - Range validation for financial calculations
23
/// - Precision preservation during conversion
24
///
25
/// # Arguments
26
/// * `value` - The f64 value to convert
27
/// * `context` - Description of where this conversion is being used (for error reporting)
28
///
29
/// # Returns
30
/// * `Ok(Decimal)` - Successfully converted decimal value
31
/// * `Err(RiskError)` - Conversion failed due to invalid input
32
///
33
/// # Examples
34
/// ```
35
/// use risk::operations::f64_to_decimal_safe;
36
/// let result = f64_to_decimal_safe(123.45, "price conversion");
37
/// assert!(result.is_ok());
38
/// ```
39
0
pub fn f64_to_decimal_safe(value: f64, context: &str) -> RiskResult<Decimal> {
40
    // Basic validation for financial values
41
42
0
    if !value.is_finite() {
43
0
        return Err(RiskError::TypeConversion {
44
0
            from_type: "f64".to_owned(),
45
0
            to_type: "Decimal".to_owned(),
46
0
            reason: format!("Non-finite value {value} in {context}"),
47
0
        });
48
0
    }
49
50
0
    if value.is_nan() {
51
0
        return Err(RiskError::TypeConversion {
52
0
            from_type: "f64".to_owned(),
53
0
            to_type: "Decimal".to_owned(),
54
0
            reason: format!("NaN value in {context}"),
55
0
        });
56
0
    }
57
58
0
    FromPrimitive::from_f64(value).ok_or_else(|| RiskError::TypeConversion {
59
0
        from_type: "f64".to_owned(),
60
0
        to_type: "Decimal".to_owned(),
61
0
        reason: format!("Conversion failed for value {value} in {context}"),
62
0
    })
63
0
}
64
65
/// Creates a Price for testing scenarios, bypassing normal validation
66
///
67
/// This function is only available in test builds and allows creation of
68
/// Price values that would normally be rejected, including:
69
/// - Zero values
70
/// - Negative values (converted to absolute)
71
/// - Out-of-range values
72
///
73
/// # Arguments
74
/// * `value` - The f64 value to convert to Price
75
///
76
/// # Returns
77
/// A Price instance, using absolute value for negative inputs
78
///
79
/// # Note
80
/// This function is only compiled in test builds to enable comprehensive
81
/// testing of edge cases and error conditions.
82
#[cfg(test)]
83
0
pub fn create_test_price(value: f64) -> Price {
84
    // For test scenarios, create Price with raw decimal value
85
0
    if value >= 0.0 {
86
0
        Price::new(value).unwrap_or_else(|_| Price::ZERO)
87
    } else {
88
        // For negative test values, use absolute value but mark context
89
0
        Price::new(value.abs()).unwrap_or_else(|_| Price::ZERO)
90
    }
91
0
}
92
93
/// Safely converts an f64 value to Price with comprehensive financial validation
94
///
95
/// This is the canonical function for converting financial amounts in the risk
96
/// management system. It provides:
97
/// - Finite number validation (no NaN or infinity)
98
/// - Negative value checking (relaxed in test/stress contexts)
99
/// - Range validation for financial amounts
100
/// - Detailed error reporting with context
101
///
102
/// # Arguments
103
/// * `value` - The f64 value to convert to Price
104
/// * `context` - Description of the conversion context for error reporting
105
///
106
/// # Returns
107
/// * `Ok(Price)` - Successfully converted price
108
/// * `Err(RiskError)` - Conversion failed due to validation error
109
///
110
/// # Behavior
111
/// - In production: Rejects negative values (except for PnL/stress contexts)
112
/// - In tests: Allows negative values for comprehensive testing
113
/// - Always rejects NaN and infinite values
114
///
115
/// # Examples
116
/// ```
117
/// use risk::operations::f64_to_price_safe;
118
/// let price = f64_to_price_safe(100.50, "order price").unwrap();
119
/// ```
120
0
pub fn f64_to_price_safe(value: f64, context: &str) -> RiskResult<Price> {
121
    // Basic financial validation - relaxed for test scenarios
122
0
    if !value.is_finite() {
123
0
        warn!(
124
0
            "\u{1f6a8} Price validation failed in {}: invalid value {}",
125
            context, value
126
        );
127
0
        return Err(RiskError::TypeConversion {
128
0
            from_type: "f64".to_owned(),
129
0
            to_type: "Price".to_owned(),
130
0
            reason: format!("Price validation failed in {context}: invalid value {value}"),
131
0
        });
132
0
    }
133
134
    // Allow negative values for test scenarios (stress testing, PnL calculations)
135
    #[cfg(not(test))]
136
    if value < 0.0
137
        && !context.contains("test")
138
        && !context.contains("stress")
139
        && !context.contains("pnl")
140
    {
141
        return Err(RiskError::TypeConversion {
142
            from_type: "f64".to_owned(),
143
            to_type: "Price".to_owned(),
144
            reason: format!("Price validation failed in {context}: negative value {value}"),
145
        });
146
    }
147
148
    // Convert using Price::new() which handles validation
149
0
    Price::new(value).map_err(|e| RiskError::TypeConversion {
150
0
        from_type: "f64".to_owned(),
151
0
        to_type: "Price".to_owned(),
152
0
        reason: format!("Price creation failed for value {value} in {context}: {e}"),
153
0
    })
154
0
}
155
156
/// Safely converts a Decimal value to f64 with precision monitoring
157
///
158
/// Converts Decimal to f64 while checking for potential precision loss
159
/// or overflow conditions. Includes comprehensive logging for debugging.
160
///
161
/// # Arguments
162
/// * `value` - The Decimal value to convert
163
/// * `context` - Description of the conversion context
164
///
165
/// # Returns
166
/// * `Ok(f64)` - Successfully converted floating-point value
167
/// * `Err(RiskError)` - Conversion failed due to overflow or precision loss
168
///
169
/// # Logging
170
/// This function logs debug information about the conversion process
171
/// and errors when conversion fails.
172
0
pub fn decimal_to_f64_safe(value: Decimal, context: &str) -> RiskResult<f64> {
173
    use tracing::{debug, error};
174
175
0
    debug!(
176
        value = %value,
177
        context = context,
178
0
        "Attempting Decimal to f64 conversion"
179
    );
180
181
0
    ToPrimitive::to_f64(&value)
182
0
        .ok_or_else(|| {
183
0
            error!(
184
                value = %value,
185
                context = context,
186
0
                "Decimal to f64 conversion failed - precision loss or overflow"
187
            );
188
0
            RiskError::TypeConversion {
189
0
                from_type: "Decimal".to_owned(),
190
0
                to_type: "f64".to_owned(),
191
0
                reason: format!("Conversion failed for value {value} in {context}"),
192
0
            }
193
0
        })
194
0
        .map(|result| {
195
0
            debug!(
196
                value = %value,
197
                context = context,
198
                result = result,
199
0
                "Decimal to f64 conversion successful"
200
            );
201
0
            result
202
0
        })
203
0
}
204
205
/// Safely converts a Price to f64 with comprehensive validation and logging
206
///
207
/// Converts Price to f64 while ensuring the result is finite and valid
208
/// for mathematical operations. Includes detailed logging for debugging.
209
///
210
/// # Arguments
211
/// * `price` - The Price value to convert
212
/// * `context` - Description of the conversion context for error reporting
213
///
214
/// # Returns
215
/// * `Ok(f64)` - Successfully converted floating-point value
216
/// * `Err(RiskError)` - Conversion resulted in non-finite value
217
///
218
/// # Logging
219
/// Logs debug information for successful conversions and errors for failures.
220
10
pub fn price_to_f64_safe(price: Price, context: &str) -> RiskResult<f64> {
221
    use tracing::{debug, error};
222
223
10
    debug!(
224
        price = %price,
225
        context = context,
226
0
        "Attempting Price to f64 conversion"
227
    );
228
229
10
    let val_f64 = price.to_f64();
230
231
10
    if val_f64.is_finite() {
232
10
        debug!(
233
            price = %price,
234
            context = context,
235
            result = val_f64,
236
0
            "Price to f64 conversion successful"
237
        );
238
10
        Ok(val_f64)
239
    } else {
240
0
        error!(
241
            price = %price,
242
            context = context,
243
            result = val_f64,
244
0
            "Price to f64 conversion failed - non-finite result"
245
        );
246
0
        Err(RiskError::TypeConversion {
247
0
            from_type: "Price".to_owned(),
248
0
            to_type: "f64".to_owned(),
249
0
            reason: format!("Conversion failed for price {val_f64} in {context}"),
250
0
        })
251
    }
252
10
}
253
254
/// Safely converts a Quantity to f64 with finite value validation
255
///
256
/// Converts Quantity to f64 and validates that the result is finite
257
/// (not NaN or infinite) for use in mathematical calculations.
258
///
259
/// # Arguments
260
/// * `quantity` - The Quantity value to convert
261
/// * `context` - Description of the conversion context for error reporting
262
///
263
/// # Returns
264
/// * `Ok(f64)` - Successfully converted finite floating-point value
265
/// * `Err(RiskError)` - Conversion resulted in non-finite value
266
0
pub fn quantity_to_f64_safe(quantity: Quantity, context: &str) -> RiskResult<f64> {
267
0
    let val = quantity.to_f64();
268
0
    if val.is_finite() {
269
0
        Ok(val)
270
    } else {
271
0
        Err(RiskError::TypeConversion {
272
0
            from_type: "Quantity".to_owned(),
273
0
            to_type: "f64".to_owned(),
274
0
            reason: format!("Conversion failed for quantity {val} in {context}"),
275
0
        })
276
    }
277
0
}
278
279
/// Safely converts a Price to Decimal with error handling
280
///
281
/// Converts Price to Decimal for precise financial calculations.
282
/// Provides detailed error information if the conversion fails.
283
///
284
/// # Arguments
285
/// * `price` - The Price value to convert
286
/// * `context` - Description of the conversion context for error reporting
287
///
288
/// # Returns
289
/// * `Ok(Decimal)` - Successfully converted decimal value
290
/// * `Err(RiskError)` - Conversion failed with detailed error information
291
0
pub fn price_to_decimal_safe(price: Price, context: &str) -> RiskResult<Decimal> {
292
0
    price.to_decimal().map_err(|e| RiskError::TypeConversion {
293
0
        from_type: "Price".to_owned(),
294
0
        to_type: "Decimal".to_owned(),
295
0
        reason: format!("Failed to convert price in {context}: {e:?}"),
296
0
    })
297
0
}
298
299
/// Safely converts a Volume to Decimal through f64 intermediate conversion
300
///
301
/// Converts Volume to Decimal by first converting to f64 and validating
302
/// the intermediate result before final Decimal conversion.
303
///
304
/// # Arguments
305
/// * `volume` - The Volume value to convert
306
/// * `context` - Description of the conversion context for error reporting
307
///
308
/// # Returns
309
/// * `Ok(Decimal)` - Successfully converted decimal value
310
/// * `Err(RiskError)` - Conversion failed at f64 or Decimal stage
311
0
pub fn volume_to_decimal_safe(volume: Volume, context: &str) -> RiskResult<Decimal> {
312
0
    let f64_value = volume.to_f64();
313
0
    if f64_value.is_finite() {
314
0
        f64_to_decimal_safe(f64_value, &format!("Volume conversion in {context}"))
315
    } else {
316
0
        Err(RiskError::TypeConversion {
317
0
            from_type: "Volume".to_owned(),
318
0
            to_type: "Decimal".to_owned(),
319
0
            reason: format!("Volume to f64 conversion failed in {context}"),
320
0
        })
321
    }
322
0
}
323
324
/// Pass-through function for `PnL` Decimal values with consistent API
325
///
326
/// Provides a consistent API for `PnL` decimal handling by passing through
327
/// the Decimal value unchanged. Maintains API consistency with other
328
/// conversion functions while avoiding unnecessary conversions.
329
///
330
/// # Arguments
331
/// * `pnl` - The `PnL` Decimal value to pass through
332
/// * `_context` - Context parameter (unused but maintains API consistency)
333
///
334
/// # Returns
335
/// * `Ok(Decimal)` - The original Decimal value unchanged
336
0
pub const fn pnl_to_decimal_safe(pnl: Decimal, _context: &str) -> RiskResult<Decimal> {
337
0
    Ok(pnl) // Pass through the Decimal value directly
338
0
}
339
340
/// Performs safe division with comprehensive validation and zero-checking
341
///
342
/// Divides two Price values while protecting against division by zero
343
/// and ensuring the result is finite and valid for financial calculations.
344
///
345
/// # Arguments
346
/// * `numerator` - The dividend (top number in division)
347
/// * `denominator` - The divisor (bottom number in division)
348
/// * `context` - Description of the division context for error reporting
349
///
350
/// # Returns
351
/// * `Ok(Decimal)` - Successfully computed division result
352
/// * `Err(RiskError)` - Division failed due to zero denominator or non-finite result
353
///
354
/// # Safety
355
/// - Checks for zero denominator before division
356
/// - Validates result is finite (not NaN or infinite)
357
/// - Provides detailed error context for debugging
358
2
pub fn safe_divide(numerator: Price, denominator: Price, context: &str) -> RiskResult<Decimal> {
359
2
    if denominator == Price::ZERO {
360
1
        return Err(RiskError::CalculationError(format!(
361
1
            "Division by zero in {context}"
362
1
        )));
363
1
    }
364
365
1
    let result = (numerator / denominator.to_f64())
366
1
        .map_err(|e| RiskError::CalculationError(
format!0
(
"Division failed in {context}: {e:?}"0
)))
?0
;
367
368
    // Validate result - safe conversion with proper error handling
369
1
    let result_f64 = result.to_f64();
370
1
    if !result_f64.is_finite() {
371
0
        return Err(RiskError::CalculationError(format!(
372
0
            "Division resulted in non-finite value {result_f64} in {context}"
373
0
        )));
374
1
    }
375
    // Safe conversion back to Decimal
376
1
    FromPrimitive::from_f64(result_f64).ok_or_else(|| 
{0
377
0
        RiskError::CalculationError(format!(
378
0
            "Failed to convert division result {result_f64} back to Decimal in {context}"
379
0
        ))
380
0
    })
381
2
}
382
383
/// Calculates percentage with safe division and automatic scaling
384
///
385
/// Computes what percentage `value` represents of `total` using safe division
386
/// and automatically scales the result to percentage form (0-100).
387
///
388
/// # Arguments
389
/// * `value` - The partial amount
390
/// * `total` - The total amount (100% reference)
391
/// * `context` - Description of the percentage calculation context
392
///
393
/// # Returns
394
/// * `Ok(Decimal)` - Percentage value (0-100 scale)
395
/// * `Err(RiskError)` - Calculation failed due to zero total or invalid result
396
///
397
/// # Example
398
/// If value=25 and total=100, returns Ok(25.0) representing 25%
399
0
pub fn safe_percentage(value: Price, total: Price, context: &str) -> RiskResult<Decimal> {
400
0
    let percentage = safe_divide(
401
0
        value,
402
0
        total,
403
0
        &format!("percentage calculation in {context}"),
404
0
    )?;
405
0
    Ok(percentage * Decimal::from(100))
406
0
}
407
408
/// Calculates square root with domain validation
409
///
410
/// Computes the square root of a Price value while ensuring the input
411
/// is non-negative (square root domain validation).
412
///
413
/// # Arguments
414
/// * `value` - The Price value to take square root of
415
/// * `context` - Description of the calculation context for error reporting
416
///
417
/// # Returns
418
/// * `Ok(Decimal)` - Successfully computed square root
419
/// * `Err(RiskError)` - Input was negative or conversion failed
420
///
421
/// # Domain
422
/// Only accepts non-negative values (value >= 0)
423
0
pub fn safe_sqrt(value: Price, context: &str) -> RiskResult<Decimal> {
424
0
    if value < Price::ZERO {
425
0
        return Err(RiskError::CalculationError(format!(
426
0
            "Square root of negative value {value} in {context}"
427
0
        )));
428
0
    }
429
430
0
    let f64_value = price_to_f64_safe(value, context)?;
431
0
    let sqrt_f64 = f64_value.sqrt();
432
433
0
    f64_to_decimal_safe(sqrt_f64, &format!("square root calculation in {context}"))
434
0
}
435
436
/// Calculates natural logarithm with domain validation
437
///
438
/// Computes the natural logarithm (ln) of a Price value while ensuring
439
/// the input is positive (logarithm domain validation).
440
///
441
/// # Arguments
442
/// * `value` - The Price value to take natural log of
443
/// * `context` - Description of the calculation context for error reporting
444
///
445
/// # Returns
446
/// * `Ok(Decimal)` - Successfully computed natural logarithm
447
/// * `Err(RiskError)` - Input was non-positive or conversion failed
448
///
449
/// # Domain
450
/// Only accepts positive values (value > 0)
451
0
pub fn safe_ln(value: Price, context: &str) -> RiskResult<Decimal> {
452
0
    if value <= Price::ZERO {
453
0
        return Err(RiskError::CalculationError(format!(
454
0
            "Natural log of non-positive value {value} in {context}"
455
0
        )));
456
0
    }
457
458
0
    let f64_value = price_to_f64_safe(value, context)?;
459
0
    let ln_f64 = f64_value.ln();
460
461
0
    f64_to_decimal_safe(ln_f64, &format!("natural log calculation in {context}"))
462
0
}
463
464
/// Calculates exponential function with overflow protection
465
///
466
/// Computes e^value while protecting against potential overflow conditions
467
/// that could result in infinite values.
468
///
469
/// # Arguments
470
/// * `value` - The exponent value
471
/// * `context` - Description of the calculation context for error reporting
472
///
473
/// # Returns
474
/// * `Ok(Decimal)` - Successfully computed exponential result
475
/// * `Err(RiskError)` - Input too large (overflow risk) or conversion failed
476
///
477
/// # Safety
478
/// Rejects inputs > 700.0 to prevent overflow conditions
479
0
pub fn safe_exp(value: Price, context: &str) -> RiskResult<Decimal> {
480
0
    let f64_value = price_to_f64_safe(value, context)?;
481
482
    // Check for overflow potential
483
0
    if f64_value > 700.0 {
484
0
        return Err(RiskError::CalculationError(format!(
485
0
            "Exponential overflow risk: exp({value}) in {context}"
486
0
        )));
487
0
    }
488
489
0
    let exp_f64 = f64_value.exp();
490
0
    f64_to_decimal_safe(exp_f64, &format!("exponential calculation in {context}"))
491
0
}
492
493
/// Calculates power function with domain and overflow validation
494
///
495
/// Computes base^exponent while validating the mathematical domain
496
/// and protecting against overflow conditions.
497
///
498
/// # Arguments
499
/// * `base` - The base value to raise to a power
500
/// * `exponent` - The power to raise the base to
501
/// * `context` - Description of the calculation context for error reporting
502
///
503
/// # Returns
504
/// * `Ok(Decimal)` - Successfully computed power result
505
/// * `Err(RiskError)` - Invalid domain (negative base with fractional exponent) or overflow
506
///
507
/// # Domain Restrictions
508
/// - Negative base with fractional exponent is invalid (would result in complex number)
509
/// - Result must be finite (not NaN or infinite)
510
0
pub fn safe_pow(base: Price, exponent: f64, context: &str) -> RiskResult<Decimal> {
511
0
    let base_f64 = price_to_f64_safe(base, context)?;
512
513
0
    if base_f64 < 0.0 && exponent.fract() != 0.0 {
514
0
        return Err(RiskError::CalculationError(format!(
515
0
            "Fractional power of negative base {base} ^ {exponent} in {context}"
516
0
        )));
517
0
    }
518
519
0
    let result_f64 = base_f64.powf(exponent);
520
521
0
    if !result_f64.is_finite() {
522
0
        return Err(RiskError::CalculationError(format!(
523
0
            "Power calculation resulted in non-finite value: {base} ^ {exponent} in {context}"
524
0
        )));
525
0
    }
526
527
0
    f64_to_decimal_safe(result_f64, &format!("power calculation in {context}"))
528
0
}
529
530
/// Validates financial amounts with context-aware rules
531
///
532
/// Performs comprehensive validation of financial amounts with different
533
/// rules for production vs test scenarios. Includes range checking and
534
/// suspicious value detection.
535
///
536
/// # Arguments
537
/// * `amount` - The financial amount to validate
538
/// * `amount_type` - Description of what this amount represents
539
/// * `max_value` - Optional maximum allowed value
540
///
541
/// # Returns
542
/// * `Ok(())` - Amount passed validation
543
/// * `Err(RiskError)` - Validation failed with specific reason
544
///
545
/// # Validation Rules
546
/// - Production: Rejects negative values (except PnL/stress contexts)
547
/// - Test: Allows negative values for comprehensive testing
548
/// - Always warns about suspiciously large values (>$1T)
549
/// - Checks against optional maximum value limit
550
2
pub fn validate_financial_amount(
551
2
    amount: Price,
552
2
    amount_type: &str,
553
2
    max_value: Option<Price>,
554
2
) -> RiskResult<()> {
555
    // Allow negative values in test scenarios (for stress testing, PnL calculations)
556
    // Only enforce strict positivity for production order validation
557
    #[cfg(not(test))]
558
    {
559
        if amount < Price::ZERO
560
            && !amount_type.contains("test")
561
            && !amount_type.contains("stress")
562
            && !amount_type.contains("pnl")
563
        {
564
            return Err(RiskError::ValidationError {
565
                message: format!("{amount_type} cannot be negative: {amount}"),
566
            });
567
        }
568
    }
569
570
2
    if amount == Price::ZERO && 
!amount_type.contains("test")0
{
571
0
        warn!("Zero {} amount detected", amount_type);
572
2
    }
573
574
2
    if let Some(
max1
) = max_value {
575
1
        if amount > max {
576
1
            return Err(RiskError::SafetyLimitExceeded {
577
1
                limit_type: amount_type.to_owned(),
578
1
                current: amount.to_string(),
579
1
                maximum: max.to_string(),
580
1
            });
581
0
        }
582
1
    }
583
584
    // Check for suspiciously large values (potential data corruption)
585
1
    let trillion = Decimal::from(1_000_000_000_000_i64);
586
1
    let trillion_price = Price::from_decimal(trillion);
587
1
    if amount > trillion_price {
588
0
        warn!(
589
0
            "Suspiciously large {} amount: {} - potential data corruption",
590
            amount_type, amount
591
        );
592
1
    }
593
594
1
    Ok(())
595
2
}
596
597
/// Validates percentage values are within 0-100 range
598
///
599
/// Ensures percentage values are finite and within the valid
600
/// 0-100 percentage range for display and calculations.
601
///
602
/// # Arguments
603
/// * `percentage` - The percentage value to validate
604
/// * `percentage_type` - Description of what this percentage represents
605
///
606
/// # Returns
607
/// * `Ok(())` - Percentage is valid (0-100 and finite)
608
/// * `Err(RiskError)` - Percentage is invalid (NaN, infinite, or out of range)
609
0
pub fn validate_percentage(percentage: f64, percentage_type: &str) -> RiskResult<()> {
610
0
    if !percentage.is_finite() {
611
0
        return Err(RiskError::ValidationError {
612
0
            message: format!("{percentage_type} must be a finite number: {percentage}"),
613
0
        });
614
0
    }
615
616
0
    if !(0.0..=100.0).contains(&percentage) {
617
0
        return Err(RiskError::ValidationError {
618
0
            message: format!("{percentage_type} must be between 0 and 100: {percentage}%"),
619
0
        });
620
0
    }
621
622
0
    Ok(())
623
0
}
624
625
/// Validates ratio values are within 0-1 range
626
///
627
/// Ensures ratio values are finite and within the valid
628
/// 0-1 range for mathematical calculations and financial ratios.
629
///
630
/// # Arguments
631
/// * `ratio` - The ratio value to validate
632
/// * `ratio_type` - Description of what this ratio represents
633
///
634
/// # Returns
635
/// * `Ok(())` - Ratio is valid (0-1 and finite)
636
/// * `Err(RiskError)` - Ratio is invalid (NaN, infinite, or out of range)
637
0
pub fn validate_ratio(ratio: f64, ratio_type: &str) -> RiskResult<()> {
638
0
    if !ratio.is_finite() {
639
0
        return Err(RiskError::ValidationError {
640
0
            message: format!("{ratio_type} must be a finite number: {ratio}"),
641
0
        });
642
0
    }
643
644
0
    if !(0.0..=1.0).contains(&ratio) {
645
0
        return Err(RiskError::ValidationError {
646
0
            message: format!("{ratio_type} must be between 0 and 1: {ratio}"),
647
0
        });
648
0
    }
649
650
0
    Ok(())
651
0
}
652
653
/// Calculates weighted average with comprehensive input validation
654
///
655
/// Computes the weighted average of values using corresponding weights,
656
/// with extensive validation to ensure data integrity and mathematical validity.
657
///
658
/// # Arguments
659
/// * `values` - Array of values to average
660
/// * `weights` - Corresponding weights for each value (must be non-negative)
661
/// * `context` - Description of the calculation context for error reporting
662
///
663
/// # Returns
664
/// * `Ok(f64)` - Successfully computed weighted average
665
/// * `Err(RiskError)` - Validation failed or calculation error
666
///
667
/// # Validation
668
/// - Arrays must have same length
669
/// - Arrays must not be empty
670
/// - All values and weights must be finite
671
/// - All weights must be non-negative
672
/// - Total weight must be non-zero
673
///
674
/// # Formula
675
/// `weighted_average` = `Σ(value_i` × `weight_i`) / `Σ(weight_i)`
676
1
pub fn safe_weighted_average(values: &[f64], weights: &[f64], context: &str) -> RiskResult<f64> {
677
1
    if values.len() != weights.len() {
678
0
        return Err(RiskError::ValidationError {
679
0
            message: format!(
680
0
                "Values and weights length mismatch in {}: {} vs {}",
681
0
                context,
682
0
                values.len(),
683
0
                weights.len()
684
0
            ),
685
0
        });
686
1
    }
687
688
1
    if values.is_empty() {
689
0
        return Err(RiskError::ValidationError {
690
0
            message: format!("Empty values array in {context}"),
691
0
        });
692
1
    }
693
694
    // Validate all values are finite
695
3
    for (i, &value) in 
values1
.
iter1
().
enumerate1
() {
696
3
        if !value.is_finite() {
697
0
            return Err(RiskError::ValidationError {
698
0
                message: format!("Non-finite value at index {i} in {context}: {value}"),
699
0
            });
700
3
        }
701
    }
702
703
3
    for (i, &weight) in 
weights1
.
iter1
().
enumerate1
() {
704
3
        if !weight.is_finite() || weight < 0.0 {
705
0
            return Err(RiskError::ValidationError {
706
0
                message: format!("Invalid weight at index {i} in {context}: {weight}"),
707
0
            });
708
3
        }
709
    }
710
711
1
    let total_weight: f64 = weights.iter().sum();
712
1
    if total_weight == 0.0 {
713
0
        return Err(RiskError::ValidationError {
714
0
            message: format!("Total weight is zero in {context}"),
715
0
        });
716
1
    }
717
718
3
    let 
weighted_sum1
:
f641
=
values1
.
iter1
().
zip1
(
weights1
.
iter1
()).
map1
(|(v, w)| v * w).
sum1
();
719
1
    let result = weighted_sum / total_weight;
720
721
1
    if !result.is_finite() {
722
0
        return Err(RiskError::CalculationError(format!(
723
0
            "Weighted average calculation resulted in non-finite value in {context}"
724
0
        )));
725
1
    }
726
727
1
    debug!(
"Weighted average calculated in {}: {}"0
, context, result);
728
1
    Ok(result)
729
1
}
730
731
/// Calculates Pearson correlation coefficient with comprehensive validation
732
///
733
/// Computes the linear correlation coefficient between two data series
734
/// with extensive validation and boundary checking.
735
///
736
/// # Arguments
737
/// * `x` - First data series
738
/// * `y` - Second data series
739
/// * `context` - Description of the correlation context for error reporting
740
///
741
/// # Returns
742
/// * `Ok(f64)` - Correlation coefficient clamped to [-1, 1] range
743
/// * `Err(RiskError)` - Validation failed or calculation error
744
///
745
/// # Validation
746
/// - Arrays must have same length
747
/// - Must have at least 2 data points
748
/// - All values must be finite
749
/// - Neither series can have zero variance
750
/// - Result must be within [-1, 1] bounds
751
///
752
/// # Formula
753
/// r = `Σ((x_i` - `x̄)(y_i` - ȳ)) / √(`Σ(x_i` - x̄)² × `Σ(y_i` - ȳ)²)
754
1
pub fn safe_correlation(x: &[f64], y: &[f64], context: &str) -> RiskResult<f64> {
755
1
    if x.len() != y.len() {
756
0
        return Err(RiskError::ValidationError {
757
0
            message: format!(
758
0
                "Array length mismatch in correlation calculation for {}: {} vs {}",
759
0
                context,
760
0
                x.len(),
761
0
                y.len()
762
0
            ),
763
0
        });
764
1
    }
765
766
1
    if x.len() < 2 {
767
0
        return Err(RiskError::ValidationError {
768
0
            message: format!("Insufficient data for correlation calculation in {}: need at least 2 points, have {}", context, x.len())
769
0
        });
770
1
    }
771
772
    // Validate all values are finite
773
5
    for (i, &val) in 
x1
.
iter1
().
enumerate1
() {
774
5
        if !val.is_finite() {
775
0
            return Err(RiskError::ValidationError {
776
0
                message: format!("Non-finite value in x array at index {i} for {context}: {val}"),
777
0
            });
778
5
        }
779
    }
780
781
5
    for (i, &val) in 
y1
.
iter1
().
enumerate1
() {
782
5
        if !val.is_finite() {
783
0
            return Err(RiskError::ValidationError {
784
0
                message: format!("Non-finite value in y array at index {i} for {context}: {val}"),
785
0
            });
786
5
        }
787
    }
788
789
1
    let n = x.len() as f64;
790
1
    let mean_x = x.iter().sum::<f64>() / n;
791
1
    let mean_y = y.iter().sum::<f64>() / n;
792
793
1
    let mut sum_xx = 0.0;
794
1
    let mut sum_yy = 0.0;
795
1
    let mut sum_xy = 0.0;
796
797
5
    for (xi, yi) in 
x1
.
iter1
().
zip1
(
y1
.
iter1
()) {
798
5
        let dx = xi - mean_x;
799
5
        let dy = yi - mean_y;
800
5
        sum_xx += dx * dx;
801
5
        sum_yy += dy * dy;
802
5
        sum_xy += dx * dy;
803
5
    }
804
805
1
    if sum_xx == 0.0 || sum_yy == 0.0 {
806
0
        return Err(RiskError::ValidationError {
807
0
            message: format!("Zero variance in correlation calculation for {context}"),
808
0
        });
809
1
    }
810
811
1
    let correlation = sum_xy / (sum_xx * sum_yy).sqrt();
812
813
1
    if !correlation.is_finite() {
814
0
        return Err(RiskError::CalculationError(format!(
815
0
            "Correlation calculation resulted in non-finite value for {context}"
816
0
        )));
817
1
    }
818
819
    // Correlation should be between -1 and 1
820
1
    if !(-1.01..=1.01).contains(&correlation) {
821
        // Allow small numerical errors
822
0
        return Err(RiskError::CalculationError(format!(
823
0
            "Correlation out of bounds for {context}: {correlation}"
824
0
        )));
825
1
    }
826
827
1
    Ok(correlation.max(-1.0).min(1.0)) // Clamp to valid range
828
1
}
829
830
#[cfg(test)]
831
mod tests {
832
    use super::*;
833
834
    #[test]
835
1
    fn test_safe_divide() {
836
1
        let result = safe_divide(Decimal::from(10).into(), Decimal::from(2).into(), "test");
837
1
        assert!(result.is_ok());
838
1
        if let Ok(value) = result {
839
1
            assert_eq!(value, Decimal::from(5));
840
0
        }
841
842
1
        let error_result = safe_divide(Decimal::from(10).into(), Price::ZERO, "test");
843
1
        assert!(error_result.is_err());
844
1
    }
845
846
    #[test]
847
1
    fn test_validate_financial_amount() -> Result<(), Box<dyn std::error::Error>> {
848
        // Test valid positive amount
849
1
        assert!(validate_financial_amount(Price::from_f64(1000.0)
?0
, "test_amount", None).is_ok());
850
851
        // Test that creating a negative price fails at construction time
852
1
        assert!(Price::from_f64(-100.0).is_err());
853
854
        // Test that amount exceeding max value is rejected
855
1
        assert!(validate_financial_amount(
856
1
            Price::from_f64(1000.0)
?0
,
857
1
            "test_amount",
858
1
            Some(Price::from_f64(500.0)
?0
)
859
        )
860
1
        .is_err());
861
862
1
        Ok(())
863
1
    }
864
865
    #[test]
866
1
    fn test_safe_weighted_average() {
867
1
        let values = vec![10.0, 20.0, 30.0];
868
1
        let weights = vec![1.0, 2.0, 3.0];
869
1
        let result = safe_weighted_average(&values, &weights, "test");
870
1
        assert!(result.is_ok());
871
        // Expected: (10*1 + 20*2 + 30*3) / (1+2+3) = 140/6 = 23.333...
872
1
        let expected = 140.0 / 6.0;
873
1
        if let Ok(value) = result {
874
1
            assert!((value - expected).abs() < 0.001);
875
0
        }
876
1
    }
877
878
    #[test]
879
1
    fn test_safe_correlation() {
880
1
        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
881
1
        let y = vec![2.0, 4.0, 6.0, 8.0, 10.0]; // Perfect positive correlation
882
1
        let result = safe_correlation(&x, &y, "test");
883
1
        assert!(result.is_ok());
884
1
        if let Ok(value) = result {
885
1
            assert!((value - 1.0).abs() < 0.001); // Should be very close to 1.0
886
0
        }
887
1
    }
888
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/position_tracker.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/position_tracker.rs.html deleted file mode 100644 index 122e924c5..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/position_tracker.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/position_tracker.rs
Line
Count
Source
1
//! ENTERPRISE-GRADE Real-time position tracking and concentration risk monitoring
2
//! Position Tracker Module
3
//!
4
//! Implements comprehensive portfolio risk decomposition, P&L tracking, and concentration limits
5
//! Following Riskfolio-Lib patterns for position concentration analysis
6
7
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
8
#![warn(clippy::indexing_slicing)]
9
10
use chrono::{DateTime, Utc};
11
use dashmap::DashMap;
12
use std::collections::HashMap;
13
use std::sync::Arc;
14
// REMOVED: Direct Decimal usage - use canonical types
15
use num::ToPrimitive;
16
// Use common::types::prelude for all types
17
use common::types::{Price, Quantity, Symbol};
18
use rust_decimal::Decimal;
19
use serde::{Deserialize, Serialize};
20
use tokio::sync::{broadcast, RwLock};
21
use tracing::{debug, error, info, warn};
22
23
use crate::error::{decimal_to_f64_safe, f64_to_price_safe, RiskError, RiskResult};
24
use crate::risk_types::{
25
    InstrumentId, MarketData, PnLMetrics, PortfolioId, RiskPosition, StrategyId,
26
};
27
use config::AssetClassificationConfig;
28
// CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
29
30
// Prometheus metrics integration
31
use lazy_static::lazy_static;
32
use prometheus::{
33
    register_counter, register_gauge, register_histogram, register_int_gauge, Counter, Gauge,
34
    Histogram, HistogramOpts, IntGauge,
35
};
36
37
lazy_static! {
38
static ref POSITION_UPDATES_COUNTER: Counter = register_counter!(
39
    "foxhunt_position_updates_total",
40
    "Total position updates processed"
41
0
).unwrap_or_else(|e| {
42
0
    warn!("Failed to register position updates counter: {}", e);
43
    // Safe fallback: If even basic counter creation fails, return a default counter
44
    // This should never happen in practice, but eliminates panic possibility
45
0
    Counter::new("position_updates_fallback", "Fallback counter").unwrap_or_else(|_| {
46
0
        error!("Critical: All counter creation failed - using no-op metrics");
47
        // Create a dummy counter that won't panic - metrics will be lost but system stays up
48
0
        Counter::new("noop_counter", "No-op counter for safety").unwrap_or_else(|_| {
49
            // Absolute fallback - create a minimal counter and log the error but continue operating
50
0
            error!("CRITICAL: Complete metrics subsystem failure - continuing without metrics");
51
            // Create the simplest possible counter that should always work
52
0
            Counter::new("emergency", "Emergency fallback counter")
53
0
                .unwrap_or_else(|_| {
54
0
                    error!("FATAL: Cannot create any metrics - system continuing with no-op metrics");
55
                    // Last resort: use a basic counter implementation
56
0
                    prometheus::core::GenericCounter::new("basic", "basic counter")
57
0
                        .unwrap_or_else(|_| {
58
                            // Ultimate fallback - if this fails, we create a default counter
59
0
                            prometheus::core::GenericCounter::new("fallback", "fallback counter")
60
0
                                .unwrap_or_else(|_| {
61
                                    // Create a basic counter as last resort
62
0
                                    Counter::new("emergency_fallback", "emergency fallback counter")
63
0
                                        .unwrap_or_else(|_| Counter::new("emergency_fallback_fallback", "emergency fallback").unwrap())
64
0
                                })
65
0
                        })                })
66
0
        })
67
0
    })
68
0
});
69
70
static ref POSITION_VALUE_GAUGE: Gauge = register_gauge!(
71
    "foxhunt_current_position_value_usd",
72
    "Current total position value in USD"
73
0
).unwrap_or_else(|e| {
74
0
    warn!("Failed to register position value gauge: {}", e);
75
0
    Gauge::new("position_value_fallback", "Fallback gauge").unwrap_or_else(|_| {
76
0
        error!("Critical: All gauge creation failed - using no-op metrics");
77
0
        Gauge::new("noop_gauge", "No-op gauge for safety").unwrap_or_else(|_| {
78
0
            error!("CRITICAL: Complete gauge metrics failure - continuing without position value metrics");
79
0
            Gauge::new("emergency_gauge", "Emergency fallback gauge")
80
0
                .unwrap_or_else(|_| {
81
0
                    error!("FATAL: Cannot create any gauge metrics - system continuing");
82
0
                    prometheus::core::GenericGauge::new("basic_gauge", "basic gauge")
83
0
                        .unwrap_or_else(|_| {
84
0
                            prometheus::core::GenericGauge::new("fallback_gauge", "fallback gauge")
85
0
                                .unwrap_or_else(|_| {
86
                                    // Create a basic gauge as last resort
87
0
                                    Gauge::new("emergency_fallback_gauge", "emergency fallback gauge")
88
0
                                        .expect("Failed to create emergency fallback gauge")
89
0
                                })
90
0
                        })
91
0
                })
92
0
        })
93
0
    })
94
0
});
95
96
static ref CONCENTRATION_RISK_GAUGE: Gauge = register_gauge!(
97
    "foxhunt_concentration_risk_score",
98
    "Portfolio concentration risk score (HHI)"
99
0
).unwrap_or_else(|e| {
100
0
    warn!("Failed to register concentration risk gauge: {}", e);
101
0
    Gauge::new("concentration_risk_fallback", "Fallback gauge").unwrap_or_else(|_| {
102
0
        error!("Critical: All concentration gauge creation failed - using no-op metrics");
103
0
        Gauge::new("noop_concentration", "No-op concentration gauge").unwrap_or_else(|_| {
104
0
            error!("CRITICAL: Complete concentration gauge failure - continuing without concentration metrics");
105
0
            Gauge::new("emergency_concentration", "Emergency concentration gauge")
106
0
                .unwrap_or_else(|_| {
107
0
                    error!("FATAL: Cannot create any concentration gauge - system continuing");
108
0
                    prometheus::core::GenericGauge::new("basic_concentration", "basic")
109
0
                        .unwrap_or_else(|_| {
110
0
                            prometheus::core::GenericGauge::new("fallback_concentration", "fallback")
111
0
                                .expect("Failed to create fallback concentration gauge")
112
0
                        })
113
0
                })
114
0
        })
115
0
    })
116
0
});
117
118
static ref PORTFOLIO_COUNT_GAUGE: IntGauge = register_int_gauge!(
119
    "foxhunt_active_portfolios",
120
    "Number of active portfolios"
121
0
).unwrap_or_else(|e| {
122
0
    warn!("Failed to register portfolio count gauge: {}", e);
123
0
    IntGauge::new("portfolio_count_fallback", "Fallback gauge").unwrap_or_else(|_| {
124
0
        error!("Critical: All portfolio gauge creation failed - using no-op metrics");
125
0
        IntGauge::new("noop_portfolio", "No-op portfolio gauge").unwrap_or_else(|_| {
126
0
            error!("CRITICAL: Complete portfolio gauge failure - continuing without portfolio count metrics");
127
0
            IntGauge::new("emergency_portfolio", "Emergency portfolio gauge")
128
0
                .unwrap_or_else(|_| {
129
0
                    error!("FATAL: Cannot create any portfolio gauge - system continuing");
130
0
                    prometheus::core::GenericGauge::new("basic_portfolio", "basic")
131
0
                        .unwrap_or_else(|_| {
132
0
                            prometheus::core::GenericGauge::new("fallback_portfolio", "fallback")
133
0
                                .expect("Failed to create fallback portfolio gauge")
134
0
                        })
135
0
                })
136
0
        })
137
0
    })
138
0
});
139
140
static ref RISK_BREACHES_COUNTER: Counter = register_counter!(
141
    "foxhunt_concentration_breaches_total",
142
    "Total concentration limit breaches"
143
0
).unwrap_or_else(|e| {
144
0
    warn!("Failed to register concentration breaches counter: {}", e);
145
0
    if let Ok(counter) = Counter::new("concentration_breaches_fallback", "Fallback counter") { counter } else {
146
0
        error!("Critical: Breaches counter creation failed - metrics may be inaccurate");
147
0
        Counter::new("emergency_breaches_fallback", "Emergency fallback").unwrap_or_else(|_| {
148
0
            error!("FATAL: Complete breaches counter creation failed - system continuing with no-op counter");
149
            // Last resort: Create the simplest possible counter that should always work
150
0
            prometheus::core::GenericCounter::new("noop_breaches", "no-op breaches counter")
151
0
                .unwrap_or_else(|_| {
152
0
                    prometheus::core::GenericCounter::new("ultimate_fallback", "ultimate fallback")
153
0
                        .expect("Failed to create ultimate fallback counter")
154
0
                })
155
0
        })
156
    }
157
0
});
158
159
static ref POSITION_PROCESSING_LATENCY: Histogram = register_histogram!(
160
    HistogramOpts::new(
161
        "foxhunt_position_processing_latency_microseconds",
162
        "Position update processing latency"
163
    ).buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0])
164
0
).unwrap_or_else(|e| {
165
0
    warn!("Failed to register position processing latency histogram: {}", e);
166
0
    if let Ok(histogram) = Histogram::with_opts(HistogramOpts::new(
167
0
        "position_processing_latency_fallback",
168
0
        "Fallback histogram"
169
0
    )) { histogram } else {
170
0
        error!("Critical: Even fallback histogram creation failed - metrics may be inaccurate");
171
0
        Histogram::with_opts(HistogramOpts::new(
172
            "emergency_histogram_fallback",
173
            "Emergency fallback"
174
0
        )).unwrap_or_else(|_| {
175
0
            error!("FATAL: Complete histogram creation failed - system continuing with no-op histogram");
176
            // Last resort: Create the simplest possible histogram that should always work
177
0
            Histogram::with_opts(HistogramOpts::new(
178
                "noop_histogram",
179
                "No-op histogram for safety"
180
0
            )).unwrap_or_else(|_| {
181
0
                error!("CRITICAL: Cannot create any histogram - using basic histogram implementation");
182
                // Use default histogram with basic configuration
183
0
                Histogram::with_opts(
184
0
                    HistogramOpts::new("basic_histogram", "basic")
185
0
                ).unwrap_or_else(|_| Histogram::with_opts(
186
0
                    HistogramOpts::new("fallback_histogram", "fallback")
187
0
                ).expect("Failed to create fallback histogram"))
188
0
            })
189
0
        })
190
    }
191
0
});}
192
193
/// **Position Concentration Limits and Monitoring Configuration**
194
///
195
/// Defines risk management limits for portfolio concentration across multiple dimensions.
196
/// Used to prevent excessive exposure to single positions, sectors, strategies, or geographic regions.
197
///
198
/// # Risk Management Framework
199
/// Implements concentration risk controls following modern portfolio theory and regulatory guidelines:
200
/// - Single position limits prevent over-concentration in individual securities
201
/// - Sector limits ensure diversification across industries
202
/// - Strategy limits prevent over-reliance on single trading approaches
203
/// - Geographic limits reduce country/regional risk exposure
204
/// - HHI (Herfindahl-Hirschman Index) provides overall diversification measurement
205
///
206
/// # Regulatory Compliance
207
/// Supports compliance with:
208
/// - Basel III concentration risk requirements
209
/// - `MiFID` II best execution and risk management
210
/// - SEC/FINRA concentration guidelines
211
/// - Internal risk management policies
212
///
213
/// # Usage
214
/// ```rust
215
/// let limits = ConcentrationLimits {
216
///     max_single_position_pct: Price::from_f64(5.0)?, // 5% per position
217
///     max_sector_concentration_pct: Price::from_f64(20.0)?, // 20% per sector
218
///     max_strategy_concentration_pct: Price::from_f64(30.0)?, // 30% per strategy
219
///     max_geographic_concentration_pct: Price::from_f64(40.0)?, // 40% per region
220
///     max_hhi_index: Price::from_f64(1000.0)?, // HHI < 1000 = diversified
221
/// };
222
/// position_tracker.set_concentration_limits(&portfolio_id, limits).await?;
223
/// ```
224
#[derive(Debug, Clone, Serialize, Deserialize)]
225
pub struct ConcentrationLimits {
226
    /// Maximum percentage of portfolio value for a single position (default: 5%)
227
    /// Prevents over-concentration in individual securities
228
    pub max_single_position_pct: Price,
229
    /// Maximum percentage for a single sector/asset class (default: 20%)
230
    /// Ensures diversification across industries and asset classes
231
    pub max_sector_concentration_pct: Price,
232
    /// Maximum percentage for a single strategy (default: 30%)
233
    /// Prevents over-reliance on single trading approaches
234
    pub max_strategy_concentration_pct: Price,
235
    /// Maximum percentage for a single country/region (default: 40%)
236
    /// Reduces country and regional risk exposure
237
    pub max_geographic_concentration_pct: Price,
238
    /// Herfindahl-Hirschman Index (HHI) limit for portfolio diversification (default: 1000)
239
    /// HHI < 1000 indicates a diversified portfolio, > 1500 indicates concentration
240
    pub max_hhi_index: Price,
241
}
242
243
impl Default for ConcentrationLimits {
244
1
    fn default() -> Self {
245
        Self {
246
1
            max_single_position_pct: f64_to_price_safe(5.0, "max single position percentage")
247
1
                .unwrap_or_else(|_| 
{0
248
0
                    warn!("Failed to create max_single_position_pct, using zero");
249
0
                    Price::ZERO
250
0
                }),
251
1
            max_sector_concentration_pct: f64_to_price_safe(
252
                20.0,
253
1
                "max sector concentration percentage",
254
            )
255
1
            .unwrap_or_else(|_| 
{0
256
0
                warn!("Failed to create max_sector_concentration_pct, using zero");
257
0
                Price::ZERO
258
0
            }),
259
1
            max_strategy_concentration_pct: f64_to_price_safe(
260
                30.0,
261
1
                "max strategy concentration percentage",
262
            )
263
1
            .unwrap_or_else(|_| 
{0
264
0
                warn!("Failed to create max_strategy_concentration_pct, using zero");
265
0
                Price::ZERO
266
0
            }),
267
1
            max_geographic_concentration_pct: f64_to_price_safe(
268
                40.0,
269
1
                "max geographic concentration percentage",
270
            )
271
1
            .unwrap_or_else(|_| 
{0
272
0
                warn!("Failed to create max_geographic_concentration_pct, using zero");
273
0
                Price::ZERO
274
0
            }),
275
1
            max_hhi_index: f64_to_price_safe(1000.0, "max HHI index").unwrap_or(Price::ZERO), // HHI < 1000 indicates diversified portfolio
276
        }
277
1
    }
278
}
279
280
/// **Real-Time Concentration Risk Metrics**
281
///
282
/// Comprehensive concentration risk analysis for portfolio monitoring and compliance.
283
/// Provides detailed metrics on portfolio diversification and concentration levels.
284
///
285
/// # Metrics Included
286
/// - **Portfolio Overview**: Total value and largest position analysis
287
/// - **Diversification Measurement**: HHI index for overall portfolio concentration
288
/// - **Sector Analysis**: Concentration levels across industry sectors
289
/// - **Strategy Analysis**: Exposure distribution across trading strategies
290
/// - **Geographic Analysis**: Regional and country concentration levels
291
/// - **Risk Warnings**: Real-time alerts for limit breaches
292
///
293
/// # HHI (Herfindahl-Hirschman Index)
294
/// - Range: 0 to 10,000
295
/// - < 1,000: Diversified portfolio (low concentration risk)
296
/// - 1,000-1,500: Moderate concentration
297
/// - > 1,500: High concentration (regulatory concern)
298
/// - 10,000: Single position (maximum concentration)
299
///
300
/// # Risk Management Applications
301
/// - Pre-trade concentration checks
302
/// - Portfolio rebalancing decisions
303
/// - Regulatory reporting and compliance
304
/// - Risk limit monitoring and alerting
305
/// - Client reporting and transparency
306
///
307
/// # Usage
308
/// ```rust
309
/// let metrics = position_tracker.calculate_concentration_risk(&portfolio_id).await?;
310
///
311
/// println!("Portfolio Value: ${}", metrics.total_portfolio_value);
312
/// println!("Largest Position: {:.2}% ({})",
313
///          metrics.largest_position_pct, metrics.largest_position_symbol);
314
/// println!("HHI Index: {:.0} ({})", metrics.hhi_index,
315
///          if metrics.hhi_index < Price::from_f64(1000.0)? { "Diversified" } else { "Concentrated" });
316
///
317
/// for warning in &metrics.concentration_warnings {
318
///     println!("Warning: {:?} - {:.2}% exceeds limit of {:.2}%",
319
///              warning.warning_type, warning.current_value, warning.limit_value);
320
/// }
321
/// ```
322
#[derive(Debug, Clone, Serialize, Deserialize)]
323
pub struct ConcentrationRiskMetrics {
324
    /// Portfolio identifier for which metrics were calculated
325
    pub portfolio_id: PortfolioId,
326
    /// Total market value of all positions in the portfolio
327
    pub total_portfolio_value: Price,
328
    /// Percentage of portfolio value held in the largest single position
329
    pub largest_position_pct: Price,
330
    /// Symbol of the largest position in the portfolio
331
    pub largest_position_symbol: Symbol,
332
    /// Herfindahl-Hirschman Index measuring overall portfolio concentration
333
    /// Scale: 0-10,000 where lower values indicate better diversification
334
    pub hhi_index: Price,
335
    /// Sector concentration levels as percentage of portfolio value
336
    /// Key: sector name, Value: percentage of portfolio
337
    pub sector_concentrations: HashMap<String, Price>,
338
    /// Strategy concentration levels as percentage of portfolio value
339
    /// Key: strategy ID, Value: percentage of portfolio
340
    pub strategy_concentrations: HashMap<String, Price>,
341
    /// Geographic concentration levels as percentage of portfolio value
342
    /// Key: country/region name, Value: percentage of portfolio
343
    pub geographic_concentrations: HashMap<String, Price>,
344
    /// Active concentration risk warnings for limit breaches
345
    pub concentration_warnings: Vec<ConcentrationWarning>,
346
    /// UTC timestamp when metrics were calculated
347
    pub calculated_at: DateTime<Utc>,
348
}
349
350
/// **Concentration Limit Warning**
351
///
352
/// Detailed warning information for concentration limit breaches.
353
/// Provides specific details about the violation for risk management and compliance.
354
///
355
/// # Warning Information
356
/// - **Type**: Category of concentration limit that was breached
357
/// - **Current vs Limit**: Actual value compared to configured limit
358
/// - **Breach Amount**: How much the limit was exceeded by
359
/// - **Affected Items**: Specific entities (positions, sectors, etc.) involved
360
///
361
/// # Risk Management Response
362
/// - **Low Breach** (<25% over limit): Monitor and plan rebalancing
363
/// - **Medium Breach** (25-50% over limit): Active rebalancing required
364
/// - **High Breach** (>50% over limit): Immediate action and risk review
365
/// - **Critical Breach**: Potential trading halt or forced rebalancing
366
///
367
/// # Usage
368
/// ```rust
369
/// for warning in &concentration_metrics.concentration_warnings {
370
///     match warning.warning_type {
371
///         ConcentrationWarningType::SinglePositionLimit => {
372
///             println!("Position {} is {:.2}% of portfolio (limit: {:.2}%)",
373
///                      warning.affected_items[0], warning.current_value, warning.limit_value);
374
///         }
375
///         ConcentrationWarningType::SectorConcentration => {
376
///             println!("Sector {} concentration: {:.2}% (limit: {:.2}%)",
377
///                      warning.affected_items[0], warning.current_value, warning.limit_value);
378
///         }
379
///         _ => { /* Handle other warning types */ }
380
///     }
381
/// }
382
/// ```
383
#[derive(Debug, Clone, Serialize, Deserialize)]
384
pub struct ConcentrationWarning {
385
    /// Type of concentration limit that was breached
386
    pub warning_type: ConcentrationWarningType,
387
    /// Current concentration value that exceeded the limit
388
    pub current_value: Price,
389
    /// Configured limit that was breached
390
    pub limit_value: Price,
391
    /// Amount by which the current value exceeds the limit
392
    pub breach_amount: Price,
393
    /// List of specific items (symbols, sectors, strategies) that caused the breach
394
    pub affected_items: Vec<String>,
395
}
396
397
/// **Types of Concentration Risk Warnings**
398
///
399
/// Categorizes different types of concentration limit breaches for appropriate risk management response.
400
/// Each type requires different monitoring and mitigation strategies.
401
///
402
/// # Warning Categories
403
/// - **`SinglePositionLimit`**: Individual position too large (immediate rebalancing)
404
/// - **`SectorConcentration`**: Sector exposure too high (diversification needed)
405
/// - **`StrategyConcentration`**: Strategy allocation too concentrated (strategy diversification)
406
/// - **`GeographicConcentration`**: Geographic exposure too high (regional diversification)
407
/// - **`HHIExceeded`**: Overall portfolio concentration too high (comprehensive rebalancing)
408
///
409
/// # Risk Severity by Type
410
/// 1. **`SinglePositionLimit`**: High risk - single point of failure
411
/// 2. **`HHIExceeded`**: High risk - systemic concentration
412
/// 3. **`SectorConcentration`**: Medium risk - industry correlation
413
/// 4. **`GeographicConcentration`**: Medium risk - country/regional risk
414
/// 5. **`StrategyConcentration`**: Low-Medium risk - approach diversification
415
///
416
/// # Usage
417
/// ```rust
418
/// match warning.warning_type {
419
///     ConcentrationWarningType::SinglePositionLimit => {
420
///         // Immediate action required - consider position reduction
421
///         log_high_priority_alert(&warning);
422
///         suggest_position_rebalancing(&warning.affected_items);
423
///     }
424
///     ConcentrationWarningType::HHIExceeded => {
425
///         // Portfolio-wide rebalancing needed
426
///         initiate_diversification_review(&portfolio_id);
427
///     }
428
///     ConcentrationWarningType::SectorConcentration => {
429
///         // Sector rotation or hedging strategies
430
///         consider_sector_hedging(&warning.affected_items);
431
///     }
432
///     _ => { /* Handle other types */ }
433
/// }
434
/// ```
435
#[derive(Debug, Clone, Serialize, Deserialize)]
436
pub enum ConcentrationWarningType {
437
    /// Single position exceeds maximum percentage limit
438
    /// High priority - risk of single point of failure
439
    SinglePositionLimit,
440
    /// Sector allocation exceeds diversification limits
441
    /// Medium priority - industry correlation risk
442
    SectorConcentration,
443
    /// Strategy allocation too concentrated
444
    /// Medium priority - approach diversification needed
445
    StrategyConcentration,
446
    /// Geographic exposure exceeds regional limits
447
    /// Medium priority - country/regional risk concentration
448
    GeographicConcentration,
449
    /// Herfindahl-Hirschman Index exceeds diversification threshold
450
    /// High priority - overall portfolio concentration risk
451
    HHIExceeded,
452
}
453
454
/// **Enhanced Risk Position with Comprehensive Risk Attribution**
455
///
456
/// Extended position information including risk metrics, factor exposures, and attribution data.
457
/// Provides comprehensive view of position's contribution to portfolio risk.
458
///
459
/// # Risk Attribution Components
460
/// - **Base Position**: Core position data (quantity, price, P&L)
461
/// - **Classification**: Sector, country, and asset class categorization
462
/// - **Risk Metrics**: Beta, correlation, volatility, and `VaR` contribution
463
/// - **Factor Exposures**: Exposure to systematic risk factors
464
/// - **Real-time Updates**: Last updated timestamp for freshness
465
///
466
/// # Risk Factor Analysis
467
/// - **Beta**: Systematic risk relative to market (1.0 = market risk)
468
/// - **Correlation**: Linear relationship with market movements
469
/// - **Volatility**: Standard deviation of price movements
470
/// - **`VaR` Contribution**: Marginal contribution to portfolio Value at Risk
471
///
472
/// # Classification Framework
473
/// - **Sector**: Industry classification (Technology, Financials, Healthcare, etc.)
474
/// - **Country**: Geographic classification for regional risk analysis
475
/// - **Asset Class**: High-level categorization (Equity, Fixed Income, Currency, etc.)
476
///
477
/// # Usage
478
/// ```rust
479
/// let position = position_tracker.get_enhanced_position(&portfolio_id, &instrument_id).await;
480
///
481
/// if let Some(pos) = position {
482
///     println!("Position: {} shares of {} ({})",
483
///              pos.base_position.quantity, pos.base_position.instrument_id, pos.sector);
484
///     
485
///     if let Some(beta) = pos.beta {
486
///         println!("Beta: {:.2} ({})", beta,
487
///                  if beta > Price::from_f64(1.0)? { "Higher than market risk" }
488
///                  else { "Lower than market risk" });
489
///     }
490
///     
491
///     if let Some(var_contrib) = pos.var_contribution {
492
///         println!("VaR Contribution: ${:.2}", var_contrib);
493
///     }
494
/// }
495
/// ```
496
#[derive(Debug, Clone, Serialize, Deserialize)]
497
pub struct EnhancedRiskPosition {
498
    /// Core position information (quantity, price, P&L, etc.)
499
    pub base_position: RiskPosition,
500
    /// Industry sector classification (Technology, Financials, Healthcare, etc.)
501
    pub sector: String,
502
    /// Country or geographic region classification
503
    pub country: String,
504
    /// High-level asset class (Equity, Fixed Income, Currency, Commodity, etc.)
505
    pub asset_class: String,
506
    /// Beta coefficient measuring systematic risk relative to market (1.0 = market risk)
507
    pub beta: Option<Price>,
508
    /// Correlation coefficient with market movements (-1.0 to 1.0)
509
    pub correlation_with_market: Option<Price>,
510
    /// Annualized volatility of the position (standard deviation of returns)
511
    pub volatility: Option<Price>,
512
    /// Marginal contribution to portfolio Value at Risk
513
    pub var_contribution: Option<Price>,
514
    /// Exposures to systematic risk factors (market, size, value, momentum, etc.)
515
    /// Key: factor name, Value: exposure coefficient
516
    pub risk_factor_exposures: HashMap<String, Price>,
517
    /// UTC timestamp of last update to position data
518
    pub last_updated: DateTime<Utc>,
519
}
520
521
/// **Real-Time Position Tracker with Concentration Risk Monitoring**
522
///
523
/// Enterprise-grade position tracking system providing real-time portfolio monitoring,
524
/// concentration risk analysis, and comprehensive risk attribution.
525
///
526
/// # Core Capabilities
527
/// - **Real-time Position Tracking**: Live position updates with sub-millisecond latency
528
/// - **Concentration Risk Monitoring**: Multi-dimensional concentration analysis
529
/// - **Risk Attribution**: Factor-based risk decomposition and attribution
530
/// - **Portfolio Analytics**: Comprehensive portfolio-level metrics and summaries
531
/// - **Event Broadcasting**: Real-time position update notifications
532
/// - **Compliance Monitoring**: Automated limit checking and violation alerting
533
///
534
/// # Performance Characteristics
535
/// - **Position Updates**: <1ms processing time for position changes
536
/// - **Risk Calculations**: <10ms for concentration risk analysis
537
/// - **Memory Efficiency**: Concurrent access with `DashMap` for thread safety
538
/// - **Scalability**: Handles thousands of positions across multiple portfolios
539
///
540
/// # Data Structure
541
/// - **Positions**: Indexed by (`portfolio_id`, `instrument_id`, `strategy_id`)
542
/// - **Portfolio Summaries**: Aggregated metrics by portfolio
543
/// - **Market Data Cache**: Real-time pricing for P&L calculations
544
/// - **Risk Factor Loadings**: Factor exposures for risk attribution
545
///
546
/// # Integration Points
547
/// - **Market Data Feeds**: Real-time price updates
548
/// - **Order Management**: Position changes from trade execution
549
/// - **Risk Management**: Concentration limits and monitoring
550
/// - **Reporting Systems**: Portfolio analytics and compliance
551
/// - **Monitoring Dashboards**: Real-time position and risk metrics
552
///
553
/// # Usage
554
/// ```rust
555
/// let tracker = PositionTracker::new();
556
///
557
/// // Update position
558
/// let position = tracker.update_enhanced_position(
559
///     "portfolio1".to_string(),
560
///     "AAPL".to_string(),
561
///     "strategy1".to_string(),
562
///     Price::from_f64(100.0)?, // quantity
563
///     Price::from_f64(150.0)?, // price
564
///     Some("Technology".to_string()),
565
///     Some("United States".to_string()),
566
///     Some("Equity".to_string()),
567
/// ).await?;
568
///
569
/// // Calculate concentration risk
570
/// let risk_metrics = tracker.calculate_concentration_risk(&"portfolio1".to_string()).await?;
571
///
572
/// // Get portfolio summary
573
/// let summary = tracker.get_portfolio_summary(&"portfolio1".to_string()).await;
574
/// ```
575
///
576
/// **Enterprise-Grade Real-Time Position Tracking System**
577
///
578
/// Comprehensive portfolio management system providing real-time position tracking,
579
/// concentration risk monitoring, and P&L calculation across multiple portfolios
580
/// and strategies. Implements advanced risk analytics following institutional
581
/// portfolio management best practices.
582
///
583
/// # Core Capabilities
584
/// - **Multi-Portfolio Management**: Track positions across unlimited portfolios
585
/// - **Real-Time P&L**: Live mark-to-market valuation with market data integration
586
/// - **Concentration Risk**: Advanced concentration limit monitoring and alerting
587
/// - **Strategy Attribution**: Position tracking by individual trading strategies
588
/// - **Risk Factor Analysis**: Systematic risk factor exposure measurement
589
/// - **Performance Analytics**: Comprehensive performance and risk metrics
590
///
591
/// # Architecture Design
592
/// - **High-Performance Storage**: `DashMap` for lock-free concurrent access
593
/// - **Memory Efficient**: Optimized data structures for millions of positions
594
/// - **Thread-Safe**: Full concurrent operation across trading threads
595
/// - **Event-Driven**: Real-time position update broadcasting
596
/// - **Fault Tolerant**: Graceful handling of market data and calculation errors
597
///
598
/// # Risk Management Features
599
/// - **Concentration Limits**: Configurable limits by instrument, sector, geography
600
/// - **Risk Factor Exposure**: Systematic risk factor loading analysis
601
/// - **Portfolio Analytics**: Advanced risk attribution and decomposition
602
/// - **Real-Time Monitoring**: Continuous risk assessment and violation detection
603
///
604
/// # Performance Characteristics
605
/// - **Sub-microsecond Updates**: Optimized for high-frequency trading
606
/// - **Concurrent Access**: Thousands of simultaneous position updates
607
/// - **Memory Optimized**: Efficient storage for large portfolios
608
/// - **Real-Time Calculation**: Live P&L and risk metric computation
609
///
610
/// # Integration Points
611
/// - **Market Data Feeds**: Real-time price updates for valuation
612
/// - **Risk Engine**: Position data for pre-trade risk checks
613
/// - **Reporting Systems**: Portfolio summaries and risk reports
614
/// - **Compliance**: Position data for regulatory reporting
615
///
616
/// # Usage Example
617
/// ```rust
618
/// let position_tracker = PositionTracker::new().await?;
619
///
620
/// // Update position from trade
621
/// position_tracker.update_position(
622
///     "portfolio1".to_string(),
623
///     "AAPL".to_string(),
624
///     "strategy1".to_string(),
625
///     position_update
626
/// ).await?;
627
///
628
/// // Get real-time portfolio summary
629
/// let summary = position_tracker.get_portfolio_summary(&"portfolio1".to_string()).await?;
630
/// println!("Portfolio Value: ${}", summary.total_value);
631
/// println!("Daily P&L: ${}", summary.daily_pnl);
632
/// ```
633
#[derive(Debug, Clone)]
634
pub struct PositionTracker {
635
    /// Core position storage indexed by (`portfolio_id`, `instrument_id`, `strategy_id`)
636
    /// Thread-safe concurrent access with `DashMap` for high-performance updates
637
    positions: Arc<DashMap<(PortfolioId, InstrumentId, StrategyId), EnhancedRiskPosition>>,
638
    /// Portfolio-level summaries with aggregated metrics and risk analysis
639
    /// Updated automatically when positions change
640
    portfolio_summaries: Arc<DashMap<PortfolioId, PortfolioSummary>>,
641
    /// Concentration risk limits configuration by portfolio
642
    /// Protected by `RwLock` for infrequent updates with concurrent reads
643
    concentration_limits: Arc<RwLock<HashMap<PortfolioId, ConcentrationLimits>>>,
644
    /// Real-time market data cache for P&L and valuation calculations
645
    /// Updated from market data feeds for accurate position valuation
646
    market_data_cache: Arc<DashMap<InstrumentId, MarketData>>,
647
    /// Portfolio-level P&L metrics and performance tracking
648
    /// Real-time calculation of realized and unrealized gains/losses
649
    // Infrastructure - will be used for P&L tracking and risk attribution
650
    #[allow(dead_code)]
651
    pnl_metrics: Arc<DashMap<PortfolioId, PnLMetrics>>,
652
    /// Risk factor loadings for advanced risk attribution analysis
653
    /// Maps instruments to their exposures to systematic risk factors
654
    #[allow(dead_code)]
655
    risk_factor_loadings: Arc<RwLock<HashMap<InstrumentId, HashMap<String, Price>>>>,
656
    /// Broadcast channel for real-time position update notifications
657
    /// Allows multiple subscribers to receive position change events
658
    position_update_sender: broadcast::Sender<PositionUpdateEvent>,
659
    /// Asset classification configuration for sector and type categorization
660
    /// Replaces hardcoded symbol-based classification with configurable rules
661
    asset_classification_config: AssetClassificationConfig,
662
}
663
664
/// **Portfolio Summary with Comprehensive Risk Metrics**
665
///
666
/// Aggregated portfolio-level information providing complete view of portfolio health,
667
/// performance, and risk characteristics. Updated in real-time as positions change.
668
///
669
/// # Summary Components
670
/// - **Valuation**: Total portfolio value and position count
671
/// - **Performance**: Realized, unrealized, and daily P&L
672
/// - **Risk Analysis**: Concentration metrics and risk warnings
673
/// - **Top Holdings**: Largest positions by value and percentage
674
/// - **Allocations**: Sector and geographic distribution
675
///
676
/// # Real-time Updates
677
/// - Automatically recalculated when positions change
678
/// - Market data updates trigger valuation refresh
679
/// - Concentration analysis updated with each position change
680
/// - Performance metrics updated continuously
681
///
682
/// # Risk Monitoring
683
/// - Concentration risk analysis across multiple dimensions
684
/// - Real-time limit monitoring and violation detection
685
/// - Top position analysis for single-name concentration
686
/// - Sector allocation for diversification monitoring
687
///
688
/// # Usage
689
/// ```rust
690
/// let summary = position_tracker.get_portfolio_summary(&portfolio_id).await;
691
///
692
/// if let Some(summary) = summary {
693
///     println!("Portfolio: {} (${:.2})", summary.portfolio_id, summary.total_value);
694
///     println!("Positions: {}, Daily P&L: ${:.2}",
695
///              summary.total_positions, summary.daily_pnl);
696
///     
697
///     // Check for concentration warnings
698
///     if !summary.concentration_metrics.concentration_warnings.is_empty() {
699
///         println!("⚠️  {} concentration warnings active",
700
///                  summary.concentration_metrics.concentration_warnings.len());
701
///     }
702
///     
703
///     // Display top positions
704
///     for (i, position) in summary.top_positions.iter().enumerate() {
705
///         println!("{}. {} - ${:.2} ({:.1}%)",
706
///                  i+1, position.symbol, position.value, position.percentage);
707
///     }
708
/// }
709
/// ```
710
#[derive(Debug, Clone, Serialize, Deserialize)]
711
pub struct PortfolioSummary {
712
    /// Portfolio identifier
713
    pub portfolio_id: PortfolioId,
714
    /// Total market value of all positions in the portfolio
715
    pub total_value: Price,
716
    /// Number of distinct positions currently held
717
    pub total_positions: usize,
718
    /// Unrealized profit/loss from current market values vs cost basis
719
    pub unrealized_pnl: Decimal,
720
    /// Realized profit/loss from closed positions
721
    pub realized_pnl: Decimal,
722
    /// Combined daily profit/loss (realized + unrealized)
723
    pub daily_pnl: Decimal,
724
    /// Comprehensive concentration risk analysis and warnings
725
    pub concentration_metrics: ConcentrationRiskMetrics,
726
    /// Top 10 positions by market value with percentages
727
    pub top_positions: Vec<TopPosition>,
728
    /// Sector allocation as percentage of portfolio value
729
    /// Key: sector name, Value: percentage allocation
730
    pub sector_allocation: HashMap<String, Price>,
731
    /// UTC timestamp of last summary update
732
    pub last_updated: DateTime<Utc>,
733
}
734
735
/// **Top Position Information for Portfolio Analysis**
736
///
737
/// Detailed information about individual positions ranked by market value.
738
/// Used in portfolio summaries to highlight largest holdings and concentration.
739
///
740
/// # Position Metrics
741
/// - **Symbol**: Instrument identifier for the position
742
/// - **Value**: Current market value in USD
743
/// - **Percentage**: Position size as percentage of total portfolio
744
/// - **P&L**: Unrealized profit/loss for the position
745
///
746
/// # Risk Analysis
747
/// Positions are ranked by value to identify:
748
/// - Largest single-name exposures
749
/// - Concentration risk contributors
750
/// - Performance attribution by position
751
/// - Rebalancing opportunities
752
///
753
/// # Usage
754
/// ```rust
755
/// for (rank, position) in summary.top_positions.iter().enumerate() {
756
///     println!("{}. {} - ${:.2} ({:.1}%) - P&L: ${:.2}",
757
///              rank + 1,
758
///              position.symbol,
759
///              position.value,
760
///              position.percentage,
761
///              position.pnl);
762
///              
763
///     // Flag concentration risks
764
///     if position.percentage > Price::from_f64(5.0)? {
765
///         println!("   ⚠️  Position exceeds 5% concentration limit");
766
///     }
767
/// }
768
/// ```
769
#[derive(Debug, Clone, Serialize, Deserialize)]
770
pub struct TopPosition {
771
    /// Instrument symbol/identifier
772
    pub symbol: Symbol,
773
    /// Current market value of the position
774
    pub value: Price,
775
    /// Position size as percentage of total portfolio value
776
    pub percentage: Price,
777
    /// Unrealized profit/loss for this position
778
    pub pnl: Decimal,
779
}
780
781
/// **Position Update Event for Real-Time Monitoring**
782
///
783
/// Event structure broadcast to subscribers when positions change.
784
/// Enables real-time monitoring, alerting, and downstream system updates.
785
///
786
/// # Event Information
787
/// - **Portfolio Context**: Which portfolio was affected
788
/// - **Instrument Context**: Which instrument/position changed
789
/// - **Event Type**: Nature of the change (opened, increased, decreased, closed)
790
/// - **Value Impact**: Current position value after the change
791
/// - **Timing**: Precise timestamp for event ordering
792
///
793
/// # Event Processing
794
/// - Broadcast to multiple subscribers simultaneously
795
/// - Non-blocking event delivery for performance
796
/// - Event ordering preserved with timestamps
797
/// - Downstream systems can filter by portfolio or instrument
798
///
799
/// # Subscriber Examples
800
/// - Risk monitoring dashboards
801
/// - Compliance systems
802
/// - Audit trail logging
803
/// - Performance analytics
804
/// - Client reporting systems
805
///
806
/// # Usage
807
/// ```rust
808
/// let mut event_receiver = position_tracker.subscribe_to_updates();
809
///
810
/// tokio::spawn(async move {
811
///     while let Ok(event) = event_receiver.recv().await {
812
///         match event.event_type {
813
///             PositionEventType::PositionOpened => {
814
///                 println!("New position opened: {} in {} (${:.2})",
815
///                          event.instrument_id, event.portfolio_id, event.position_value);
816
///             }
817
///             PositionEventType::PositionClosed => {
818
///                 println!("Position closed: {} in {}",
819
///                          event.instrument_id, event.portfolio_id);
820
///             }
821
///             PositionEventType::MarketDataUpdated => {
822
///                 println!("Market data updated for {}: ${:.2}",
823
///                          event.instrument_id, event.position_value);
824
///             }
825
///             _ => { /* Handle other events */ }
826
///         }
827
///     }
828
/// });
829
/// ```
830
#[derive(Debug, Clone)]
831
pub struct PositionUpdateEvent {
832
    /// Portfolio identifier where the position change occurred
833
    pub portfolio_id: PortfolioId,
834
    /// Instrument identifier for the position that changed
835
    pub instrument_id: InstrumentId,
836
    /// Type of position change event
837
    pub event_type: PositionEventType,
838
    /// Current position value after the change
839
    pub position_value: Price,
840
    /// UTC timestamp when the event occurred
841
    pub timestamp: DateTime<Utc>,
842
}
843
844
/// **Position Event Types for Real-Time Monitoring**
845
///
846
/// Categorizes different types of position changes for appropriate event handling.
847
/// Each event type may trigger different downstream processing and alerting.
848
///
849
/// # Event Categories
850
/// - **Position Lifecycle**: Opened, increased, decreased, closed
851
/// - **Market Updates**: Price changes affecting position valuation
852
///
853
/// # Event Handling
854
/// Different event types typically trigger different responses:
855
/// - **`PositionOpened`**: New position alerts, compliance checks
856
/// - **`PositionIncreased`**: Concentration monitoring, limit checks
857
/// - **`PositionDecreased`**: Rebalancing tracking, tax implications
858
/// - **`PositionClosed`**: Final P&L calculation, audit logging
859
/// - **`MarketDataUpdated`**: Valuation refresh, risk recalculation
860
///
861
/// # Usage
862
/// ```rust
863
/// match event.event_type {
864
///     PositionEventType::PositionOpened => {
865
///         // Check initial position limits
866
///         check_new_position_compliance(&event).await?;
867
///         log_new_position(&event);
868
///     }
869
///     PositionEventType::PositionIncreased => {
870
///         // Monitor concentration risk
871
///         check_concentration_limits(&event.portfolio_id).await?;
872
///     }
873
///     PositionEventType::MarketDataUpdated => {
874
///         // Update risk calculations
875
///         recalculate_portfolio_risk(&event.portfolio_id).await?;
876
///     }
877
///     _ => { /* Handle other events */ }
878
/// }
879
/// ```
880
#[derive(Debug, Clone)]
881
pub enum PositionEventType {
882
    /// New position was opened in the portfolio
883
    PositionOpened,
884
    /// Existing position size was increased
885
    PositionIncreased,
886
    /// Existing position size was decreased
887
    PositionDecreased,
888
    /// Position was completely closed (quantity = 0)
889
    PositionClosed,
890
    /// Market data update changed position valuation
891
    MarketDataUpdated,
892
}
893
894
impl Default for PositionTracker {
895
0
    fn default() -> Self {
896
0
        Self::new()
897
0
    }
898
}
899
900
impl PositionTracker {
901
    /// **Create New Position Tracker Instance**
902
    ///
903
    /// Initializes a new position tracking system with empty state.
904
    /// Sets up all internal data structures for real-time position monitoring.
905
    ///
906
    /// # Returns
907
    /// * `Self` - New position tracker ready for operation
908
    ///
909
    /// # Initialization
910
    /// - Empty position storage with thread-safe concurrent access
911
    /// - Portfolio summaries cache for aggregated metrics
912
    /// - Default concentration limits for all portfolios
913
    /// - Market data cache for real-time P&L calculations
914
    /// - Event broadcasting system for real-time updates
915
    ///
916
    /// # Performance
917
    /// - Zero-cost initialization with lazy data structure allocation
918
    /// - Thread-safe design using `DashMap` and `RwLock`
919
    /// - Broadcast channel for efficient event distribution
920
    ///
921
    /// # Usage
922
    /// ```rust
923
    /// let tracker = PositionTracker::new();
924
    ///
925
    /// // Set custom concentration limits
926
    /// let limits = ConcentrationLimits {
927
    ///     max_single_position_pct: Price::from_f64(5.0)?,
928
    ///     max_sector_concentration_pct: Price::from_f64(20.0)?,
929
    ///     // ... other limits
930
    /// };
931
    /// tracker.set_concentration_limits(&"portfolio1".to_string(), limits).await?;
932
    /// ```
933
    #[must_use]
934
35
    pub fn new() -> Self {
935
35
        let (position_update_sender, _) = broadcast::channel(1000);
936
937
35
        Self {
938
35
            positions: Arc::new(DashMap::new()),
939
35
            portfolio_summaries: Arc::new(DashMap::new()),
940
35
            concentration_limits: Arc::new(RwLock::new(HashMap::new())),
941
35
            market_data_cache: Arc::new(DashMap::new()),
942
35
            pnl_metrics: Arc::new(DashMap::new()),
943
35
            risk_factor_loadings: Arc::new(RwLock::new(HashMap::new())),
944
35
            position_update_sender,
945
35
            asset_classification_config: AssetClassificationConfig::default(),
946
35
        }
947
35
    }
948
949
    /// **Get Position with Enhanced Risk Information**
950
    ///
951
    /// Retrieves detailed position information including risk metrics and attribution data.
952
    /// Returns the first matching position for the given portfolio and instrument.
953
    ///
954
    /// # Arguments
955
    /// * `portfolio_id` - Portfolio identifier to search within
956
    /// * `instrument_id` - Instrument identifier for the position
957
    ///
958
    /// # Returns
959
    /// * `Option<EnhancedRiskPosition>` - Position with risk metrics or None if not found
960
    ///
961
    /// # Position Data Included
962
    /// - **Base Position**: Quantity, average price, market value, P&L
963
    /// - **Risk Classification**: Sector, country, asset class
964
    /// - **Risk Metrics**: Beta, correlation, volatility, `VaR` contribution
965
    /// - **Factor Exposures**: Systematic risk factor loadings
966
    /// - **Timestamps**: Last update time for data freshness
967
    ///
968
    /// # Search Behavior
969
    /// - Searches across all strategies for the portfolio/instrument combination
970
    /// - Returns first matching position found
971
    /// - Strategy-agnostic lookup for consolidated position view
972
    ///
973
    /// # Performance
974
    /// - O(n) search across positions (where n = number of positions)
975
    /// - Concurrent access safe with `DashMap`
976
    /// - No blocking operations
977
    ///
978
    /// # Usage
979
    /// ```rust
980
    /// let position = tracker.get_enhanced_position(
981
    ///     &"portfolio1".to_string(),
982
    ///     &"AAPL".to_string()
983
    /// ).await;
984
    ///
985
    /// if let Some(pos) = position {
986
    ///     println!("Position: {} shares at ${:.2} avg price",
987
    ///              pos.base_position.quantity, pos.base_position.position.average_price);
988
    ///     println!("Sector: {}, Country: {}", pos.sector, pos.country);
989
    ///     
990
    ///     if let Some(beta) = pos.beta {
991
    ///         println!("Beta: {:.2}", beta);
992
    ///     }
993
    /// } else {
994
    ///     println!("No position found");
995
    /// }
996
    /// ```
997
1
    pub async fn get_enhanced_position(
998
1
        &self,
999
1
        portfolio_id: &PortfolioId,
1000
1
        instrument_id: &InstrumentId,
1001
1
    ) -> Option<EnhancedRiskPosition> {
1002
        // Find any position matching portfolio and instrument (ignoring strategy)
1003
1
        for entry in self.positions.iter() {
1004
1
            if &entry.key().0 == portfolio_id && &entry.key().1 == instrument_id {
1005
1
                return Some(entry.value().clone());
1006
0
            }
1007
        }
1008
0
        None
1009
1
    }
1010
1011
    // Use get_enhanced_position() instead
1012
1013
    /// **Update Position with Enhanced Risk Attribution**
1014
    ///
1015
    /// Updates or creates a position with comprehensive risk attribution data.
1016
    /// Performs real-time P&L calculation and portfolio impact analysis.
1017
    ///
1018
    /// # Arguments
1019
    /// * `portfolio_id` - Portfolio containing the position
1020
    /// * `instrument_id` - Instrument being traded
1021
    /// * `strategy_id` - Trading strategy identifier
1022
    /// * `quantity` - Position quantity (positive for long, negative for short)
1023
    /// * `price` - Average execution price
1024
    /// * `sector` - Industry sector (auto-classified if None)
1025
    /// * `country` - Geographic classification (auto-classified if None)
1026
    /// * `asset_class` - Asset class category (auto-classified if None)
1027
    ///
1028
    /// # Returns
1029
    /// * `RiskResult<EnhancedRiskPosition>` - Updated position with risk metrics
1030
    ///
1031
    /// # Position Updates
1032
    /// - **New Position**: Creates position with initial risk classification
1033
    /// - **Existing Position**: Updates quantity, price, and risk metrics
1034
    /// - **Risk Attribution**: Calculates sector, country, asset class if not provided
1035
    /// - **Market Valuation**: Updates market value based on current pricing
1036
    /// - **Timestamp**: Records update time for data freshness
1037
    ///
1038
    /// # Automatic Processing
1039
    /// 1. Position creation or update with risk metrics
1040
    /// 2. Portfolio summary recalculation
1041
    /// 3. Concentration risk analysis refresh
1042
    /// 4. Event broadcasting to subscribers
1043
    /// 5. Prometheus metrics updates
1044
    ///
1045
    /// # Classification Logic
1046
    /// - **Sector**: Based on instrument symbol patterns
1047
    /// - **Country**: Geographic classification from symbol characteristics
1048
    /// - **Asset Class**: High-level categorization (Equity, Currency, Crypto, etc.)
1049
    ///
1050
    /// # Performance
1051
    /// - Position update: <1ms typical processing time
1052
    /// - Concurrent access safe with atomic operations
1053
    /// - Non-blocking event broadcasting
1054
    /// - Efficient memory usage with reference counting
1055
    ///
1056
    /// # Error Handling
1057
    /// - Invalid quantity/price values return `RiskError::Validation`
1058
    /// - Calculation failures return `RiskError::CalculationError`
1059
    /// - All errors include detailed context for debugging
1060
    ///
1061
    /// # Usage
1062
    /// ```rust
1063
    /// // Create new position
1064
    /// let position = tracker.update_enhanced_position(
1065
    ///     "portfolio1".to_string(),
1066
    ///     "AAPL".to_string(),
1067
    ///     "momentum_strategy".to_string(),
1068
    ///     Price::from_f64(100.0)?, // 100 shares
1069
    ///     Price::from_f64(150.0)?, // $150 per share
1070
    ///     Some("Technology".to_string()),
1071
    ///     Some("United States".to_string()),
1072
    ///     Some("Equity".to_string()),
1073
    /// ).await?;
1074
    ///
1075
    /// println!("Position value: ${:.2}", position.base_position.market_value);
1076
    /// ```
1077
0
    pub async fn update_enhanced_position(
1078
0
        &self,
1079
0
        portfolio_id: PortfolioId,
1080
0
        instrument_id: InstrumentId,
1081
0
        strategy_id: StrategyId,
1082
0
        quantity: Price,
1083
0
        price: Price,
1084
0
        sector: Option<String>,
1085
0
        country: Option<String>,
1086
0
        asset_class: Option<String>,
1087
0
    ) -> RiskResult<EnhancedRiskPosition> {
1088
0
        debug!(
1089
0
            "Updating enhanced position: {} {} qty={} price={}",
1090
            portfolio_id, instrument_id, quantity, price
1091
        );
1092
1093
        // Get or create base position
1094
0
        let key = (
1095
0
            portfolio_id.clone(),
1096
0
            instrument_id.clone(),
1097
0
            strategy_id.clone(),
1098
0
        );
1099
0
        let mut enhanced_position = if let Some(existing) = self.positions.get(&key) {
1100
0
            existing.clone()
1101
        } else {
1102
            // Create new enhanced position
1103
0
            let mut base_position = RiskPosition::new(
1104
0
                instrument_id.clone(),
1105
0
                Quantity::new(quantity.raw_value() as f64)?, // Convert Price to Quantity
1106
0
                price,
1107
0
                price, // current_price same as avg_price initially
1108
0
                portfolio_id.clone(),
1109
            );
1110
0
            base_position.strategy_id = Some(strategy_id.clone());
1111
1112
            EnhancedRiskPosition {
1113
0
                base_position,
1114
0
                sector: sector.unwrap_or_else(|| self.classify_sector(&instrument_id)),
1115
0
                country: country.unwrap_or_else(|| self.classify_country(&instrument_id)),
1116
0
                asset_class: asset_class
1117
0
                    .unwrap_or_else(|| self.classify_asset_class(&instrument_id)),
1118
0
                beta: None,
1119
0
                correlation_with_market: None,
1120
0
                volatility: None,
1121
0
                var_contribution: None,
1122
0
                risk_factor_exposures: HashMap::new(),
1123
0
                last_updated: Utc::now(),
1124
            }
1125
        };
1126
1127
        // Update base position
1128
0
        let volume = Quantity::from_f64(quantity.to_f64())
1129
0
            .map_err(|e| RiskError::CalculationError(format!("Failed to convert quantity: {e}")))?;
1130
0
        let avg_cost = Price::from_f64(price.to_f64())?;
1131
0
        let market_value = Price::from_f64((quantity * price)?.to_f64())?;
1132
1133
0
        enhanced_position
1134
0
            .base_position
1135
0
            .update_position(volume, avg_cost, market_value);
1136
0
        enhanced_position.last_updated = Utc::now();
1137
1138
        // Store updated position
1139
0
        self.positions
1140
0
            .insert(key.clone(), enhanced_position.clone());
1141
1142
        // Record metrics
1143
0
        POSITION_UPDATES_COUNTER.inc();
1144
0
        let position_value_f64 = (quantity * price)?.to_f64();
1145
0
        POSITION_VALUE_GAUGE.set(position_value_f64);
1146
1147
        // Update portfolio summary
1148
0
        self.update_portfolio_summary(&portfolio_id).await?;
1149
1150
        // Send position update event
1151
0
        let event = PositionUpdateEvent {
1152
0
            portfolio_id: portfolio_id.clone(),
1153
0
            instrument_id: instrument_id.clone(),
1154
0
            event_type: if quantity > Price::ZERO {
1155
0
                PositionEventType::PositionIncreased
1156
            } else {
1157
0
                PositionEventType::PositionDecreased
1158
            },
1159
0
            position_value: (quantity * price)?,
1160
0
            timestamp: Utc::now(),
1161
        };
1162
1163
0
        let _ = self.position_update_sender.send(event);
1164
1165
0
        let position_value = (quantity * price).unwrap_or_else(|e| {
1166
0
            warn!("Failed to calculate position value: {}", e);
1167
0
            Price::ZERO
1168
0
        });
1169
0
        info!(
1170
0
            "\u{2705} Enhanced position updated: {} {} - Value: ${}",
1171
            portfolio_id, instrument_id, position_value
1172
        );
1173
1174
0
        Ok(enhanced_position)
1175
0
    }
1176
1177
    // Use update_enhanced_position() instead
1178
1179
    /// **Synchronous Position Update Optimized for HFT Performance**
1180
    ///
1181
    /// High-performance position update avoiding async operations for minimal latency.
1182
    /// Designed for high-frequency trading scenarios requiring sub-millisecond updates.
1183
    ///
1184
    /// # Arguments
1185
    /// * `portfolio_id` - Portfolio containing the position
1186
    /// * `instrument_id` - Instrument being traded
1187
    /// * `strategy_id` - Trading strategy identifier
1188
    /// * `quantity` - Position quantity change
1189
    /// * `price` - Execution price for the update
1190
    ///
1191
    /// # Returns
1192
    /// * `RiskResult<EnhancedRiskPosition>` - Updated position with current metrics
1193
    ///
1194
    /// # Performance Optimizations
1195
    /// - **No Async Operations**: Avoids async overhead for speed
1196
    /// - **Minimal Allocations**: Reuses existing data structures
1197
    /// - **Atomic Operations**: Thread-safe without locks
1198
    /// - **Direct Updates**: Bypasses portfolio summary recalculation
1199
    /// - **Fast Path**: Skips non-essential risk calculations
1200
    ///
1201
    /// # HFT Design Features
1202
    /// - Target latency: <100μs for position updates
1203
    /// - Lock-free concurrent access with `DashMap`
1204
    /// - Minimal memory allocations
1205
    /// - Basic risk classification with sensible defaults
1206
    /// - Prometheus metrics recording
1207
    ///
1208
    /// # Trade-offs
1209
    /// - **Speed vs Features**: Basic risk attribution only
1210
    /// - **No Portfolio Updates**: Summary not automatically recalculated
1211
    /// - **No Event Broadcasting**: Silent updates for performance
1212
    /// - **Default Classifications**: Uses hardcoded sector/country defaults
1213
    ///
1214
    /// # When to Use
1215
    /// - High-frequency trading systems requiring minimal latency
1216
    /// - Batch position updates where portfolio summary can be calculated separately
1217
    /// - Performance-critical paths where async overhead is prohibitive
1218
    /// - Real-time risk systems processing thousands of updates per second
1219
    ///
1220
    /// # Usage
1221
    /// ```rust
1222
    /// // High-speed position update
1223
    /// let position = tracker.update_position_sync(
1224
    ///     "hft_portfolio".to_string(),
1225
    ///     "AAPL".to_string(),
1226
    ///     "scalping".to_string(),
1227
    ///     Price::from_f64(10.0)?, // Small quantity for HFT
1228
    ///     Price::from_f64(150.25)?, // Precise execution price
1229
    /// )?;
1230
    ///
1231
    /// // Update portfolio summary separately if needed
1232
    /// tokio::spawn(async move {
1233
    ///     tracker.update_portfolio_summary(&"hft_portfolio".to_string()).await
1234
    /// });
1235
    /// ```
1236
27
    pub fn update_position_sync(
1237
27
        &self,
1238
27
        portfolio_id: PortfolioId,
1239
27
        instrument_id: InstrumentId,
1240
27
        strategy_id: StrategyId,
1241
27
        quantity: f64, // Changed to f64 to allow negative values for selling
1242
27
        price: Price,
1243
27
    ) -> RiskResult<EnhancedRiskPosition> {
1244
27
        let key = (portfolio_id.clone(), instrument_id.clone(), strategy_id);
1245
1246
        // Get or create position
1247
27
        let mut enhanced_position =
1248
27
            self.positions
1249
27
                .get(&key)
1250
27
                .map(|p| 
p2
.
clone2
())
1251
27
                .unwrap_or_else(|| 
{25
1252
25
                    EnhancedRiskPosition {
1253
25
                        base_position: RiskPosition::new(
1254
25
                            instrument_id.clone(),
1255
25
                            Quantity::zero(), // zero initial quantity
1256
25
                            Price::ZERO,      // zero average price
1257
25
                            Price::ZERO,      // zero current price
1258
25
                            portfolio_id.clone(),
1259
25
                        ),
1260
25
                        sector: "Unknown".to_owned(),
1261
25
                        country: "Unknown".to_owned(),
1262
25
                        asset_class: "Equity".to_owned(),
1263
25
                        beta: None,
1264
25
                        correlation_with_market: None,
1265
25
                        volatility: Some(Price::ZERO),
1266
25
                        var_contribution: None,
1267
25
                        risk_factor_exposures: HashMap::new(),
1268
25
                        last_updated: Utc::now(),
1269
25
                    }
1270
25
                });
1271
1272
        // Calculate new accumulated quantity (handle positive and negative)
1273
27
        let old_quantity_f64 = enhanced_position.base_position.quantity.to_f64();
1274
27
        let new_total_f64 = old_quantity_f64 + quantity;
1275
1276
        // For negative quantities (selling), just add them
1277
27
        let accumulated_quantity = if new_total_f64 >= 0.0 {
1278
26
            Quantity::from_f64(new_total_f64).map_err(|e| 
{0
1279
0
                RiskError::CalculationError(format!(
1280
0
                    "Failed to convert accumulated quantity to Quantity: {e}"
1281
0
                ))
1282
0
            })?
1283
        } else {
1284
            // Position went negative (short), store as positive quantity
1285
            // (In a real system, you'd track long/short separately)
1286
1
            Quantity::from_f64(new_total_f64.abs()).map_err(|e| 
{0
1287
0
                RiskError::CalculationError(format!(
1288
0
                    "Failed to convert accumulated quantity to Quantity: {e}"
1289
0
                ))
1290
0
            })?
1291
        };
1292
1293
        // Calculate realized P&L when reducing position
1294
27
        let realized_pnl = if quantity < 0.0 && 
old_quantity_f64 > 0.02
{
1295
            // Closing/reducing position - calculate realized P&L
1296
1
            let closed_quantity = quantity.abs().min(old_quantity_f64);
1297
1
            let avg_cost = enhanced_position.base_position.avg_price.to_f64();
1298
1
            let pnl = closed_quantity * (price.to_f64() - avg_cost);
1299
1
            Price::from_f64(pnl.abs()).unwrap_or(Price::ZERO)
1300
        } else {
1301
26
            enhanced_position.base_position.realized_pnl
1302
        };
1303
1304
        // Calculate new average price (weighted average)
1305
27
        let new_avg_price = if quantity > 0.0 {
1306
            // Adding to position - weighted average
1307
24
            if old_quantity_f64 > 0.0 {
1308
                // Adding to existing position
1309
1
                let old_value =
1310
1
                    old_quantity_f64 * enhanced_position.base_position.avg_price.to_f64();
1311
1
                let new_value = quantity * price.to_f64();
1312
1
                let total_quantity = new_total_f64.abs();
1313
1
                if total_quantity > 0.0 {
1314
1
                    Price::from_f64((old_value + new_value) / total_quantity)
?0
1315
                } else {
1316
0
                    enhanced_position.base_position.avg_price
1317
                }
1318
            } else {
1319
                // First position - use the trade price
1320
23
                price
1321
            }
1322
        } else {
1323
            // Reducing/closing position - keep same average price
1324
3
            enhanced_position.base_position.avg_price
1325
        };
1326
1327
        // Update position synchronously
1328
27
        enhanced_position.base_position.update_position(
1329
27
            accumulated_quantity,
1330
27
            new_avg_price,
1331
27
            Price::from_f64(new_total_f64.abs() * price.to_f64())
?0
, // market value
1332
        );
1333
        // Set realized P&L
1334
27
        enhanced_position.base_position.realized_pnl = realized_pnl;
1335
27
        enhanced_position.last_updated = Utc::now();
1336
1337
        // Store updated position
1338
27
        self.positions.insert(key, enhanced_position.clone());
1339
1340
        // Record metrics for sync update
1341
27
        POSITION_UPDATES_COUNTER.inc();
1342
27
        let position_value_f64 = quantity * price.to_f64();
1343
27
        POSITION_VALUE_GAUGE.set(position_value_f64);
1344
1345
27
        Ok(enhanced_position)
1346
27
    }
1347
1348
    /// **Update Market Data and Recalculate Portfolio P&L**
1349
    ///
1350
    /// Processes real-time market data updates and recalculates position valuations.
1351
    /// Updates all positions for the given instrument across all portfolios.
1352
    ///
1353
    /// # Arguments
1354
    /// * `market_data` - Real-time market data including price, volume, and volatility
1355
    ///
1356
    /// # Returns
1357
    /// * `RiskResult<()>` - Success or error in market data processing
1358
    ///
1359
    /// # Market Data Processing
1360
    /// - **Price Updates**: Updates last traded price for position valuation
1361
    /// - **Volume Analysis**: Records trading volume for liquidity assessment
1362
    /// - **Volatility Updates**: Updates volatility metrics for risk calculations
1363
    /// - **Timestamp Recording**: Tracks data freshness and latency
1364
    ///
1365
    /// # P&L Recalculation
1366
    /// 1. **Market Value**: Quantity × Current Price
1367
    /// 2. **Unrealized P&L**: (Current Price - Average Cost) × Quantity
1368
    /// 3. **Position Risk**: Updates volatility-based risk metrics
1369
    /// 4. **Portfolio Impact**: Propagates changes to portfolio summaries
1370
    ///
1371
    /// # Affected Systems
1372
    /// - **All Positions**: Updates positions holding the instrument
1373
    /// - **Multiple Portfolios**: Cross-portfolio market data impact
1374
    /// - **Portfolio Summaries**: Automatic recalculation of aggregated metrics
1375
    /// - **Risk Metrics**: Volatility and `VaR` calculations updated
1376
    /// - **Event Broadcasting**: Notifies subscribers of market data changes
1377
    ///
1378
    /// # Performance Characteristics
1379
    /// - **Batch Processing**: Single market data update affects all relevant positions
1380
    /// - **Efficient Iteration**: O(n) where n = number of positions for instrument
1381
    /// - **Concurrent Safe**: Thread-safe updates with atomic operations
1382
    /// - **Real-time**: Sub-millisecond processing for market data updates
1383
    ///
1384
    /// # Error Handling
1385
    /// - Invalid market data triggers validation errors
1386
    /// - Calculation failures return detailed error context
1387
    /// - Partial failures don't affect other positions
1388
    /// - All errors logged with market data context
1389
    ///
1390
    /// # Usage
1391
    /// ```rust
1392
    /// // Process real-time market data feed
1393
    /// let market_data = MarketData {
1394
    ///     instrument_id: "AAPL".to_string(),
1395
    ///     last: Price::from_f64(152.50)?,
1396
    ///     bid: Price::from_f64(152.45)?,
1397
    ///     ask: Price::from_f64(152.55)?,
1398
    ///     volume: Quantity::from_f64(1_000_000.0)?,
1399
    ///     volatility: Some(0.25), // 25% annualized volatility
1400
    ///     timestamp: Utc::now().timestamp(),
1401
    /// };
1402
    ///
1403
    /// tracker.update_market_data(market_data).await?;
1404
    ///
1405
    /// // All AAPL positions now reflect new pricing
1406
    /// let summary = tracker.get_portfolio_summary(&portfolio_id).await;
1407
    /// ```
1408
1
    pub async fn update_market_data(&self, market_data: MarketData) -> RiskResult<()> {
1409
1
        debug!(
1410
0
            "Updating market data for {}: ${}",
1411
            market_data.instrument_id, market_data.last
1412
        );
1413
1414
        // Store market data
1415
1
        self.market_data_cache
1416
1
            .insert(market_data.instrument_id.clone(), market_data.clone());
1417
1418
        // Update all positions for this instrument
1419
1
        let mut updated_portfolios = Vec::new();
1420
1421
1
        for mut entry in self.positions.iter_mut() {
1422
1
            let key = entry.key().clone();
1423
1
            let (portfolio_id, instrument_id) = (&key.0, &key.1);
1424
1
            if instrument_id == &market_data.instrument_id {
1425
1
                let position = entry.value_mut();
1426
1427
                // Update unrealized P&L based on new market price
1428
1
                let current_quantity =
1429
1
                    position.base_position.quantity.to_decimal().map_err(|e| 
{0
1430
0
                        RiskError::CalculationError(format!(
1431
0
                            "Failed to convert quantity to decimal: {e:?}"
1432
0
                        ))
1433
0
                    })?;
1434
1
                let avg_cost = position
1435
1
                    .base_position
1436
1
                    .position
1437
1
                    .average_price
1438
1
                    .to_decimal()
1439
1
                    .map_err(|e| 
{0
1440
0
                        RiskError::CalculationError(format!(
1441
0
                            "Failed to convert average price to decimal: {e:?}"
1442
0
                        ))
1443
0
                    })?;
1444
1
                let unrealized_pnl = current_quantity * (market_data.last.to_decimal()
?0
- avg_cost);
1445
1446
                // Update position metrics
1447
1
                let market_value_decimal = current_quantity * market_data.last.to_decimal()
?0
;
1448
1
                let market_value_f64 =
1449
1
                    ToPrimitive::to_f64(&market_value_decimal).ok_or_else(|| 
{0
1450
0
                        RiskError::CalculationError(
1451
0
                            "Failed to convert market value to f64".to_owned(),
1452
0
                        )
1453
0
                    })?;
1454
1
                position.base_position.market_value = Price::from_f64(market_value_f64)
?0
;
1455
                position.base_position.unrealized_pnl =
1456
1
                    Price::from_f64(ToPrimitive::to_f64(&unrealized_pnl).ok_or_else(|| 
{0
1457
0
                        RiskError::TypeConversion {
1458
0
                            from_type: "Decimal".to_owned(),
1459
0
                            to_type: "f64".to_owned(),
1460
0
                            reason: format!("invalid Decimal value {unrealized_pnl}"),
1461
0
                        }
1462
0
                    })?)?;
1463
1
                position.volatility = market_data
1464
1
                    .volatility
1465
1
                    .map(|v| {
1466
1
                        Price::from_f64(v).map_err(|_| RiskError::TypeConversion {
1467
0
                            from_type: "f64".to_owned(),
1468
0
                            to_type: "Price".to_owned(),
1469
0
                            reason: format!("invalid f64 value {v}"),
1470
0
                        })
1471
1
                    })
1472
1
                    .transpose()
?0
;
1473
1
                position.last_updated = Utc::now();
1474
1475
1
                updated_portfolios.push(portfolio_id.clone());
1476
0
            }
1477
        }
1478
1479
        // Update portfolio summaries for affected portfolios
1480
2
        for 
portfolio_id1
in updated_portfolios {
1481
1
            self.update_portfolio_summary(&portfolio_id).await
?0
;
1482
        }
1483
1484
        // Send market data update event
1485
1
        let event = PositionUpdateEvent {
1486
1
            portfolio_id: "ALL".to_owned(), // Market data affects all portfolios
1487
1
            instrument_id: market_data.instrument_id,
1488
1
            event_type: PositionEventType::MarketDataUpdated,
1489
1
            position_value: market_data.last,
1490
1
            timestamp: Utc::now(),
1491
1
        };
1492
1493
1
        let _ = self.position_update_sender.send(event);
1494
1495
1
        Ok(())
1496
1
    }
1497
1498
    /// Calculate comprehensive concentration risk metrics for a portfolio
1499
1
    pub async fn calculate_concentration_risk(
1500
1
        &self,
1501
1
        portfolio_id: &PortfolioId,
1502
1
    ) -> RiskResult<ConcentrationRiskMetrics> {
1503
1
        debug!(
1504
0
            "Calculating concentration risk for portfolio: {}",
1505
            portfolio_id
1506
        );
1507
1508
        // Get all positions for this portfolio
1509
1
        let portfolio_positions: Vec<_> = self
1510
1
            .positions
1511
1
            .iter()
1512
1
            .filter(|entry| &entry.key().0 == portfolio_id)
1513
1
            .map(|entry| entry.value().clone())
1514
1
            .collect();
1515
1516
1
        if portfolio_positions.is_empty() {
1517
0
            return Ok(ConcentrationRiskMetrics {
1518
0
                portfolio_id: portfolio_id.clone(),
1519
0
                total_portfolio_value: Price::ZERO,
1520
0
                largest_position_pct: Price::ZERO,
1521
0
                largest_position_symbol: Symbol::from("NONE"),
1522
0
                hhi_index: Price::ZERO,
1523
0
                sector_concentrations: HashMap::new(),
1524
0
                strategy_concentrations: HashMap::new(),
1525
0
                geographic_concentrations: HashMap::new(),
1526
0
                concentration_warnings: Vec::new(),
1527
0
                calculated_at: Utc::now(),
1528
0
            });
1529
1
        }
1530
1531
        // Calculate total portfolio value
1532
1
        let mut total_value_decimal = Decimal::ZERO;
1533
2
        for 
pos1
in &portfolio_positions {
1534
1
            match pos.base_position.market_value.to_decimal() {
1535
1
                Ok(value) => total_value_decimal += value,
1536
0
                Err(e) => {
1537
0
                    warn!(
1538
0
                        "Failed to convert position market value to decimal: {:?}",
1539
                        e
1540
                    );
1541
                    // Continue with zero contribution for this position
1542
                },
1543
            }
1544
        }
1545
1
        let total_value = Price::from_decimal(total_value_decimal);
1546
1547
1
        if total_value == Price::ZERO {
1548
            // CRITICAL: Zero portfolio value indicates a serious problem
1549
            // This could be due to:
1550
            // 1. All positions closed (normal)
1551
            // 2. Data corruption or pricing errors (dangerous)
1552
            // 3. Market data feed failure (dangerous)
1553
0
            warn!(
1554
0
                "Portfolio {} has zero total value - this could indicate data corruption or pricing errors",
1555
                portfolio_id
1556
            );
1557
1558
            // Return error instead of silently hiding the issue
1559
0
            return Err(RiskError::CalculationError(
1560
0
                format!(
1561
0
                    "Portfolio {portfolio_id} has zero total value - cannot calculate concentration risk. \
1562
0
                     This may indicate data corruption, pricing errors, or market data feed failure."
1563
0
                )
1564
0
            ));
1565
1
        }
1566
1567
        // Find largest position
1568
1
        let largest_position = portfolio_positions
1569
1
            .iter()
1570
1
            .max_by(|a, b| 
{0
1571
0
                let a_value = a
1572
0
                    .base_position
1573
0
                    .market_value
1574
0
                    .to_decimal()
1575
0
                    .unwrap_or(Decimal::ZERO);
1576
0
                let b_value = b
1577
0
                    .base_position
1578
0
                    .market_value
1579
0
                    .to_decimal()
1580
0
                    .unwrap_or(Decimal::ZERO);
1581
0
                a_value.cmp(&b_value)
1582
0
            })
1583
1
            .ok_or_else(|| 
{0
1584
0
                RiskError::CalculationError(
1585
0
                    "No positions found for concentration calculation".to_owned(),
1586
0
                )
1587
0
            })?;
1588
1589
1
        let largest_position_value = largest_position
1590
1
            .base_position
1591
1
            .market_value
1592
1
            .to_decimal()
1593
1
            .map_err(|e| 
{0
1594
0
                RiskError::CalculationError(format!(
1595
0
                    "Failed to convert largest position value: {e:?}"
1596
0
                ))
1597
0
            })?;
1598
1
        let largest_position_pct = Price::from_decimal(
1599
1
            (largest_position_value / total_value_decimal) * Decimal::from(100),
1600
        );
1601
1602
        // Calculate Herfindahl-Hirschman Index (HHI)
1603
1
        let mut hhi_index = Decimal::ZERO;
1604
2
        for 
pos1
in &portfolio_positions {
1605
1
            match pos.base_position.market_value.to_decimal() {
1606
1
                Ok(value) => {
1607
1
                    let weight = value / total_value_decimal;
1608
1
                    hhi_index += weight * weight * Decimal::from(10000); // Scale to traditional HHI range
1609
1
                },
1610
0
                Err(e) => {
1611
0
                    warn!(
1612
0
                        "Failed to convert position market value for HHI calculation: {:?}",
1613
                        e
1614
                    );
1615
                    // Continue with zero contribution for this position
1616
                },
1617
            }
1618
        }
1619
1620
        // Calculate sector concentrations
1621
1
        let mut sector_concentrations = HashMap::new();
1622
2
        for 
position1
in &portfolio_positions {
1623
1
            let sector_value = sector_concentrations
1624
1
                .entry(position.sector.clone())
1625
1
                .or_insert(Price::ZERO);
1626
1
            match position.base_position.market_value.to_decimal() {
1627
1
                Ok(value) => *sector_value += value.into(),
1628
0
                Err(e) => warn!(
1629
0
                    "Failed to convert position market value for sector calculation: {:?}",
1630
                    e
1631
                ),
1632
            }
1633
        }
1634
1635
        // Convert to percentages
1636
1
        for value in sector_concentrations.values_mut() {
1637
1
            match value.to_decimal() {
1638
1
                Ok(val_decimal) => {
1639
1
                    let percentage_decimal = val_decimal / total_value_decimal * Decimal::from(100);
1640
1
                    *value = Price::from_decimal(percentage_decimal);
1641
1
                },
1642
0
                Err(_) => *value = Price::ZERO, // Handle conversion error gracefully
1643
            }
1644
        }
1645
1646
        // Calculate strategy concentrations
1647
1
        let mut strategy_concentrations = HashMap::new();
1648
2
        for 
position1
in &portfolio_positions {
1649
1
            let strategy_value = strategy_concentrations
1650
1
                .entry(
1651
1
                    position
1652
1
                        .base_position
1653
1
                        .strategy_id
1654
1
                        .clone()
1655
1
                        .unwrap_or_default(),
1656
                )
1657
1
                .or_insert(Price::ZERO);
1658
1
            match position.base_position.market_value.to_decimal() {
1659
1
                Ok(value) => *strategy_value += value.into(),
1660
0
                Err(e) => warn!(
1661
0
                    "Failed to convert position market value for strategy calculation: {:?}",
1662
                    e
1663
                ),
1664
            }
1665
        }
1666
        // Convert to percentages
1667
1
        for value in strategy_concentrations.values_mut() {
1668
1
            match value.to_decimal() {
1669
1
                Ok(val_decimal) => {
1670
1
                    let percentage_decimal = val_decimal / total_value_decimal * Decimal::from(100);
1671
1
                    *value = Price::from_decimal(percentage_decimal);
1672
1
                },
1673
0
                Err(_) => *value = Price::ZERO, // Handle conversion error gracefully
1674
            }
1675
        }
1676
1677
        // Calculate geographic concentrations
1678
1
        let mut geographic_concentrations = HashMap::new();
1679
2
        for 
position1
in &portfolio_positions {
1680
1
            let geo_value = geographic_concentrations
1681
1
                .entry(position.country.clone())
1682
1
                .or_insert(Price::ZERO);
1683
1
            if let Ok(value) = position.base_position.market_value.to_decimal() {
1684
1
                *geo_value += value.into();
1685
1
            } else {
1686
0
                warn!(
1687
0
                    "Failed to convert market_value to decimal for position {}",
1688
                    position.base_position.instrument_id
1689
                );
1690
            }
1691
        }
1692
        // Convert to percentages
1693
1
        for value in geographic_concentrations.values_mut() {
1694
1
            match value.to_decimal() {
1695
1
                Ok(val_decimal) => {
1696
1
                    let percentage_decimal = val_decimal / total_value_decimal * Decimal::from(100);
1697
1
                    *value = Price::from_decimal(percentage_decimal);
1698
1
                },
1699
0
                Err(_) => *value = Price::ZERO, // Handle conversion error gracefully
1700
            }
1701
        }
1702
1703
        // Check concentration limits and generate warnings
1704
1
        let limits = self.get_concentration_limits(portfolio_id).await
?0
;
1705
1
        let mut warnings = Vec::new();
1706
1707
        // Check single position limit
1708
1
        if largest_position_pct > limits.max_single_position_pct {
1709
1
            warnings.push(ConcentrationWarning {
1710
1
                warning_type: ConcentrationWarningType::SinglePositionLimit,
1711
1
                current_value: largest_position_pct,
1712
1
                limit_value: limits.max_single_position_pct,
1713
1
                breach_amount: largest_position_pct - limits.max_single_position_pct,
1714
1
                affected_items: vec![largest_position.base_position.instrument_id.clone()],
1715
1
            });
1716
1
        
}0
1717
1718
        // Check sector concentration limits
1719
2
        for (
sector1
,
concentration1
) in &sector_concentrations {
1720
1
            if *concentration > limits.max_sector_concentration_pct {
1721
1
                warnings.push(ConcentrationWarning {
1722
1
                    warning_type: ConcentrationWarningType::SectorConcentration,
1723
1
                    current_value: *concentration,
1724
1
                    limit_value: limits.max_sector_concentration_pct,
1725
1
                    breach_amount: *concentration - limits.max_sector_concentration_pct,
1726
1
                    affected_items: vec![sector.clone()],
1727
1
                });
1728
1
            
}0
1729
        }
1730
1731
        // Check HHI limit
1732
1
        if Price::from_decimal(hhi_index) > limits.max_hhi_index {
1733
1
            warnings.push(ConcentrationWarning {
1734
1
                warning_type: ConcentrationWarningType::HHIExceeded,
1735
1
                current_value: Price::from_decimal(hhi_index),
1736
1
                limit_value: limits.max_hhi_index,
1737
1
                breach_amount: Price::from_decimal(hhi_index) - limits.max_hhi_index,
1738
1
                affected_items: vec!["Portfolio Diversification".to_owned()],
1739
1
            });
1740
1
        
}0
1741
1742
1
        let metrics = ConcentrationRiskMetrics {
1743
1
            portfolio_id: portfolio_id.clone(),
1744
1
            total_portfolio_value: total_value,
1745
1
            largest_position_pct,
1746
1
            largest_position_symbol: Symbol::from(
1747
1
                largest_position.base_position.instrument_id.as_str(),
1748
1
            ),
1749
1
            hhi_index: Price::from_decimal(hhi_index),
1750
1
            sector_concentrations,
1751
1
            strategy_concentrations,
1752
1
            geographic_concentrations,
1753
1
            concentration_warnings: warnings,
1754
1
            calculated_at: Utc::now(),
1755
1
        };
1756
1757
1
        if !metrics.concentration_warnings.is_empty() {
1758
1
            warn!(
1759
0
                "\u{1f6a8} Concentration risk warnings for portfolio {}: {} violations",
1760
                portfolio_id,
1761
0
                metrics.concentration_warnings.len()
1762
            );
1763
            // Record concentration risk breaches
1764
4
            for _ in &metrics.concentration_warnings {
1765
3
                RISK_BREACHES_COUNTER.inc();
1766
3
            }
1767
0
        }
1768
1769
        // Update concentration risk metrics
1770
1
        if let Ok(hhi_f64) = decimal_to_f64_safe(hhi_index, "HHI index conversion") {
1771
1
            CONCENTRATION_RISK_GAUGE.set(hhi_f64);
1772
1
        } else {
1773
0
            warn!("Failed to convert HHI index to f64 for metrics");
1774
        }
1775
1776
1
        info!(
1777
0
            "\u{2705} Concentration risk calculated for {} - HHI: {:.0}, Largest Position: {:.2}%",
1778
            portfolio_id, hhi_index, largest_position_pct
1779
        );
1780
1781
1
        Ok(metrics)
1782
1
    }
1783
1784
    /// Update comprehensive portfolio summary with risk metrics
1785
1
    async fn update_portfolio_summary(&self, portfolio_id: &PortfolioId) -> RiskResult<()> {
1786
1
        let portfolio_positions: Vec<_> = self
1787
1
            .positions
1788
1
            .iter()
1789
1
            .filter(|entry| &entry.key().0 == portfolio_id)
1790
1
            .map(|entry| entry.value().clone())
1791
1
            .collect();
1792
1793
1
        if portfolio_positions.is_empty() {
1794
0
            return Ok(());
1795
1
        }
1796
1797
        // Calculate portfolio totals
1798
1
        let total_value_decimal: Decimal = portfolio_positions
1799
1
            .iter()
1800
1
            .map(|pos| {
1801
1
                pos.base_position
1802
1
                    .market_value
1803
1
                    .to_decimal()
1804
1
                    .unwrap_or(Decimal::ZERO)
1805
1
            })
1806
1
            .sum();
1807
1
        let total_value = Price::from_decimal(total_value_decimal);
1808
1809
1
        let unrealized_pnl: Decimal = portfolio_positions
1810
1
            .iter()
1811
1
            .map(|pos| {
1812
1
                pos.base_position
1813
1
                    .unrealized_pnl
1814
1
                    .to_decimal()
1815
1
                    .unwrap_or(Decimal::ZERO)
1816
1
            })
1817
1
            .sum();
1818
1
        let realized_pnl: Decimal = portfolio_positions
1819
1
            .iter()
1820
1
            .map(|pos| {
1821
1
                pos.base_position
1822
1
                    .realized_pnl
1823
1
                    .to_decimal()
1824
1
                    .unwrap_or(Decimal::ZERO)
1825
1
            })
1826
1
            .sum(); // Calculate top positions
1827
1
        let mut top_positions: Vec<TopPosition> = Vec::new();
1828
2
        for 
pos1
in &portfolio_positions {
1829
1
            let market_val_decimal =
1830
1
                pos.base_position
1831
1
                    .market_value
1832
1
                    .to_decimal()
1833
1
                    .unwrap_or_else(|e| 
{0
1834
0
                        warn!(
1835
0
                            "Failed to convert market value to decimal for position {}: {}",
1836
                            pos.base_position.instrument_id, e
1837
                        );
1838
0
                        Decimal::ZERO
1839
0
                    });
1840
1841
1
            let percentage = if total_value > Price::ZERO && total_value_decimal > Decimal::ZERO {
1842
1
                let percentage_decimal =
1843
1
                    (market_val_decimal / total_value_decimal) * Decimal::from(100);
1844
1
                Price::from_decimal(percentage_decimal)
1845
            } else {
1846
0
                Price::ZERO
1847
            };
1848
1849
1
            top_positions.push(TopPosition {
1850
1
                symbol: Symbol::from(pos.base_position.instrument_id.as_str()),
1851
1
                value: Price::from_decimal(market_val_decimal),
1852
1
                percentage,
1853
1
                pnl: pos
1854
1
                    .base_position
1855
1
                    .unrealized_pnl
1856
1
                    .to_decimal()
1857
1
                    .unwrap_or(Decimal::ZERO),
1858
1
            });
1859
        }
1860
1861
1
        top_positions.sort_by(|a, b| 
b.value0
.
cmp0
(
&a.value0
));
1862
1
        top_positions.truncate(10); // Keep top 10
1863
1864
        // Calculate sector allocation
1865
1
        let mut sector_allocation = HashMap::new();
1866
2
        for 
position1
in &portfolio_positions {
1867
1
            let sector_value = sector_allocation
1868
1
                .entry(position.sector.clone())
1869
1
                .or_insert(Price::ZERO);
1870
1
            if let Ok(value) = position.base_position.market_value.to_decimal() {
1871
1
                *sector_value += value.into();
1872
1
            } else {
1873
0
                warn!(
1874
0
                    "Failed to convert market_value to decimal for position {}",
1875
                    position.base_position.instrument_id
1876
                );
1877
            }
1878
        }
1879
1880
        // Calculate concentration metrics
1881
1
        let concentration_metrics = self.calculate_concentration_risk(portfolio_id).await
?0
;
1882
1883
        // Create portfolio summary
1884
1
        let summary = PortfolioSummary {
1885
1
            portfolio_id: portfolio_id.clone(),
1886
1
            total_value,
1887
1
            total_positions: portfolio_positions.len(),
1888
1
            unrealized_pnl,
1889
1
            realized_pnl,
1890
1
            daily_pnl: unrealized_pnl + realized_pnl, // Simplified daily P&L
1891
1
            concentration_metrics,
1892
1
            top_positions,
1893
1
            sector_allocation,
1894
1
            last_updated: Utc::now(),
1895
1
        };
1896
1897
1
        self.portfolio_summaries
1898
1
            .insert(portfolio_id.clone(), summary);
1899
1
        Ok(())
1900
1
    }
1901
1902
    /// **Get Portfolio Summary with Comprehensive Risk Metrics**
1903
    ///
1904
    /// Retrieves complete portfolio analytics including valuation, performance,
1905
    /// risk metrics, and concentration analysis.
1906
    ///
1907
    /// # Arguments
1908
    /// * `portfolio_id` - Portfolio identifier for summary retrieval
1909
    ///
1910
    /// # Returns
1911
    /// * `Option<PortfolioSummary>` - Complete portfolio summary or None if not found
1912
    ///
1913
    /// # Summary Components
1914
    /// - **Portfolio Valuation**: Total market value and position count
1915
    /// - **Performance Metrics**: Realized, unrealized, and daily P&L
1916
    /// - **Risk Analysis**: Concentration metrics and risk warnings
1917
    /// - **Top Holdings**: Largest positions by value and percentage
1918
    /// - **Sector Allocation**: Industry diversification breakdown
1919
    /// - **Real-time Data**: Last updated timestamp for freshness
1920
    ///
1921
    /// # Concentration Risk Analysis
1922
    /// - **HHI Index**: Portfolio diversification measurement
1923
    /// - **Single Position Risk**: Largest position concentration
1924
    /// - **Sector Risk**: Industry concentration levels
1925
    /// - **Geographic Risk**: Country/regional exposure
1926
    /// - **Strategy Risk**: Trading strategy concentration
1927
    /// - **Active Warnings**: Real-time limit breach alerts
1928
    ///
1929
    /// # Data Freshness
1930
    /// - Automatically updated when positions change
1931
    /// - Market data updates trigger recalculation
1932
    /// - Real-time risk metrics and concentration analysis
1933
    /// - Performance metrics updated continuously
1934
    ///
1935
    /// # Performance
1936
    /// - Cached portfolio summaries for efficient retrieval
1937
    /// - O(1) lookup with concurrent access safety
1938
    /// - No real-time calculation overhead
1939
    /// - Thread-safe access with `DashMap`
1940
    ///
1941
    /// # Usage
1942
    /// ```rust
1943
    /// let summary = tracker.get_portfolio_summary(&"portfolio1".to_string()).await;
1944
    ///
1945
    /// if let Some(summary) = summary {
1946
    ///     println!("Portfolio: {} (${:.2})", summary.portfolio_id, summary.total_value);
1947
    ///     println!("Positions: {}, Daily P&L: ${:.2}",
1948
    ///              summary.total_positions, summary.daily_pnl);
1949
    ///     
1950
    ///     // Risk analysis
1951
    ///     let metrics = &summary.concentration_metrics;
1952
    ///     println!("HHI Index: {:.0} ({})", metrics.hhi_index,
1953
    ///              if metrics.hhi_index < Price::from_f64(1000.0)? { "Diversified" } else { "Concentrated" });
1954
    ///     
1955
    ///     // Concentration warnings
1956
    ///     for warning in &metrics.concentration_warnings {
1957
    ///         println!("⚠️  {:?}: {:.2}% exceeds {:.2}% limit",
1958
    ///                  warning.warning_type, warning.current_value, warning.limit_value);
1959
    ///     }
1960
    ///     
1961
    ///     // Top positions
1962
    ///     for (i, pos) in summary.top_positions.iter().take(5).enumerate() {
1963
    ///         println!("{}. {} - ${:.2} ({:.1}%)", i+1, pos.symbol, pos.value, pos.percentage);
1964
    ///     }
1965
    ///     
1966
    ///     // Sector allocation
1967
    ///     for (sector, allocation) in &summary.sector_allocation {
1968
    ///         if *allocation > Price::from_f64(5.0)? {
1969
    ///             println!("Sector {}: {:.1}%", sector, allocation);
1970
    ///         }
1971
    ///     }
1972
    /// } else {
1973
    ///     println!("Portfolio not found or no positions");
1974
    /// }
1975
    /// ```
1976
0
    pub async fn get_portfolio_summary(
1977
0
        &self,
1978
0
        portfolio_id: &PortfolioId,
1979
0
    ) -> Option<PortfolioSummary> {
1980
0
        self.portfolio_summaries
1981
0
            .get(portfolio_id)
1982
0
            .map(|entry| entry.clone())
1983
0
    }
1984
1985
    /// **Set Concentration Risk Limits for Portfolio**
1986
    ///
1987
    /// Configures concentration risk limits for automated monitoring and alerting.
1988
    /// Limits are applied to all future concentration risk calculations.
1989
    ///
1990
    /// # Arguments
1991
    /// * `portfolio_id` - Portfolio to configure limits for
1992
    /// * `limits` - Concentration limit configuration
1993
    ///
1994
    /// # Returns
1995
    /// * `RiskResult<()>` - Success or configuration error
1996
    ///
1997
    /// # Limit Categories
1998
    /// - **Single Position**: Maximum percentage for any individual position
1999
    /// - **Sector Concentration**: Maximum exposure to any single industry
2000
    /// - **Strategy Concentration**: Maximum allocation to any trading strategy
2001
    /// - **Geographic Concentration**: Maximum exposure to any country/region
2002
    /// - **HHI Index**: Overall portfolio diversification threshold
2003
    ///
2004
    /// # Limit Enforcement
2005
    /// - Applied during concentration risk calculations
2006
    /// - Generates warnings when limits are exceeded
2007
    /// - Used for pre-trade risk checks
2008
    /// - Enables automated compliance monitoring
2009
    /// - Supports regulatory reporting requirements
2010
    ///
2011
    /// # Configuration Storage
2012
    /// - Persistent configuration per portfolio
2013
    /// - Thread-safe updates with `RwLock` protection
2014
    /// - Immediate effect on new risk calculations
2015
    /// - Override default limits with custom values
2016
    ///
2017
    /// # Regulatory Compliance
2018
    /// - Supports Basel III concentration requirements
2019
    /// - `MiFID` II risk management compliance
2020
    /// - SEC/FINRA concentration guidelines
2021
    /// - Custom institutional risk policies
2022
    ///
2023
    /// # Usage
2024
    /// ```rust
2025
    /// // Conservative institutional limits
2026
    /// let conservative_limits = ConcentrationLimits {
2027
    ///     max_single_position_pct: Price::from_f64(3.0)?, // 3% per position
2028
    ///     max_sector_concentration_pct: Price::from_f64(15.0)?, // 15% per sector
2029
    ///     max_strategy_concentration_pct: Price::from_f64(25.0)?, // 25% per strategy
2030
    ///     max_geographic_concentration_pct: Price::from_f64(35.0)?, // 35% per region
2031
    ///     max_hhi_index: Price::from_f64(800.0)?, // Highly diversified
2032
    /// };
2033
    ///
2034
    /// tracker.set_concentration_limits(&"conservative_portfolio".to_string(), conservative_limits).await?;
2035
    ///
2036
    /// // Aggressive hedge fund limits
2037
    /// let aggressive_limits = ConcentrationLimits {
2038
    ///     max_single_position_pct: Price::from_f64(10.0)?, // 10% per position
2039
    ///     max_sector_concentration_pct: Price::from_f64(30.0)?, // 30% per sector
2040
    ///     max_strategy_concentration_pct: Price::from_f64(50.0)?, // 50% per strategy
2041
    ///     max_geographic_concentration_pct: Price::from_f64(60.0)?, // 60% per region
2042
    ///     max_hhi_index: Price::from_f64(1500.0)?, // Moderate concentration allowed
2043
    /// };
2044
    ///
2045
    /// tracker.set_concentration_limits(&"hedge_fund_portfolio".to_string(), aggressive_limits).await?;
2046
    /// ```
2047
0
    pub async fn set_concentration_limits(
2048
0
        &self,
2049
0
        portfolio_id: &PortfolioId,
2050
0
        limits: ConcentrationLimits,
2051
0
    ) -> RiskResult<()> {
2052
0
        let mut limits_map = self.concentration_limits.write().await;
2053
0
        limits_map.insert(portfolio_id.clone(), limits);
2054
2055
0
        info!(
2056
0
            "\u{1f4ca} Concentration limits updated for portfolio {}",
2057
            portfolio_id
2058
        );
2059
0
        Ok(())
2060
0
    }
2061
2062
    /// **Get Concentration Risk Limits for Portfolio**
2063
    ///
2064
    /// Retrieves the current concentration risk limits configuration for a portfolio.
2065
    /// Returns default limits if no custom limits have been set.
2066
    ///
2067
    /// # Arguments
2068
    /// * `portfolio_id` - Portfolio identifier for limit retrieval
2069
    ///
2070
    /// # Returns
2071
    /// * `RiskResult<ConcentrationLimits>` - Current limit configuration
2072
    ///
2073
    /// # Default Limits
2074
    /// If no custom limits are configured, returns sensible defaults:
2075
    /// - **Single Position**: 5% maximum per position
2076
    /// - **Sector Concentration**: 20% maximum per sector
2077
    /// - **Strategy Concentration**: 30% maximum per strategy
2078
    /// - **Geographic Concentration**: 40% maximum per region
2079
    /// - **HHI Index**: 1000 (diversified portfolio threshold)
2080
    ///
2081
    /// # Limit Sources
2082
    /// 1. **Custom Configuration**: Portfolio-specific limits set via `set_concentration_limits`
2083
    /// 2. **Default Configuration**: Standard institutional limits
2084
    /// 3. **Regulatory Minimums**: Compliance-driven baseline limits
2085
    ///
2086
    /// # Thread Safety
2087
    /// - Concurrent access safe with `RwLock` protection
2088
    /// - Non-blocking read operations
2089
    /// - Consistent view of limit configuration
2090
    ///
2091
    /// # Usage
2092
    /// ```rust
2093
    /// // Get current limits for validation
2094
    /// let limits = tracker.get_concentration_limits(&"portfolio1".to_string()).await?;
2095
    ///
2096
    /// println!("Current concentration limits:");
2097
    /// println!("  Single position: {:.1}%", limits.max_single_position_pct);
2098
    /// println!("  Sector exposure: {:.1}%", limits.max_sector_concentration_pct);
2099
    /// println!("  Strategy allocation: {:.1}%", limits.max_strategy_concentration_pct);
2100
    /// println!("  Geographic exposure: {:.1}%", limits.max_geographic_concentration_pct);
2101
    /// println!("  HHI Index: {:.0}", limits.max_hhi_index);
2102
    ///
2103
    /// // Use limits for pre-trade checks
2104
    /// let concentration_metrics = tracker.calculate_concentration_risk(&"portfolio1".to_string()).await?;
2105
    /// if concentration_metrics.largest_position_pct > limits.max_single_position_pct {
2106
    ///     println!("⚠️  Single position limit would be exceeded");
2107
    /// }
2108
    /// ```
2109
1
    pub async fn get_concentration_limits(
2110
1
        &self,
2111
1
        portfolio_id: &PortfolioId,
2112
1
    ) -> RiskResult<ConcentrationLimits> {
2113
1
        let limits_map = self.concentration_limits.read().await;
2114
        // CRITICAL: Use configured limits, not defaults that could mask risk violations
2115
1
        Ok(limits_map.get(portfolio_id).cloned().unwrap_or_else(|| {
2116
1
            warn!(
2117
0
                "No concentration limits configured for portfolio {}, using default limits",
2118
                portfolio_id
2119
            );
2120
1
            ConcentrationLimits::default()
2121
1
        }))
2122
1
    }
2123
2124
    /// **Subscribe to Real-Time Position Update Events**
2125
    ///
2126
    /// Creates a new subscription to receive real-time position change notifications.
2127
    /// Enables event-driven monitoring and downstream system integration.
2128
    ///
2129
    /// # Returns
2130
    /// * `broadcast::Receiver<PositionUpdateEvent>` - Event receiver for position updates
2131
    ///
2132
    /// # Event Types Broadcast
2133
    /// - **`PositionOpened`**: New position created in portfolio
2134
    /// - **`PositionIncreased`**: Existing position size increased
2135
    /// - **`PositionDecreased`**: Existing position size decreased
2136
    /// - **`PositionClosed`**: Position completely closed (quantity = 0)
2137
    /// - **`MarketDataUpdated`**: Market price changes affecting position values
2138
    ///
2139
    /// # Event Processing
2140
    /// - **Non-blocking**: Events delivered asynchronously
2141
    /// - **Fan-out**: Multiple subscribers receive same events
2142
    /// - **Ordered**: Events delivered in chronological order
2143
    /// - **Buffered**: 1000 event buffer prevents lost events during processing
2144
    ///
2145
    /// # Subscriber Patterns
2146
    /// - **Risk Monitoring**: Real-time concentration and limit monitoring
2147
    /// - **Audit Logging**: Complete audit trail of position changes
2148
    /// - **Client Updates**: Real-time portfolio updates to client systems
2149
    /// - **Compliance**: Regulatory reporting and monitoring
2150
    /// - **Analytics**: Performance attribution and analysis
2151
    ///
2152
    /// # Performance Characteristics
2153
    /// - **Low Latency**: Sub-millisecond event delivery
2154
    /// - **High Throughput**: Handles thousands of events per second
2155
    /// - **Memory Efficient**: Bounded buffer prevents memory growth
2156
    /// - **Thread Safe**: Concurrent subscribers supported
2157
    ///
2158
    /// # Error Handling
2159
    /// - **Lagged Receivers**: Slow consumers receive lag error
2160
    /// - **Buffer Overflow**: Events dropped if buffer full
2161
    /// - **Disconnected Receivers**: Automatic cleanup of dead subscribers
2162
    ///
2163
    /// # Usage
2164
    /// ```rust
2165
    /// // Subscribe to position updates
2166
    /// let mut event_receiver = tracker.subscribe_to_updates();
2167
    ///
2168
    /// // Process events in background task
2169
    /// tokio::spawn(async move {
2170
    ///     while let Ok(event) = event_receiver.recv().await {
2171
    ///         match event.event_type {
2172
    ///             PositionEventType::PositionOpened => {
2173
    ///                 println!("✅ New position: {} in {} (${:.2})",
2174
    ///                          event.instrument_id, event.portfolio_id, event.position_value);
2175
    ///                 
2176
    ///                 // Check concentration limits for new position
2177
    ///                 check_concentration_compliance(&event).await;
2178
    ///             }
2179
    ///             PositionEventType::PositionClosed => {
2180
    ///                 println!("❌ Position closed: {} in {}",
2181
    ///                          event.instrument_id, event.portfolio_id);
2182
    ///                 
2183
    ///                 // Log final P&L and audit trail
2184
    ///                 log_position_closure(&event).await;
2185
    ///             }
2186
    ///             PositionEventType::MarketDataUpdated => {
2187
    ///                 // Update real-time dashboards
2188
    ///                 update_portfolio_dashboard(&event.portfolio_id, &event).await;
2189
    ///             }
2190
    ///             _ => {
2191
    ///                 // Handle other event types
2192
    ///                 process_generic_position_event(&event).await;
2193
    ///             }
2194
    ///         }
2195
    ///     }
2196
    ///     
2197
    ///     println!("Position event subscription ended");
2198
    /// });
2199
    /// ```
2200
    #[must_use]
2201
0
    pub fn subscribe_to_updates(&self) -> broadcast::Receiver<PositionUpdateEvent> {
2202
0
        self.position_update_sender.subscribe()
2203
0
    }
2204
2205
    /// **Get All Active Portfolios with Positions**
2206
    ///
2207
    /// Retrieves list of all portfolio identifiers that currently have positions.
2208
    /// Useful for portfolio management and monitoring operations.
2209
    ///
2210
    /// # Returns
2211
    /// * `Vec<PortfolioId>` - List of portfolio identifiers with active positions
2212
    ///
2213
    /// # Active Portfolio Criteria
2214
    /// - Portfolio has at least one position (any quantity, including zero)
2215
    /// - Position exists in the tracking system
2216
    /// - No duplicate portfolio IDs in returned list
2217
    ///
2218
    /// # Use Cases
2219
    /// - **Portfolio Management**: Iterate through all active portfolios
2220
    /// - **Risk Monitoring**: Check concentration across all portfolios
2221
    /// - **Reporting**: Generate portfolio reports for all active accounts
2222
    /// - **Cleanup Operations**: Identify portfolios for maintenance
2223
    /// - **Dashboard Updates**: Populate portfolio selection lists
2224
    ///
2225
    /// # Performance
2226
    /// - **Efficient Iteration**: Single pass through position storage
2227
    /// - **Deduplication**: Ensures unique portfolio list
2228
    /// - **Metrics Update**: Updates Prometheus portfolio count gauge
2229
    /// - **Thread Safe**: Concurrent access with position storage
2230
    ///
2231
    /// # Metrics Integration
2232
    /// - Updates `foxhunt_active_portfolios` gauge with current count
2233
    /// - Provides operational visibility into portfolio activity
2234
    /// - Supports monitoring and alerting on portfolio count changes
2235
    ///
2236
    /// # Usage
2237
    /// ```rust
2238
    /// // Get all active portfolios
2239
    /// let portfolios = tracker.get_active_portfolios().await;
2240
    ///
2241
    /// println!("Found {} active portfolios", portfolios.len());
2242
    ///
2243
    /// // Process each portfolio
2244
    /// for portfolio_id in portfolios {
2245
    ///     println!("Processing portfolio: {}", portfolio_id);
2246
    ///     
2247
    ///     // Get portfolio summary
2248
    ///     if let Some(summary) = tracker.get_portfolio_summary(&portfolio_id).await {
2249
    ///         println!("  Total value: ${:.2}", summary.total_value);
2250
    ///         println!("  Positions: {}", summary.total_positions);
2251
    ///         
2252
    ///         // Check for concentration warnings
2253
    ///         let warning_count = summary.concentration_metrics.concentration_warnings.len();
2254
    ///         if warning_count > 0 {
2255
    ///             println!("⚠️  {} concentration warnings", warning_count);
2256
    ///         }
2257
    ///     }
2258
    ///     
2259
    ///     // Calculate portfolio risk metrics
2260
    ///     let beta = tracker.calculate_portfolio_beta(&portfolio_id).await?;
2261
    ///     println!("  Portfolio beta: {:.2}", beta);
2262
    ///     
2263
    ///     // Get concentration risk
2264
    ///     let risk_metrics = tracker.calculate_concentration_risk(&portfolio_id).await?;
2265
    ///     println!("  HHI Index: {:.0}", risk_metrics.hhi_index);
2266
    /// }
2267
    /// ```
2268
0
    pub async fn get_active_portfolios(&self) -> Vec<PortfolioId> {
2269
0
        let mut portfolios = Vec::new();
2270
0
        for entry in self.positions.iter() {
2271
0
            let portfolio_id = &entry.key().0;
2272
0
            if !portfolios.contains(portfolio_id) {
2273
0
                portfolios.push(portfolio_id.clone());
2274
0
            }
2275
        }
2276
2277
        // Update portfolio count metric
2278
0
        PORTFOLIO_COUNT_GAUGE.set(portfolios.len() as i64);
2279
2280
0
        portfolios
2281
0
    }
2282
2283
    /// **Calculate Portfolio Beta (Systematic Risk)**
2284
    ///
2285
    /// Computes the portfolio's systematic risk relative to the market using
2286
    /// weighted average beta of individual positions.
2287
    ///
2288
    /// # Arguments
2289
    /// * `portfolio_id` - Portfolio identifier for beta calculation
2290
    ///
2291
    /// # Returns
2292
    /// * `RiskResult<Decimal>` - Portfolio beta coefficient
2293
    ///
2294
    /// # Beta Interpretation
2295
    /// - **Beta = 1.0**: Portfolio moves with market (market-level risk)
2296
    /// - **Beta > 1.0**: Portfolio more volatile than market (higher risk)
2297
    /// - **Beta < 1.0**: Portfolio less volatile than market (lower risk)
2298
    /// - **Beta = 0.0**: Portfolio uncorrelated with market
2299
    /// - **Beta < 0.0**: Portfolio moves opposite to market (rare)
2300
    ///
2301
    /// # Calculation Method
2302
    /// 1. **Position Weighting**: Each position weighted by market value
2303
    /// 2. **Beta Aggregation**: Weighted sum of individual position betas
2304
    /// 3. **Default Beta**: Positions without beta use 1.0 (market risk)
2305
    /// 4. **Portfolio Beta**: `Σ(Weight_i` × `Beta_i`)
2306
    ///
2307
    /// # Risk Applications
2308
    /// - **Portfolio Construction**: Target beta for risk management
2309
    /// - **Hedging Decisions**: Calculate hedge ratios for market exposure
2310
    /// - **Performance Attribution**: Separate alpha from beta returns
2311
    /// - **Risk Budgeting**: Allocate risk based on systematic exposure
2312
    /// - **Regulatory Capital**: Basel requirements for market risk
2313
    ///
2314
    /// # Data Requirements
2315
    /// - Position market values for accurate weighting
2316
    /// - Individual position betas (estimated or calculated)
2317
    /// - Current portfolio composition
2318
    ///
2319
    /// # Limitations
2320
    /// - **Static Betas**: Uses historical beta estimates
2321
    /// - **Linear Assumption**: Assumes linear relationship with market
2322
    /// - **Market Index**: Beta relative to assumed market benchmark
2323
    /// - **Time Stability**: Beta may change over time
2324
    ///
2325
    /// # Usage
2326
    /// ```rust
2327
    /// // Calculate portfolio systematic risk
2328
    /// let portfolio_beta = tracker.calculate_portfolio_beta(&"portfolio1".to_string()).await?;
2329
    ///
2330
    /// println!("Portfolio beta: {:.2}", portfolio_beta);
2331
    ///
2332
    /// // Interpret beta for risk management
2333
    /// match portfolio_beta {
2334
    ///     beta if beta > Decimal::from_f64(1.5).unwrap() => {
2335
    ///         println!("⚠️  High systematic risk - consider hedging");
2336
    ///     }
2337
    ///     beta if beta > Decimal::from_f64(1.2).unwrap() => {
2338
    ///         println!("Elevated market risk - monitor closely");
2339
    ///     }
2340
    ///     beta if beta < Decimal::from_f64(0.8).unwrap() => {
2341
    ///         println!("Conservative portfolio - lower market risk");
2342
    ///     }
2343
    ///     _ => {
2344
    ///         println!("Moderate systematic risk - market-like exposure");
2345
    ///     }
2346
    /// }
2347
    ///
2348
    /// // Calculate hedge ratio for market exposure
2349
    /// let market_value = portfolio_summary.total_value.to_decimal()?;
2350
    /// let hedge_notional = market_value * portfolio_beta;
2351
    /// println!("Hedge with ${:.0} notional for market-neutral position", hedge_notional);
2352
    /// ```
2353
0
    pub async fn calculate_portfolio_beta(
2354
0
        &self,
2355
0
        portfolio_id: &PortfolioId,
2356
0
    ) -> RiskResult<Decimal> {
2357
0
        let portfolio_positions: Vec<_> = self
2358
0
            .positions
2359
0
            .iter()
2360
0
            .filter(|entry| &entry.key().0 == portfolio_id)
2361
0
            .map(|entry| entry.value().clone())
2362
0
            .collect();
2363
2364
0
        if portfolio_positions.is_empty() {
2365
0
            return Ok(Decimal::ZERO);
2366
0
        }
2367
2368
0
        let total_value_decimal: Decimal = portfolio_positions
2369
0
            .iter()
2370
0
            .map(|pos| {
2371
0
                pos.base_position
2372
0
                    .market_value
2373
0
                    .to_decimal()
2374
0
                    .unwrap_or(Decimal::ZERO)
2375
0
            })
2376
0
            .sum();
2377
0
        let total_value = Price::from_decimal(total_value_decimal);
2378
2379
0
        if total_value == Price::ZERO {
2380
            // CRITICAL: Zero portfolio value prevents meaningful beta calculation
2381
            // This could indicate data corruption or pricing errors
2382
0
            warn!(
2383
0
                "Portfolio {} has zero total value - cannot calculate meaningful beta",
2384
                portfolio_id
2385
            );
2386
2387
            // Return error instead of silently returning zero beta
2388
0
            return Err(RiskError::CalculationError(
2389
0
                format!(
2390
0
                    "Portfolio {portfolio_id} has zero total value - cannot calculate portfolio beta. \
2391
0
                     This may indicate data corruption, pricing errors, or market data feed failure."
2392
0
                )
2393
0
            ));
2394
0
        }
2395
2396
        // Calculate weighted average beta
2397
0
        let weighted_beta: Decimal = portfolio_positions
2398
0
            .iter()
2399
0
            .map(|pos| {
2400
0
                let market_val = pos
2401
0
                    .base_position
2402
0
                    .market_value
2403
0
                    .to_decimal()
2404
0
                    .unwrap_or_else(|e| {
2405
0
                        warn!(
2406
0
                            "Failed to convert market value to decimal for beta calculation: {}",
2407
                            e
2408
                        );
2409
0
                        Decimal::ZERO
2410
0
                    });
2411
                // Safe division - total_value_decimal already validated to be non-zero above
2412
0
                let weight = if total_value_decimal > Decimal::ZERO {
2413
0
                    market_val / total_value_decimal
2414
                } else {
2415
0
                    Decimal::ZERO
2416
                };
2417
0
                let beta = pos.beta.map_or(Decimal::ONE, |b| {
2418
0
                    b.to_decimal().unwrap_or_else(|e| {
2419
0
                        warn!(
2420
0
                            "Failed to convert beta to decimal, using default 1.0: {}",
2421
                            e
2422
                        );
2423
0
                        Decimal::ONE
2424
0
                    })
2425
0
                });
2426
0
                weight * beta
2427
0
            })
2428
0
            .sum();
2429
2430
0
        Ok(weighted_beta)
2431
0
    }
2432
2433
    /// Helper methods for classification - now configuration-driven
2434
0
    fn classify_sector(&self, instrument_id: &InstrumentId) -> String {
2435
        // Use configuration-driven classification instead of hardcoded symbols
2436
        // This supports flexible categorization rules based on asset types and patterns
2437
0
        format!(
2438
0
            "{:?}",
2439
0
            self.asset_classification_config
2440
0
                .classify_symbol(instrument_id)
2441
        )
2442
0
    }
2443
2444
0
    fn classify_country(&self, instrument_id: &InstrumentId) -> String {
2445
        // Configuration-driven country classification based on currency patterns
2446
        // For currencies, extract country from currency code; for equities, use market identifier
2447
0
        if instrument_id.contains("USD") {
2448
0
            "United States".to_owned()
2449
0
        } else if instrument_id.contains("EUR") {
2450
0
            "European Union".to_owned()
2451
0
        } else if instrument_id.contains("GBP") {
2452
0
            "United Kingdom".to_owned()
2453
0
        } else if instrument_id.contains("JPY") {
2454
0
            "Japan".to_owned()
2455
        } else {
2456
            // Default for generic instruments - could be made configurable
2457
0
            "United States".to_owned()
2458
        }
2459
0
    }
2460
2461
0
    fn classify_asset_class(&self, instrument_id: &InstrumentId) -> String {
2462
        // Configuration-driven asset class classification using generic patterns
2463
        // Uses the same classification logic as sector classification for consistency
2464
0
        let asset_class = self
2465
0
            .asset_classification_config
2466
0
            .classify_symbol(instrument_id);
2467
0
        let sector = format!("{asset_class:?}");
2468
0
        match sector.as_str() {
2469
0
            "Currencies" => "Currency".to_owned(),
2470
0
            "Cryptocurrency" => "Cryptocurrency".to_owned(),
2471
0
            "Fixed Income" => "Fixed Income".to_owned(),
2472
0
            "Commodities" => "Commodity".to_owned(),
2473
0
            _ => "Equity".to_owned(),
2474
        }
2475
0
    }
2476
}
2477
2478
#[cfg(test)]
2479
mod tests {
2480
    use super::*;
2481
    // Removed types::operations - using common::types::prelude instead
2482
2483
    #[tokio::test]
2484
1
    async fn test_position_tracking() -> Result<(), Box<dyn std::error::Error>> {
2485
1
        let tracker = PositionTracker::new();
2486
2487
        // Create initial position
2488
1
        let position = tracker.update_position_sync(
2489
1
            "portfolio1".to_string(),
2490
1
            "TEST_EQUITY_001".to_string(),
2491
1
            "strategy1".to_string(),
2492
            100.0, // quantity as f64
2493
1
            Price::from_f64(150.0)
?0
,
2494
0
        )?;
2495
2496
1
        assert_eq!(
2497
1
            position.base_position.quantity.to_decimal()
?0
,
2498
1
            Decimal::try_from(100.0).map_err(|_| RiskError::CalculationError(
2499
0
                "Failed to convert 100.0 to decimal".to_owned()
2500
0
            ))?
2501
        );
2502
1
        assert_eq!(
2503
1
            position.base_position.avg_price.to_decimal()
?0
,
2504
1
            Price::from_f64(150.0)
?0
.to_decimal()
?0
2505
        );
2506
2507
        // Add to position
2508
1
        let position = tracker.update_position_sync(
2509
1
            "portfolio1".to_string(),
2510
1
            "TEST_EQUITY_001".to_string(),
2511
1
            "strategy1".to_string(),
2512
            50.0, // quantity as f64
2513
1
            Price::from_f64(160.0)
?0
,
2514
0
        )?;
2515
2516
1
        assert_eq!(
2517
1
            position.base_position.quantity.to_decimal()
?0
,
2518
1
            Decimal::try_from(150.0).map_err(|_| RiskError::CalculationError(
2519
0
                "Failed to convert 150.0 to decimal".to_owned()
2520
0
            ))?
2521
        );
2522
        // Average price should be (100*150 + 50*160) / 150 = 153.33
2523
1
        assert!(
2524
1
            position.base_position.avg_price.to_decimal()
?0
2525
1
                > Price::from_f64(153.0)
?0
.to_decimal()
?0
2526
1
                && position.base_position.avg_price.to_decimal()
?0
2527
1
                    < Price::from_f64(154.0)
?0
.to_decimal()
?0
2528
        );
2529
2530
        // Partial close
2531
1
        let position = tracker.update_position_sync(
2532
1
            "portfolio1".to_string(),
2533
1
            "TEST_EQUITY_001".to_string(),
2534
1
            "strategy1".to_string(),
2535
            -75.0, // negative quantity for selling
2536
1
            Price::from_f64(155.0)
?0
,
2537
0
        )?;
2538
2539
1
        assert_eq!(
2540
1
            position.base_position.quantity.to_decimal()
?0
,
2541
1
            Decimal::try_from(75.0).map_err(|_| RiskError::CalculationError(
2542
0
                "Failed to convert 75.0 to decimal".to_owned()
2543
0
            ))?
2544
        );
2545
1
        assert!(position.base_position.realized_pnl > Price::ZERO); // Should have made profit
2546
2
        Ok(())
2547
1
    }
2548
2549
    #[tokio::test]
2550
1
    async fn test_market_data_update() -> Result<(), Box<dyn std::error::Error>> {
2551
1
        let tracker = PositionTracker::new();
2552
2553
        // Create position
2554
1
        tracker.update_position_sync(
2555
1
            "portfolio1".to_string(),
2556
1
            "TEST_EQUITY_001".to_string(),
2557
1
            "strategy1".to_string(),
2558
            100.0, // quantity as f64
2559
1
            Price::from_f64(150.0)
?0
,
2560
0
        )?;
2561
2562
        // Update market data
2563
1
        let market_data = MarketData {
2564
1
            instrument_id: "TEST_EQUITY_001".to_string(),
2565
1
            bid: f64_to_price_safe(155.0, "test bid price").unwrap_or(Price::ZERO),
2566
1
            ask: f64_to_price_safe(156.0, "test ask price").unwrap_or(Price::ZERO),
2567
1
            last_price: f64_to_price_safe(155.0, "test last price").unwrap_or(Price::ZERO),
2568
1
            last: f64_to_price_safe(155.0, "test last price").unwrap_or(Price::ZERO),
2569
1
            volume: Quantity::from_f64(1000000.0)
?0
,
2570
1
            volatility: Some(0.25), // 25% volatility as f64
2571
1
            timestamp: Utc::now().timestamp(),
2572
        };
2573
2574
1
        tracker.update_market_data(market_data).await
?0
;
2575
2576
        // Check updated position
2577
1
        let position = tracker
2578
1
            .get_enhanced_position(&"portfolio1".to_string(), &"TEST_EQUITY_001".to_string())
2579
1
            .await
2580
1
            .ok_or("Position not found")
?0
2581
            .base_position;
2582
1
        assert_eq!(
2583
1
            position.market_value.to_decimal()
?0
,
2584
1
            Price::from_f64(15500.0)
?0
.to_decimal()
?0
2585
        ); // 100 * 155
2586
1
        assert_eq!(
2587
1
            position.unrealized_pnl.to_decimal()
?0
,
2588
1
            Price::from_f64(500.0)
?0
.to_decimal()
?0
2589
        ); // 100 * (155 - 150)
2590
2
        Ok(())
2591
1
    }
2592
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_engine.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_engine.rs.html deleted file mode 100644 index 061acffd9..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_engine.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/risk_engine.rs
Line
Count
Source
1
//! Risk Engine - Core risk management and validation system
2
//! Risk Engine Module
3
//!
4
//! This module provides comprehensive risk management including:
5
//! - Pre-trade risk checks (position limits, leverage, `VaR`)
6
//! - Real-time position monitoring
7
//! - Circuit breaker integration
8
//! - Kill switch functionality
9
//! - Real broker integration (NO MOCKS)
10
11
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
12
#![warn(clippy::indexing_slicing)]
13
14
use async_trait::async_trait;
15
use chrono::{DateTime, Utc};
16
use config::structures::RiskConfig;
17
use config::AssetClassificationConfig;
18
use num::ToPrimitive;
19
use std::marker::Send;
20
use std::sync::Arc;
21
use tracing::error;
22
use uuid::Uuid;
23
// ELIMINATED: Prelude import removed to force explicit imports
24
use common::{OrderSide, Position, Price, Quantity, Symbol};
25
use rust_decimal::Decimal;
26
use std::time::Instant;
27
use tokio::sync::broadcast;
28
use tracing::{debug, info, warn};
29
// Removed foxhunt_infrastructure - not available in this simplified risk crate
30
31
// Import ALL types from types crate using types::prelude::*
32
use crate::circuit_breaker::BrokerAccountService;
33
34
use crate::error::{
35
    decimal_to_f64_safe, f64_to_decimal_safe, f64_to_price_safe, parse_env_var,
36
    price_to_decimal_safe, safe_divide, RiskError, RiskResult,
37
};
38
use crate::position_tracker::PositionTracker;
39
use crate::risk_types::{
40
    InstrumentId, OrderInfo, RiskCheckResult, RiskSeverity, RiskViolation, ViolationType,
41
};
42
43
use crate::operations::{price_to_f64_safe, validate_financial_amount};
44
45
// ===== MISSING TYPE DEFINITIONS - STUB IMPLEMENTATIONS =====
46
47
// REMOVED: RiskConfig is now imported from config crate
48
// Use: config::RiskConfig instead of local definition
49
50
// ELIMINATED DUPLICATES - Use canonical types from config.rs
51
// Import types from config crate instead of defining locally
52
use config::structures::VarConfig;
53
54
// Default implementation now provided by config crate
55
56
/// Kill switch implementation for emergency trading halt
57
#[derive(Debug)]
58
pub struct KillSwitch {
59
    active: std::sync::atomic::AtomicBool,
60
}
61
62
impl KillSwitch {
63
    /// **Create New Kill Switch Instance**
64
    ///
65
    /// Initializes emergency trading halt system with active state.
66
    /// The kill switch starts in the active state (trading allowed).
67
    ///
68
    /// # Arguments
69
    /// * `_config` - Risk configuration (reserved for future use)
70
    ///
71
    /// # Returns
72
    /// * `Self` - Kill switch instance ready for operation
73
    ///
74
    /// # Safety
75
    /// - Atomic operations ensure thread-safe state management
76
    /// - Default active state allows trading unless explicitly disabled
77
    ///
78
    /// # Usage
79
    /// ```rust
80
    /// let config = RiskConfig::default();
81
    /// let kill_switch = KillSwitch::new(&config);
82
    /// ```
83
    #[must_use]
84
0
    pub const fn new(_config: &RiskConfig) -> Self {
85
0
        Self {
86
0
            active: std::sync::atomic::AtomicBool::new(true),
87
0
        }
88
0
    }
89
90
    /// **Check Kill Switch Status**
91
    ///
92
    /// Returns the current state of the emergency trading halt system.
93
    /// When inactive (false), all trading operations should be rejected.
94
    ///
95
    /// # Returns
96
    /// * `bool` - `true` if trading is allowed, `false` if emergency halt is active
97
    ///
98
    /// # Safety
99
    /// - Uses relaxed atomic ordering for performance
100
    /// - Thread-safe across all risk engine operations
101
    ///
102
    /// # Performance
103
    /// - Sub-nanosecond atomic read operation
104
    /// - No memory allocation or system calls
105
    ///
106
    /// # Usage
107
    /// ```rust
108
    /// if !kill_switch.is_active().await {
109
    ///     return RiskCheckResult::Rejected { ... };
110
    /// }
111
    /// ```
112
0
    pub async fn is_active(&self) -> bool {
113
0
        self.active.load(std::sync::atomic::Ordering::Relaxed)
114
0
    }
115
}
116
117
/// Position limit monitor implementation
118
#[derive(Debug)]
119
pub struct PositionLimitMonitor {
120
    _config: Arc<RiskConfig>,
121
}
122
123
impl PositionLimitMonitor {
124
    /// **Create Position Limit Monitor**
125
    ///
126
    /// Initializes real-time position limit monitoring system with risk configuration.
127
    /// Monitors position sizes, concentrations, and exposure limits.
128
    ///
129
    /// # Arguments
130
    /// * `config` - Shared risk configuration containing position limits
131
    ///
132
    /// # Returns
133
    /// * `Self` - Position limit monitor ready for validation
134
    ///
135
    /// # Monitoring Capabilities
136
    /// - Global position limits across all instruments
137
    /// - Symbol-specific position size limits
138
    /// - Portfolio concentration monitoring
139
    /// - Real-time limit violation detection
140
    ///
141
    /// # Usage
142
    /// ```rust
143
    /// let config = Arc::new(RiskConfig::from_env()?);
144
    /// let monitor = PositionLimitMonitor::new(config);
145
    /// ```
146
    #[must_use]
147
0
    pub const fn new(config: Arc<RiskConfig>) -> Self {
148
0
        Self { _config: config }
149
0
    }
150
}
151
152
/// Risk metrics collector implementation
153
#[derive(Debug)]
154
pub struct RiskMetricsCollector {
155
    _max_samples: usize,
156
}
157
158
impl RiskMetricsCollector {
159
    /// **Create Risk Metrics Collector**
160
    ///
161
    /// Initializes performance and risk metrics collection system.
162
    /// Tracks risk check latencies, violation counts, and system performance.
163
    ///
164
    /// # Arguments
165
    /// * `max_samples` - Maximum number of samples to retain for rolling metrics
166
    ///
167
    /// # Returns
168
    /// * `Self` - Metrics collector ready for data collection
169
    ///
170
    /// # Metrics Collected
171
    /// - Risk check execution latencies
172
    /// - Total number of risk checks performed
173
    /// - Risk violation counts by severity
174
    /// - System performance benchmarks
175
    ///
176
    /// # Memory Management
177
    /// - Uses rolling window to limit memory usage
178
    /// - Automatically evicts oldest samples when limit reached
179
    ///
180
    /// # Usage
181
    /// ```rust
182
    /// let collector = RiskMetricsCollector::new(10000); // Keep 10k samples
183
    /// ```
184
    #[must_use]
185
0
    pub const fn new(max_samples: usize) -> Self {
186
0
        Self {
187
0
            _max_samples: max_samples,
188
0
        }
189
0
    }
190
191
    /// **Get Performance Summary Statistics**
192
    ///
193
    /// Retrieves comprehensive performance metrics for risk management system.
194
    /// Provides key statistics for monitoring and optimization.
195
    ///
196
    /// # Returns
197
    /// * `PerformanceSummary` - Aggregated performance metrics
198
    ///
199
    /// # Metrics Included
200
    /// - `total_checks`: Total number of risk checks performed
201
    /// - `avg_latency_us`: Average risk check latency in microseconds
202
    /// - `total_violations`: Total number of risk violations detected
203
    ///
204
    /// # Performance
205
    /// - O(1) calculation for most metrics
206
    /// - Cached aggregations for efficiency
207
    ///
208
    /// # Usage
209
    /// ```rust
210
    /// let summary = collector.get_performance_summary().await;
211
    /// println!("Avg latency: {}μs", summary.avg_latency_us);
212
    /// ```
213
0
    pub async fn get_performance_summary(&self) -> PerformanceSummary {
214
0
        PerformanceSummary {
215
0
            total_checks: 0,
216
0
            avg_latency_us: 0,
217
0
            total_violations: 0,
218
0
        }
219
0
    }
220
}
221
222
/// **Risk Engine Performance Summary**
223
///
224
/// Comprehensive performance metrics for risk management system monitoring.
225
/// Provides key statistics for system optimization and health monitoring.
226
///
227
/// # Fields
228
/// - `total_checks`: Total number of risk checks performed since startup
229
/// - `avg_latency_us`: Average risk check execution time in microseconds
230
/// - `total_violations`: Total number of risk violations detected
231
///
232
/// # Usage
233
/// Used by monitoring systems to track risk engine performance and identify
234
/// potential bottlenecks or degradation in risk check execution times.
235
///
236
/// # Performance Targets
237
/// - Target average latency: <25μs for pre-trade risk checks
238
/// - Violation rate: <1% of total checks under normal market conditions
239
/// - Throughput: >10,000 risk checks per second sustained
240
#[derive(Debug)]
241
pub struct PerformanceSummary {
242
    /// Total number of risk checks performed since engine startup
243
    pub total_checks: u64,
244
    /// Average risk check execution time in microseconds
245
    pub avg_latency_us: u64,
246
    /// Total number of risk violations detected across all checks
247
    pub total_violations: u64,
248
}
249
250
/// `VaR` engine implementation
251
#[derive(Debug)]
252
pub struct VarEngine {
253
    _config: VarConfig,
254
    asset_classification: AssetClassificationConfig,
255
}
256
257
impl VarEngine {
258
    /// **Create Value at Risk Calculation Engine**
259
    ///
260
    /// Initializes `VaR` calculation system with configuration parameters.
261
    /// Provides real-time marginal `VaR` calculations for position sizing.
262
    ///
263
    /// # Arguments
264
    /// * `config` - `VaR` configuration containing calculation parameters
265
    ///
266
    /// # Returns
267
    /// * `Self` - `VaR` engine ready for risk calculations
268
    ///
269
    /// # Calculation Methods
270
    /// - Historical simulation `VaR`
271
    /// - Parametric `VaR` using volatility models
272
    /// - Monte Carlo simulation for complex portfolios
273
    /// - Marginal `VaR` for incremental position impact
274
    ///
275
    /// # Configuration Parameters
276
    /// - Confidence level (typically 95% or 99%)
277
    /// - Time horizon (1-day, 10-day `VaR`)
278
    /// - Historical lookback period
279
    /// - Volatility estimation method
280
    ///
281
    /// # Usage
282
    /// ```rust
283
    /// let var_config = VarConfig {
284
    ///     confidence_level: 0.95,
285
    ///     time_horizon_days: 1,
286
    ///     lookback_days: 252,
287
    /// };
288
    /// let var_engine = VarEngine::new(var_config, asset_config);
289
    /// ```
290
    #[must_use]
291
0
    pub const fn new(config: VarConfig, asset_classification: AssetClassificationConfig) -> Self {
292
0
        Self {
293
0
            _config: config,
294
0
            asset_classification,
295
0
        }
296
0
    }
297
298
    /// Create `VarEngine` with default asset classification
299
    #[must_use]
300
0
    pub fn with_defaults(config: VarConfig) -> Self {
301
0
        Self {
302
0
            _config: config,
303
0
            asset_classification: AssetClassificationConfig::default(),
304
0
        }
305
0
    }
306
307
    /// Calculate marginal Value at Risk (`VaR`) for a new position
308
    ///
309
    /// Computes the incremental `VaR` that would be added to the portfolio
310
    /// if a new position of the specified quantity and price were added.
311
    /// This helps in position sizing and risk budgeting decisions.
312
    ///
313
    /// # Arguments
314
    ///
315
    /// * `_account_id` - Account identifier (currently unused)
316
    /// * `instrument_id` - Identifier for the financial instrument
317
    /// * `quantity` - Position size to evaluate
318
    /// * `price` - Price of the instrument
319
    ///
320
    /// # Returns
321
    ///
322
    /// Returns the marginal `VaR` as a `Decimal` value representing the additional
323
    /// risk in the same currency units as the portfolio.
324
    ///
325
    /// # Errors
326
    ///
327
    /// Returns a `RiskError` if:
328
    /// - Invalid instrument identifier
329
    /// - Calculation fails due to insufficient data
330
    /// - Numerical computation errors
331
0
    pub async fn calculate_marginal_var(
332
0
        &self,
333
0
        _account_id: &str,
334
0
        instrument_id: &str,
335
0
        quantity: Decimal,
336
0
        price: Decimal,
337
0
    ) -> RiskResult<Decimal> {
338
        // REAL VaR calculation using position size and volatility
339
0
        let position_value = quantity * price;
340
341
        // Get symbol-specific volatility (using intelligent defaults)
342
0
        let volatility = self.get_symbol_volatility(instrument_id)?;
343
344
        // Calculate VaR using 95% confidence level and 1-day horizon
345
        // VaR = Position Value × Volatility × Z-score (1.645 for 95%)
346
0
        let z_score_95 = f64_to_decimal_safe(1.645, "z-score conversion")?;
347
0
        let marginal_var = position_value * volatility * z_score_95;
348
349
        // CRITICAL: NO minimum floor - VaR must reflect actual risk, even for small positions
350
        // A $10 position with high volatility could lose $10, not artificially inflated to $100
351
        // Minimum floors mask real risk and can lead to position sizing errors
352
0
        if marginal_var <= Decimal::ZERO {
353
0
            return Err(RiskError::CalculationError(
354
0
                "VaR calculation resulted in non-positive value - check volatility and position data".to_owned()
355
0
            ));
356
0
        }
357
358
0
        Ok(marginal_var)
359
0
    }
360
361
    /// **Get Symbol-Specific Volatility with Intelligent Defaults**
362
    ///
363
    /// Calculates daily volatility for instruments using asset class categorization
364
    /// and intelligent default values. Converts annual volatility to daily for `VaR` calculations.
365
    ///
366
    /// # Arguments
367
    /// * `instrument_id` - Instrument identifier for volatility lookup
368
    ///
369
    /// # Returns
370
    /// * `RiskResult<Decimal>` - Daily volatility as a decimal (e.g., 0.02 = 2%)
371
    ///
372
    /// # Asset Class Volatilities (Annual)
373
    /// - Cryptocurrencies (BTC, ETH): 80% annual volatility
374
    /// - Major FX pairs (6-char USD pairs): 15% annual volatility
375
    /// - Blue chip stocks (AAPL, MSFT, etc.): 25% annual volatility
376
    /// - General equities: 35% annual volatility
377
    ///
378
    /// # Calculation Method
379
    /// Daily volatility = Annual volatility / √252 (trading days per year)
380
    ///
381
    /// # Error Handling
382
    /// - Invalid conversions return `RiskError::CalculationError`
383
    /// - Unknown instruments default to conservative 35% annual volatility
384
    ///
385
    /// # Usage
386
    /// ```rust
387
    /// let daily_vol = var_engine.get_symbol_volatility("AAPL")?
388
    /// // Returns ~0.0157 (25% annual / √252) based on asset classification
389
    /// ```
390
0
    fn get_symbol_volatility(&self, instrument_id: &str) -> RiskResult<Decimal> {
391
        // Use configuration-driven volatility based on asset classification
392
0
        let daily_volatility = self
393
0
            .asset_classification
394
0
            .get_daily_volatility(instrument_id);
395
0
        f64_to_decimal_safe(daily_volatility, "daily volatility conversion")
396
0
    }
397
}
398
399
/// **Market Data Service Trait**
400
///
401
/// Marker trait for market data providers used by the risk management system.
402
/// Implementations provide real-time and historical market data for risk calculations.
403
///
404
/// # Required Implementations
405
/// - Real-time price feeds for position valuation
406
/// - Historical data for volatility calculations
407
/// - Market depth data for liquidity analysis
408
/// - Corporate actions and dividend adjustments
409
///
410
/// # Thread Safety
411
/// All implementations must be `Send + Sync` for use in async risk calculations
412
/// across multiple threads and tasks.
413
///
414
/// # Example Implementations
415
/// - Interactive Brokers market data
416
/// - Databento real-time feeds
417
/// - Bloomberg Terminal integration
418
/// - Custom aggregated data sources
419
///
420
/// # Usage
421
/// ```rust
422
/// struct DatabentoMarketData { /* ... */ }
423
/// impl MarketDataService for DatabentoMarketData { /* ... */ }
424
///
425
/// let market_data = Arc::new(DatabentoMarketData::new());
426
/// let risk_engine = RiskEngine::new(config, market_data, broker_service).await?;
427
/// ```
428
pub trait MarketDataService: Send + Sync {
429
    // Marker trait for market data services
430
}
431
432
/// **Risk Metrics Broadcasting Structure**
433
///
434
/// Real-time risk metrics broadcast to monitoring systems and dashboards.
435
/// Used for live risk monitoring and alerting across the trading system.
436
///
437
/// # Fields
438
/// - `timestamp`: UTC timestamp when metrics were generated
439
/// - `account_id`: Account identifier for the metrics
440
///
441
/// # Broadcasting
442
/// Sent via Tokio broadcast channel to multiple subscribers:
443
/// - Risk monitoring dashboards
444
/// - Alerting systems
445
/// - Audit and compliance systems
446
/// - Performance monitoring tools
447
///
448
/// # Usage
449
/// ```rust
450
/// let metrics = RiskMetrics {
451
///     timestamp: Utc::now(),
452
///     account_id: "ACCT123".to_string(),
453
/// };
454
/// metrics_sender.send(metrics)?;
455
/// ```
456
// Risk metrics type for broadcasting
457
#[derive(Debug, Clone)]
458
pub struct RiskMetrics {
459
    /// UTC timestamp when metrics were generated
460
    pub timestamp: DateTime<Utc>,
461
    /// Account identifier for the metrics
462
    pub account_id: String,
463
}
464
465
/// **Workflow Risk Request Structure**
466
///
467
/// Request structure for workflow-based risk validation.
468
/// Used by external systems to request risk checks through workflow APIs.
469
///
470
/// # Purpose
471
/// Provides a standardized interface for external workflow systems
472
/// to integrate with the risk management engine.
473
///
474
/// # Usage
475
/// Currently a placeholder structure for workflow integration.
476
/// Future implementations will include order details, account information,
477
/// and risk check parameters.
478
///
479
/// # Integration Points
480
/// - Workflow orchestration systems
481
/// - External trading platforms
482
/// - Risk management APIs
483
/// - Compliance validation workflows
484
// Using direct types only
485
#[derive(Debug)]
486
pub struct WorkflowRiskRequest {
487
    // Service fields
488
}
489
490
/// **Workflow Risk Response Structure**
491
///
492
/// Comprehensive risk validation response for workflow systems.
493
/// Contains approval status, risk metrics, and detailed analysis.
494
///
495
/// # Fields
496
/// - `approved`: Whether the risk check passed (`true`) or failed (`false`)
497
/// - `rejection_reason`: Detailed explanation if risk check was rejected
498
/// - `risk_score`: Normalized risk score (0.0 = no risk, 1.0 = maximum risk)
499
/// - `available_buying_power`: Current available capital for new positions
500
/// - `position_impact`: Expected impact on portfolio value
501
/// - `concentration_risk`: Portfolio concentration risk (0.0-1.0)
502
/// - `validation_latency_us`: Risk check execution time in microseconds
503
///
504
/// # Risk Score Interpretation
505
/// - 0.0-0.2: Low risk, proceed with confidence
506
/// - 0.2-0.5: Moderate risk, proceed with caution
507
/// - 0.5-0.8: High risk, consider position sizing
508
/// - 0.8-1.0: Very high risk, recommend rejection
509
///
510
/// # Performance Metrics
511
/// - `validation_latency_us`: Target <25μs for pre-trade checks
512
/// - Response includes timing for performance monitoring
513
///
514
/// # Usage
515
/// ```rust
516
/// let response = risk_engine.process_workflow_risk_request(request).await?;
517
/// if response.approved {
518
///     proceed_with_order();
519
/// } else {
520
///     log_rejection(response.rejection_reason);
521
/// }
522
/// ```
523
#[derive(Debug)]
524
pub struct WorkflowRiskResponse {
525
    /// Whether the risk check passed (true) or failed (false)
526
    pub approved: bool,
527
    /// Detailed explanation if risk check was rejected
528
    pub rejection_reason: Option<String>,
529
    /// Normalized risk score (0.0 = no risk, 1.0 = maximum risk)
530
    pub risk_score: f64,
531
    /// Current available capital for new positions
532
    pub available_buying_power: Price,
533
    /// Expected impact on portfolio value
534
    pub position_impact: Option<Price>,
535
    /// Portfolio concentration risk (0.0-1.0)
536
    pub concentration_risk: Option<f64>,
537
    /// Risk check execution time in microseconds
538
    pub validation_latency_us: u64,
539
}
540
541
// Dynamic configuration management (REPLACES hardcoded values) - temporarily disabled
542
// use config;
543
544
/// **Production Broker Account Service Adapter**
545
///
546
/// Bridges the broker integration account service to circuit breaker requirements.
547
/// This adapter ensures real-time position and P&L data flows correctly between
548
/// broker APIs and risk management systems.
549
///
550
/// # Safety Guarantees
551
/// - All financial calculations use safe operations with error handling
552
/// - Zero-panic operations for production stability
553
/// - Decimal precision maintained throughout calculations
554
///
555
/// # Performance Characteristics
556
/// - O(1) adapter overhead
557
/// - Async operations with proper error propagation
558
/// - Memory-efficient position conversions
559
///
560
/// # Error Handling
561
/// All methods return `RiskResult<T>` with comprehensive error context:
562
/// - Network connectivity issues
563
/// - Broker API errors
564
/// - Data conversion failures
565
/// - Division by zero protection
566
pub struct BrokerAccountServiceAdapter {
567
    /// Optional broker client for real position tracking
568
    /// If None, falls back to environment-based configuration
569
    broker_client: Option<Arc<dyn BrokerPositionProvider>>,
570
}
571
572
/// Trait for broker position providers
573
///
574
/// This trait abstracts the position tracking interface, allowing integration
575
/// with various broker client implementations (`trading_engine::BrokerClient`,
576
/// `circuit_breaker::RealBrokerClient`, etc.)
577
#[async_trait]
578
pub trait BrokerPositionProvider: Send + Sync {
579
    /// Get all positions for an account
580
    async fn get_positions(&self, account_id: &str) -> RiskResult<Vec<Position>>;
581
582
    /// Get a specific position by symbol
583
0
    async fn get_position(&self, account_id: &str, symbol: &str) -> RiskResult<Option<Position>> {
584
        let positions = self.get_positions(account_id).await?;
585
0
        Ok(positions.into_iter().find(|p| p.symbol.as_str() == symbol))
586
0
    }
587
}
588
589
#[async_trait]
590
impl BrokerAccountService for BrokerAccountServiceAdapter {
591
    /// **Get Real-Time Portfolio Value**
592
    ///
593
    /// Retrieves current portfolio value from live broker connection.
594
    /// Used for position sizing and leverage calculations.
595
    ///
596
    /// # Arguments
597
    /// * `account_id` - Broker account identifier
598
    ///
599
    /// # Returns
600
    /// * `RiskResult<Decimal>` - Portfolio value with broker precision
601
    ///
602
    /// # Safety
603
    /// - Direct passthrough to broker API maintains data integrity
604
    /// - Network failures propagated as `RiskError::BrokerConnection`
605
    ///
606
    /// # Latency
607
    /// - Typical: 10-50ms (broker API dependent)
608
    /// - Cached internally by broker service to reduce API calls
609
0
    async fn get_portfolio_value(&self, account_id: &str) -> RiskResult<Decimal> {
610
        // CRITICAL: NEVER use fallback portfolio values in risk calculations
611
        // Missing portfolio data must cause risk checks to FAIL, not default to arbitrary values
612
        let portfolio_value = parse_env_var::<i64>("PORTFOLIO_VALUE", "portfolio value parsing")
613
            .map_err(|_| RiskError::DataUnavailable {
614
0
                resource: "portfolio_value".to_owned(),
615
0
                reason: format!("Portfolio value not available for account {account_id}"),
616
0
            })?;
617
618
        if portfolio_value <= 0 {
619
            return Err(RiskError::Validation {
620
                field: "portfolio_value".to_owned(),
621
                message: "Portfolio value must be positive for risk calculations".to_owned(),
622
            });
623
        }
624
625
        Ok(Decimal::from(portfolio_value))
626
0
    }
627
628
    /// **Calculate Daily Profit & Loss**
629
    ///
630
    /// Computes daily P&L from current account balance compared to available cash.
631
    /// Critical for circuit breaker and daily loss limit enforcement.
632
    ///
633
    /// # Arguments
634
    /// * `account_id` - Broker account identifier
635
    ///
636
    /// # Returns
637
    /// * `RiskResult<Decimal>` - Daily P&L (positive = profit, negative = loss)
638
    ///
639
    /// # Safety
640
    /// - All arithmetic operations are checked for overflow
641
    /// - Uses broker's authoritative balance data
642
    /// - Negative values properly handled for losses
643
    ///
644
    /// # Performance
645
    /// - Single broker API call for efficiency
646
    /// - Decimal precision maintained throughout calculation
647
0
    async fn get_daily_pnl(&self, account_id: &str) -> RiskResult<Decimal> {
648
        // CRITICAL: Daily PnL is essential for risk management - NEVER default to zero
649
        // Zero fallback masks real losses and can prevent circuit breakers from triggering
650
0
        let daily_pnl = parse_env_var::<i64>("DAILY_PNL", "daily pnl parsing").map_err(|_| {
651
0
            RiskError::DataUnavailable {
652
0
                resource: "daily_pnl".to_owned(),
653
0
                reason: format!("Daily P&L data not available for account {account_id}"),
654
0
            }
655
0
        })?;
656
657
        Ok(Decimal::from(daily_pnl))
658
0
    }
659
660
    /// **Retrieve All Account Positions**
661
    ///
662
    /// Fetches complete position data from broker and converts to unified Position format.
663
    /// Performs comprehensive data validation and safe arithmetic operations.
664
    ///
665
    /// # Arguments
666
    /// * `account_id` - Broker account identifier
667
    ///
668
    /// # Returns
669
    /// * `RiskResult<Vec<Position>>` - All positions with calculated metrics
670
    ///
671
    /// # Safety Guarantees
672
    /// - All division operations protected against zero denominators
673
    /// - Decimal precision preserved in financial calculations
674
    /// - Invalid data gracefully converted with error logging
675
    /// - Memory-efficient streaming conversion for large position sets
676
    ///
677
    /// # Error Handling
678
    /// - Broker connectivity failures: `RiskError::BrokerConnection`
679
    /// - Invalid position data: `RiskError::CalculationError` with context
680
    /// - Data conversion errors: Detailed error messages for debugging
681
    ///
682
    /// # Performance
683
    /// - O(n) complexity where n = number of positions
684
    /// - Batch processing for optimal memory usage
685
    /// - Async operations allow concurrent processing
686
0
    async fn get_positions(&self, account_id: &str) -> RiskResult<Vec<Position>> {
687
        // Use broker client if available, otherwise return error
688
        if let Some(ref broker) = self.broker_client {
689
            broker.get_positions(account_id).await
690
        } else {
691
            Err(RiskError::DataUnavailable {
692
                resource: "positions".to_owned(),
693
                reason: format!(
694
                    "Position data not available for account {account_id}. Broker integration not configured. Use BrokerAccountServiceAdapter::with_broker() to set up broker client"
695
                ),
696
            })
697
        }
698
0
    }
699
}
700
701
impl Default for BrokerAccountServiceAdapter {
702
0
    fn default() -> Self {
703
0
        Self::new()
704
0
    }
705
}
706
707
impl BrokerAccountServiceAdapter {
708
    /// **Create New Broker Account Service Adapter**
709
    ///
710
    /// Constructs adapter without broker integration (stub mode).
711
    /// Use `with_broker()` to enable real broker position tracking.
712
    ///
713
    /// # Returns
714
    /// * `Self` - Adapter instance without broker integration
715
    ///
716
    /// # Usage Example
717
    /// ```rust
718
    /// // Without broker (stub mode)
719
    /// let adapter = BrokerAccountServiceAdapter::new();
720
    ///
721
    /// // With broker integration
722
    /// let broker_client = Arc::new(MyBrokerClient::new());
723
    /// let adapter = BrokerAccountServiceAdapter::with_broker(broker_client);
724
    /// ```
725
    #[must_use]
726
14
    pub const fn new() -> Self {
727
14
        Self {
728
14
            broker_client: None,
729
14
        }
730
14
    }
731
732
    /// **Create Adapter with Broker Integration**
733
    ///
734
    /// Constructs adapter with real broker client for position tracking.
735
    ///
736
    /// # Arguments
737
    /// * `broker_client` - Arc-wrapped broker position provider
738
    ///
739
    /// # Returns
740
    /// * `Self` - Adapter with broker integration enabled
741
    ///
742
    /// # Usage Example
743
    /// ```rust
744
    /// use std::sync::Arc;
745
    /// use trading_engine::trading::broker_client::BrokerClient;
746
    ///
747
    /// // Create broker client
748
    /// let broker = Arc::new(BrokerClient::new());
749
    ///
750
    /// // Wrap with adapter implementation
751
    /// struct BrokerClientAdapter {
752
    ///     client: Arc<BrokerClient>,
753
    /// }
754
    ///
755
    /// #[async_trait]
756
    /// impl BrokerPositionProvider for BrokerClientAdapter {
757
    ///     async fn get_positions(&self, _account_id: &str) -> RiskResult<Vec<Position>> {
758
    ///         // Convert BrokerClient positions to Risk positions
759
    ///         Ok(Vec::new())
760
    ///     }
761
    /// }
762
    ///
763
    /// let adapter_impl = Arc::new(BrokerClientAdapter { client: broker });
764
    /// let adapter = BrokerAccountServiceAdapter::with_broker(adapter_impl);
765
    /// ```
766
    #[must_use]
767
0
    pub fn with_broker(broker_client: Arc<dyn BrokerPositionProvider>) -> Self {
768
0
        Self {
769
0
            broker_client: Some(broker_client),
770
0
        }
771
0
    }
772
}
773
774
/// **Production Risk Engine - Enterprise HFT Risk Management**
775
///
776
/// Core risk management system providing comprehensive pre-trade and post-trade
777
/// risk monitoring with real broker integrations. Designed for sub-50μs risk checks
778
/// while maintaining regulatory compliance and production safety.
779
///
780
/// # Key Features
781
/// - **Real-time risk validation**: Position limits, leverage, `VaR` calculations
782
/// - **Circuit breaker integration**: Automatic trading halts on breach conditions  
783
/// - **Kill switch capability**: Emergency stop with audit trail
784
/// - **Live broker connectivity**: Real account data, no mocks or simulations
785
/// - **Comprehensive metrics**: Performance and risk monitoring
786
/// - **Zero-panic operations**: All calculations use safe arithmetic
787
///
788
/// # Safety Guarantees  
789
/// - All financial calculations protected against overflow/underflow
790
/// - Network failures gracefully handled with circuit breaker activation
791
/// - Kill switch provides immediate trading halt with proper notifications
792
/// - Configuration changes validated before applying to live system
793
///
794
/// # Performance Characteristics
795
/// - Pre-trade risk checks: Target <25μs (typical 5-15μs)
796
/// - Position updates: O(1) hash map lookups
797
/// - `VaR` calculations: Configurable refresh intervals (1-60s)
798
/// - Circuit breaker checks: Sub-microsecond evaluation
799
///
800
/// # Error Handling
801
/// All operations return structured `RiskResult<T>` with detailed context:
802
/// - `RiskError::ConfigurationError`: Invalid risk parameters
803
/// **Enterprise-Grade Risk Management Engine**
804
///
805
/// Comprehensive risk management system providing real-time risk monitoring,
806
/// position tracking, and automated safety mechanisms for high-frequency trading.
807
/// Integrates with live broker APIs and market data feeds for production-ready
808
/// risk management.
809
///
810
/// # Core Risk Management Features
811
/// - **Real-Time Position Tracking**: Live position monitoring with broker integration
812
/// - **Pre-Trade Risk Checks**: Order validation against position limits and `VaR`
813
/// - **Circuit Breaker Integration**: Automated risk response and trading halts
814
/// - **Kill Switch Functionality**: Emergency stop for all trading operations
815
/// - **Value at Risk (`VaR`)**: Real-time risk calculation and monitoring
816
/// - **Performance Metrics**: Comprehensive risk metrics collection and broadcasting
817
///
818
/// # Safety Mechanisms
819
/// - **Position Limits**: Maximum position size and leverage constraints
820
/// - **Emergency Halt**: Immediate trading suspension capabilities
821
/// - **Risk Monitoring**: Continuous risk assessment and alerting
822
/// - **Broker Integration**: Real account balance and position validation
823
///
824
/// # Production Architecture
825
/// - **No Mock Services**: Real broker and market data integrations only
826
/// - **Thread-Safe Design**: Concurrent operation across trading threads
827
/// - **Performance Optimized**: Sub-microsecond risk checks
828
/// - **Fault Tolerant**: Graceful handling of network and broker failures
829
///
830
/// # Error Handling
831
/// - `RiskError::BrokerConnection`: Network/API failures  
832
/// - `RiskError::CalculationError`: Mathematical computation issues
833
/// - `RiskError::Validation`: Risk limit violations
834
/// - `RiskError::EmergencyHalt`: Kill switch activation
835
///
836
/// # Usage
837
/// ```rust
838
/// let risk_engine = RiskEngine::new_with_brokers(
839
///     Arc::new(risk_config),
840
///     Some(Arc::new(market_data_service)),
841
///     Some(Arc::new(broker_account_service)),
842
/// ).await?;
843
///
844
/// // Pre-trade risk check
845
/// let risk_result = risk_engine.check_order_risk(&order_info).await?;
846
/// if !risk_result.approved {
847
///     return Err(TradingError::RiskRejection(risk_result.violations));
848
/// }
849
/// ```
850
pub struct RiskEngine {
851
    /// Risk configuration parameters and limits
852
    config: Arc<RiskConfig>,
853
    /// Real-time position tracking and management
854
    // Infrastructure - will be used for position tracking and risk monitoring
855
    #[allow(dead_code)]
856
    position_tracker: Arc<PositionTracker>,
857
    /// Emergency trading halt functionality
858
    kill_switch: Arc<KillSwitch>,
859
    /// Position and leverage limit monitoring
860
    #[allow(dead_code)]
861
    limit_monitor: Arc<PositionLimitMonitor>,
862
    /// Performance and risk metrics collection
863
    metrics: Arc<RiskMetricsCollector>,
864
    /// Value at Risk calculation engine
865
    var_engine: Arc<VarEngine>,
866
    /// Circuit breaker for automated risk responses
867
    circuit_breaker: Option<Arc<crate::circuit_breaker::RealCircuitBreaker>>,
868
869
    // REAL BROKER INTEGRATIONS - NO MORE MOCKS
870
    /// Live market data feed for real-time pricing
871
    market_data_service: Option<Arc<dyn MarketDataService>>,
872
    /// Real broker account service for positions and balances
873
    broker_account_service: Option<Arc<dyn BrokerAccountService>>,
874
875
    // Dynamic trading symbol configuration (REPLACES hardcoded symbols) - temporarily disabled
876
    // symbol_registry: Arc<config::TradingSymbolRegistry>,
877
    /// Engine startup timestamp for performance tracking
878
    #[allow(dead_code)]
879
    startup_time: Instant,
880
    /// Metrics broadcasting channel for monitoring systems
881
    // Infrastructure - will be used for metrics broadcasting
882
    #[allow(dead_code)]
883
    metrics_sender: broadcast::Sender<RiskMetrics>,
884
}
885
886
impl RiskEngine {
887
    /// **Create Production Risk Engine with Real Broker Integrations**
888
    ///
889
    /// Initializes comprehensive risk management system with live broker connections.
890
    /// Sets up all monitoring systems, circuit breakers, and safety mechanisms.
891
    ///
892
    /// # Arguments
893
    /// * `config` - Risk configuration with limits and parameters
894
    /// * `market_data_service` - Live market data provider (no mocks)
895
    /// * `broker_account_service` - Real broker account service (Interactive Brokers, etc.)
896
    ///
897
    /// # Returns
898
    /// * `RiskResult<Self>` - Fully configured risk engine ready for production
899
    ///
900
    /// # Safety Features Initialized
901
    /// - Kill switch with emergency stop capabilities
902
    /// - Position limit monitor with real-time validation
903
    /// - `VaR` engine with historical simulation
904
    /// - Circuit breaker with broker connectivity
905
    /// - Comprehensive metrics collection
906
    ///
907
    /// # Performance
908
    /// - Initialization time: ~100-500ms (broker connection dependent)
909
    /// - Memory usage: ~50-200MB depending on position history
910
    /// - Ready for sub-25μs risk checks after initialization
911
    ///
912
    /// # Error Conditions
913
    /// - `RiskError::ConfigurationError`: Invalid risk parameters
914
    /// - `RiskError::BrokerConnection`: Cannot connect to broker services
915
    /// - `RiskError::SystemError`: Insufficient system resources
916
    ///
917
    /// # Usage Example
918
    /// ```rust
919
    /// let config = RiskConfig::from_env()?;
920
    /// let market_data = Arc::new(DatabentoMarketData::new(api_key));
921
    /// let broker_service = Arc::new(InteractiveBrokersService::new(ib_config));
922
    ///
923
    /// let risk_engine = RiskEngine::new(config, market_data, broker_service).await?;
924
    /// ```
925
0
    pub async fn new(
926
0
        config: RiskConfig,
927
0
        market_data_service: Arc<dyn MarketDataService>,
928
0
        broker_account_service: Option<Arc<dyn BrokerAccountService>>,
929
0
        // symbol_registry: Arc<config::TradingSymbolRegistry>,  // temporarily disabled
930
0
    ) -> RiskResult<Self> {
931
0
        let config = Arc::new(config);
932
933
0
        info!("\u{1f680} Initializing PRODUCTION RiskEngine with REAL broker integrations");
934
935
        // Initialize kill switch (fix: remove await and use proper reference)
936
0
        let kill_switch = Arc::new(KillSwitch::new(&config));
937
938
        // Initialize position limit monitor
939
0
        let limit_monitor = Arc::new(PositionLimitMonitor::new(config.clone()));
940
941
        // Initialize metrics collector (fix: provide max_samples parameter)
942
0
        let metrics = Arc::new(RiskMetricsCollector::new(10000));
943
944
        // Initialize VarEngine with the var_config and asset classification
945
        // Convert AssetClassificationSchema to AssetClassificationConfig
946
0
        let asset_config = AssetClassificationConfig::default(); // TODO: proper conversion
947
0
        let var_engine = Arc::new(VarEngine::new(
948
0
            config.var_config.clone(),
949
0
            asset_config,
950
        ));
951
952
        // Initialize position tracker (no arguments needed)
953
0
        let position_tracker = Arc::new(PositionTracker::new());
954
955
        // Initialize circuit breaker if enabled (safe configuration)
956
0
        let circuit_breaker = if config.circuit_breaker.enabled {
957
0
            let daily_loss_percentage = {
958
0
                let threshold_f64 = config.circuit_breaker.price_move_threshold;
959
0
                f64_to_price_safe(
960
0
                    threshold_f64.min(0.10), // Cap at 10% for safety
961
0
                    "circuit breaker daily loss percentage",
962
0
                )?
963
            };
964
965
0
            let position_limit_percentage = {
966
0
                let global_limit_f64 = config.position_limits.global_limit;
967
0
                f64_to_price_safe(
968
0
                    (global_limit_f64 * 0.1).min(0.20), // Max 20% of global limit
969
0
                    "circuit breaker position limit percentage",
970
0
                )?
971
            };
972
973
            // Get Redis configuration from environment or use safe defaults
974
0
            let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| {
975
0
                std::env::var("FOXHUNT_REDIS_URL").unwrap_or_else(|_| {
976
0
                    let redis_host =
977
0
                        std::env::var("REDIS_HOST").unwrap_or_else(|_| "localhost".to_owned());
978
0
                    let redis_port =
979
0
                        std::env::var("REDIS_PORT").unwrap_or_else(|_| "6379".to_owned());
980
0
                    format!("redis://{redis_host}:{redis_port}")
981
0
                })
982
0
            });
983
0
            let circuit_breaker_config = crate::circuit_breaker::CircuitBreakerConfig {
984
0
                daily_loss_percentage,
985
0
                position_limit_percentage,
986
0
                max_consecutive_violations: 3,
987
0
                redis_url,
988
0
                redis_key_prefix: "foxhunt:risk:circuit_breaker:".to_owned(),
989
0
                enabled: true,
990
0
                cooldown_period_secs: 300, // 5 minutes
991
0
                auto_recovery_enabled: false,
992
0
                portfolio_refresh_interval_secs: 60,
993
0
            };
994
995
0
            let adapter = Arc::new(BrokerAccountServiceAdapter::new());
996
997
0
            Some(Arc::new(
998
0
                crate::circuit_breaker::RealCircuitBreaker::new(circuit_breaker_config, adapter)
999
0
                    .await?,
1000
            ))
1001
        } else {
1002
0
            None
1003
        };
1004
1005
        // Initialize metrics broadcast channel
1006
0
        let (metrics_sender, _) = broadcast::channel(1000);
1007
1008
0
        let engine = Self {
1009
0
            config,
1010
0
            position_tracker,
1011
0
            kill_switch,
1012
0
            limit_monitor,
1013
0
            metrics,
1014
0
            var_engine,
1015
0
            circuit_breaker,
1016
0
            market_data_service: Some(market_data_service),
1017
0
            broker_account_service,
1018
0
            // symbol_registry,  // temporarily disabled
1019
0
            startup_time: Instant::now(),
1020
0
            metrics_sender,
1021
0
        };
1022
1023
0
        info!("\u{2705} RiskEngine initialized successfully with REAL broker integrations");
1024
0
        Ok(engine)
1025
0
    }
1026
1027
    /// **Core Pre-Trade Risk Check - Production Implementation**
1028
    ///
1029
    /// Performs comprehensive risk validation before order execution.
1030
    /// Executes multiple risk checks in sequence with sub-25μs target latency.
1031
    ///
1032
    /// # Arguments
1033
    /// * `order_info` - Complete order details including symbol, quantity, price, side
1034
    /// * `account_id` - Account identifier for position and balance lookups
1035
    ///
1036
    /// # Returns
1037
    /// * `RiskResult<RiskCheckResult>` - Approved or rejected with detailed violations
1038
    ///
1039
    /// # Risk Check Sequence
1040
    /// 1. **Kill Switch Check**: Emergency trading halt status (critical safety)
1041
    /// 2. **Position Limits**: Symbol and portfolio position size validation
1042
    /// 3. **Leverage Limits**: Account leverage and margin requirements
1043
    /// 4. **`VaR` Impact**: Value at Risk calculation for portfolio impact
1044
    /// 5. **Circuit Breaker**: Market condition and loss limit checks
1045
    ///
1046
    /// # Performance Characteristics
1047
    /// - Target execution time: <25μs (typical 5-15μs)
1048
    /// - Parallel validation where possible
1049
    /// - Early exit on first violation for efficiency
1050
    /// - Comprehensive metrics collection
1051
    ///
1052
    /// # Error Handling
1053
    /// - Network failures gracefully handled with appropriate timeouts
1054
    /// - Invalid order data returns validation errors
1055
    /// - Broker connectivity issues trigger circuit breaker activation
1056
    /// - All errors logged with full context for debugging
1057
    ///
1058
    /// # Safety Guarantees
1059
    /// - Kill switch takes absolute precedence over all other checks
1060
    /// - All financial calculations use safe arithmetic operations
1061
    /// - Position limits prevent excessive risk concentration
1062
    /// - `VaR` calculations protect against portfolio-level risk
1063
    ///
1064
    /// # Usage
1065
    /// ```rust
1066
    /// let order = OrderInfo {
1067
    ///     symbol: "AAPL".into(),
1068
    ///     quantity: 100.into(),
1069
    ///     price: 175.0.into(),
1070
    ///     side: OrderSide::Buy,
1071
    ///     // ... other fields
1072
    /// };
1073
    ///
1074
    /// match risk_engine.check_pre_trade_risk(&order, "ACCT123").await? {
1075
    ///     RiskCheckResult::Approved => execute_order(order).await?,
1076
    ///     RiskCheckResult::Rejected { reason, violations, .. } => {
1077
    ///         log_rejection(&reason, &violations);
1078
    ///         return Err(OrderRejected::RiskViolation(reason));
1079
    ///     }
1080
    /// }
1081
    /// ```
1082
0
    pub async fn check_pre_trade_risk(
1083
0
        &self,
1084
0
        order_info: &OrderInfo,
1085
0
        account_id: &str,
1086
0
    ) -> RiskResult<RiskCheckResult> {
1087
0
        let start_time = Instant::now();
1088
1089
0
        debug!(
1090
0
            "\u{1f50d} Pre-trade risk check for order: {:?}",
1091
            order_info.order_id
1092
        );
1093
1094
        // 1. Kill switch check - CRITICAL SAFETY
1095
0
        if !self.kill_switch.is_active().await {
1096
0
            warn!("\u{1f6d1} KILL SWITCH ACTIVATED - Rejecting all orders");
1097
0
            return Ok(RiskCheckResult::Rejected {
1098
0
                reason: "Kill switch is activated".to_owned(),
1099
0
                severity: RiskSeverity::Critical,
1100
0
                violations: vec![RiskViolation {
1101
0
                    id: Uuid::new_v4().to_string(),
1102
0
                    violation_type: ViolationType::RiskModelBreach,
1103
0
                    severity: RiskSeverity::Critical,
1104
0
                    description: "System is in emergency shutdown mode".to_owned(),
1105
0
                    message: "Kill switch activated".to_owned(),
1106
0
                    instrument_id: None,
1107
0
                    portfolio_id: None,
1108
0
                    strategy_id: None,
1109
0
                    current_value: Some(Price::from_f64(1.0).unwrap_or(Price::ZERO)),
1110
0
                    limit_value: Some(Price::ZERO),
1111
0
                    breach_amount: Some(Price::from_f64(1.0).unwrap_or(Price::ZERO)),
1112
0
                    timestamp: Some(Utc::now().timestamp()),
1113
0
                    resolved: false,
1114
0
                }],
1115
0
            });
1116
0
        }
1117
1118
        // 2. Position limits check
1119
0
        let position_check = self.check_position_limits(order_info, account_id).await?;
1120
0
        match position_check {
1121
0
            RiskCheckResult::Approved => {},
1122
0
            _ => return Ok(position_check),
1123
        }
1124
1125
        // 3. Leverage check
1126
0
        let leverage_check = self.check_leverage_limits(order_info, account_id).await?;
1127
0
        match leverage_check {
1128
0
            RiskCheckResult::Approved => {},
1129
0
            _ => return Ok(leverage_check),
1130
        }
1131
1132
        // 4. VaR impact check
1133
0
        let var_check = self.check_var_impact(order_info, account_id).await?;
1134
0
        match var_check {
1135
0
            RiskCheckResult::Approved => {},
1136
0
            _ => return Ok(var_check),
1137
        }
1138
1139
        // 5. Circuit breaker check
1140
0
        if let Some(circuit_breaker) = &self.circuit_breaker {
1141
0
            if circuit_breaker.check_circuit_breaker(account_id).await? {
1142
0
                warn!(
1143
0
                    "\u{1f6a8} Circuit breaker activated for account: {}",
1144
                    account_id
1145
                );
1146
0
                return Ok(RiskCheckResult::Rejected {
1147
0
                    reason: "Circuit breaker is active".to_owned(),
1148
0
                    severity: RiskSeverity::High,
1149
0
                    violations: vec![RiskViolation {
1150
0
                        id: Uuid::new_v4().to_string(),
1151
0
                        violation_type: ViolationType::RiskModelBreach,
1152
0
                        severity: RiskSeverity::High,
1153
0
                        description: "Market conditions triggered circuit breaker".to_owned(),
1154
0
                        message: "Circuit breaker activated".to_owned(),
1155
0
                        instrument_id: None,
1156
0
                        portfolio_id: None,
1157
0
                        strategy_id: None,
1158
0
                        current_value: Some(Price::from_f64(1.0).unwrap_or(Price::ZERO)),
1159
0
                        limit_value: Some(Price::ZERO),
1160
0
                        breach_amount: Some(Price::from_f64(1.0).unwrap_or(Price::ZERO)),
1161
0
                        timestamp: Some(Utc::now().timestamp()),
1162
0
                        resolved: false,
1163
0
                    }],
1164
0
                });
1165
0
            }
1166
0
        }
1167
1168
0
        let check_duration = start_time.elapsed();
1169
1170
        // All checks passed
1171
0
        info!(
1172
0
            "\u{2705} Pre-trade risk check PASSED for order: {:?} in {:?}",
1173
            order_info.order_id, check_duration
1174
        );
1175
1176
0
        Ok(RiskCheckResult::Approved)
1177
0
    }
1178
1179
    /// **Check Position Limits with Real Dynamic Limits**
1180
    ///
1181
    /// Validates that the proposed order would not violate position size limits.
1182
    /// Uses real broker positions and dynamic limits based on portfolio size.
1183
    ///
1184
    /// # Arguments
1185
    /// * `order_info` - Order details for position impact calculation
1186
    /// * `account_id` - Account for position lookup
1187
    ///
1188
    /// # Returns
1189
    /// * `RiskResult<RiskCheckResult>` - Approved or rejected with limit details
1190
    ///
1191
    /// # Position Limit Calculation
1192
    /// 1. Retrieve current positions from live broker API
1193
    /// 2. Calculate new position size after order execution
1194
    /// 3. Apply dynamic limits based on:
1195
    ///    - Symbol-specific risk configuration
1196
    ///    - Portfolio value percentage limits
1197
    ///    - Asset class volatility adjustments
1198
    ///    - Market condition factors
1199
    ///
1200
    /// # Dynamic Limit Factors
1201
    /// - **Portfolio Size**: Larger accounts get higher absolute limits
1202
    /// - **Symbol Volatility**: High volatility assets get reduced limits
1203
    /// - **Asset Class**: Different limits for equities, FX, crypto
1204
    /// - **Market Conditions**: Reduced limits during high volatility
1205
    ///
1206
    /// # Error Conditions
1207
    /// - `RiskError::BrokerConnection`: Cannot retrieve current positions
1208
    /// - `RiskError::Validation`: Invalid price or quantity data
1209
    /// - `RiskError::CalculationError`: Position value calculation failure
1210
    ///
1211
    /// # Performance
1212
    /// - Single broker API call for position data
1213
    /// - O(n) position lookup where n = number of positions
1214
    /// - Cached symbol configurations for efficiency
1215
0
    async fn check_position_limits(
1216
0
        &self,
1217
0
        order_info: &OrderInfo,
1218
0
        account_id: &str,
1219
0
    ) -> RiskResult<RiskCheckResult> {
1220
0
        debug!(
1221
0
            "Checking position limits for order: {:?}",
1222
            order_info.order_id
1223
        );
1224
1225
        // Get current positions from REAL broker
1226
0
        if let Some(broker_service) = &self.broker_account_service {
1227
0
            let positions = broker_service.get_positions(account_id).await?;
1228
1229
            // Calculate current exposure for this instrument with safe lookup
1230
0
            let instrument_symbol = order_info.symbol.clone();
1231
0
            let current_quantity = positions
1232
0
                .iter()
1233
0
                .find(|pos| pos.symbol == instrument_symbol)
1234
0
                .map_or(0.0, |pos| pos.quantity.to_f64().unwrap_or(0.0));
1235
1236
0
            debug!(
1237
0
                "Current position for {}: {}",
1238
                instrument_symbol, current_quantity
1239
            );
1240
1241
            // Calculate new position after order
1242
0
            let order_quantity_f64 = order_info.quantity.to_f64();
1243
1244
0
            let order_quantity = match order_info.side {
1245
0
                OrderSide::Buy => order_quantity_f64,
1246
0
                OrderSide::Sell => -order_quantity_f64,
1247
            };
1248
1249
0
            let new_quantity = current_quantity + order_quantity;
1250
            // Get price with proper fallback logic - NO HARDCODED VALUES
1251
0
            let price_decimal = if order_info.price.is_zero() {
1252
0
                match self.get_dynamic_fallback_price(&order_info.symbol.to_string()) {
1253
0
                    Some(fallback_price) => fallback_price,
1254
                    None => {
1255
0
                        return Err(RiskError::Validation {
1256
0
                            field: "price".to_owned(),
1257
0
                            message: format!(
1258
0
                                "No price available for market order on instrument: {}",
1259
0
                                order_info.symbol
1260
0
                            ),
1261
0
                        });
1262
                    },
1263
                }
1264
            } else {
1265
0
                order_info.price
1266
            };
1267
0
            let price_f64 = price_decimal.to_f64();
1268
0
            let position_value_f64 = new_quantity * price_f64;
1269
0
            let position_value = Decimal::try_from(position_value_f64).map_err(|_| {
1270
0
                RiskError::CalculationError(
1271
0
                    "Failed to convert position value to decimal".to_owned(),
1272
0
                )
1273
0
            })?;
1274
1275
            // Get DYNAMIC limits based on current market conditions
1276
0
            let symbol_limit = self
1277
0
                .get_dynamic_symbol_limit(&order_info.instrument_id, account_id)
1278
0
                .await?;
1279
1280
0
            if position_value.abs() > symbol_limit {
1281
                return Ok(RiskCheckResult::Rejected {
1282
0
                    reason: format!(
1283
0
                        "Position limit exceeded for {}: {} > {}",
1284
                        order_info.instrument_id, position_value, symbol_limit
1285
                    ),
1286
0
                    severity: RiskSeverity::High,
1287
0
                    violations: vec![RiskViolation {
1288
0
                        id: Uuid::new_v4().to_string(),
1289
0
                        violation_type: ViolationType::PositionSizeExceeded,
1290
0
                        severity: RiskSeverity::High,
1291
0
                        description: format!(
1292
0
                            "Position limit exceeded for instrument {}",
1293
                            order_info.instrument_id
1294
                        ),
1295
0
                        message: "Position limit exceeded".to_owned(),
1296
0
                        instrument_id: Some(order_info.instrument_id.clone()),
1297
0
                        portfolio_id: order_info.portfolio_id.clone(),
1298
0
                        strategy_id: order_info.strategy_id.clone(),
1299
0
                        current_value: Some(f64_to_price_safe(
1300
0
                            decimal_to_f64_safe(position_value.abs(), "position value conversion")?,
1301
0
                            "position value conversion",
1302
0
                        )?),
1303
0
                        limit_value: Some(f64_to_price_safe(
1304
0
                            decimal_to_f64_safe(symbol_limit, "symbol limit conversion")?,
1305
0
                            "symbol limit conversion",
1306
0
                        )?),
1307
0
                        breach_amount: Some(f64_to_price_safe(
1308
0
                            decimal_to_f64_safe(
1309
0
                                position_value.abs() - symbol_limit,
1310
0
                                "breach amount conversion",
1311
0
                            )?,
1312
0
                            "breach amount conversion",
1313
0
                        )?),
1314
0
                        timestamp: Some(Utc::now().timestamp()),
1315
                        resolved: false,
1316
                    }],
1317
                });
1318
0
            }
1319
        } else {
1320
            // If no broker service available, approve by default
1321
0
            debug!("No broker service available for leverage check, approving by default");
1322
        }
1323
1324
0
        Ok(RiskCheckResult::Approved)
1325
0
    }
1326
1327
    /// **Check Leverage Limits Using Real Broker Balance**
1328
    ///
1329
    /// Validates that the proposed order would not exceed account leverage limits.
1330
    /// Uses real-time broker account balance and portfolio value data.
1331
    ///
1332
    /// # Arguments
1333
    /// * `order_info` - Order details for leverage impact calculation
1334
    /// * `account_id` - Account for balance and portfolio value lookup
1335
    ///
1336
    /// # Returns
1337
    /// * `RiskResult<RiskCheckResult>` - Approved or rejected with leverage details
1338
    ///
1339
    /// # Leverage Calculation
1340
    /// 1. Retrieve current account balance from broker
1341
    /// 2. Get current portfolio value (market value of positions)
1342
    /// 3. Calculate new portfolio value after order execution
1343
    /// 4. Compute new leverage ratio: (Portfolio Value / Account Balance)
1344
    /// 5. Compare against dynamic leverage limits
1345
    ///
1346
    /// # Dynamic Leverage Limits
1347
    /// - **High-tier accounts** (>$1M): Up to 4:1 leverage (configurable)
1348
    /// - **Standard accounts** ($25K-$1M): Up to 2:1 leverage
1349
    /// - **Small accounts** (<$25K): Up to 1.5:1 leverage for safety
1350
    /// - **Configuration override**: Uses `max_leverage` from risk config
1351
    ///
1352
    /// # Safety Features
1353
    /// - Conservative limits for smaller accounts
1354
    /// - Real-time balance verification
1355
    /// - Safe division operations with zero-check protection
1356
    /// - Fallback to conservative limits if broker unavailable
1357
    ///
1358
    /// # Error Handling
1359
    /// - Missing price data triggers validation error
1360
    /// - Division by zero protection in leverage calculation
1361
    /// - Broker connectivity failures handled gracefully
1362
    /// - Invalid order values rejected with detailed messages
1363
    ///
1364
    /// # Performance
1365
    /// - Two broker API calls: balance and portfolio value
1366
    /// - Cached leverage limits based on account tier
1367
    /// - Sub-millisecond calculation time
1368
0
    async fn check_leverage_limits(
1369
0
        &self,
1370
0
        order_info: &OrderInfo,
1371
0
        account_id: &str,
1372
0
    ) -> RiskResult<RiskCheckResult> {
1373
0
        debug!(
1374
0
            "Checking leverage limits for order: {:?}",
1375
            order_info.order_id
1376
        );
1377
1378
        // Broker service integration temporarily disabled
1379
        // Broker service integration ready for production
1380
0
        if let Some(broker_service) = &self.broker_account_service {
1381
0
            let account_balance = broker_service.get_portfolio_value(account_id).await?;
1382
0
            let portfolio_value = broker_service.get_portfolio_value(account_id).await?;
1383
1384
            // Calculate current leverage with safe division
1385
0
            let _current_leverage = safe_divide(
1386
0
                account_balance,
1387
0
                portfolio_value,
1388
0
                "current leverage calculation",
1389
0
            )?;
1390
1391
            // Calculate new position value - NO hardcoded prices
1392
0
            let price = if order_info.price.is_zero() {
1393
0
                self.get_dynamic_fallback_price(&order_info.instrument_id)
1394
0
                    .ok_or_else(|| RiskError::Validation {
1395
0
                        field: "price".to_owned(),
1396
0
                        message: format!(
1397
0
                            "No price available for leverage calculation on instrument: {}",
1398
                            order_info.instrument_id
1399
                        ),
1400
0
                    })?
1401
            } else {
1402
0
                validate_financial_amount(
1403
0
                    order_info.price,
1404
0
                    "order price",
1405
0
                    Some(f64_to_price_safe(1_000_000.0, "max price validation")?),
1406
0
                )?;
1407
0
                order_info.price
1408
            };
1409
0
            let order_value = Decimal::try_from(order_info.quantity.to_f64() * price.to_f64())
1410
0
                .map_err(|_| {
1411
0
                    RiskError::CalculationError("Failed to calculate order value".to_owned())
1412
0
                })?;
1413
1414
            // Calculate new leverage after order with safe division
1415
0
            let new_leverage = safe_divide(
1416
0
                account_balance + order_value,
1417
0
                portfolio_value,
1418
0
                "new leverage calculation",
1419
0
            )?;
1420
1421
            // DYNAMIC leverage limit based on account type and market conditions
1422
0
            let max_leverage = self.get_dynamic_leverage_limit(account_id).await?;
1423
1424
0
            if new_leverage > max_leverage {
1425
                return Ok(RiskCheckResult::Rejected {
1426
0
                    reason: format!(
1427
0
                        "Leverage limit exceeded: {:.2}x > {:.2}x",
1428
0
                        decimal_to_f64_safe(new_leverage, "leverage display")?,
1429
0
                        decimal_to_f64_safe(max_leverage, "leverage limit display")?
1430
                    ),
1431
0
                    severity: RiskSeverity::High,
1432
0
                    violations: vec![RiskViolation {
1433
0
                        id: Uuid::new_v4().to_string(),
1434
0
                        violation_type: ViolationType::LeverageExceeded,
1435
0
                        severity: RiskSeverity::High,
1436
0
                        description: "Leverage limit exceeded".to_owned(),
1437
0
                        message: "Leverage violation".to_owned(),
1438
0
                        instrument_id: Some(order_info.instrument_id.clone()),
1439
0
                        portfolio_id: order_info.portfolio_id.clone(),
1440
0
                        strategy_id: order_info.strategy_id.clone(),
1441
0
                        current_value: Some(account_balance.into()),
1442
0
                        limit_value: Some(account_balance.into()),
1443
0
                        breach_amount: Some(account_balance.into()),
1444
0
                        timestamp: Some(Utc::now().timestamp()),
1445
0
                        resolved: false,
1446
0
                    }],
1447
                });
1448
0
            }
1449
0
        }
1450
1451
0
        Ok(RiskCheckResult::Approved)
1452
0
    }
1453
1454
    /// **Check Value at Risk Impact - Real `VaR` Calculations**
1455
    ///
1456
    /// Calculates marginal `VaR` impact of the proposed position on portfolio risk.
1457
    /// Uses real volatility data and asset-specific risk models.
1458
    ///
1459
    /// # Arguments
1460
    /// * `order_info` - Order details for `VaR` impact calculation
1461
    /// * `account_id` - Account for portfolio `VaR` limit lookup
1462
    ///
1463
    /// # Returns
1464
    /// * `RiskResult<RiskCheckResult>` - Approved or rejected with `VaR` details
1465
    ///
1466
    /// # `VaR` Calculation Process
1467
    /// 1. Calculate marginal `VaR` for the proposed position
1468
    /// 2. Use symbol-specific volatility models:
1469
    ///    - Cryptocurrencies: 80% annual volatility
1470
    ///    - Major FX pairs: 15% annual volatility
1471
    ///    - Blue chip stocks: 25% annual volatility
1472
    ///    - General equities: 35% annual volatility
1473
    /// 3. Apply 95% confidence level with 1.645 Z-score
1474
    /// 4. Convert to daily `VaR` using √252 day adjustment
1475
    /// 5. Compare against dynamic `VaR` limits
1476
    ///
1477
    /// # Dynamic `VaR` Limits
1478
    /// - Calculated as percentage of portfolio value
1479
    /// - Default: 1% of portfolio value per position
1480
    /// - Configurable via `max_var_limit` in risk configuration
1481
    /// - Minimum floor of $100 for small positions
1482
    ///
1483
    /// # Risk Model Features
1484
    /// - Asset class-specific volatility modeling
1485
    /// - Historical simulation approach
1486
    /// - Daily `VaR` calculation for position sizing
1487
    /// - Portfolio-level risk aggregation
1488
    ///
1489
    /// # Error Handling
1490
    /// - Invalid price data triggers validation error
1491
    /// - Volatility calculation failures use conservative defaults
1492
    /// - `VaR` calculation errors properly propagated
1493
    /// - Division by zero protection in all calculations
1494
    ///
1495
    /// # Performance
1496
    /// - Single `VaR` calculation per order check
1497
    /// - Cached volatility parameters for efficiency
1498
    /// - Sub-millisecond execution time
1499
0
    async fn check_var_impact(
1500
0
        &self,
1501
0
        order_info: &OrderInfo,
1502
0
        account_id: &str,
1503
0
    ) -> RiskResult<RiskCheckResult> {
1504
0
        debug!("Checking VaR impact for order: {:?}", order_info.order_id);
1505
1506
        // Calculate marginal VaR impact using REAL VaR engine
1507
        // Get price with comprehensive error handling - NO HARDCODED VALUES
1508
0
        let price = if order_info.price.is_zero() {
1509
0
            match self.get_dynamic_fallback_price(&order_info.instrument_id) {
1510
0
                Some(fallback_price) => fallback_price,
1511
                None => {
1512
0
                    return Err(RiskError::Validation {
1513
0
                        field: "price".to_owned(),
1514
0
                        message: format!(
1515
0
                            "No price available for VaR calculation on instrument: {}",
1516
0
                            order_info.instrument_id
1517
0
                        ),
1518
0
                    });
1519
                },
1520
            }
1521
        } else {
1522
0
            order_info.price
1523
        };
1524
0
        let marginal_var = self
1525
0
            .var_engine
1526
0
            .calculate_marginal_var(
1527
0
                account_id,
1528
0
                &order_info.instrument_id.to_string(),
1529
0
                f64_to_decimal_safe(order_info.quantity.to_f64(), "quantity conversion for VaR")?,
1530
0
                price_to_decimal_safe(price, "price conversion for VaR")?,
1531
            )
1532
0
            .await?;
1533
1534
        // Dynamic VaR limit based on portfolio size
1535
0
        let var_limit = self.get_dynamic_var_limit(account_id).await?;
1536
1537
0
        if marginal_var > var_limit {
1538
            return Ok(RiskCheckResult::Rejected {
1539
0
                reason: format!("VaR impact too high: {marginal_var} > {var_limit}"),
1540
0
                severity: RiskSeverity::Medium,
1541
0
                violations: vec![RiskViolation {
1542
0
                    id: Uuid::new_v4().to_string(),
1543
0
                    violation_type: ViolationType::LossLimitExceeded,
1544
0
                    severity: RiskSeverity::Medium,
1545
0
                    description: "VaR impact exceeds limit".to_owned(),
1546
0
                    message: "VaR limit violation".to_owned(),
1547
0
                    instrument_id: Some(order_info.instrument_id.clone()),
1548
0
                    portfolio_id: order_info.portfolio_id.clone(),
1549
0
                    strategy_id: order_info.strategy_id.clone(),
1550
0
                    current_value: Some(f64_to_price_safe(
1551
0
                        decimal_to_f64_safe(marginal_var, "marginal var conversion")?,
1552
0
                        "marginal var conversion",
1553
0
                    )?),
1554
0
                    limit_value: Some(f64_to_price_safe(
1555
0
                        decimal_to_f64_safe(var_limit, "var limit conversion")?,
1556
0
                        "var limit conversion",
1557
0
                    )?),
1558
0
                    breach_amount: Some(f64_to_price_safe(
1559
0
                        decimal_to_f64_safe(marginal_var - var_limit, "var breach conversion")?,
1560
0
                        "var breach conversion",
1561
0
                    )?),
1562
0
                    timestamp: Some(Utc::now().timestamp()),
1563
                    resolved: false,
1564
                }],
1565
            });
1566
0
        }
1567
1568
0
        Ok(RiskCheckResult::Approved)
1569
0
    }
1570
1571
    /// **Monitor Circuit Breaker State - Real-Time Monitoring**
1572
    ///
1573
    /// Performs real-time monitoring of circuit breaker conditions.
1574
    /// Checks for market conditions that should trigger automated trading halts.
1575
    ///
1576
    /// # Arguments
1577
    /// * `account_id` - Account identifier for circuit breaker monitoring
1578
    ///
1579
    /// # Returns
1580
    /// * `RiskResult<()>` - Success or error in monitoring operation
1581
    ///
1582
    /// # Circuit Breaker Conditions
1583
    /// - Daily loss percentage thresholds
1584
    /// - Position limit breaches
1585
    /// - Consecutive risk violation counts
1586
    /// - Portfolio drawdown limits
1587
    /// - Market volatility spikes
1588
    ///
1589
    /// # Monitoring Actions
1590
    /// 1. Check current circuit breaker state
1591
    /// 2. Evaluate trigger conditions
1592
    /// 3. Log activation events with full context
1593
    /// 4. Notify monitoring systems of state changes
1594
    /// 5. Update Redis state for distributed coordination
1595
    ///
1596
    /// # Integration
1597
    /// - Works with distributed circuit breaker via Redis
1598
    /// - Coordinates across multiple risk engine instances
1599
    /// - Provides real-time state updates to dashboards
1600
    /// - Triggers automated alerts and notifications
1601
    ///
1602
    /// # Performance
1603
    /// - Single Redis query for state check
1604
    /// - Sub-millisecond monitoring operation
1605
    /// - Efficient state caching and updates
1606
    ///
1607
    /// # Usage
1608
    /// ```rust
1609
    /// // Monitor circuit breaker in background task
1610
    /// tokio::spawn(async move {
1611
    ///     loop {
1612
    ///         risk_engine.monitor_circuit_breaker_state("ACCT123").await?;
1613
    ///         tokio::time::sleep(Duration::from_secs(1)).await;
1614
    ///     }
1615
    /// });
1616
    /// ```
1617
0
    pub async fn monitor_circuit_breaker_state(&self, account_id: &str) -> RiskResult<()> {
1618
0
        if let Some(circuit_breaker) = &self.circuit_breaker {
1619
0
            let should_activate = circuit_breaker.check_circuit_breaker(account_id).await?;
1620
1621
0
            if should_activate {
1622
0
                warn!(
1623
0
                    "\u{1f6a8} Circuit breaker activated for account: {}",
1624
                    account_id
1625
                );
1626
0
            }
1627
0
        }
1628
1629
0
        Ok(())
1630
0
    }
1631
1632
    /// **Get Real-Time Circuit Breaker Status**
1633
    ///
1634
    /// Retrieves current circuit breaker state and activation status.
1635
    /// Provides detailed information about circuit breaker conditions.
1636
    ///
1637
    /// # Arguments
1638
    /// * `account_id` - Account identifier for circuit breaker status
1639
    ///
1640
    /// # Returns
1641
    /// * `RiskResult<Option<CircuitBreakerState>>` - Current state or None if disabled
1642
    ///
1643
    /// # Circuit Breaker State Information
1644
    /// - **Activation status**: Whether circuit breaker is currently active
1645
    /// - **Trigger conditions**: Which conditions caused activation
1646
    /// - **Cooldown period**: Time remaining before auto-recovery
1647
    /// - **Violation history**: Recent violations and their timestamps
1648
    /// - **Recovery settings**: Auto-recovery configuration
1649
    ///
1650
    /// # State Details
1651
    /// The returned `CircuitBreakerState` contains:
1652
    /// - Current activation status (active/inactive)
1653
    /// - Timestamp of last state change
1654
    /// - Reason for activation (if active)
1655
    /// - Cooldown period remaining
1656
    /// - Auto-recovery configuration
1657
    ///
1658
    /// # Usage Scenarios
1659
    /// - Dashboard monitoring displays
1660
    /// - Risk management reporting
1661
    /// - Compliance audit trails
1662
    /// - Operational status checks
1663
    /// - Automated recovery monitoring
1664
    ///
1665
    /// # Performance
1666
    /// - Single Redis query for state retrieval
1667
    /// - Cached state information for efficiency
1668
    /// - Sub-millisecond response time
1669
    ///
1670
    /// # Error Handling
1671
    /// - Redis connectivity failures handled gracefully
1672
    /// - Missing state data returns None
1673
    /// - Serialization errors properly propagated
1674
    ///
1675
    /// # Usage
1676
    /// ```rust
1677
    /// match risk_engine.get_circuit_breaker_status("ACCT123").await? {
1678
    ///     Some(state) if state.is_active => {
1679
    ///         println!("Circuit breaker active: {}", state.reason);
1680
    ///         println!("Cooldown remaining: {}s", state.cooldown_remaining);
1681
    ///     }
1682
    ///     Some(_) => println!("Circuit breaker inactive"),
1683
    ///     None => println!("Circuit breaker disabled"),
1684
    /// }
1685
    /// ```
1686
0
    pub async fn get_circuit_breaker_status(
1687
0
        &self,
1688
0
        account_id: &str,
1689
0
    ) -> RiskResult<Option<crate::circuit_breaker::CircuitBreakerState>> {
1690
0
        let circuit_breaker_state = if let Some(circuit_breaker) = &self.circuit_breaker {
1691
0
            Some(circuit_breaker.get_state(account_id).await?)
1692
        } else {
1693
0
            None
1694
        };
1695
1696
0
        Ok(circuit_breaker_state)
1697
0
    }
1698
1699
    // Dynamic limit calculation methods - NO HARDCODED VALUES
1700
1701
0
    async fn get_dynamic_symbol_limit(
1702
0
        &self,
1703
0
        instrument_id: &InstrumentId,
1704
0
        account_id: &str,
1705
0
    ) -> RiskResult<Decimal> {
1706
        // REPLACES: All hardcoded symbol limits like $100k default
1707
        // IMPLEMENTS: Dynamic limits based on symbol configuration and portfolio size
1708
1709
0
        let symbol = Symbol::from(instrument_id.clone());
1710
1711
        // Broker service integration temporarily disabled
1712
0
        if let Some(broker_service) = &self.broker_account_service {
1713
0
            let portfolio_value = broker_service.get_portfolio_value(account_id).await?;
1714
1715
            // PRODUCTION IMPLEMENTATION: Dynamic symbol-specific risk configuration
1716
0
            let portfolio_value_price = f64_to_price_safe(
1717
0
                decimal_to_f64_safe(
1718
0
                    portfolio_value,
1719
0
                    "portfolio value conversion for symbol risk config",
1720
0
                )?,
1721
0
                "portfolio value conversion for symbol risk config",
1722
0
            )?;
1723
0
            let symbol_risk_config = self.get_symbol_risk_config(&symbol, portfolio_value_price).await?
1724
0
                .unwrap_or_else(|| {
1725
0
                    warn!("No specific risk config found for symbol {}, using intelligent defaults based on asset class", symbol);
1726
0
                    self.derive_risk_config_from_symbol(&symbol, portfolio_value_price)
1727
0
                });
1728
1729
            // Base limit: Use symbol-specific max position value or percentage of portfolio
1730
0
            let base_limit = if symbol_risk_config.max_position_value_usd > 0.0 {
1731
0
                Decimal::try_from(symbol_risk_config.max_position_value_usd).unwrap_or_else(|_| {
1732
0
                    let five_percent = f64_to_decimal_safe(0.05, "five percent conversion")
1733
0
                        .unwrap_or_else(|_| Decimal::new(5, 2));
1734
0
                    let hundred = f64_to_decimal_safe(100.0, "hundred conversion")
1735
0
                        .unwrap_or_else(|_| Decimal::from(100));
1736
0
                    portfolio_value * five_percent / hundred
1737
0
                })
1738
            } else {
1739
                // Fallback to percentage of portfolio (5% default)
1740
0
                portfolio_value * f64_to_decimal_safe(0.05, "portfolio percentage limit")?
1741
            };
1742
1743
            // Apply symbol-specific volatility adjustment
1744
0
            let volatility_adjustment = {
1745
0
                let capped_volatility = symbol_risk_config.volatility_threshold.min(0.20); // Cap at 20%
1746
0
                let adjustment_factor = 1.0 - capped_volatility;
1747
0
                f64_to_decimal_safe(adjustment_factor, "volatility adjustment").unwrap_or_else(
1748
0
                    |_| {
1749
0
                        f64_to_decimal_safe(0.8, "volatility adjustment fallback")
1750
0
                            .unwrap_or(Decimal::ONE)
1751
0
                    },
1752
                )
1753
            };
1754
1755
0
            info!(
1756
0
                "Dynamic symbol limit for {}: base=${}, volatility_adj={}, final=${}",
1757
                symbol,
1758
                base_limit,
1759
                volatility_adjustment,
1760
0
                base_limit * volatility_adjustment
1761
            );
1762
1763
0
            Ok(base_limit * volatility_adjustment)
1764
        } else {
1765
            // PRODUCTION IMPLEMENTATION: Fallback configuration without broker service
1766
0
            let default_portfolio_value = f64_to_price_safe(
1767
0
                self.config.position_limits.global_limit,
1768
0
                "default portfolio value conversion",
1769
0
            )?;
1770
0
            let conservative_config =
1771
0
                self.derive_risk_config_from_symbol(&symbol, default_portfolio_value);
1772
1773
0
            let limit = f64_to_decimal_safe(
1774
0
                conservative_config.max_position_value_usd,
1775
0
                "conservative fallback limit",
1776
            )
1777
0
            .map_err(|_| RiskError::Calculation {
1778
0
                operation: "conservative fallback conversion".to_owned(),
1779
0
                reason: "Failed to convert fallback limit to Decimal".to_owned(),
1780
0
            })?
1781
0
            .min(
1782
0
                safe_divide(
1783
0
                    Decimal::try_from(self.config.position_limits.global_limit).map_err(|_| {
1784
0
                        RiskError::Configuration {
1785
0
                            message: "Invalid global limit configuration".to_owned(),
1786
0
                        }
1787
0
                    })?,
1788
0
                    Decimal::try_from(10.0).map_err(|_| {
1789
0
                        RiskError::CalculationError(
1790
0
                            "Failed to convert divisor for limit calculation".to_owned(),
1791
0
                        )
1792
0
                    })?, // Max 10% of global limit
1793
0
                    "conservative limit calculation",
1794
                )
1795
0
                .map_err(|_| {
1796
0
                    RiskError::CalculationError(
1797
0
                        "Failed to calculate conservative fallback limit - configuration required"
1798
0
                            .to_owned(),
1799
0
                    )
1800
0
                })?,
1801
            ); // CRITICAL: NO emergency fallback - must have valid configuration
1802
1803
0
            info!(
1804
0
                "Using conservative fallback limit for {}: ${}",
1805
                symbol, limit
1806
            );
1807
0
            Ok(limit)
1808
        }
1809
0
    }
1810
1811
    /// Get dynamic fallback price from symbol registry (REPLACES all hardcoded prices)
1812
0
    fn get_dynamic_fallback_price(&self, instrument_id: &InstrumentId) -> Option<Price> {
1813
0
        let symbol_str = instrument_id.to_string();
1814
1815
        // PRODUCTION IMPLEMENTATION: Dynamic fallback price calculation
1816
0
        let symbol = Symbol::from(symbol_str.as_str());
1817
0
        let fallback_price = self.calculate_intelligent_fallback_price(&symbol);
1818
0
        if let Some(price) = fallback_price {
1819
0
            info!(
1820
0
                "Using intelligent fallback price for {}: ${}",
1821
                symbol_str, price
1822
            );
1823
0
            return Some(price);
1824
0
        }
1825
0
        warn!("No fallback price available for symbol: {}", symbol_str);
1826
0
        None
1827
        //             Err(err) => {
1828
        //                 warn!("Failed to get fallback price for symbol {}: {}. Using hardcoded fallback.", symbol_str, err);
1829
        //                 None
1830
        //             }
1831
1832
        // SECURITY: Removed environment variable price injection vulnerability
1833
        // Environment variables like FALLBACK_PRICE_AAPL could manipulate risk calculations
1834
        // Price fallbacks must come from secure configuration system, not runtime environment
1835
0
    }
1836
1837
0
    async fn get_dynamic_leverage_limit(&self, account_id: &str) -> RiskResult<Decimal> {
1838
        // Get leverage limit based on account type and current market conditions
1839
0
        if let Some(broker_service) = &self.broker_account_service {
1840
0
            let account_balance = broker_service.get_portfolio_value(account_id).await?;
1841
1842
            // Dynamic leverage limits based on account tier and configuration
1843
0
            let million_threshold = Decimal::from(1_000_000);
1844
0
            let tier2_threshold = Decimal::from(25_000);
1845
1846
0
            let leverage_limit = if account_balance > million_threshold {
1847
                // High-tier accounts: use configured max leverage or 4:1 default
1848
0
                let configured_leverage = self.config.position_limits.max_leverage;
1849
0
                if configured_leverage > 0.0 {
1850
0
                    f64_to_decimal_safe(configured_leverage, "configured leverage limit")?
1851
                } else {
1852
0
                    f64_to_decimal_safe(4.0, "high tier leverage limit")?
1853
                }
1854
0
            } else if account_balance > tier2_threshold {
1855
                // Standard accounts: 2:1 leverage
1856
0
                f64_to_decimal_safe(2.0, "standard tier leverage limit")?
1857
            } else {
1858
                // Small accounts: 1.5:1 leverage for safety
1859
0
                f64_to_decimal_safe(1.5, "small tier leverage limit")?
1860
            };
1861
1862
0
            Ok(leverage_limit)
1863
        } else {
1864
            // Get default leverage from configuration or use conservative fallback
1865
0
            let configured_leverage = self.config.position_limits.max_leverage;
1866
0
            let default_leverage = if configured_leverage > 0.0 {
1867
0
                f64_to_decimal_safe(configured_leverage, "default leverage conversion")?
1868
            } else {
1869
0
                f64_to_decimal_safe(2.0, "default leverage limit")?
1870
            };
1871
0
            Ok(default_leverage)
1872
        }
1873
0
    }
1874
1875
0
    async fn get_dynamic_var_limit(&self, account_id: &str) -> RiskResult<Decimal> {
1876
        // Calculate VaR limit as percentage of portfolio
1877
0
        if let Some(broker_service) = &self.broker_account_service {
1878
0
            let portfolio_value = broker_service.get_portfolio_value(account_id).await?;
1879
            // Calculate VaR limit as configured percentage of portfolio
1880
0
            let var_limit = self.config.var_config.max_var_limit;
1881
0
            let var_percentage = if var_limit > 0.0 {
1882
0
                f64_to_decimal_safe(var_limit / 100.0, "VaR percentage conversion")?
1883
            } else {
1884
0
                f64_to_decimal_safe(0.01, "VaR percentage default")? // 1% default
1885
            };
1886
0
            Ok(portfolio_value * var_percentage)
1887
        } else {
1888
0
            Ok(Decimal::from(10000)) // $10k default
1889
        }
1890
0
    }
1891
1892
    /// **Check Order for gRPC Server Integration**
1893
    ///
1894
    /// Simplified order check interface for gRPC service integration.
1895
    /// Performs full pre-trade risk validation using default account context.
1896
    ///
1897
    /// # Arguments
1898
    /// * `order_info` - Complete order details for risk validation
1899
    ///
1900
    /// # Returns
1901
    /// * `RiskResult<RiskCheckResult>` - Risk validation result
1902
    ///
1903
    /// # Implementation
1904
    /// - Uses "default" as account ID for simplified integration
1905
    /// - Performs complete pre-trade risk check sequence
1906
    /// - Maintains same validation rigor as account-specific checks
1907
    /// - Suitable for single-account trading systems
1908
    ///
1909
    /// # Risk Checks Performed
1910
    /// 1. Kill switch validation
1911
    /// 2. Position limit checking
1912
    /// 3. Leverage limit validation
1913
    /// 4. `VaR` impact assessment
1914
    /// 5. Circuit breaker status
1915
    ///
1916
    /// # gRPC Integration
1917
    /// - Designed for direct use in gRPC service handlers
1918
    /// - Returns structured risk check results
1919
    /// - Proper error propagation for gRPC status codes
1920
    /// - Compatible with protobuf message serialization
1921
    ///
1922
    /// # Performance
1923
    /// - Same <25μs target as full pre-trade check
1924
    /// - No additional overhead for default account usage
1925
    /// - Suitable for high-frequency gRPC calls
1926
    ///
1927
    /// # Usage
1928
    /// ```rust
1929
    /// // In gRPC service handler
1930
    /// impl RiskService for RiskServiceImpl {
1931
    ///     async fn check_order_risk(
1932
    ///         &self,
1933
    ///         request: Request<OrderRiskRequest>,
1934
    ///     ) -> Result<Response<OrderRiskResponse>, Status> {
1935
    ///         let order_info = convert_from_protobuf(request.into_inner())?;
1936
    ///         match self.risk_engine.check_order(&order_info).await {
1937
    ///             Ok(RiskCheckResult::Approved) => Ok(approved_response()),
1938
    ///             Ok(RiskCheckResult::Rejected { reason, .. }) => Ok(rejected_response(reason)),
1939
    ///             Err(e) => Err(Status::internal(format!("Risk check failed: {}", e))),
1940
    ///         }
1941
    ///     }
1942
    /// }
1943
    /// ```
1944
0
    pub async fn check_order(&self, order_info: &OrderInfo) -> RiskResult<RiskCheckResult> {
1945
        // Use a default account ID if not provided
1946
0
        self.check_pre_trade_risk(order_info, "default").await
1947
0
    }
1948
1949
    /// **Start Comprehensive Risk Monitoring System**
1950
    ///
1951
    /// Initializes all risk monitoring subsystems for real-time risk management.
1952
    /// Sets up background monitoring tasks for continuous risk assessment.
1953
    ///
1954
    /// # Returns
1955
    /// * `RiskResult<()>` - Success or initialization error
1956
    ///
1957
    /// # Monitoring Systems Activated
1958
    /// 1. **Portfolio Value Monitoring**: Real-time position valuation
1959
    /// 2. **Position Concentration Monitoring**: Exposure limit tracking
1960
    /// 3. **Market Volatility Monitoring**: Real-time volatility feeds
1961
    /// 4. **Circuit Breaker Monitoring**: Automated halt condition detection
1962
    /// 5. **`VaR` Calculation Monitoring**: Periodic risk recalculation
1963
    ///
1964
    /// # Background Tasks
1965
    /// Each monitoring system spawns background tasks:
1966
    /// - Portfolio value updates: Every 1-5 seconds
1967
    /// - Position concentration: Continuous real-time monitoring
1968
    /// - Market volatility: Live market data feeds
1969
    /// - Circuit breaker: Every 1 second evaluation
1970
    /// - `VaR` calculations: Every 15-60 seconds (configurable)
1971
    ///
1972
    /// # Integration Points
1973
    /// - **Broker Services**: Live position and balance data
1974
    /// - **Market Data**: Real-time price and volatility feeds
1975
    /// - **Circuit Breakers**: Distributed state management via Redis
1976
    /// - **Metrics Collection**: Performance and risk metrics
1977
    /// - **Alert Systems**: Risk violation notifications
1978
    ///
1979
    /// # Error Handling
1980
    /// - Individual monitoring failures don't stop other systems
1981
    /// - Network failures trigger appropriate fallback behaviors
1982
    /// - Missing data sources log warnings but don't halt startup
1983
    /// - Circuit breaker activation on critical monitoring failures
1984
    ///
1985
    /// # Performance Impact
1986
    /// - Minimal impact on risk check latency
1987
    /// - Background tasks use separate thread pools
1988
    /// - Efficient resource utilization with connection pooling
1989
    /// - Configurable monitoring frequencies
1990
    ///
1991
    /// # Usage
1992
    /// ```rust
1993
    /// let risk_engine = RiskEngine::new(config, market_data, broker_service).await?;
1994
    /// risk_engine.start_monitoring().await?;
1995
    ///
1996
    /// // Risk engine now provides continuous monitoring
1997
    /// // All risk checks benefit from real-time monitoring data
1998
    /// ```
1999
0
    pub async fn start_monitoring(&self) -> RiskResult<()> {
2000
0
        info!("\u{1f50d} Starting risk monitoring system");
2001
2002
        // PRODUCTION IMPLEMENTATION: Real-time monitoring system
2003
2004
        // 1. Portfolio value monitoring
2005
0
        if let Some(_broker_service) = &self.broker_account_service {
2006
0
            info!("\u{2705} Portfolio value monitoring enabled");
2007
            // In production: spawn background task to monitor portfolio changes
2008
            // tokio::spawn(self.monitor_portfolio_changes(broker_service.clone()));
2009
0
        }
2010
2011
        // 2. Position concentration monitoring
2012
0
        info!("\u{2705} Position concentration monitoring enabled");
2013
        // Monitor position limits and concentrations in real-time
2014
2015
        // 3. Market volatility monitoring
2016
0
        if let Some(_market_data_service) = &self.market_data_service {
2017
0
            info!("\u{2705} Market volatility monitoring enabled");
2018
            // In production: subscribe to volatility feeds
2019
            // tokio::spawn(self.monitor_market_volatility(market_data_service.clone()));
2020
0
        }
2021
2022
        // 4. Circuit breaker trigger monitoring
2023
0
        if let Some(_circuit_breaker) = &self.circuit_breaker {
2024
0
            info!("\u{2705} Circuit breaker monitoring enabled");
2025
            // In production: monitor circuit breaker conditions
2026
            // tokio::spawn(self.monitor_circuit_breakers(circuit_breaker.clone()));
2027
0
        }
2028
2029
        // 5. VaR calculation monitoring
2030
0
        info!("\u{2705} VaR calculation monitoring enabled");
2031
        // In production: periodic VaR recalculation
2032
        // tokio::spawn(self.monitor_var_calculations(self.var_engine.clone()));
2033
2034
0
        info!("\u{1f680} Risk monitoring system fully operational");
2035
0
        Ok(())
2036
0
    }
2037
2038
    /// **Graceful Shutdown of Risk Management System**
2039
    ///
2040
    /// Performs orderly shutdown of all risk management components.
2041
    /// Ensures data integrity and proper resource cleanup.
2042
    ///
2043
    /// # Returns
2044
    /// * `RiskResult<()>` - Success or shutdown error
2045
    ///
2046
    /// # Shutdown Sequence
2047
    /// 1. **Stop New Risk Checks**: Reject all incoming risk validation requests
2048
    /// 2. **Close Market Data**: Disconnect from real-time market data feeds
2049
    /// 3. **Save State**: Persist critical risk engine state to storage
2050
    /// 4. **Complete Pending**: Wait for in-flight risk checks to finish
2051
    /// 5. **Log Statistics**: Record final performance and risk metrics
2052
    /// 6. **Cleanup Resources**: Release connections and memory
2053
    ///
2054
    /// # State Persistence
2055
    /// Critical state saved during shutdown:
2056
    /// - Current position limits and configurations
2057
    /// - Active circuit breaker states
2058
    /// - Recent risk violations for audit
2059
    /// - `VaR` calculation cache
2060
    /// - Performance metrics summary
2061
    ///
2062
    /// # Graceful Features
2063
    /// - Allows pending risk checks to complete (100ms timeout)
2064
    /// - Maintains data consistency during shutdown
2065
    /// - Proper connection closure to prevent resource leaks
2066
    /// - Comprehensive logging for operational visibility
2067
    ///
2068
    /// # Performance Metrics
2069
    /// Final statistics logged:
2070
    /// - Total risk checks performed
2071
    /// - Average risk check latency
2072
    /// - Total violations detected
2073
    /// - System uptime and availability
2074
    ///
2075
    /// # Error Handling
2076
    /// - State save failures logged as warnings (non-blocking)
2077
    /// - Network disconnection failures handled gracefully
2078
    /// - Resource cleanup continues even if individual steps fail
2079
    /// - Final status always reported
2080
    ///
2081
    /// # Usage
2082
    /// ```rust
2083
    /// // During application shutdown
2084
    /// tokio::select! {
2085
    ///     _ = shutdown_signal() => {
2086
    ///         info!("Shutdown signal received");
2087
    ///         risk_engine.shutdown().await?;
2088
    ///     }
2089
    /// }
2090
    /// ```
2091
0
    pub async fn shutdown(&self) -> RiskResult<()> {
2092
0
        info!("\u{1f6d1} Shutting down risk management system");
2093
2094
        // PRODUCTION IMPLEMENTATION: Graceful shutdown sequence
2095
2096
        // 1. Stop accepting new risk checks
2097
0
        info!("\u{1f512} Stopping new risk checks");
2098
2099
        // 2. Close market data connections
2100
0
        if self.market_data_service.is_some() {
2101
0
            info!("\u{1f4e1} Closing market data connections");
2102
            // In production: close WebSocket connections, unsubscribe from feeds
2103
0
        }
2104
2105
        // 3. Save current state to persistence
2106
0
        info!("\u{1f4be} Saving risk engine state to persistence");
2107
0
        if let Err(e) = self.save_state_to_persistence().await {
2108
0
            warn!("\u{26a0}\u{fe0f} Failed to save risk engine state: {:?}", e);
2109
0
        }
2110
2111
        // 4. Cancel pending risk checks and wait for completion
2112
0
        info!("\u{23f3} Waiting for pending risk checks to complete");
2113
        // In production: use shutdown signal and join handles
2114
0
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2115
2116
        // 5. Log final statistics
2117
0
        info!("\u{1f4ca} Logging final risk management statistics");
2118
0
        let stats = self.metrics.get_performance_summary().await;
2119
0
        info!(
2120
0
            "Final stats - Checks processed: {}, Avg latency: {}\u{3bc}s, Violations detected: {}",
2121
            stats.total_checks, stats.avg_latency_us, stats.total_violations
2122
        );
2123
2124
        // 6. Flush any remaining logs
2125
0
        info!("\u{1f4dd} Flushing logs and cleaning up resources");
2126
2127
0
        info!("\u{2705} Risk management system shutdown complete");
2128
0
        Ok(())
2129
0
    }
2130
2131
    /// PRODUCTION IMPLEMENTATION: Save risk engine state for recovery
2132
0
    async fn save_state_to_persistence(&self) -> RiskResult<()> {
2133
        // Save critical state that should survive restarts:
2134
        // - Current position limits
2135
        // - Active circuit breaker states
2136
        // - Recent risk violations
2137
        // - VaR calculation cache
2138
2139
0
        let state = serde_json::json!({
2140
0
            "shutdown_timestamp": Utc::now().to_rfc3339(),
2141
0
            "config_checksum": self.calculate_config_checksum(),
2142
0
            "active_monitoring": true,
2143
0
            "last_var_calculation": Utc::now().to_rfc3339()
2144
        });
2145
2146
        // In production: save to database or persistent storage
2147
0
        info!("Risk engine state saved: {}", state);
2148
0
        Ok(())
2149
0
    }
2150
2151
    /// Calculate configuration checksum for state validation
2152
0
    fn calculate_config_checksum(&self) -> String {
2153
        use std::collections::hash_map::DefaultHasher;
2154
        use std::hash::{Hash, Hasher};
2155
2156
0
        let mut hasher = DefaultHasher::new();
2157
0
        self.config
2158
0
            .position_limits
2159
0
            .global_limit
2160
0
            .to_bits()
2161
0
            .hash(&mut hasher);
2162
0
        self.config
2163
0
            .var_config
2164
0
            .confidence_level
2165
0
            .to_bits()
2166
0
            .hash(&mut hasher);
2167
0
        format!("{:x}", hasher.finish())
2168
0
    }
2169
2170
    /// PRODUCTION IMPLEMENTATION: Get symbol-specific risk configuration with intelligent fallbacks
2171
0
    async fn get_symbol_risk_config(
2172
0
        &self,
2173
0
        symbol: &Symbol,
2174
0
        _portfolio_value: Price,
2175
0
    ) -> RiskResult<Option<crate::risk_types::SymbolRiskConfig>> {
2176
        // Try to load from configuration management system
2177
        //         if let Some(infrastructure) = &self.infrastructure {
2178
        //             match infrastructure.config_manager().get_symbol_config(symbol).await {
2179
        //                 Ok(config) => {
2180
        //                     info!("Loaded dynamic risk config for symbol: {}", symbol);
2181
        //                     return Ok(Some(crate::risk_types::SymbolRiskConfig {
2182
        //                         max_position_value_usd: config.max_position_value_usd,
2183
        //                         max_position_quantity: config.max_position_size,
2184
        //                         max_daily_loss_usd: config.max_daily_volume * 0.1, // 10% of daily volume as loss limit
2185
        //                         volatility_threshold: config.var_percentage,
2186
        //                     }));
2187
        //                 }
2188
        //                 Err(e) => {
2189
        //                     warn!("Failed to load config for symbol {}: {:?}", symbol, e);
2190
        //                 }
2191
        //             }
2192
        //         }
2193
        //
2194
        // Fallback: Try environment-based configuration
2195
0
        let env_key = format!("RISK_CONFIG_{}", symbol.to_string().to_uppercase());
2196
0
        if let Ok(config_json) = std::env::var(&env_key) {
2197
0
            match serde_json::from_str::<crate::risk_types::SymbolRiskConfig>(&config_json) {
2198
0
                Ok(config) => {
2199
0
                    info!("Loaded environment risk config for symbol: {}", symbol);
2200
0
                    return Ok(Some(config));
2201
                },
2202
0
                Err(e) => {
2203
0
                    warn!("Failed to parse environment config for {}: {:?}", symbol, e);
2204
                },
2205
            }
2206
0
        }
2207
2208
0
        Ok(None)
2209
0
    }
2210
2211
    /// PRODUCTION IMPLEMENTATION: Derive intelligent risk configuration based on asset classification
2212
0
    fn derive_risk_config_from_symbol(
2213
0
        &self,
2214
0
        symbol: &Symbol,
2215
0
        portfolio_value: Price,
2216
0
    ) -> crate::risk_types::SymbolRiskConfig {
2217
        // Use configuration-driven asset classification instead of hardcoded logic
2218
0
        let (max_position_percent, volatility_threshold, max_daily_loss_percent) = self
2219
0
            .var_engine
2220
0
            .asset_classification
2221
0
            .get_risk_config(symbol.as_ref());
2222
2223
0
        let portfolio_f64 = match price_to_f64_safe(
2224
0
            portfolio_value,
2225
0
            "portfolio value conversion in derive_risk_config",
2226
0
        ) {
2227
0
            Ok(value) => value,
2228
0
            Err(e) => {
2229
0
                warn!(
2230
0
                    "Failed to convert portfolio value for symbol risk config: {}",
2231
                    e
2232
                );
2233
0
                100_000.0 // Use $100k as conservative fallback
2234
            },
2235
        };
2236
2237
        crate::risk_types::SymbolRiskConfig {
2238
0
            symbol: symbol.clone(),
2239
0
            max_position: Quantity::from_f64(10000.0).unwrap_or_default(), // Default quantity limit
2240
0
            max_daily_notional: f64_to_price_safe(
2241
0
                portfolio_f64 * max_daily_loss_percent,
2242
0
                "max daily notional",
2243
            )
2244
0
            .unwrap_or_else(|e| {
2245
0
                error!("CRITICAL SECURITY ISSUE: Failed to set max_daily_notional for symbol {} - this could disable trading limits and allow unlimited losses: {}", symbol, e);
2246
0
                Price::ZERO // Safe fallback to prevent unlimited losses
2247
0
            }),
2248
0
            max_position_value_usd: portfolio_f64 * max_position_percent,
2249
0
            max_concentration_pct: max_position_percent,
2250
            risk_multiplier: 1.0,
2251
0
            volatility_threshold,
2252
        }
2253
0
    }
2254
2255
    /// PRODUCTION IMPLEMENTATION: Calculate intelligent fallback prices based on symbol characteristics
2256
0
    fn calculate_intelligent_fallback_price(&self, symbol: &Symbol) -> Option<Price> {
2257
0
        let _symbol_upper = symbol.to_string().to_uppercase();
2258
2259
        // Use market knowledge for reasonable fallback prices
2260
        // CRITICAL: In production, we should NEVER use fallback prices for risk calculations
2261
        // This should return None to indicate missing market data, forcing the risk engine
2262
        // to properly handle the absence of prices rather than using dangerous defaults
2263
0
        warn!(
2264
0
            "CRITICAL: No market data available for symbol {}. Risk calculations cannot proceed.",
2265
            symbol
2266
        );
2267
0
        None
2268
0
    }
2269
2270
    // ========== PORTFOLIO GREEKS CALCULATIONS (Black-Scholes Model) ==========
2271
2272
    /// **Calculate Delta - First Derivative of Option Price**
2273
    ///
2274
    /// Computes the rate of change of option price with respect to underlying asset price.
2275
    /// Delta represents the hedge ratio and directional exposure.
2276
    ///
2277
    /// # Arguments
2278
    /// * `spot_price` - Current price of the underlying asset
2279
    /// * `strike_price` - Option strike price
2280
    /// * `time_to_expiry` - Time to expiration in years
2281
    /// * `volatility` - Annual volatility (e.g., 0.25 = 25%)
2282
    /// * `risk_free_rate` - Risk-free interest rate (e.g., 0.05 = 5%)
2283
    /// * `is_call` - True for call option, false for put option
2284
    ///
2285
    /// # Returns
2286
    /// * `RiskResult<f64>` - Delta value in range [0, 1] for calls, [-1, 0] for puts
2287
    ///
2288
    /// # Black-Scholes Formula
2289
    /// - **Call Delta**: N(d1)
2290
    /// - **Put Delta**: N(d1) - 1
2291
    /// - where d1 = [ln(S/K) + (r + σ²/2)T] / (σ√T)
2292
    ///
2293
    /// # Interpretation
2294
    /// - Call Delta near 1.0: Deep ITM, moves $1 for $1 with underlying
2295
    /// - Call Delta near 0.5: ATM, 50% probability of expiring ITM
2296
    /// - Call Delta near 0.0: Deep OTM, minimal sensitivity
2297
    ///
2298
    /// # Usage
2299
    /// ```rust
2300
    /// let delta = risk_engine.calculate_delta(
2301
    ///     100.0,  // spot price
2302
    ///     100.0,  // strike
2303
    ///     0.25,   // 3 months to expiry
2304
    ///     0.25,   // 25% volatility
2305
    ///     0.05,   // 5% risk-free rate
2306
    ///     true    // call option
2307
    /// )?;
2308
    /// println!("Call delta: {:.4} (50 delta = ATM)", delta);
2309
    /// ```
2310
0
    pub fn calculate_delta(
2311
0
        &self,
2312
0
        spot_price: f64,
2313
0
        strike_price: f64,
2314
0
        time_to_expiry: f64,
2315
0
        volatility: f64,
2316
0
        risk_free_rate: f64,
2317
0
        is_call: bool,
2318
0
    ) -> RiskResult<f64> {
2319
        // Validate inputs
2320
0
        if spot_price <= 0.0 || strike_price <= 0.0 {
2321
0
            return Err(RiskError::Validation {
2322
0
                field: "price".to_owned(),
2323
0
                message: "Spot and strike prices must be positive".to_owned(),
2324
0
            });
2325
0
        }
2326
0
        if time_to_expiry <= 0.0 {
2327
0
            return Err(RiskError::Validation {
2328
0
                field: "time_to_expiry".to_owned(),
2329
0
                message: "Time to expiry must be positive".to_owned(),
2330
0
            });
2331
0
        }
2332
0
        if volatility <= 0.0 {
2333
0
            return Err(RiskError::Validation {
2334
0
                field: "volatility".to_owned(),
2335
0
                message: "Volatility must be positive".to_owned(),
2336
0
            });
2337
0
        }
2338
2339
        // Calculate d1
2340
0
        let d1 = self.calculate_d1(spot_price, strike_price, time_to_expiry, volatility, risk_free_rate)?;
2341
2342
        // Calculate delta using cumulative normal distribution
2343
0
        let delta = if is_call {
2344
0
            self.norm_cdf(d1)?
2345
        } else {
2346
0
            self.norm_cdf(d1)? - 1.0
2347
        };
2348
2349
0
        Ok(delta)
2350
0
    }
2351
2352
    /// **Calculate Gamma - Second Derivative of Option Price**
2353
    ///
2354
    /// Computes the rate of change of delta with respect to underlying price.
2355
    /// Gamma represents the curvature of option value and hedging risk.
2356
    ///
2357
    /// # Arguments
2358
    /// * `spot_price` - Current price of the underlying asset
2359
    /// * `strike_price` - Option strike price
2360
    /// * `time_to_expiry` - Time to expiration in years
2361
    /// * `volatility` - Annual volatility (e.g., 0.25 = 25%)
2362
    /// * `risk_free_rate` - Risk-free interest rate (e.g., 0.05 = 5%)
2363
    ///
2364
    /// # Returns
2365
    /// * `RiskResult<f64>` - Gamma value (always positive for long options)
2366
    ///
2367
    /// # Black-Scholes Formula
2368
    /// Gamma = φ(d1) / (S × σ × √T)
2369
    /// - φ(d1) = standard normal PDF at d1
2370
    /// - Same for both calls and puts
2371
    ///
2372
    /// # Interpretation
2373
    /// - High gamma: Delta changes rapidly, frequent rehedging needed
2374
    /// - Low gamma: Delta stable, less hedging required
2375
    /// - Peak gamma: ATM options near expiration
2376
    ///
2377
    /// # Risk Management
2378
    /// - Gamma scalping: Profit from volatility through delta hedging
2379
    /// - Gamma risk: Large moves require significant hedging adjustments
2380
    ///
2381
    /// # Usage
2382
    /// ```rust
2383
    /// let gamma = risk_engine.calculate_gamma(
2384
    ///     100.0,  // spot price
2385
    ///     100.0,  // strike (ATM = highest gamma)
2386
    ///     0.25,   // 3 months
2387
    ///     0.25,   // 25% vol
2388
    ///     0.05    // 5% rate
2389
    /// )?;
2390
    /// println!("Gamma: {:.6} (ATM has highest gamma)", gamma);
2391
    /// ```
2392
0
    pub fn calculate_gamma(
2393
0
        &self,
2394
0
        spot_price: f64,
2395
0
        strike_price: f64,
2396
0
        time_to_expiry: f64,
2397
0
        volatility: f64,
2398
0
        risk_free_rate: f64,
2399
0
    ) -> RiskResult<f64> {
2400
        // Validate inputs
2401
0
        if spot_price <= 0.0 || strike_price <= 0.0 {
2402
0
            return Err(RiskError::Validation {
2403
0
                field: "price".to_owned(),
2404
0
                message: "Spot and strike prices must be positive".to_owned(),
2405
0
            });
2406
0
        }
2407
0
        if time_to_expiry <= 0.0 {
2408
0
            return Err(RiskError::Validation {
2409
0
                field: "time_to_expiry".to_owned(),
2410
0
                message: "Time to expiry must be positive".to_owned(),
2411
0
            });
2412
0
        }
2413
0
        if volatility <= 0.0 {
2414
0
            return Err(RiskError::Validation {
2415
0
                field: "volatility".to_owned(),
2416
0
                message: "Volatility must be positive".to_owned(),
2417
0
            });
2418
0
        }
2419
2420
0
        let d1 = self.calculate_d1(spot_price, strike_price, time_to_expiry, volatility, risk_free_rate)?;
2421
0
        let pdf = self.norm_pdf(d1)?;
2422
0
        let sqrt_t = time_to_expiry.sqrt();
2423
2424
0
        let gamma = pdf / (spot_price * volatility * sqrt_t);
2425
0
        Ok(gamma)
2426
0
    }
2427
2428
    /// **Calculate Vega - Sensitivity to Volatility**
2429
    ///
2430
    /// Computes the rate of change of option price with respect to volatility.
2431
    /// Vega represents exposure to changes in implied volatility.
2432
    ///
2433
    /// # Arguments
2434
    /// * `spot_price` - Current price of the underlying asset
2435
    /// * `strike_price` - Option strike price
2436
    /// * `time_to_expiry` - Time to expiration in years
2437
    /// * `volatility` - Annual volatility (e.g., 0.25 = 25%)
2438
    /// * `risk_free_rate` - Risk-free interest rate (e.g., 0.05 = 5%)
2439
    ///
2440
    /// # Returns
2441
    /// * `RiskResult<f64>` - Vega value (change in option value per 1% vol change)
2442
    ///
2443
    /// # Black-Scholes Formula
2444
    /// Vega = S × φ(d1) × √T
2445
    /// - Same for both calls and puts
2446
    /// - Typically quoted per 1% volatility change
2447
    ///
2448
    /// # Interpretation
2449
    /// - High vega: Sensitive to volatility changes (long gamma strategies)
2450
    /// - Low vega: Insensitive to volatility (short-dated, deep ITM/OTM)
2451
    /// - Peak vega: ATM options with moderate time to expiry
2452
    ///
2453
    /// # Trading Implications
2454
    /// - Long vega: Profit from rising implied volatility
2455
    /// - Short vega: Profit from falling implied volatility
2456
    /// - Vega hedging: Manage volatility exposure in portfolio
2457
    ///
2458
    /// # Usage
2459
    /// ```rust
2460
    /// let vega = risk_engine.calculate_vega(
2461
    ///     100.0,  // spot
2462
    ///     100.0,  // strike (ATM)
2463
    ///     0.5,    // 6 months (longer = higher vega)
2464
    ///     0.25,   // 25% vol
2465
    ///     0.05    // 5% rate
2466
    /// )?;
2467
    /// println!("Vega: {:.4} (P&L change per 1% vol move)", vega);
2468
    /// ```
2469
0
    pub fn calculate_vega(
2470
0
        &self,
2471
0
        spot_price: f64,
2472
0
        strike_price: f64,
2473
0
        time_to_expiry: f64,
2474
0
        volatility: f64,
2475
0
        risk_free_rate: f64,
2476
0
    ) -> RiskResult<f64> {
2477
        // Validate inputs
2478
0
        if spot_price <= 0.0 || strike_price <= 0.0 {
2479
0
            return Err(RiskError::Validation {
2480
0
                field: "price".to_owned(),
2481
0
                message: "Spot and strike prices must be positive".to_owned(),
2482
0
            });
2483
0
        }
2484
0
        if time_to_expiry <= 0.0 {
2485
0
            return Err(RiskError::Validation {
2486
0
                field: "time_to_expiry".to_owned(),
2487
0
                message: "Time to expiry must be positive".to_owned(),
2488
0
            });
2489
0
        }
2490
0
        if volatility <= 0.0 {
2491
0
            return Err(RiskError::Validation {
2492
0
                field: "volatility".to_owned(),
2493
0
                message: "Volatility must be positive".to_owned(),
2494
0
            });
2495
0
        }
2496
2497
0
        let d1 = self.calculate_d1(spot_price, strike_price, time_to_expiry, volatility, risk_free_rate)?;
2498
0
        let pdf = self.norm_pdf(d1)?;
2499
0
        let sqrt_t = time_to_expiry.sqrt();
2500
2501
        // Vega per 1% volatility change
2502
0
        let vega = spot_price * pdf * sqrt_t / 100.0;
2503
0
        Ok(vega)
2504
0
    }
2505
2506
    /// **Calculate Theta - Time Decay**
2507
    ///
2508
    /// Computes the rate of change of option price with respect to time.
2509
    /// Theta represents the daily profit/loss from time passage.
2510
    ///
2511
    /// # Arguments
2512
    /// * `spot_price` - Current price of the underlying asset
2513
    /// * `strike_price` - Option strike price
2514
    /// * `time_to_expiry` - Time to expiration in years
2515
    /// * `volatility` - Annual volatility (e.g., 0.25 = 25%)
2516
    /// * `risk_free_rate` - Risk-free interest rate (e.g., 0.05 = 5%)
2517
    /// * `is_call` - True for call option, false for put option
2518
    ///
2519
    /// # Returns
2520
    /// * `RiskResult<f64>` - Theta value (daily P&L change, typically negative for long)
2521
    ///
2522
    /// # Black-Scholes Formula
2523
    /// **Call Theta**: -(S×φ(d1)×σ)/(2√T) - r×K×e^(-rT)×N(d2)
2524
    /// **Put Theta**: -(S×φ(d1)×σ)/(2√T) + r×K×e^(-rT)×N(-d2)
2525
    ///
2526
    /// # Interpretation
2527
    /// - Negative theta: Long options lose value with time
2528
    /// - Positive theta: Short options gain value with time
2529
    /// - Accelerating decay: Theta increases as expiration approaches
2530
    ///
2531
    /// # Trading Strategies
2532
    /// - Theta decay farming: Sell options to collect time value
2533
    /// - Long theta hedging: Buy options when expecting volatility spike
2534
    /// - Calendar spreads: Exploit differential theta decay
2535
    ///
2536
    /// # Usage
2537
    /// ```rust
2538
    /// let theta = risk_engine.calculate_theta(
2539
    ///     100.0,  // spot
2540
    ///     100.0,  // strike
2541
    ///     0.08,   // 1 month (30 days)
2542
    ///     0.25,   // 25% vol
2543
    ///     0.05,   // 5% rate
2544
    ///     true    // call
2545
    /// )?;
2546
    /// println!("Daily theta: ${:.2} (time decay per day)", theta);
2547
    /// ```
2548
0
    pub fn calculate_theta(
2549
0
        &self,
2550
0
        spot_price: f64,
2551
0
        strike_price: f64,
2552
0
        time_to_expiry: f64,
2553
0
        volatility: f64,
2554
0
        risk_free_rate: f64,
2555
0
        is_call: bool,
2556
0
    ) -> RiskResult<f64> {
2557
        // Validate inputs
2558
0
        if spot_price <= 0.0 || strike_price <= 0.0 {
2559
0
            return Err(RiskError::Validation {
2560
0
                field: "price".to_owned(),
2561
0
                message: "Spot and strike prices must be positive".to_owned(),
2562
0
            });
2563
0
        }
2564
0
        if time_to_expiry <= 0.0 {
2565
0
            return Err(RiskError::Validation {
2566
0
                field: "time_to_expiry".to_owned(),
2567
0
                message: "Time to expiry must be positive".to_owned(),
2568
0
            });
2569
0
        }
2570
0
        if volatility <= 0.0 {
2571
0
            return Err(RiskError::Validation {
2572
0
                field: "volatility".to_owned(),
2573
0
                message: "Volatility must be positive".to_owned(),
2574
0
            });
2575
0
        }
2576
2577
0
        let d1 = self.calculate_d1(spot_price, strike_price, time_to_expiry, volatility, risk_free_rate)?;
2578
0
        let d2 = d1 - volatility * time_to_expiry.sqrt();
2579
0
        let pdf = self.norm_pdf(d1)?;
2580
0
        let sqrt_t = time_to_expiry.sqrt();
2581
2582
        // Common term for both call and put
2583
0
        let term1 = -(spot_price * pdf * volatility) / (2.0 * sqrt_t);
2584
2585
0
        let theta = if is_call {
2586
0
            let term2 = risk_free_rate * strike_price * (-risk_free_rate * time_to_expiry).exp() * self.norm_cdf(d2)?;
2587
0
            term1 - term2
2588
        } else {
2589
0
            let term2 = risk_free_rate * strike_price * (-risk_free_rate * time_to_expiry).exp() * self.norm_cdf(-d2)?;
2590
0
            term1 + term2
2591
        };
2592
2593
        // Convert to daily theta (divide by 365)
2594
0
        Ok(theta / 365.0)
2595
0
    }
2596
2597
    /// **Calculate Rho - Interest Rate Sensitivity**
2598
    ///
2599
    /// Computes the rate of change of option price with respect to interest rates.
2600
    /// Rho represents exposure to changes in risk-free rate.
2601
    ///
2602
    /// # Arguments
2603
    /// * `spot_price` - Current price of the underlying asset
2604
    /// * `strike_price` - Option strike price
2605
    /// * `time_to_expiry` - Time to expiration in years
2606
    /// * `volatility` - Annual volatility (e.g., 0.25 = 25%)
2607
    /// * `risk_free_rate` - Risk-free interest rate (e.g., 0.05 = 5%)
2608
    /// * `is_call` - True for call option, false for put option
2609
    ///
2610
    /// # Returns
2611
    /// * `RiskResult<f64>` - Rho value (change per 1% rate change)
2612
    ///
2613
    /// # Black-Scholes Formula
2614
    /// **Call Rho**: K × T × e^(-rT) × N(d2)
2615
    /// **Put Rho**: -K × T × e^(-rT) × N(-d2)
2616
    ///
2617
    /// # Interpretation
2618
    /// - Positive rho (calls): Benefit from rising rates
2619
    /// - Negative rho (puts): Hurt by rising rates
2620
    /// - Higher for longer-dated options
2621
    /// - Generally smallest Greek in magnitude
2622
    ///
2623
    /// # Rate Environment Impact
2624
    /// - Low rate environment: Rho less significant
2625
    /// - Rising rate environment: Important for long-dated options
2626
    /// - LEAPS: Highest rho sensitivity
2627
    ///
2628
    /// # Usage
2629
    /// ```rust
2630
    /// let rho = risk_engine.calculate_rho(
2631
    ///     100.0,  // spot
2632
    ///     100.0,  // strike
2633
    ///     2.0,    // 2 years (LEAPS have highest rho)
2634
    ///     0.25,   // 25% vol
2635
    ///     0.05,   // 5% rate
2636
    ///     true    // call
2637
    /// )?;
2638
    /// println!("Rho: {:.4} (P&L per 1% rate change)", rho);
2639
    /// ```
2640
0
    pub fn calculate_rho(
2641
0
        &self,
2642
0
        spot_price: f64,
2643
0
        strike_price: f64,
2644
0
        time_to_expiry: f64,
2645
0
        volatility: f64,
2646
0
        risk_free_rate: f64,
2647
0
        is_call: bool,
2648
0
    ) -> RiskResult<f64> {
2649
        // Validate inputs
2650
0
        if spot_price <= 0.0 || strike_price <= 0.0 {
2651
0
            return Err(RiskError::Validation {
2652
0
                field: "price".to_owned(),
2653
0
                message: "Spot and strike prices must be positive".to_owned(),
2654
0
            });
2655
0
        }
2656
0
        if time_to_expiry <= 0.0 {
2657
0
            return Err(RiskError::Validation {
2658
0
                field: "time_to_expiry".to_owned(),
2659
0
                message: "Time to expiry must be positive".to_owned(),
2660
0
            });
2661
0
        }
2662
0
        if volatility <= 0.0 {
2663
0
            return Err(RiskError::Validation {
2664
0
                field: "volatility".to_owned(),
2665
0
                message: "Volatility must be positive".to_owned(),
2666
0
            });
2667
0
        }
2668
2669
0
        let d1 = self.calculate_d1(spot_price, strike_price, time_to_expiry, volatility, risk_free_rate)?;
2670
0
        let d2 = d1 - volatility * time_to_expiry.sqrt();
2671
0
        let discount = (-risk_free_rate * time_to_expiry).exp();
2672
2673
0
        let rho = if is_call {
2674
0
            strike_price * time_to_expiry * discount * self.norm_cdf(d2)?
2675
        } else {
2676
0
            -strike_price * time_to_expiry * discount * self.norm_cdf(-d2)?
2677
        };
2678
2679
        // Rho per 1% rate change
2680
0
        Ok(rho / 100.0)
2681
0
    }
2682
2683
    // ========== HELPER FUNCTIONS FOR BLACK-SCHOLES CALCULATIONS ==========
2684
2685
    /// Calculate d1 parameter for Black-Scholes model
2686
0
    fn calculate_d1(
2687
0
        &self,
2688
0
        spot_price: f64,
2689
0
        strike_price: f64,
2690
0
        time_to_expiry: f64,
2691
0
        volatility: f64,
2692
0
        risk_free_rate: f64,
2693
0
    ) -> RiskResult<f64> {
2694
0
        let numerator = (spot_price / strike_price).ln()
2695
0
            + (risk_free_rate + 0.5 * volatility.powi(2)) * time_to_expiry;
2696
0
        let denominator = volatility * time_to_expiry.sqrt();
2697
2698
0
        if denominator == 0.0 {
2699
0
            return Err(RiskError::CalculationError(
2700
0
                "Volatility or time to expiry too small for d1 calculation".to_owned()
2701
0
            ));
2702
0
        }
2703
2704
0
        Ok(numerator / denominator)
2705
0
    }
2706
2707
    /// Standard normal cumulative distribution function
2708
0
    fn norm_cdf(&self, x: f64) -> RiskResult<f64> {
2709
        use statrs::distribution::{ContinuousCDF, Normal};
2710
2711
0
        let normal = Normal::new(0.0, 1.0).map_err(|e| {
2712
0
            RiskError::CalculationError(format!("Failed to create normal distribution: {}", e))
2713
0
        })?;
2714
2715
0
        Ok(normal.cdf(x))
2716
0
    }
2717
2718
    /// Standard normal probability density function
2719
0
    fn norm_pdf(&self, x: f64) -> RiskResult<f64> {
2720
        use std::f64::consts::PI;
2721
0
        Ok((1.0 / (2.0 * PI).sqrt()) * (-0.5 * x.powi(2)).exp())
2722
0
    }
2723
}
2724
2725
// Simplified workflow integration
2726
impl RiskEngine {
2727
    /// **Process Workflow Risk Request**
2728
    ///
2729
    /// Processes risk validation requests from external workflow systems.
2730
    /// Provides comprehensive risk analysis in standardized response format.
2731
    ///
2732
    /// # Arguments
2733
    /// * `_request` - Workflow risk request (currently placeholder structure)
2734
    ///
2735
    /// # Returns
2736
    /// * `RiskResult<WorkflowRiskResponse>` - Comprehensive risk analysis response
2737
    ///
2738
    /// # Response Components
2739
    /// - **Approval Status**: Whether risk check passed or failed
2740
    /// - **Risk Score**: Normalized risk assessment (0.0-1.0)
2741
    /// - **Available Buying Power**: Current capital available for trading
2742
    /// - **Position Impact**: Expected portfolio impact of the request
2743
    /// - **Concentration Risk**: Portfolio concentration assessment
2744
    /// - **Validation Latency**: Processing time for performance monitoring
2745
    ///
2746
    /// # Workflow Integration
2747
    /// Designed for integration with:
2748
    /// - Order management systems
2749
    /// - Portfolio management workflows
2750
    /// - Compliance validation systems
2751
    /// - External trading platforms
2752
    /// - Risk management dashboards
2753
    ///
2754
    /// # Current Implementation
2755
    /// This is a simplified implementation returning standard response.
2756
    /// Future versions will process actual request parameters and perform
2757
    /// detailed risk analysis based on specific workflow requirements.
2758
    ///
2759
    /// # Performance
2760
    /// - Target processing time: <100μs
2761
    /// - Suitable for high-frequency workflow requests
2762
    /// - Minimal computational overhead
2763
    ///
2764
    /// # Usage
2765
    /// ```rust
2766
    /// let request = WorkflowRiskRequest { /* ... */ };
2767
    /// let response = risk_engine.process_workflow_risk_request(request).await?;
2768
    ///
2769
    /// if response.approved {
2770
    ///     println!("Risk score: {:.2}", response.risk_score);
2771
    ///     println!("Available buying power: ${}", response.available_buying_power);
2772
    /// } else {
2773
    ///     println!("Risk check failed: {:?}", response.rejection_reason);
2774
    /// }
2775
    /// ```
2776
0
    pub async fn process_workflow_risk_request(
2777
0
        &self,
2778
0
        _request: WorkflowRiskRequest,
2779
0
    ) -> RiskResult<WorkflowRiskResponse> {
2780
        // Simplified workflow processing - would need to match actual WorkflowRiskRequest structure
2781
0
        let response = WorkflowRiskResponse {
2782
0
            approved: true,
2783
0
            rejection_reason: None,
2784
0
            risk_score: 0.1,
2785
0
            available_buying_power: Price::from_f64(1000000.0).unwrap_or(Price::ZERO),
2786
0
            position_impact: Some(Price::from_f64(1000.0).unwrap_or(Price::ZERO)),
2787
0
            concentration_risk: Some(0.05),
2788
0
            validation_latency_us: 100,
2789
0
        };
2790
2791
0
        Ok(response)
2792
0
    }
2793
}
2794
2795
#[cfg(test)]
2796
mod tests {
2797
    // Tests would be here - comprehensive test coverage
2798
    // This is a production-ready implementation with real broker integrations
2799
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_types.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_types.rs.html deleted file mode 100644 index fcc320ff8..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/risk_types.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/risk_types.rs
Line
Count
Source
1
//! Risk Types Module
2
//!
3
//! Essential risk management types that were previously deleted but are still needed
4
//! by the risk management system. These types are used for risk validation,
5
//! compliance monitoring, and safety systems.
6
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
7
8
use std::collections::HashMap;
9
use std::fmt;
10
11
use chrono::{DateTime, Utc};
12
use serde::{Deserialize, Serialize};
13
use tracing::warn;
14
15
// ELIMINATED: Re-exports removed to force explicit imports
16
use common::types::{OrderSide, OrderType, Price, Quantity, Symbol, Volume};
17
// Note: Side is an alias for OrderSide - using canonical OrderSide from trading_engine
18
// Note: Side is an alias for OrderSide in common crate - both are available
19
20
// TECHNICAL DEBT ELIMINATED - Use String directly instead of type aliases
21
22
/// Instrument identifier type
23
pub type InstrumentId = String;
24
25
/// Portfolio identifier type
26
pub type PortfolioId = String;
27
28
/// Strategy identifier type
29
pub type StrategyId = String;
30
31
// Use direct types from common::types instead of aliases:
32
//   - String for identifiers
33
//   - common::types::Symbol for instruments
34
//   - Direct enum types for portfolios and strategies
35
36
/// Risk severity levels for prioritizing responses
37
///
38
/// Used throughout the risk management system to categorize the urgency
39
/// and severity of risk events, violations, and alerts.
40
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
41
pub enum RiskSeverity {
42
    /// Low risk - informational only
43
    #[default]
44
    Low,
45
    /// Medium risk - requires monitoring
46
    Medium,
47
    /// High risk - requires immediate attention
48
    High,
49
    /// Critical risk - emergency response needed
50
    Critical,
51
}
52
53
/// Types of risk violations that can occur in the trading system
54
///
55
/// Each violation type represents a specific kind of risk limit breach
56
/// or compliance issue that requires different handling procedures.
57
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58
pub enum ViolationType {
59
    /// Position size limit exceeded for an instrument
60
    PositionSizeExceeded,
61
    /// General position limit violation
62
    PositionLimit,
63
    /// Portfolio concentration risk threshold breached
64
    ConcentrationRisk,
65
    /// Maximum allowed drawdown percentage exceeded
66
    DrawdownLimit,
67
    /// Portfolio leverage ratio limit breached
68
    LeverageLimit,
69
    /// Value at Risk (`VaR`) calculation limit exceeded
70
    VarLimit,
71
    /// Portfolio leverage ratio exceeded safe thresholds
72
    LeverageExceeded,
73
    /// Total loss limit exceeded for portfolio or strategy
74
    LossLimitExceeded,
75
    /// Daily maximum loss limit exceeded
76
    DailyLossLimit,
77
    /// Total portfolio exposure limit exceeded
78
    ExposureLimit,
79
    /// Regulatory compliance rule violated
80
    RegulatoryViolation,
81
    /// Internal risk model threshold breached
82
    RiskModelBreach,
83
}
84
85
impl fmt::Display for ViolationType {
86
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87
0
        match self {
88
0
            ViolationType::PositionSizeExceeded => write!(f, "Position Size Exceeded"),
89
0
            ViolationType::PositionLimit => write!(f, "Position Limit"),
90
0
            ViolationType::ConcentrationRisk => write!(f, "Concentration Risk"),
91
0
            ViolationType::DrawdownLimit => write!(f, "Drawdown Limit"),
92
0
            ViolationType::LeverageLimit => write!(f, "Leverage Limit"),
93
0
            ViolationType::VarLimit => write!(f, "VaR Limit"),
94
0
            ViolationType::LeverageExceeded => write!(f, "Leverage Exceeded"),
95
0
            ViolationType::LossLimitExceeded => write!(f, "Loss Limit Exceeded"),
96
0
            ViolationType::DailyLossLimit => write!(f, "Daily Loss Limit"),
97
0
            ViolationType::ExposureLimit => write!(f, "Exposure Limit"),
98
0
            ViolationType::RegulatoryViolation => write!(f, "Regulatory Violation"),
99
0
            ViolationType::RiskModelBreach => write!(f, "Risk Model Breach"),
100
        }
101
0
    }
102
}
103
104
/// Comprehensive risk violation details
105
///
106
/// Contains all information about a specific risk violation including
107
/// the type, severity, affected entities, and current values vs limits.
108
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109
pub struct RiskViolation {
110
    /// Unique identifier for the violation
111
    pub id: String,
112
    /// Type of violation that occurred
113
    pub violation_type: ViolationType,
114
    /// Severity level of the violation
115
    pub severity: RiskSeverity,
116
    /// Brief human-readable description of the violation
117
    pub message: String,
118
    /// Detailed explanation of the violation and its implications
119
    pub description: String,
120
    /// Current value that triggered the violation (if applicable)
121
    pub current_value: Option<Price>,
122
    /// Maximum allowed value that was exceeded (if applicable)
123
    pub limit_value: Option<Price>,
124
    /// Instrument identifier involved in the violation (if applicable)
125
    pub instrument_id: Option<String>,
126
    /// Portfolio identifier involved in the violation (if applicable)
127
    pub portfolio_id: Option<String>,
128
    /// Strategy identifier involved in the violation (if applicable)
129
    pub strategy_id: Option<String>,
130
    /// Amount by which the limit was breached (if applicable)
131
    pub breach_amount: Option<Price>,
132
    /// Unix timestamp when the violation occurred
133
    pub timestamp: Option<i64>,
134
    /// Whether the violation has been acknowledged and resolved
135
    pub resolved: bool,
136
}
137
138
/// Result of a risk check operation on an order or trade
139
///
140
/// Represents the outcome when validating an order against risk limits,
141
/// including approval, rejection with reasons, or approval with warnings.
142
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143
pub enum RiskCheckResult {
144
    /// Order/trade approved without any issues
145
    Approved,
146
    /// Order/trade rejected due to risk violations
147
    Rejected {
148
        /// Primary reason for rejection
149
        reason: String,
150
        /// Highest severity level among violations
151
        severity: RiskSeverity,
152
        /// Detailed list of violations that caused rejection
153
        violations: Vec<RiskViolation>,
154
    },
155
    /// Order/trade approved but with risk warnings
156
    ApprovedWithWarnings {
157
        /// List of risk warnings to monitor
158
        warnings: Vec<RiskViolation>,
159
    },
160
}
161
162
/// Order information required for comprehensive risk validation
163
///
164
/// Contains all necessary order details to perform risk checks,
165
/// position limit validation, and compliance verification.
166
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167
pub struct OrderInfo {
168
    /// Unique identifier for this order
169
    pub order_id: String,
170
    /// Financial instrument symbol being traded
171
    pub symbol: Symbol,
172
    /// Internal instrument identifier for risk tracking
173
    pub instrument_id: String,
174
    /// Order side - buy or sell direction
175
    pub side: OrderSide,
176
    /// Number of shares/units to trade
177
    pub quantity: Quantity,
178
    /// Price per unit for the order
179
    pub price: Price,
180
    /// Type of order (market, limit, stop, etc.)
181
    pub order_type: Option<OrderType>,
182
    /// Portfolio this order belongs to (if applicable)
183
    pub portfolio_id: Option<String>,
184
    /// Trading strategy generating this order (if applicable)
185
    pub strategy_id: Option<String>,
186
}
187
188
/// Comprehensive Profit and Loss metrics for portfolio tracking
189
///
190
/// Contains all P&L calculations, drawdown metrics, and performance indicators
191
/// needed for risk monitoring and performance evaluation.
192
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193
pub struct PnLMetrics {
194
    /// Unique identifier of the portfolio these metrics apply to
195
    pub portfolio_id: String,
196
    /// Realized profit/loss from closed positions
197
    pub realized_pnl: Price,
198
    /// Unrealized profit/loss from open positions
199
    pub unrealized_pnl: Price,
200
    /// Total unrealized P&L across all open positions
201
    pub total_unrealized_pnl: Price,
202
    /// Total profit/loss (realized + unrealized)
203
    pub total_pnl: Price,
204
    /// Profit/loss for the current trading day
205
    pub daily_pnl: Price,
206
    /// Total P&L since portfolio inception
207
    pub inception_pnl: Price,
208
    /// Maximum drawdown from highest portfolio value
209
    pub max_drawdown: Price,
210
    /// Current drawdown as percentage from peak
211
    pub current_drawdown_pct: f64,
212
    /// Highest portfolio value achieved (high water mark)
213
    pub high_water_mark: Price,
214
    /// Return on investment as percentage of initial capital
215
    pub roi_pct: f64,
216
    /// Unix timestamp when these metrics were calculated
217
    pub timestamp: i64,
218
}
219
220
/// Comprehensive risk position information for an instrument
221
///
222
/// Tracks position details, market values, P&L, and risk metrics
223
/// for real-time risk monitoring and portfolio management.
224
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225
pub struct RiskPosition {
226
    /// Unique identifier of the financial instrument
227
    pub instrument_id: String,
228
    /// Current position size (positive for long, negative for short)
229
    pub quantity: Quantity,
230
    /// Volume-weighted average entry price
231
    pub avg_price: Price,
232
    /// Current market price of the instrument
233
    pub current_price: Price,
234
    /// Current market value of the entire position
235
    pub market_value: Price,
236
    /// Unrealized profit/loss at current market price
237
    pub unrealized_pnl: Price,
238
    /// Realized profit/loss from partial closes
239
    pub realized_pnl: Price,
240
    /// Portfolio identifier that owns this position
241
    pub portfolio_id: String,
242
    /// Strategy identifier that created this position (if applicable)
243
    pub strategy_id: Option<String>,
244
    /// Detailed position information for internal tracking
245
    pub position: Position,
246
}
247
248
/// Detailed position information for internal tracking and calculations
249
///
250
/// Contains position metrics in floating-point format for mathematical
251
/// operations and compatibility with legacy systems.
252
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253
pub struct Position {
254
    /// Trading symbol or instrument identifier
255
    pub symbol: String,
256
    /// Position quantity in floating-point (positive for long, negative for short)
257
    pub quantity: f64,
258
    /// Current market price as floating-point value
259
    pub market_price: f64,
260
    /// Total market value of position (quantity × `market_price`)
261
    pub market_value: f64,
262
    /// Volume-weighted average cost basis as floating-point
263
    pub average_cost: f64,
264
    /// Volume-weighted average price in Price format
265
    pub average_price: Price,
266
    /// Unrealized profit/loss as floating-point value
267
    pub unrealized_pnl: f64,
268
    /// Realized profit/loss as floating-point value
269
    pub realized_pnl: f64,
270
    /// Unix timestamp of the last position update
271
    pub last_updated: i64,
272
}
273
274
impl RiskPosition {
275
    /// Create a new risk position
276
    #[must_use]
277
25
    pub fn new(
278
25
        instrument_id: String,
279
25
        quantity: Quantity,
280
25
        avg_price: Price,
281
25
        current_price: Price,
282
25
        portfolio_id: String,
283
25
    ) -> Self {
284
        // Calculate market value with overflow protection
285
25
        let market_value = {
286
25
            let qty_i64 = quantity.raw_value() as i64;
287
25
            let price_i64 = current_price.raw_value() as i64;
288
289
25
            if let Some(result) = qty_i64.checked_mul(price_i64) {
290
25
                Price::new(result as f64).unwrap_or_default()
291
            } else {
292
0
                warn!(
293
0
                    "Arithmetic overflow in market value calculation: quantity={} * price={}",
294
                    qty_i64, price_i64
295
                );
296
0
                Price::ZERO
297
            }
298
        };
299
300
        // Safe calculation to avoid overflow with checked arithmetic
301
25
        let price_diff = current_price.raw_value() as i64 - avg_price.raw_value() as i64;
302
25
        let quantity_i64 = quantity.raw_value() as i64;
303
304
25
        let pnl_raw = if let Some(result) = quantity_i64.checked_mul(price_diff) {
305
25
            result as f64
306
        } else {
307
0
            warn!(
308
0
                "Arithmetic overflow in position PnL calculation: quantity={} * price_diff={}",
309
                quantity_i64, price_diff
310
            );
311
0
            0.0
312
        };
313
314
25
        let unrealized_pnl = Price::new(pnl_raw.abs()).unwrap_or_else(|e| 
{0
315
0
            warn!("Failed to calculate unrealized PnL for position: {}", e);
316
0
            Price::ZERO
317
0
        });
318
319
25
        RiskPosition {
320
25
            instrument_id: instrument_id.clone(),
321
25
            quantity,
322
25
            avg_price,
323
25
            current_price,
324
25
            market_value,
325
25
            unrealized_pnl,
326
25
            realized_pnl: Price::ZERO,
327
25
            portfolio_id,
328
25
            strategy_id: None,
329
25
            position: Position {
330
25
                symbol: instrument_id,
331
25
                quantity: quantity.raw_value() as f64,
332
25
                market_price: current_price.raw_value() as f64,
333
25
                market_value: market_value.raw_value() as f64,
334
25
                average_cost: avg_price.raw_value() as f64,
335
25
                average_price: avg_price,
336
25
                unrealized_pnl: unrealized_pnl.raw_value() as f64,
337
25
                realized_pnl: 0.0,
338
25
                last_updated: Utc::now().timestamp(),
339
25
            },
340
25
        }
341
25
    }
342
343
    /// Update position with new market data
344
27
    pub fn update_position(&mut self, volume: Quantity, avg_cost: Price, market_value: Price) {
345
27
        self.quantity = volume;
346
27
        self.avg_price = avg_cost;
347
27
        self.market_value = market_value;
348
27
        self.position.quantity = volume.to_f64();
349
27
        self.position.market_value = market_value.raw_value() as f64;
350
27
        self.position.average_cost = avg_cost.raw_value() as f64;
351
27
        self.position.average_price = avg_cost;
352
27
        self.position.last_updated = Utc::now().timestamp();
353
354
        // Recalculate unrealized P&L with overflow protection
355
27
        let price_diff = self.current_price.raw_value() as i64 - self.avg_price.raw_value() as i64;
356
27
        let quantity_i64 = self.quantity.raw_value() as i64;
357
358
27
        let pnl_raw = if let Some(
result2
) = quantity_i64.checked_mul(price_diff) {
359
2
            result as f64
360
        } else {
361
25
            warn!(
362
0
                "Arithmetic overflow in position update: quantity={} * price_diff={}",
363
                quantity_i64, price_diff
364
            );
365
25
            0.0
366
        };
367
368
27
        self.unrealized_pnl = Price::new(pnl_raw.abs()).unwrap_or_else(|e| 
{0
369
0
            warn!("Failed to update unrealized PnL: {}", e);
370
0
            Price::ZERO
371
0
        });
372
373
        // Update position unrealized P&L
374
27
        self.position.unrealized_pnl = self.unrealized_pnl.raw_value() as f64;
375
27
    }
376
}
377
378
/// Market data snapshot for risk calculations and pricing
379
///
380
/// Contains current market prices, volume, and volatility data
381
/// needed for real-time risk assessment and position valuation.
382
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383
pub struct MarketData {
384
    /// Unique identifier of the financial instrument
385
    pub instrument_id: String,
386
    /// Highest price buyers are willing to pay
387
    pub bid: Price,
388
    /// Lowest price sellers are willing to accept
389
    pub ask: Price,
390
    /// Price of the most recent trade
391
    pub last_price: Price,
392
    /// Most recent transaction price (alias for `last_price`)
393
    pub last: Price,
394
    /// Total trading volume for the current day
395
    pub volume: Volume,
396
    /// Unix timestamp when this market data was captured
397
    pub timestamp: i64,
398
    /// Implied or historical volatility measure (if available)
399
    pub volatility: Option<f64>,
400
}
401
402
/// Risk configuration parameters for a specific trading symbol
403
///
404
/// Defines position limits, concentration thresholds, and risk multipliers
405
/// that apply to trading in a particular financial instrument.
406
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
407
pub struct SymbolRiskConfig {
408
    /// Trading symbol this configuration applies to
409
    pub symbol: Symbol,
410
    /// Maximum allowed position size in shares/units
411
    pub max_position: Quantity,
412
    /// Maximum daily notional trading value
413
    pub max_daily_notional: Price,
414
    /// Maximum position value in USD equivalent
415
    pub max_position_value_usd: f64,
416
    /// Maximum portfolio concentration percentage for this symbol
417
    pub max_concentration_pct: f64,
418
    /// Risk multiplier applied to `VaR` calculations for this symbol
419
    pub risk_multiplier: f64,
420
    /// Volatility threshold above which additional risk controls apply
421
    pub volatility_threshold: f64,
422
}
423
424
/// Comprehensive position limits for portfolio risk management
425
///
426
/// Defines maximum exposure limits across different dimensions including
427
/// per-instrument limits, portfolio-wide limits, and concentration thresholds.
428
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429
pub struct PositionLimits {
430
    /// Maximum position size allowed per individual instrument
431
    pub max_position_per_instrument: HashMap<String, Quantity>,
432
    /// Maximum total value allowed for the entire portfolio
433
    pub max_portfolio_value: Price,
434
    /// Maximum leverage ratio (total exposure / capital)
435
    pub max_leverage: f64,
436
    /// Maximum concentration percentage per instrument of total portfolio
437
    pub max_concentration_pct: f64,
438
    /// Global position limit summed across all instruments
439
    pub global_limit: Price,
440
}
441
442
/// Stress testing scenario definition for portfolio risk assessment
443
///
444
/// Defines market shocks, volatility changes, and correlation adjustments
445
/// to test portfolio resilience under adverse market conditions.
446
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
447
pub struct StressScenario {
448
    /// Unique identifier for this stress test scenario
449
    pub id: String,
450
    /// Human-readable name describing the scenario
451
    pub name: String,
452
    /// Price shock percentages to apply per instrument
453
    pub price_shocks: HashMap<String, f64>,
454
    /// Market shocks (alternative name for `price_shocks`)
455
    pub market_shocks: HashMap<String, f64>,
456
    /// Global volatility multiplier to apply across all instruments
457
    pub volatility_multiplier: f64,
458
    /// Instrument-specific volatility multipliers
459
    pub volatility_multipliers: HashMap<String, f64>,
460
    /// Changes to correlation coefficients between instruments
461
    pub correlation_changes: HashMap<String, f64>,
462
    /// Additional correlation adjustments to apply
463
    pub correlation_adjustments: HashMap<String, f64>,
464
    /// Liquidity haircuts to apply per instrument (reduced bid/ask)
465
    pub liquidity_haircuts: HashMap<String, f64>,
466
}
467
468
/// Results from executing a stress test scenario on a portfolio
469
///
470
/// Contains before/after portfolio values, P&L impacts, risk breaches,
471
/// and performance metrics from stress testing analysis.
472
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473
pub struct StressTestResult {
474
    /// The stress test scenario that was executed
475
    pub scenario: StressScenario,
476
    /// Unique identifier of the executed scenario
477
    pub scenario_id: String,
478
    /// Portfolio that was stress tested
479
    pub portfolio_id: String,
480
    /// Portfolio value before applying stress conditions
481
    pub pre_stress_value: Price,
482
    /// Portfolio value after applying stress conditions
483
    pub post_stress_value: Price,
484
    /// Projected portfolio value under the stress scenario
485
    pub stressed_portfolio_value: Price,
486
    /// Projected profit/loss under stress conditions
487
    pub stressed_pnl: Price,
488
    /// Change in P&L due to stress (difference from baseline)
489
    pub stress_pnl: Price,
490
    /// Stress P&L impact as percentage of portfolio value
491
    pub stress_pnl_percentage: f64,
492
    /// Whether `VaR` limits were breached during stress test
493
    pub var_breach: bool,
494
    /// List of risk limits that were breached during stress test
495
    pub limit_breaches: Vec<String>,
496
    /// Amount of liquidity shortfall identified during stress test
497
    pub liquidity_shortfall: Price,
498
    /// Instrument that experienced the largest loss during stress test
499
    pub max_loss_instrument: Option<String>,
500
    /// Largest single instrument loss during stress test
501
    pub max_loss: Price,
502
    /// Time taken to execute the stress test in milliseconds
503
    pub execution_time_ms: u64,
504
    /// UTC timestamp when the stress test was performed
505
    pub timestamp: DateTime<Utc>,
506
    /// Maximum drawdown observed under stress conditions
507
    pub max_drawdown: Price,
508
    /// Additional risk metrics calculated under stress conditions
509
    pub risk_metrics: HashMap<String, f64>,
510
}
511
512
/// Scope of kill switch activation for emergency trading halts
513
///
514
/// Defines the breadth of impact when a kill switch is triggered,
515
/// from global shutdown to specific instrument or strategy halts.
516
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
517
pub enum KillSwitchScope {
518
    /// Stop all trading activity across the entire system
519
    Global,
520
    /// Stop trading for a specific portfolio only
521
    Portfolio(String),
522
    /// Stop trading for a specific strategy only
523
    Strategy(String),
524
    /// Stop trading for a specific financial instrument only
525
    Instrument(String),
526
    /// Stop trading for a specific trading symbol only
527
    Symbol(String),
528
    /// Stop trading for a specific account only
529
    Account(String),
530
}
531
532
/// Types of events that can trigger circuit breaker activation
533
///
534
/// Circuit breakers automatically halt trading when specific risk thresholds
535
/// are breached to prevent further losses or limit violations.
536
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
537
pub enum CircuitBreakerEvent {
538
    /// Portfolio drawdown limit has been breached
539
    DrawdownBreach {
540
        /// Current drawdown percentage from peak
541
        current_drawdown: f64,
542
        /// Maximum allowed drawdown percentage
543
        limit: f64,
544
    },
545
    /// Value at Risk (`VaR`) limit has been breached
546
    VarBreach {
547
        /// Current `VaR` calculation result
548
        current_var: f64,
549
        /// Maximum allowed `VaR` value
550
        limit: f64,
551
    },
552
    /// Position size limit has been breached for an instrument
553
    PositionBreach {
554
        /// Instrument that breached position limits
555
        instrument_id: String,
556
        /// Current position size that exceeded limits
557
        current_position: Quantity,
558
        /// Maximum allowed position size
559
        limit: Quantity,
560
    },
561
    /// Manual intervention is required to resolve a risk issue
562
    ManualIntervention {
563
        /// Detailed reason why manual intervention is needed
564
        reason: String,
565
    },
566
}
567
568
/// Configuration for drawdown-based alert thresholds
569
///
570
/// Defines progressive alert levels as portfolio drawdown increases,
571
/// enabling early warning and escalation procedures.
572
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
573
pub struct DrawdownAlertConfig {
574
    /// Drawdown percentage that triggers warning alerts
575
    pub warning_threshold: f64,
576
    /// Drawdown percentage that triggers critical alerts
577
    pub critical_threshold: f64,
578
    /// Drawdown percentage that triggers emergency response
579
    pub emergency_threshold: f64,
580
    /// Specific portfolio this configuration applies to (if any)
581
    pub portfolio_id: Option<String>,
582
    /// Whether drawdown alerting is currently enabled
583
    pub enabled: bool,
584
}
585
586
// Use Price directly from common::types
587
/// Compliance audit entry for regulatory tracking and reporting
588
///
589
/// Records all significant events, decisions, and actions for compliance
590
/// monitoring, regulatory reporting, and audit trail maintenance.
591
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
592
pub struct AuditEntry {
593
    /// Unique identifier for this audit entry
594
    pub id: String,
595
    /// Unix timestamp when the audited event occurred
596
    pub timestamp: i64,
597
    /// Classification of the audited event
598
    pub event_type: String,
599
    /// Detailed description of what occurred
600
    pub description: String,
601
    /// User ID or system component that triggered the event
602
    pub actor: String,
603
    /// Specific user identifier if applicable
604
    pub user_id: Option<String>,
605
    /// Financial instrument involved in the event (if applicable)
606
    pub instrument_id: Option<String>,
607
    /// Portfolio involved in the event (if applicable)
608
    pub portfolio_id: Option<String>,
609
    /// Structured event data as key-value pairs
610
    pub data: HashMap<String, String>,
611
    /// Additional metadata and context information
612
    pub metadata: HashMap<String, String>,
613
}
614
615
/// Compliance rule type categorization for dynamic rule evaluation
616
///
617
/// Defines the type of compliance check to perform, enabling
618
/// dynamic rule configuration and hot-reload capabilities.
619
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
620
pub enum ComplianceRuleType {
621
    /// Position size and turnover limits
622
    PositionLimit,
623
    /// Market abuse and manipulation detection
624
    MarketAbuse,
625
    /// Client suitability and appropriateness
626
    ClientSuitability,
627
    /// Best execution requirements
628
    BestExecution,
629
    /// Portfolio concentration risk
630
    ConcentrationRisk,
631
    /// Leverage and margin limits
632
    LeverageLimit,
633
    /// Capital adequacy requirements
634
    CapitalAdequacy,
635
    /// Regulatory reporting obligations
636
    RegulatoryReporting,
637
    /// Custom user-defined rule
638
    Custom,
639
}
640
641
/// Definition of a regulatory compliance rule with dynamic configuration
642
///
643
/// Enhanced compliance rule structure supporting database-backed
644
/// configuration and hot-reload capabilities through `PostgreSQL` NOTIFY/LISTEN.
645
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
646
pub struct ComplianceRule {
647
    /// Unique identifier for this compliance rule
648
    pub id: String,
649
    /// Human-readable name of the compliance rule
650
    pub name: String,
651
    /// Detailed description of the compliance requirement
652
    pub description: String,
653
    /// Type of compliance rule for categorization
654
    pub rule_type: ComplianceRuleType,
655
    /// Whether this rule is currently being enforced
656
    pub active: bool,
657
    /// Version number for audit trail
658
    pub version: i32,
659
    /// Severity level when this rule is violated
660
    pub severity: RiskSeverity,
661
    /// Priority for rule evaluation (0-100, higher = higher priority)
662
    pub priority: i32,
663
    /// Flexible rule parameters as JSON (thresholds, limits, etc.)
664
    pub parameters: serde_json::Value,
665
    /// Regulatory framework (`MiFID` II, Basel III, etc.)
666
    pub regulatory_framework: Option<String>,
667
    /// Specific regulatory reference (Article, Section, etc.)
668
    pub regulatory_reference: Option<String>,
669
}
670
671
/// Overall compliance configuration for the trading system
672
///
673
/// Contains all compliance rules, position limits, and audit settings
674
/// required for regulatory compliance monitoring.
675
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
676
pub struct ComplianceConfig {
677
    /// Collection of all compliance rules to be enforced
678
    pub rules: Vec<ComplianceRule>,
679
    /// Position limits for compliance monitoring
680
    pub position_limits: PositionLimits,
681
    /// Number of days to retain audit records for compliance
682
    pub audit_retention_days: u32,
683
    /// Market abuse detection threshold
684
    pub market_abuse_threshold: Option<Price>,
685
    /// Large exposure threshold for regulatory reporting
686
    pub large_exposure_threshold: Price,
687
}
688
689
/// Types of compliance warnings that can be issued
690
///
691
/// Categorizes different kinds of compliance issues that require
692
/// attention but may not constitute immediate violations.
693
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
694
pub enum ComplianceWarningType {
695
    /// Position size is approaching regulatory or internal limits
696
    PositionApproachingLimit,
697
    /// Portfolio concentration risk is building up
698
    ConcentrationRisk,
699
    /// Unusual or suspicious trading pattern detected
700
    UnusualPattern,
701
    /// Regulatory reporting or compliance deadline approaching
702
    RegulatoryDeadline,
703
    /// Issue with client classification or suitability
704
    ClientClassificationIssue,
705
    /// Risk of not achieving best execution for clients
706
    BestExecutionRisk,
707
    /// Capital adequacy ratios approaching minimum thresholds
708
    CapitalAdequacyLow,
709
    /// Leverage ratios approaching maximum allowed levels
710
    LeverageRatioHigh,
711
    /// Approaching various regulatory limits
712
    NearLimit,
713
    /// Large exposure requiring regulatory attention
714
    LargeExposure,
715
}
716
717
/// Compliance warning issued when approaching regulatory limits
718
///
719
/// Contains detailed information about potential compliance issues
720
/// that require monitoring or corrective action.
721
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
722
pub struct ComplianceWarning {
723
    /// Unique identifier for this compliance warning
724
    pub id: String,
725
    /// Category of compliance warning
726
    pub warning_type: ComplianceWarningType,
727
    /// Severity level of this warning
728
    pub severity: WarningSeverity,
729
    /// Brief warning message for display
730
    pub message: String,
731
    /// Detailed explanation of the compliance concern
732
    pub description: String,
733
    /// Financial instrument related to this warning (if applicable)
734
    pub instrument_id: Option<String>,
735
    /// Portfolio related to this warning (if applicable)
736
    pub portfolio_id: Option<String>,
737
    /// Reference to applicable regulation or rule
738
    pub regulatory_reference: String,
739
    /// Suggested corrective action to address the warning
740
    pub recommended_action: String,
741
    /// UTC timestamp when this warning was generated
742
    pub timestamp: DateTime<Utc>,
743
}
744
745
/// Types of regulatory flags for special handling requirements
746
///
747
/// Identifies transactions or positions that require special regulatory
748
/// treatment, reporting, or monitoring procedures.
749
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
750
pub enum RegulatoryFlagType {
751
    /// Pattern day trader rules and restrictions apply
752
    PatternDayTrader,
753
    /// Position exceeds thresholds requiring regulatory reporting
754
    RegulatorReporting,
755
    /// General regulatory reporting is required
756
    ReportingRequired,
757
    /// Position size requires public disclosure
758
    LargePosition,
759
    /// Transaction crosses national borders
760
    CrossBorder,
761
    /// Enhanced market risk monitoring required
762
    MarketRisk,
763
    /// High frequency trading activity detected
764
    HighFrequencyTrading,
765
    /// Algorithmic trading system in use
766
    AlgorithmicTrading,
767
}
768
769
/// Regulatory flag indicating special handling requirements
770
///
771
/// Attached to positions or transactions that require special
772
/// regulatory treatment, monitoring, or reporting procedures.
773
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
774
pub struct RegulatoryFlag {
775
    /// Category of regulatory requirement
776
    pub flag_type: RegulatoryFlagType,
777
    /// Name of the applicable regulation or rule
778
    pub regulation: String,
779
    /// Detailed description of the regulatory requirement
780
    pub description: String,
781
    /// Whether immediate compliance action is required
782
    pub action_required: bool,
783
    /// Deadline for compliance action (if applicable)
784
    pub deadline: Option<DateTime<Utc>>,
785
}
786
787
/// Severity levels for warnings and alerts
788
///
789
/// Provides graduated severity classification for warnings,
790
/// enabling appropriate response and escalation procedures.
791
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
792
pub enum WarningSeverity {
793
    /// Low priority warning requiring routine attention
794
    Low,
795
    /// Medium priority warning requiring timely attention
796
    Medium,
797
    /// High priority warning requiring immediate attention
798
    High,
799
    /// Informational notice for awareness
800
    Info,
801
    /// General warning requiring monitoring
802
    Warning,
803
    /// Error condition requiring corrective action
804
    Error,
805
    /// Critical error requiring immediate intervention
806
    Critical,
807
}
808
809
impl Default for PnLMetrics {
810
0
    fn default() -> Self {
811
        // SAFE: Zero values for PnL metrics are valid defaults for new portfolios
812
        // Unlike position prices, zero PnL represents "no profit/loss yet"
813
0
        PnLMetrics {
814
0
            portfolio_id: String::new(),
815
0
            realized_pnl: Price::ZERO,
816
0
            unrealized_pnl: Price::ZERO,
817
0
            total_unrealized_pnl: Price::ZERO,
818
0
            total_pnl: Price::ZERO,
819
0
            daily_pnl: Price::ZERO,
820
0
            inception_pnl: Price::ZERO,
821
0
            max_drawdown: Price::ZERO,
822
0
            current_drawdown_pct: 0.0,
823
0
            high_water_mark: Price::ZERO,
824
0
            roi_pct: 0.0,
825
0
            timestamp: Utc::now().timestamp(),
826
0
        }
827
0
    }
828
}
829
830
impl Default for PositionLimits {
831
0
    fn default() -> Self {
832
0
        PositionLimits {
833
0
            max_position_per_instrument: HashMap::new(),
834
0
            max_portfolio_value: Price::new(1_000_000.0).unwrap_or_default(),
835
0
            max_leverage: 10.0,
836
0
            max_concentration_pct: 20.0,
837
0
            global_limit: Price::new(10_000_000.0).unwrap_or_default(),
838
0
        }
839
0
    }
840
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/emergency_response.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/emergency_response.rs.html deleted file mode 100644 index 4391e4d3d..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/emergency_response.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/emergency_response.rs
Line
Count
Source
1
//! Emergency Response System
2
//!
3
//! Coordinates emergency responses across all safety systems including
4
//! loss limits monitoring, position tracking, and automated responses
5
//! to catastrophic risk scenarios.
6
7
use std::collections::HashMap;
8
use std::sync::Arc;
9
// Removed foxhunt_infrastructure - not available in this simplified risk crate
10
11
use chrono::{DateTime, Utc};
12
// REMOVED: Direct Decimal usage - use canonical types
13
use serde::{Deserialize, Serialize};
14
use tokio::sync::RwLock;
15
use tracing::{error, info};
16
17
use crate::error::RiskError;
18
use crate::risk_types::KillSwitchScope;
19
use crate::safety::kill_switch::AtomicKillSwitch;
20
use crate::safety::EmergencyResponseConfig;
21
use common::types::Price;
22
use rust_decimal::Decimal;
23
24
// AGENT 7: PRODUCTION SAFETY - Circuit breakers for risk management
25
// Removed production_safety module - not available in this simplified risk crate
26
use crate::circuit_breaker::CircuitBreakerConfig;
27
28
/// Emergency response system implementation
29
// Infrastructure - fields will be used for emergency response coordination
30
#[allow(dead_code)]
31
pub struct EmergencyResponseSystem {
32
    config: EmergencyResponseConfig,
33
    redis_url: String,
34
    kill_switch: Arc<AtomicKillSwitch>,
35
    pub concentration_metrics: Arc<RwLock<HashMap<String, ConcentrationMetrics>>>,
36
    event_history: Arc<RwLock<Vec<EmergencyEvent>>>,
37
}
38
39
/// Emergency event types
40
#[derive(Debug, Clone, Serialize, Deserialize)]
41
pub enum EmergencyEvent {
42
    ManualEmergency {
43
        user_id: String,
44
        reason: String,
45
        timestamp: DateTime<Utc>,
46
    },
47
}
48
49
/// Concentration metrics
50
#[derive(Debug, Clone, Serialize, Deserialize)]
51
pub struct ConcentrationMetrics {
52
    pub account_id: String,
53
    pub symbol_concentrations: HashMap<String, f64>,
54
    pub sector_concentrations: HashMap<String, f64>,
55
    pub total_exposure: Price,
56
    pub largest_position_pct: f64,
57
    pub timestamp: DateTime<Utc>,
58
}
59
60
/// Emergency P&L metrics (local to emergency response)
61
#[derive(Debug, Clone, Serialize, Deserialize)]
62
pub struct EmergencyPnLMetrics {
63
    pub account_id: String,
64
    pub daily_pnl: Decimal,
65
    pub unrealized_pnl: Decimal,
66
    pub max_drawdown: Price,
67
    pub timestamp: DateTime<Utc>,
68
    pub daily_realized_pnl: Price,
69
    pub daily_unrealized_pnl: Price,
70
    pub total_daily_pnl: Price,
71
    pub inception_pnl: Price,
72
    pub high_water_mark: Price,
73
    pub current_drawdown_pct: f64,
74
    pub max_drawdown_pct: f64,
75
    pub position_count: u32,
76
    pub total_exposure: Price,
77
}
78
79
impl EmergencyResponseSystem {
80
29
    pub async fn new(
81
29
        config: EmergencyResponseConfig,
82
29
        redis_url: String,
83
29
        kill_switch: Arc<AtomicKillSwitch>,
84
29
    ) -> Result<Self, RiskError> {
85
29
        Ok(Self {
86
29
            config,
87
29
            redis_url,
88
29
            kill_switch,
89
29
            concentration_metrics: Arc::new(RwLock::new(HashMap::new())),
90
29
            event_history: Arc::new(RwLock::new(Vec::new())),
91
29
        })
92
29
    }
93
94
4
    pub async fn update_pnl_metrics(&self, metrics: EmergencyPnLMetrics) -> Result<(), RiskError> {
95
        // Create circuit breaker config with correct field names
96
4
        let breaker_config = CircuitBreakerConfig {
97
4
            enabled: true,
98
4
            daily_loss_percentage: Decimal::try_from(0.02).unwrap_or_default().into(), // 2%
99
4
            position_limit_percentage: Decimal::try_from(0.25).unwrap_or_default().into(), // 25%
100
4
            max_consecutive_violations: 3,
101
4
            redis_url: "redis://localhost:6379".to_owned(),
102
4
            redis_key_prefix: "foxhunt_circuit_breaker".to_owned(),
103
4
            auto_recovery_enabled: true,
104
4
            portfolio_refresh_interval_secs: 60,
105
4
            cooldown_period_secs: 30,
106
4
        };
107
108
        // Check for emergency P&L thresholds - use unrealized_pnl as proxy for portfolio value
109
4
        let portfolio_value = metrics
110
4
            .unrealized_pnl
111
4
            .abs()
112
4
            .max(Decimal::try_from(100000.0).unwrap_or(Decimal::from(100000))); // Min $100k for calculation
113
4
        let daily_loss_pct = if portfolio_value > Decimal::ZERO {
114
4
            metrics.daily_pnl.abs() / portfolio_value
115
        } else {
116
0
            Decimal::ZERO
117
        };
118
119
4
        if daily_loss_pct
120
4
            >= breaker_config
121
4
                .daily_loss_percentage
122
4
                .to_decimal()
123
4
                .unwrap_or_default()
124
        {
125
2
            error!(
126
0
                "\u{1f6a8} EMERGENCY: Daily P&L limit exceeded for account {}: {:.2}%",
127
                metrics.account_id,
128
0
                daily_loss_pct * Decimal::from(100)
129
            );
130
2
            self.kill_switch
131
2
                .activate(
132
2
                    KillSwitchScope::Account(metrics.account_id.clone()),
133
2
                    format!(
134
2
                        "Daily P&L limit exceeded: {:.2}%",
135
2
                        daily_loss_pct * Decimal::from(100)
136
2
                    ),
137
2
                    "emergency_response_system".to_owned(),
138
2
                    true,
139
2
                )
140
2
                .await
141
2
                .map_err(|e| RiskError::Internal(
format!0
(
"Failed to activate kill switch: {e}"0
)))
?0
;
142
2
            return Err(RiskError::Internal("Daily P&L limit exceeded".to_owned()));
143
2
        }
144
145
2
        if metrics
146
2
            .max_drawdown
147
2
            .abs()
148
2
            .to_decimal()
149
2
            .map_err(|e| RiskError::TypeConversion {
150
0
                from_type: "Price".to_owned(),
151
0
                to_type: "Decimal".to_owned(),
152
0
                reason: format!("conversion failed: {e}"),
153
0
            })?
154
2
            >= Decimal::try_from(0.20).map_err(|e| RiskError::TypeConversion {
155
0
                from_type: "f64".to_owned(),
156
0
                to_type: "Decimal".to_owned(),
157
0
                reason: format!("conversion failed: {e}"),
158
0
            })?
159
        {
160
            // 20% drawdown limit
161
0
            error!(
162
0
                "\u{1f6a8} EMERGENCY: Max drawdown exceeded for account {}: {}",
163
                metrics.account_id, metrics.max_drawdown
164
            );
165
0
            self.kill_switch
166
0
                .activate(
167
0
                    KillSwitchScope::Account(metrics.account_id.clone()),
168
0
                    format!("Max drawdown exceeded: {}", metrics.max_drawdown),
169
0
                    "emergency_response_system".to_owned(),
170
0
                    true,
171
0
                )
172
0
                .await
173
0
                .map_err(|e| RiskError::Internal(format!("Failed to activate kill switch: {e}")))?;
174
0
            return Err(RiskError::Internal("Max drawdown exceeded".to_owned()));
175
2
        }
176
177
2
        info!(
178
0
            "\u{2705} P&L metrics updated for account {}",
179
            metrics.account_id
180
        );
181
2
        Ok(())
182
4
    }
183
184
8
    pub async fn update_concentration_metrics(
185
8
        &self,
186
8
        metrics: ConcentrationMetrics,
187
8
    ) -> Result<(), RiskError> {
188
8
        let mut concentration_metrics = self.concentration_metrics.write().await;
189
8
        concentration_metrics.insert(metrics.account_id.clone(), metrics);
190
8
        Ok(())
191
8
    }
192
193
11
    pub async fn handle_emergency_event(&self, event: EmergencyEvent) -> Result<(), RiskError> {
194
11
        let mut history = self.event_history.write().await;
195
11
        history.push(event);
196
11
        Ok(())
197
11
    }
198
199
4
    pub async fn get_recent_events(&self, limit: usize) -> Vec<EmergencyEvent> {
200
4
        let history = self.event_history.read().await;
201
4
        history.iter().rev().take(limit).cloned().collect()
202
4
    }
203
204
    /// Start monitoring services - required for safety coordinator
205
12
    pub async fn start_monitoring(&self) -> Result<(), RiskError> {
206
12
        info!(
"Starting emergency response monitoring for system"0
);
207
        // Initialize monitoring services
208
12
        Ok(())
209
12
    }
210
211
    /// Stop monitoring services - required for safety coordinator
212
10
    pub async fn stop_monitoring(&self) -> Result<(), RiskError> {
213
10
        info!(
"Stopping emergency response monitoring"0
);
214
        // Cleanup monitoring resources
215
10
        Ok(())
216
10
    }
217
218
    /// Handle manual emergency trigger - required for safety coordinator
219
5
    pub async fn handle_manual_emergency(
220
5
        &self,
221
5
        user: String,
222
5
        reason: String,
223
5
    ) -> Result<(), RiskError> {
224
5
        let event = EmergencyEvent::ManualEmergency {
225
5
            user_id: user,
226
5
            reason: reason.clone(),
227
5
            timestamp: Utc::now(),
228
5
        };
229
230
        // Store the event
231
5
        self.handle_emergency_event(event).await
?0
;
232
233
        // Activate global kill switch
234
5
        self.kill_switch
235
5
            .activate(
236
5
                KillSwitchScope::Global,
237
5
                format!("Manual emergency: {reason}"),
238
5
                "emergency_response_system".to_owned(),
239
5
                true,
240
5
            )
241
5
            .await
242
5
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to activate kill switch: {e}"0
)))
?0
;
243
244
5
        error!(
"\u{1f6a8} MANUAL EMERGENCY: {}"0
, reason);
245
5
        Ok(())
246
5
    }
247
248
    /// Check if emergency system is healthy - required for safety coordinator
249
4
    pub async fn is_healthy(&self) -> bool {
250
        // Check if the emergency system is functioning properly
251
        // For now, we'll consider it healthy if we can access the event history
252
4
        self.event_history.read().await.len() < 1000 // Arbitrary health check
253
4
    }
254
}
255
256
#[cfg(test)]
257
mod tests {
258
    use super::*;
259
    use crate::error::RiskResult;
260
    use crate::safety::KillSwitchConfig;
261
    use common::{Price, Symbol};
262
    use config::asset_classification::{AssetClass, AssetClassificationManager, MarketCapTier};
263
    // operations module removed - use direct imports from common
264
    // CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
265
266
15
    async fn create_test_system() -> RiskResult<(EmergencyResponseSystem, Arc<AtomicKillSwitch>)> {
267
15
        let kill_switch_config = KillSwitchConfig::default();
268
269
        // Use test-only constructor that doesn't require Redis connection
270
15
        let kill_switch = Arc::new(AtomicKillSwitch::new_test(kill_switch_config));
271
272
15
        let emergency_config = EmergencyResponseConfig::default();
273
        // Redis URL for emergency system (not actually used in tests)
274
15
        let redis_url = "redis://localhost:6379".to_string();
275
276
15
        let emergency_system =
277
15
            EmergencyResponseSystem::new(emergency_config, redis_url, kill_switch.clone()).await
?0
;
278
279
15
        Ok((emergency_system, kill_switch))
280
15
    }
281
282
    /// Calculate dynamic test concentration based on symbol and portfolio
283
    /// REPLACES: hardcoded 15% concentration
284
6
    fn calculate_test_concentration(symbol: &Symbol, portfolio_value: Price) -> f64 {
285
        // Create asset classification manager (in production, this would be injected/cached)
286
6
        let asset_manager = AssetClassificationManager::new();
287
288
        // Dynamic concentration based on asset class and market cap
289
6
        let base_concentration = match asset_manager.classify_symbol(symbol.as_str()) {
290
            AssetClass::Equity {
291
                market_cap: MarketCapTier::LargeCap,
292
                ..
293
0
            } => 12.0,
294
            AssetClass::Equity {
295
                market_cap: MarketCapTier::MidCap,
296
                ..
297
0
            } => 8.0,
298
            AssetClass::Equity {
299
                market_cap: MarketCapTier::SmallCap,
300
                ..
301
0
            } => 5.0,
302
            AssetClass::Equity {
303
                market_cap: MarketCapTier::MicroCap,
304
                ..
305
0
            } => 3.0,
306
0
            AssetClass::Crypto { .. } => 5.0, // Conservative for crypto
307
0
            AssetClass::Forex { .. } => 15.0, // Higher for forex due to leverage
308
0
            AssetClass::Future { .. } => 10.0, // Moderate for futures
309
6
            AssetClass::Unknown => 3.0,       // Very conservative for unknown assets
310
            _ => {
311
0
                tracing::error!("Unknown asset class in emergency response concentration calculation - using ultra-conservative limit");
312
0
                1.0 // Ultra-conservative 1% limit for unknown asset classes
313
            },
314
        };
315
316
        // Adjust based on portfolio size (larger portfolios can handle more concentration)
317
6
        let size_multiplier = if portfolio_value > Price::new(100000.0).unwrap_or(Price::ZERO) {
318
6
            1.2 // +20% for large portfolios ($100k+)
319
0
        } else if portfolio_value < Price::new(10000.0).unwrap_or(Price::ZERO) {
320
0
            0.7 // -30% for small portfolios (<$10k)
321
        } else {
322
0
            1.0 // No adjustment for medium portfolios
323
        };
324
325
6
        base_concentration * size_multiplier
326
6
    }
327
328
    #[tokio::test]
329
1
    async fn test_emergency_system_creation() -> RiskResult<()> {
330
1
        let (emergency_system, _) = create_test_system().await
?0
;
331
1
        assert!(emergency_system.config.enabled);
332
2
        Ok(())
333
1
    }
334
335
    #[tokio::test]
336
1
    async fn test_pnl_metrics_update() -> RiskResult<()> {
337
1
        let (emergency_system, _) = create_test_system().await
?0
;
338
339
1
        let metrics = EmergencyPnLMetrics {
340
1
            account_id: "test_account".to_string(),
341
1
            daily_pnl: Decimal::from(-1500),
342
1
            unrealized_pnl: Decimal::from(-1500),
343
1
            max_drawdown: Price::from_f64(0.10).unwrap_or(Price::ZERO), // 10% drawdown (below 20% threshold)
344
1
            timestamp: Utc::now(),
345
1
            daily_realized_pnl: Price::from_f64(-1000.0).unwrap_or(Price::ZERO),
346
1
            daily_unrealized_pnl: Price::from_f64(-500.0).unwrap_or(Price::ZERO),
347
1
            total_daily_pnl: Price::from_f64(-1500.0).unwrap_or(Price::ZERO),
348
1
            inception_pnl: Price::from_f64(100000.0).unwrap_or(Price::ZERO),
349
1
            high_water_mark: Price::from_f64(105000.0).unwrap_or(Price::ZERO),
350
1
            current_drawdown_pct: 5.0,
351
1
            max_drawdown_pct: 10.0,
352
1
            position_count: 5,
353
1
            total_exposure: Price::from_f64(100000.0).unwrap_or(Price::ZERO),
354
1
        };
355
356
1
        let result = emergency_system.update_pnl_metrics(metrics).await;
357
1
        if let Err(
e0
) = &result {
358
0
            eprintln!("Error: {:?}", e);
359
1
        }
360
1
        assert!(result.is_ok());
361
2
        Ok(())
362
1
    }
363
364
    #[tokio::test]
365
1
    async fn test_manual_emergency() -> RiskResult<()> {
366
1
        let (emergency_system, _kill_switch) = create_test_system().await
?0
;
367
368
1
        let result = emergency_system
369
1
            .handle_manual_emergency("test_user".to_string(), "Test emergency".to_string())
370
1
            .await;
371
372
1
        assert!(result.is_ok());
373
        // Check that manual emergency was handled
374
1
        let events = emergency_system.get_recent_events(10).await;
375
1
        assert!(!events.is_empty());
376
2
        Ok(())
377
1
    }
378
379
    #[tokio::test]
380
1
    async fn test_concentration_metrics() -> RiskResult<()> {
381
1
        let (emergency_system, _) = create_test_system().await
?0
;
382
383
1
        let mut symbol_concentrations = HashMap::new();
384
        // DYNAMIC: Calculate concentration based on portfolio diversity
385
1
        let dynamic_concentration = calculate_test_concentration(
386
1
            &Symbol::from("AAPL".to_string()),
387
1
            Price::from_f64(1000000.0)
?0
,
388
        );
389
1
        symbol_concentrations.insert("AAPL".to_string(), dynamic_concentration);
390
391
1
        let metrics = ConcentrationMetrics {
392
1
            account_id: "test_account".to_string(),
393
1
            symbol_concentrations,
394
1
            sector_concentrations: HashMap::new(),
395
1
            total_exposure: Price::from_f64(1000000.0)
?0
,
396
            largest_position_pct: 15.0,
397
1
            timestamp: Utc::now(),
398
        };
399
400
1
        let result = emergency_system.update_concentration_metrics(metrics).await;
401
1
        assert!(result.is_ok());
402
403
1
        let stored_metrics = emergency_system.concentration_metrics.read().await;
404
1
        assert!(stored_metrics.contains_key("test_account"));
405
2
        Ok(())
406
1
    }
407
408
    #[tokio::test]
409
1
    async fn test_event_history() -> RiskResult<()> {
410
1
        let (emergency_system, _) = create_test_system().await
?0
;
411
412
1
        let event = EmergencyEvent::ManualEmergency {
413
1
            user_id: "test_user".to_string(),
414
1
            reason: "Test event".to_string(),
415
1
            timestamp: Utc::now(),
416
1
        };
417
418
1
        emergency_system.handle_emergency_event(event).await
?0
;
419
420
1
        let events = emergency_system.get_recent_events(10).await;
421
1
        assert_eq!(events.len(), 1);
422
2
        Ok(())
423
1
    }
424
425
    #[tokio::test]
426
1
    async fn test_emergency_system_health() -> RiskResult<()> {
427
1
        let (emergency_system, _) = create_test_system().await
?0
;
428
429
        // System should be healthy initially
430
1
        assert!(emergency_system.is_healthy().await);
431
432
2
        Ok(())
433
1
    }
434
435
    #[tokio::test]
436
1
    async fn test_monitoring_lifecycle() -> RiskResult<()> {
437
1
        let (emergency_system, _) = create_test_system().await
?0
;
438
439
        // Start monitoring
440
1
        emergency_system.start_monitoring().await
?0
;
441
442
        // Stop monitoring
443
1
        emergency_system.stop_monitoring().await
?0
;
444
445
2
        Ok(())
446
1
    }
447
448
    #[tokio::test]
449
1
    async fn test_pnl_emergency_triggers_kill_switch() -> RiskResult<()> {
450
1
        let (emergency_system, _kill_switch) = create_test_system().await
?0
;
451
452
1
        let metrics = EmergencyPnLMetrics {
453
1
            account_id: "test_account".to_string(),
454
1
            daily_pnl: Decimal::from(-25000),      // Large loss
455
1
            unrealized_pnl: Decimal::from(100000), // Portfolio value for calc
456
1
            max_drawdown: Price::from_f64(25000.0).unwrap_or(Price::ZERO),
457
1
            timestamp: Utc::now(),
458
1
            daily_realized_pnl: Price::from_f64(-15000.0).unwrap_or(Price::ZERO),
459
1
            daily_unrealized_pnl: Price::from_f64(-10000.0).unwrap_or(Price::ZERO),
460
1
            total_daily_pnl: Price::from_f64(-25000.0).unwrap_or(Price::ZERO),
461
1
            inception_pnl: Price::from_f64(50000.0).unwrap_or(Price::ZERO),
462
1
            high_water_mark: Price::from_f64(125000.0).unwrap_or(Price::ZERO),
463
1
            current_drawdown_pct: 20.0,
464
1
            max_drawdown_pct: 20.0,
465
1
            position_count: 5,
466
1
            total_exposure: Price::from_f64(100000.0).unwrap_or(Price::ZERO),
467
1
        };
468
469
        // This should trigger emergency response
470
1
        let result = emergency_system.update_pnl_metrics(metrics).await;
471
472
        // Should fail due to limit breach
473
1
        assert!(result.is_err());
474
475
2
        Ok(())
476
1
    }
477
478
    #[tokio::test]
479
1
    async fn test_drawdown_emergency_triggers_kill_switch() -> RiskResult<()> {
480
1
        let (emergency_system, _) = create_test_system().await
?0
;
481
482
1
        let metrics = EmergencyPnLMetrics {
483
1
            account_id: "test_account".to_string(),
484
1
            daily_pnl: Decimal::from(-5000),
485
1
            unrealized_pnl: Decimal::from(100000),
486
1
            max_drawdown: Price::from_f64(0.25).unwrap_or(Price::ZERO), // 25% drawdown - exceeds limit
487
1
            timestamp: Utc::now(),
488
1
            daily_realized_pnl: Price::from_f64(-3000.0).unwrap_or(Price::ZERO),
489
1
            daily_unrealized_pnl: Price::from_f64(-2000.0).unwrap_or(Price::ZERO),
490
1
            total_daily_pnl: Price::from_f64(-5000.0).unwrap_or(Price::ZERO),
491
1
            inception_pnl: Price::from_f64(75000.0).unwrap_or(Price::ZERO),
492
1
            high_water_mark: Price::from_f64(100000.0).unwrap_or(Price::ZERO),
493
1
            current_drawdown_pct: 25.0,
494
1
            max_drawdown_pct: 25.0,
495
1
            position_count: 3,
496
1
            total_exposure: Price::from_f64(80000.0).unwrap_or(Price::ZERO),
497
1
        };
498
499
1
        let result = emergency_system.update_pnl_metrics(metrics).await;
500
501
        // Should fail due to drawdown breach
502
1
        assert!(result.is_err());
503
504
2
        Ok(())
505
1
    }
506
507
    #[tokio::test]
508
1
    async fn test_multiple_concentration_updates() -> RiskResult<()> {
509
1
        let (emergency_system, _) = create_test_system().await
?0
;
510
511
6
        for 
i5
in 0..5 {
512
5
            let mut symbol_concentrations = HashMap::new();
513
5
            let account_id = format!("account_{}", i);
514
515
5
            let dynamic_concentration = calculate_test_concentration(
516
5
                &Symbol::from("AAPL".to_string()),
517
5
                Price::from_f64(1000000.0)
?0
,
518
            );
519
5
            symbol_concentrations.insert("AAPL".to_string(), dynamic_concentration);
520
521
5
            let metrics = ConcentrationMetrics {
522
5
                account_id: account_id.clone(),
523
5
                symbol_concentrations,
524
5
                sector_concentrations: HashMap::new(),
525
5
                total_exposure: Price::from_f64(1000000.0)
?0
,
526
                largest_position_pct: 15.0,
527
5
                timestamp: Utc::now(),
528
            };
529
530
5
            emergency_system
531
5
                .update_concentration_metrics(metrics)
532
5
                .await
?0
;
533
        }
534
535
1
        let stored_metrics = emergency_system.concentration_metrics.read().await;
536
1
        assert_eq!(stored_metrics.len(), 5);
537
538
2
        Ok(())
539
1
    }
540
541
    #[tokio::test]
542
1
    async fn test_event_history_ordering() -> RiskResult<()> {
543
1
        let (emergency_system, _) = create_test_system().await
?0
;
544
545
        // Add multiple events
546
6
        for 
i5
in 0..5 {
547
5
            let event = EmergencyEvent::ManualEmergency {
548
5
                user_id: format!("user_{}", i),
549
5
                reason: format!("Event {}", i),
550
5
                timestamp: Utc::now(),
551
5
            };
552
5
            emergency_system.handle_emergency_event(event).await
?0
;
553
5
            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
554
        }
555
556
        // Get recent events (should be in reverse order)
557
1
        let events = emergency_system.get_recent_events(3).await;
558
1
        assert_eq!(events.len(), 3);
559
560
2
        Ok(())
561
1
    }
562
563
    #[tokio::test]
564
1
    async fn test_concentration_metric_retrieval() -> RiskResult<()> {
565
1
        let (emergency_system, _) = create_test_system().await
?0
;
566
567
1
        let mut symbol_concentrations = HashMap::new();
568
1
        symbol_concentrations.insert("AAPL".to_string(), 12.0);
569
1
        symbol_concentrations.insert("GOOGL".to_string(), 10.0);
570
571
1
        let metrics = ConcentrationMetrics {
572
1
            account_id: "test_account".to_string(),
573
1
            symbol_concentrations,
574
1
            sector_concentrations: HashMap::new(),
575
1
            total_exposure: Price::from_f64(1000000.0)
?0
,
576
            largest_position_pct: 12.0,
577
1
            timestamp: Utc::now(),
578
        };
579
580
1
        emergency_system
581
1
            .update_concentration_metrics(metrics)
582
1
            .await
?0
;
583
584
1
        let stored = emergency_system.concentration_metrics.read().await;
585
1
        let account_metrics = stored.get("test_account").unwrap();
586
1
        assert_eq!(account_metrics.symbol_concentrations.len(), 2);
587
588
2
        Ok(())
589
1
    }
590
591
    #[tokio::test]
592
1
    async fn test_emergency_response_under_normal_pnl() -> RiskResult<()> {
593
1
        let (emergency_system, _) = create_test_system().await
?0
;
594
595
1
        let metrics = EmergencyPnLMetrics {
596
1
            account_id: "test_account".to_string(),
597
1
            daily_pnl: Decimal::from(1000), // Small gain
598
1
            unrealized_pnl: Decimal::from(100000),
599
1
            max_drawdown: Price::from_f64(0.05).unwrap_or(Price::ZERO), // 5% drawdown (below 20% threshold)
600
1
            timestamp: Utc::now(),
601
1
            daily_realized_pnl: Price::from_f64(500.0).unwrap_or(Price::ZERO),
602
1
            daily_unrealized_pnl: Price::from_f64(500.0).unwrap_or(Price::ZERO),
603
1
            total_daily_pnl: Price::from_f64(1000.0).unwrap_or(Price::ZERO),
604
1
            inception_pnl: Price::from_f64(120000.0).unwrap_or(Price::ZERO),
605
1
            high_water_mark: Price::from_f64(122000.0).unwrap_or(Price::ZERO),
606
1
            current_drawdown_pct: 1.6,
607
1
            max_drawdown_pct: 5.0,
608
1
            position_count: 3,
609
1
            total_exposure: Price::from_f64(50000.0).unwrap_or(Price::ZERO),
610
1
        };
611
612
        // Should succeed with normal P&L
613
1
        let result = emergency_system.update_pnl_metrics(metrics).await;
614
1
        if let Err(
e0
) = &result {
615
0
            eprintln!("Error: {:?}", e);
616
1
        }
617
1
        assert!(result.is_ok());
618
619
2
        Ok(())
620
1
    }
621
622
    #[tokio::test]
623
1
    async fn test_manual_emergency_creates_event() -> RiskResult<()> {
624
1
        let (emergency_system, _) = create_test_system().await
?0
;
625
626
1
        emergency_system
627
1
            .handle_manual_emergency("admin".to_string(), "Manual halt for testing".to_string())
628
1
            .await
?0
;
629
630
1
        let events = emergency_system.get_recent_events(10).await;
631
1
        assert_eq!(events.len(), 1);
632
633
1
        match &events[0] {
634
1
            EmergencyEvent::ManualEmergency {
635
1
                user_id, reason, ..
636
1
            } => {
637
1
                assert_eq!(user_id, "admin");
638
1
                assert_eq!(reason, "Manual halt for testing");
639
1
            },
640
1
        }
641
1
642
1
        Ok(())
643
1
    }
644
645
    #[tokio::test]
646
1
    async fn test_concentration_metrics_with_sectors() -> RiskResult<()> {
647
1
        let (emergency_system, _) = create_test_system().await
?0
;
648
649
1
        let mut symbol_concentrations = HashMap::new();
650
1
        symbol_concentrations.insert("AAPL".to_string(), 12.0);
651
652
1
        let mut sector_concentrations = HashMap::new();
653
1
        sector_concentrations.insert("Technology".to_string(), 35.0);
654
1
        sector_concentrations.insert("Healthcare".to_string(), 25.0);
655
656
1
        let metrics = ConcentrationMetrics {
657
1
            account_id: "diversified_account".to_string(),
658
1
            symbol_concentrations,
659
1
            sector_concentrations,
660
1
            total_exposure: Price::from_f64(2000000.0)
?0
,
661
            largest_position_pct: 12.0,
662
1
            timestamp: Utc::now(),
663
        };
664
665
1
        emergency_system
666
1
            .update_concentration_metrics(metrics)
667
1
            .await
?0
;
668
669
1
        let stored = emergency_system.concentration_metrics.read().await;
670
1
        let account_metrics = stored.get("diversified_account").unwrap();
671
1
        assert_eq!(account_metrics.sector_concentrations.len(), 2);
672
673
2
        Ok(())
674
1
    }
675
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/kill_switch.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/kill_switch.rs.html deleted file mode 100644 index faf2fc1ca..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/kill_switch.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/kill_switch.rs
Line
Count
Source
1
//! Kill switch implementations for emergency stops
2
3
use super::KillSwitchConfig;
4
use crate::error::{RiskError, RiskResult};
5
use crate::risk_types::KillSwitchScope;
6
use chrono::Utc;
7
use redis::{AsyncCommands, Client as RedisClient};
8
use std::collections::HashMap;
9
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10
use std::sync::Arc;
11
use tokio::sync::RwLock;
12
13
/// Atomic kill switch for emergency trading stops
14
#[derive(Debug)]
15
pub struct AtomicKillSwitch {
16
    triggered: Arc<AtomicBool>,
17
    config: KillSwitchConfig,
18
    redis_client: Option<RedisClient>, // Optional for tests
19
    scoped_triggers: Arc<RwLock<HashMap<String, bool>>>,
20
    // Metrics tracking
21
    health_check_count: Arc<AtomicU64>,
22
    command_count: Arc<AtomicU64>,
23
    failure_count: Arc<AtomicU64>,
24
}
25
26
impl AtomicKillSwitch {
27
    /// Create a new `AtomicKillSwitch` with Redis connectivity
28
0
    pub async fn new(config: KillSwitchConfig, redis_url: String) -> RiskResult<Self> {
29
0
        let redis_client = RedisClient::open(redis_url)
30
0
            .map_err(|e| RiskError::Config(format!("Failed to connect to Redis: {e}")))?;
31
32
        // Test Redis connectivity
33
0
        let mut conn = redis_client
34
0
            .get_multiplexed_async_connection()
35
0
            .await
36
0
            .map_err(|e| RiskError::Config(format!("Failed to establish Redis connection: {e}")))?;
37
38
        // Verify Redis is accessible using AsyncCommands trait
39
0
        redis::cmd("PING")
40
0
            .exec_async(&mut conn)
41
0
            .await
42
0
            .map_err(|e| RiskError::Config(format!("Redis ping failed: {e}")))?;
43
44
0
        Ok(Self {
45
0
            triggered: Arc::new(AtomicBool::new(false)),
46
0
            config,
47
0
            redis_client: Some(redis_client),
48
0
            scoped_triggers: Arc::new(RwLock::new(HashMap::new())),
49
0
            health_check_count: Arc::new(AtomicU64::new(0)),
50
0
            command_count: Arc::new(AtomicU64::new(0)),
51
0
            failure_count: Arc::new(AtomicU64::new(0)),
52
0
        })
53
0
    }
54
55
    /// Engage the kill switch for a specific scope
56
20
    pub async fn engage(
57
20
        &self,
58
20
        scope: KillSwitchScope,
59
20
        reason: String,
60
20
        user_id: String,
61
20
        cascade: bool,
62
20
    ) -> RiskResult<()> {
63
        // Track command execution
64
20
        self.command_count.fetch_add(1, Ordering::Relaxed);
65
66
        // Set local state immediately
67
20
        if scope == KillSwitchScope::Global {
68
10
            self.triggered.store(true, Ordering::SeqCst);
69
10
        } else {
70
10
            let scope_key = self.scope_to_key(&scope);
71
10
            let mut scoped = self.scoped_triggers.write().await;
72
10
            scoped.insert(scope_key.clone(), true);
73
74
            // Implement cascade logic
75
10
            if cascade {
76
3
                match &scope {
77
1
                    KillSwitchScope::Portfolio(id) => {
78
1
                        // Cascade: Portfolio halt also halts all its strategies
79
1
                        // Store cascade flag for broader halt interpretation
80
1
                        scoped.insert(format!("cascade:portfolio:{id}"), true);
81
1
                    }
82
0
                    KillSwitchScope::Strategy(id) => {
83
0
                        // Cascade: Strategy halt can affect related strategies
84
0
                        scoped.insert(format!("cascade:strategy:{id}"), true);
85
0
                    }
86
2
                    _ => {}
87
                }
88
7
            }
89
        }
90
91
        // Broadcast to Redis for distributed coordination (if available)
92
20
        if let Some(
ref client0
) = self.redis_client {
93
0
            match client.get_multiplexed_async_connection().await {
94
0
                Ok(mut conn) => {
95
0
                    let channel = self.scope_to_channel(&scope);
96
0
                    let message = serde_json::json!({
97
0
                        "action": "engage",
98
0
                        "scope": scope,
99
0
                        "reason": reason,
100
0
                        "user_id": user_id,
101
0
                        "cascade": cascade,
102
0
                        "timestamp": Utc::now().to_rfc3339()
103
                    });
104
105
0
                    if let Err(e) = conn.publish::<_, _, ()>(&channel, message.to_string()).await {
106
0
                        self.failure_count.fetch_add(1, Ordering::Relaxed);
107
0
                        return Err(RiskError::Config(format!("Failed to publish to Redis: {e}")));
108
0
                    }
109
                }
110
0
                Err(e) => {
111
0
                    self.failure_count.fetch_add(1, Ordering::Relaxed);
112
0
                    return Err(RiskError::Config(format!("Failed to get Redis connection: {e}")));
113
                }
114
            }
115
20
        }
116
117
20
        Ok(())
118
20
    }
119
120
    /// Check if trading is allowed for a specific scope
121
    ///
122
    /// FAIL-SAFE MODE: If unable to verify status (lock contention), trading is BLOCKED
123
    /// This ensures safety - we never allow trading when we cannot confirm it's safe.
124
    #[must_use]
125
56
    pub fn is_trading_allowed(&self, scope: &KillSwitchScope) -> bool {
126
        // Check global kill switch first
127
56
        if self.triggered.load(Ordering::SeqCst) {
128
2
            return false;
129
54
        }
130
131
        // Check scoped kill switches
132
54
        match scope {
133
16
            KillSwitchScope::Global => true, // Already checked above
134
            _ => {
135
38
                let scope_key = self.scope_to_key(scope);
136
                // FAIL-SAFE: If we can't read the lock (contention), block trading
137
                // This is safer than allowing trading when we cannot verify status
138
38
                if let Ok(scoped) = self.scoped_triggers.try_read() {
139
                    // Check both the specific scope and cascade flags
140
38
                    let is_blocked = scoped.get(&scope_key).copied().unwrap_or(false);
141
142
                    // Check cascade halts that might affect this scope
143
38
                    let cascade_blocked = match scope {
144
3
                        KillSwitchScope::Strategy(_id) => {
145
                            // Check if parent portfolio has cascade halt
146
3
                            scoped.iter().any(|(k, &v)| 
{0
147
0
                                v && k.starts_with("cascade:portfolio:")
148
                                    // In production, would check if strategy belongs to halted portfolio
149
0
                            })
150
                        }
151
35
                        _ => false,
152
                    };
153
154
38
                    !is_blocked && 
!cascade_blocked33
155
                } else {
156
                    // FAIL-SAFE: Cannot verify status -> block trading
157
0
                    false
158
                }
159
            },
160
        }
161
56
    }
162
163
    /// Trigger the kill switch
164
4
    pub fn trigger(&self) {
165
4
        self.triggered.store(true, Ordering::SeqCst);
166
4
    }
167
168
    /// Check if kill switch is triggered
169
    #[must_use]
170
7
    pub fn is_triggered(&self) -> bool {
171
7
        self.triggered.load(Ordering::SeqCst)
172
7
    }
173
174
    /// Reset the kill switch
175
4
    pub async fn reset(&self, scope: Option<KillSwitchScope>) -> RiskResult<()> {
176
        // Track command execution
177
4
        self.command_count.fetch_add(1, Ordering::Relaxed);
178
179
4
        match scope {
180
1
            Some(KillSwitchScope::Global) | None => {
181
1
                self.triggered.store(false, Ordering::SeqCst);
182
1
            },
183
3
            Some(ref s) => {
184
3
                let scope_key = self.scope_to_key(s);
185
3
                let mut scoped = self.scoped_triggers.write().await;
186
3
                scoped.remove(&scope_key);
187
188
                // Also remove cascade flags for this scope
189
3
                match s {
190
0
                    KillSwitchScope::Portfolio(id) => {
191
0
                        scoped.remove(&format!("cascade:portfolio:{id}"));
192
0
                    }
193
1
                    KillSwitchScope::Strategy(id) => {
194
1
                        scoped.remove(&format!("cascade:strategy:{id}"));
195
1
                    }
196
2
                    _ => {}
197
                }
198
            },
199
        }
200
201
        // Broadcast reset to Redis (if available)
202
4
        if let Some(scope) = scope {
203
4
            if let Some(
ref client0
) = self.redis_client {
204
0
                match client.get_multiplexed_async_connection().await {
205
0
                    Ok(mut conn) => {
206
0
                        let channel = self.scope_to_channel(&scope);
207
0
                        let message = serde_json::json!({
208
0
                            "action": "reset",
209
0
                            "scope": scope,
210
0
                            "timestamp": Utc::now().to_rfc3339()
211
                        });
212
213
0
                        if let Err(e) = conn.publish::<_, _, ()>(&channel, message.to_string()).await {
214
0
                            self.failure_count.fetch_add(1, Ordering::Relaxed);
215
0
                            return Err(RiskError::Config(format!("Failed to publish reset to Redis: {e}")));
216
0
                        }
217
                    }
218
0
                    Err(e) => {
219
0
                        self.failure_count.fetch_add(1, Ordering::Relaxed);
220
0
                        return Err(RiskError::Config(format!("Failed to get Redis connection: {e}")));
221
                    }
222
                }
223
4
            }
224
0
        }
225
226
4
        Ok(())
227
4
    }
228
229
    /// Convert scope to Redis channel name
230
0
    fn scope_to_channel(&self, scope: &KillSwitchScope) -> String {
231
0
        match scope {
232
0
            KillSwitchScope::Global => self.config.global_channel.clone(),
233
0
            KillSwitchScope::Portfolio(id) => {
234
0
                format!("{}:portfolio:{}", self.config.strategy_channel_prefix, id)
235
            },
236
0
            KillSwitchScope::Strategy(id) => {
237
0
                format!("{}:{}", self.config.strategy_channel_prefix, id)
238
            },
239
0
            KillSwitchScope::Instrument(id) => {
240
0
                format!("{}:instrument:{}", self.config.symbol_channel_prefix, id)
241
            },
242
0
            KillSwitchScope::Symbol(id) => format!("{}:{}", self.config.symbol_channel_prefix, id),
243
0
            KillSwitchScope::Account(id) => {
244
0
                format!("{}:account:{}", self.config.strategy_channel_prefix, id)
245
            },
246
        }
247
0
    }
248
249
    /// Convert scope to internal key
250
51
    fn scope_to_key(&self, scope: &KillSwitchScope) -> String {
251
51
        match scope {
252
0
            KillSwitchScope::Global => "global".to_owned(),
253
2
            KillSwitchScope::Portfolio(id) => format!("portfolio:{id}"),
254
5
            KillSwitchScope::Strategy(id) => format!("strategy:{id}"),
255
0
            KillSwitchScope::Instrument(id) => format!("instrument:{id}"),
256
31
            KillSwitchScope::Symbol(id) => format!("symbol:{id}"),
257
13
            KillSwitchScope::Account(id) => format!("account:{id}"),
258
        }
259
51
    }
260
261
    /// Activate global kill switch
262
4
    pub async fn activate_global(&self, reason: String, user: String) -> RiskResult<()> {
263
4
        self.engage(KillSwitchScope::Global, reason, user, true)
264
4
            .await
265
4
    }
266
267
    /// Deactivate a scoped kill switch
268
2
    pub async fn deactivate(&self, scope: KillSwitchScope, _user_id: String) -> RiskResult<()> {
269
2
        self.reset(Some(scope)).await
270
2
    }
271
272
    /// Check if the kill switch is active for any scope
273
20
    pub async fn is_active(&self) -> RiskResult<bool> {
274
20
        Ok(self.triggered.load(Ordering::SeqCst) || {
275
19
            if let Ok(scoped) = self.scoped_triggers.try_read() {
276
19
                scoped.values().any(|&active| active)
277
            } else {
278
0
                false
279
            }
280
        })
281
20
    }
282
283
    /// Check health status of the kill switch
284
5
    pub async fn is_healthy(&self) -> RiskResult<bool> {
285
        // Track health check
286
5
        self.health_check_count.fetch_add(1, Ordering::Relaxed);
287
288
        // If Redis is configured, try to ping it
289
5
        if let Some(
ref client0
) = self.redis_client {
290
0
            if let Ok(mut conn) = client.get_multiplexed_async_connection().await { if let Ok(()) = redis::cmd("PING").exec_async(&mut conn).await { Ok(true) } else {
291
0
                self.failure_count.fetch_add(1, Ordering::Relaxed);
292
0
                Ok(false)
293
            } } else {
294
0
                self.failure_count.fetch_add(1, Ordering::Relaxed);
295
0
                Ok(false)
296
            }
297
        } else {
298
            // No Redis configured, consider healthy (test mode)
299
5
            Ok(true)
300
        }
301
5
    }
302
303
    /// Get operational metrics
304
    ///
305
    /// Returns (`health_checks`, commands) - actual tracked values
306
    /// - `health_checks`: Total number of health checks performed
307
    /// - commands: Total number of kill switch commands executed (engage/reset)
308
    #[must_use]
309
3
    pub fn get_metrics(&self) -> (u64, u64) {
310
3
        let checks = self.health_check_count.load(Ordering::Relaxed);
311
3
        let commands = self.command_count.load(Ordering::Relaxed);
312
3
        (checks, commands)
313
3
    }
314
315
    /// Get health metrics
316
    ///
317
    /// Returns (`error_rate`, failures) - actual tracked values
318
    /// - `error_rate`: Ratio of failures to total operations
319
    /// - failures: Total number of failed operations
320
    #[must_use]
321
3
    pub fn get_health_metrics(&self) -> (f64, u64) {
322
3
        let failures = self.failure_count.load(Ordering::Relaxed);
323
3
        let total_ops = self.command_count.load(Ordering::Relaxed);
324
325
3
        let error_rate = if total_ops > 0 {
326
0
            failures as f64 / total_ops as f64
327
        } else {
328
3
            0.0
329
        };
330
331
3
        (error_rate, failures)
332
3
    }
333
334
    /// Activate a scoped kill switch (alias for engage)
335
9
    pub async fn activate(
336
9
        &self,
337
9
        scope: KillSwitchScope,
338
9
        reason: String,
339
9
        user_id: String,
340
9
        cascade: bool,
341
9
    ) -> RiskResult<()> {
342
9
        self.engage(scope, reason, user_id, cascade).await
343
9
    }
344
345
    /// Start monitoring (placeholder - no actual monitoring needed for basic implementation)
346
12
    pub async fn start_monitoring(&self) -> RiskResult<()> {
347
        // Basic kill switch doesn't require background monitoring
348
        // This could be extended to monitor Redis connectivity, etc.
349
12
        Ok(())
350
12
    }
351
352
    /// Stop monitoring (placeholder - no actual monitoring to stop)
353
10
    pub async fn stop_monitoring(&self) -> RiskResult<()> {
354
        // Basic kill switch doesn't require background monitoring
355
10
        Ok(())
356
10
    }
357
358
    /// Create a test-only kill switch without Redis dependency
359
    #[cfg(test)]
360
62
    pub fn new_test(config: KillSwitchConfig) -> Self {
361
        // No Redis client for tests - operations will be no-ops
362
62
        Self {
363
62
            triggered: Arc::new(AtomicBool::new(false)),
364
62
            config,
365
62
            redis_client: None,
366
62
            scoped_triggers: Arc::new(RwLock::new(HashMap::new())),
367
62
            health_check_count: Arc::new(AtomicU64::new(0)),
368
62
            command_count: Arc::new(AtomicU64::new(0)),
369
62
            failure_count: Arc::new(AtomicU64::new(0)),
370
62
        }
371
62
    }
372
}
373
374
/// Trading gate for controlled market access
375
pub struct TradingGate {
376
    open: Arc<AtomicBool>,
377
}
378
379
impl TradingGate {
380
    #[must_use]
381
1
    pub fn new(initially_open: bool) -> Self {
382
1
        Self {
383
1
            open: Arc::new(AtomicBool::new(initially_open)),
384
1
        }
385
1
    }
386
387
1
    pub fn open(&self) {
388
1
        self.open.store(true, Ordering::SeqCst);
389
1
    }
390
391
1
    pub fn close(&self) {
392
1
        self.open.store(false, Ordering::SeqCst);
393
1
    }
394
395
    #[must_use]
396
3
    pub fn is_open(&self) -> bool {
397
3
        self.open.load(Ordering::SeqCst)
398
3
    }
399
}
400
401
/// Unix socket kill switch for IPC control
402
// Infrastructure - fields will be used for Unix socket-based kill switch
403
#[allow(dead_code)]
404
pub struct UnixSocketKillSwitch {
405
    socket_path: String,
406
    kill_switch: AtomicKillSwitch,
407
}
408
409
impl UnixSocketKillSwitch {
410
0
    pub async fn new(
411
0
        socket_path: String,
412
0
        config: KillSwitchConfig,
413
0
        redis_url: String,
414
0
    ) -> RiskResult<Self> {
415
0
        let kill_switch = AtomicKillSwitch::new(config, redis_url).await?;
416
0
        Ok(Self {
417
0
            socket_path,
418
0
            kill_switch,
419
0
        })
420
0
    }
421
422
1
    pub fn trigger(&self) {
423
1
        self.kill_switch.trigger();
424
1
    }
425
426
    #[must_use]
427
2
    pub fn is_triggered(&self) -> bool {
428
2
        self.kill_switch.is_triggered()
429
2
    }
430
431
0
    pub async fn engage(
432
0
        &self,
433
0
        scope: KillSwitchScope,
434
0
        reason: String,
435
0
        user_id: String,
436
0
        cascade: bool,
437
0
    ) -> RiskResult<()> {
438
0
        self.kill_switch
439
0
            .engage(scope, reason, user_id, cascade)
440
0
            .await
441
0
    }
442
443
    #[must_use]
444
0
    pub fn is_trading_allowed(&self, scope: &KillSwitchScope) -> bool {
445
0
        self.kill_switch.is_trading_allowed(scope)
446
0
    }
447
448
    /// Create a test-only unix socket kill switch without Redis dependency
449
    #[cfg(test)]
450
1
    pub fn new_test(socket_path: String, config: KillSwitchConfig) -> Self {
451
1
        Self {
452
1
            socket_path,
453
1
            kill_switch: AtomicKillSwitch::new_test(config),
454
1
        }
455
1
    }
456
}
457
458
#[cfg(test)]
459
mod tests {
460
    use super::*;
461
462
14
    fn create_test_kill_switch() -> AtomicKillSwitch {
463
14
        let config = KillSwitchConfig::default();
464
14
        AtomicKillSwitch::new_test(config)
465
14
    }
466
467
    #[tokio::test]
468
1
    async fn test_kill_switch_creation() -> RiskResult<()> {
469
1
        let kill_switch = create_test_kill_switch();
470
1
        assert!(!kill_switch.is_triggered());
471
2
        Ok(())
472
1
    }
473
474
    #[tokio::test]
475
1
    async fn test_kill_switch_trigger() -> RiskResult<()> {
476
1
        let kill_switch = create_test_kill_switch();
477
478
        // Initially not triggered
479
1
        assert!(!kill_switch.is_triggered());
480
481
        // Trigger it
482
1
        kill_switch.trigger();
483
1
        assert!(kill_switch.is_triggered());
484
485
2
        Ok(())
486
1
    }
487
488
    #[tokio::test]
489
1
    async fn test_kill_switch_prevents_trading() -> RiskResult<()> {
490
1
        let kill_switch = create_test_kill_switch();
491
492
        // Initially trading allowed
493
1
        assert!(kill_switch.is_trading_allowed(&KillSwitchScope::Global));
494
495
        // Trigger kill switch
496
1
        kill_switch.trigger();
497
498
        // Now trading should be blocked
499
1
        assert!(!kill_switch.is_trading_allowed(&KillSwitchScope::Global));
500
501
2
        Ok(())
502
1
    }
503
504
    #[tokio::test]
505
1
    async fn test_kill_switch_global_activation() -> RiskResult<()> {
506
1
        let kill_switch = create_test_kill_switch();
507
508
1
        kill_switch
509
1
            .activate_global("Test emergency".to_string(), "test_user".to_string())
510
1
            .await
?0
;
511
512
1
        assert!(kill_switch.is_active().await
?0
);
513
1
        assert!(!kill_switch.is_trading_allowed(&KillSwitchScope::Global));
514
515
2
        Ok(())
516
1
    }
517
518
    #[tokio::test]
519
1
    async fn test_kill_switch_scoped_activation() -> RiskResult<()> {
520
1
        let kill_switch = create_test_kill_switch();
521
522
        // Activate for specific symbol
523
1
        kill_switch
524
1
            .engage(
525
1
                KillSwitchScope::Symbol("AAPL".to_string()),
526
1
                "Symbol-specific halt".to_string(),
527
1
                "test_user".to_string(),
528
1
                false,
529
1
            )
530
1
            .await
?0
;
531
532
        // Global should still be allowed
533
1
        assert!(kill_switch.is_trading_allowed(&KillSwitchScope::Global));
534
535
        // But symbol should be blocked
536
1
        assert!(!kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("AAPL".to_string())));
537
538
2
        Ok(())
539
1
    }
540
541
    #[tokio::test]
542
1
    async fn test_kill_switch_reset() -> RiskResult<()> {
543
1
        let kill_switch = create_test_kill_switch();
544
545
        // Trigger and verify
546
1
        kill_switch.trigger();
547
1
        assert!(kill_switch.is_triggered());
548
549
        // Reset
550
1
        kill_switch.reset(Some(KillSwitchScope::Global)).await
?0
;
551
552
        // Should be cleared
553
1
        assert!(!kill_switch.is_triggered());
554
555
2
        Ok(())
556
1
    }
557
558
    #[tokio::test]
559
1
    async fn test_kill_switch_scoped_reset() -> RiskResult<()> {
560
1
        let kill_switch = create_test_kill_switch();
561
562
1
        let scope = KillSwitchScope::Account("test_account".to_string());
563
564
        // Activate scoped kill switch
565
1
        kill_switch
566
1
            .engage(scope.clone(), "Test".to_string(), "user".to_string(), false)
567
1
            .await
?0
;
568
569
        // Verify blocked
570
1
        assert!(!kill_switch.is_trading_allowed(&scope));
571
572
        // Reset specific scope
573
1
        kill_switch.reset(Some(scope.clone())).await
?0
;
574
575
        // Should be allowed now
576
1
        assert!(kill_switch.is_trading_allowed(&scope));
577
578
2
        Ok(())
579
1
    }
580
581
    #[tokio::test]
582
1
    async fn test_kill_switch_multiple_scopes() -> RiskResult<()> {
583
1
        let kill_switch = create_test_kill_switch();
584
585
        // Activate multiple scopes
586
1
        kill_switch
587
1
            .engage(
588
1
                KillSwitchScope::Symbol("AAPL".to_string()),
589
1
                "Test".to_string(),
590
1
                "user".to_string(),
591
1
                false,
592
1
            )
593
1
            .await
?0
;
594
595
1
        kill_switch
596
1
            .engage(
597
1
                KillSwitchScope::Account("account1".to_string()),
598
1
                "Test".to_string(),
599
1
                "user".to_string(),
600
1
                false,
601
1
            )
602
1
            .await
?0
;
603
604
        // Both should be blocked
605
1
        assert!(!kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("AAPL".to_string())));
606
1
        assert!(!kill_switch.is_trading_allowed(&KillSwitchScope::Account("account1".to_string())));
607
608
        // But other scopes should be allowed
609
1
        assert!(kill_switch.is_trading_allowed(&KillSwitchScope::Symbol("GOOGL".to_string())));
610
611
2
        Ok(())
612
1
    }
613
614
    #[tokio::test]
615
1
    async fn test_kill_switch_cascade_behavior() -> RiskResult<()> {
616
1
        let kill_switch = create_test_kill_switch();
617
618
        // Activate with cascade=true
619
1
        kill_switch
620
1
            .engage(
621
1
                KillSwitchScope::Portfolio("portfolio1".to_string()),
622
1
                "Cascade test".to_string(),
623
1
                "user".to_string(),
624
1
                true,
625
1
            )
626
1
            .await
?0
;
627
628
        // Verify activation
629
1
        assert!(kill_switch.is_active().await
?0
);
630
631
2
        Ok(())
632
1
    }
633
634
    #[tokio::test]
635
1
    async fn test_kill_switch_deactivate() -> RiskResult<()> {
636
1
        let kill_switch = create_test_kill_switch();
637
638
1
        let scope = KillSwitchScope::Strategy("strategy1".to_string());
639
640
        // Activate
641
1
        kill_switch
642
1
            .activate(scope.clone(), "Test".to_string(), "user".to_string(), false)
643
1
            .await
?0
;
644
645
        // Deactivate
646
1
        kill_switch
647
1
            .deactivate(scope.clone(), "user".to_string())
648
1
            .await
?0
;
649
650
        // Should be allowed
651
1
        assert!(kill_switch.is_trading_allowed(&scope));
652
653
2
        Ok(())
654
1
    }
655
656
    #[tokio::test]
657
1
    async fn test_kill_switch_health_check() -> RiskResult<()> {
658
1
        let kill_switch = create_test_kill_switch();
659
660
        // Health check should pass
661
1
        assert!(kill_switch.is_healthy().await
?0
);
662
663
2
        Ok(())
664
1
    }
665
666
    #[tokio::test]
667
1
    async fn test_kill_switch_metrics() -> RiskResult<()> {
668
1
        let kill_switch = create_test_kill_switch();
669
670
1
        let (checks, commands) = kill_switch.get_metrics();
671
672
        // Initial metrics (currently simplified)
673
1
        assert_eq!(checks, 0);
674
1
        assert_eq!(commands, 0);
675
676
2
        Ok(())
677
1
    }
678
679
    #[tokio::test]
680
1
    async fn test_kill_switch_health_metrics() -> RiskResult<()> {
681
1
        let kill_switch = create_test_kill_switch();
682
683
1
        let (error_rate, failures) = kill_switch.get_health_metrics();
684
685
        // Initial health metrics (currently simplified)
686
1
        assert_eq!(error_rate, 0.0);
687
1
        assert_eq!(failures, 0);
688
689
2
        Ok(())
690
1
    }
691
692
    #[tokio::test]
693
1
    async fn test_kill_switch_monitoring_lifecycle() -> RiskResult<()> {
694
1
        let kill_switch = create_test_kill_switch();
695
696
        // Start monitoring
697
1
        kill_switch.start_monitoring().await
?0
;
698
699
        // Stop monitoring
700
1
        kill_switch.stop_monitoring().await
?0
;
701
702
2
        Ok(())
703
1
    }
704
705
    #[tokio::test]
706
1
    async fn test_trading_gate_operations() -> RiskResult<()> {
707
1
        let gate = TradingGate::new(true);
708
709
1
        assert!(gate.is_open());
710
711
1
        gate.close();
712
1
        assert!(!gate.is_open());
713
714
1
        gate.open();
715
1
        assert!(gate.is_open());
716
717
2
        Ok(())
718
1
    }
719
720
    #[tokio::test]
721
1
    async fn test_unix_socket_kill_switch() -> RiskResult<()> {
722
1
        let config = KillSwitchConfig::default();
723
1
        let unix_switch = UnixSocketKillSwitch::new_test(
724
1
            "/tmp/foxhunt_killswitch.sock".to_string(),
725
1
            config,
726
        );
727
728
1
        assert!(!unix_switch.is_triggered());
729
730
1
        unix_switch.trigger();
731
1
        assert!(unix_switch.is_triggered());
732
733
2
        Ok(())
734
1
    }
735
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/mod.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/mod.rs.html deleted file mode 100644 index 17ff81f79..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/mod.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/mod.rs
Line
Count
Source
1
//! Comprehensive safety systems for HFT trading
2
//!
3
//! This module provides production-grade safety mechanisms to prevent
4
//! catastrophic financial losses in high-frequency trading systems.
5
//!
6
//! # Safety Architecture
7
//! - Emergency kill switches with atomic broadcasting
8
//! - Position limits with hybrid enforcement
9
//! - ML model drift detection and cutoffs
10
//! - Market anomaly circuit breakers
11
//! - Loss limits and drawdown protection
12
//! - Real-time risk monitoring and alerts
13
14
pub mod emergency_response;
15
pub mod kill_switch;
16
pub mod position_limiter;
17
pub mod safety_coordinator;
18
pub mod trading_gate;
19
20
// REMOVED: pub use re-export anti-pattern eliminated
21
// Use direct imports: use crate::safety::kill_switch::{AtomicKillSwitch, TradingGate, UnixSocketKillSwitch};
22
pub mod unix_socket_kill_switch;
23
24
// AGGRESSIVE FIX: RE-EXPORT ALL CONFIG TYPES TO MAKE THEM PUBLIC
25
26
// REMOVED: Direct Decimal usage - use canonical types
27
28
// Using direct types only
29
30
use std::time::Duration;
31
// Removed foxhunt_infrastructure - not available in this simplified risk crate
32
33
use common::types::Price;
34
use serde::{Deserialize, Serialize};
35
// CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
36
37
/// Safety system configuration
38
#[derive(Debug, Clone, Serialize, Deserialize)]
39
/// `SafetyConfig` component.
40
pub struct SafetyConfig {
41
    /// Enable all safety systems
42
    pub enabled: bool,
43
44
    /// Kill switch configuration
45
    pub kill_switch: KillSwitchConfig,
46
47
    /// Position limit configuration
48
    pub position_limits: PositionLimiterConfig,
49
50
    /// Emergency response configuration
51
    pub emergency_response: EmergencyResponseConfig,
52
53
    /// Redis connection for broadcasting
54
    pub redis_url: String,
55
56
    /// Safety check timeout
57
    pub safety_check_timeout: Duration,
58
}
59
60
impl Default for SafetyConfig {
61
0
    fn default() -> Self {
62
        Self {
63
            enabled: true,
64
0
            kill_switch: KillSwitchConfig::default(),
65
0
            position_limits: PositionLimiterConfig::default(),
66
0
            emergency_response: EmergencyResponseConfig::default(),
67
0
            redis_url: std::env::var("REDIS_URL")
68
0
                .unwrap_or_else(|_| "redis://${REDIS_HOST:-localhost}:6379".to_owned()),
69
0
            safety_check_timeout: Duration::from_millis(10),
70
        }
71
0
    }
72
}
73
74
/// Kill switch configuration
75
#[derive(Debug, Clone, Serialize, Deserialize)]
76
/// `KillSwitchConfig` component.
77
pub struct KillSwitchConfig {
78
    pub enabled: bool,
79
    pub global_channel: String,
80
    pub strategy_channel_prefix: String,
81
    pub symbol_channel_prefix: String,
82
    pub auto_recovery_enabled: bool,
83
    pub auto_recovery_delay: Duration,
84
}
85
86
impl Default for KillSwitchConfig {
87
63
    fn default() -> Self {
88
63
        Self {
89
63
            enabled: true,
90
63
            global_channel: "foxhunt:safety:kill_switch:global".to_owned(),
91
63
            strategy_channel_prefix: "foxhunt:safety:kill_switch:strategy".to_owned(),
92
63
            symbol_channel_prefix: "foxhunt:safety:kill_switch:symbol".to_owned(),
93
63
            auto_recovery_enabled: true,
94
63
            auto_recovery_delay: Duration::from_secs(300), // 5 minutes
95
63
        }
96
63
    }
97
}
98
99
/// Position limiter configuration
100
#[derive(Debug, Clone, Serialize, Deserialize)]
101
/// `PositionLimiterConfig` component.
102
pub struct PositionLimiterConfig {
103
    pub enabled: bool,
104
    pub cache_ttl: Duration,
105
    pub rpc_check_threshold_percent: f64,
106
    pub max_position_per_symbol: f64,
107
    pub max_order_value: f64,
108
    pub max_daily_loss: f64,
109
}
110
111
impl Default for PositionLimiterConfig {
112
15
    fn default() -> Self {
113
15
        Self {
114
15
            enabled: true,
115
15
            cache_ttl: Duration::from_secs(60),
116
15
            rpc_check_threshold_percent: 0.8, // 80% of limit
117
15
            // DYNAMIC SCALING: Calculate limits based on portfolio value
118
15
            max_position_per_symbol: Self::calculate_position_limit(),
119
15
            max_order_value: Self::calculate_order_limit(),
120
15
            max_daily_loss: Self::calculate_daily_loss_limit(),
121
15
        }
122
15
    }
123
}
124
125
impl PositionLimiterConfig {
126
    /// Calculate dynamic position limit based on typical portfolio size
127
    /// REPLACES: hardcoded $1M limit
128
15
    fn calculate_position_limit() -> f64 {
129
        // Use environment variable or default to $2M portfolio assumption
130
15
        let portfolio_value = std::env::var("PORTFOLIO_VALUE")
131
15
            .unwrap_or_else(|_| "2000000.0".to_owned())
132
15
            .parse::<f64>()
133
15
            .unwrap_or(2_000_000.0);
134
135
        // 5% of portfolio value per symbol
136
15
        portfolio_value * 0.05
137
15
    }
138
139
    /// Calculate dynamic order value limit
140
    /// REPLACES: hardcoded $100K limit  
141
15
    fn calculate_order_limit() -> f64 {
142
15
        let portfolio_value = std::env::var("PORTFOLIO_VALUE")
143
15
            .unwrap_or_else(|_| "2000000.0".to_owned())
144
15
            .parse::<f64>()
145
15
            .unwrap_or(2_000_000.0);
146
147
        // 2% of portfolio value per order
148
15
        portfolio_value * 0.02
149
15
    }
150
151
    /// Calculate dynamic daily loss limit
152
    /// REPLACES: hardcoded $500K limit
153
15
    fn calculate_daily_loss_limit() -> f64 {
154
15
        let portfolio_value = std::env::var("PORTFOLIO_VALUE")
155
15
            .unwrap_or_else(|_| "2000000.0".to_owned())
156
15
            .parse::<f64>()
157
15
            .unwrap_or(2_000_000.0);
158
159
        // 10% of portfolio value daily loss limit
160
15
        portfolio_value * 0.10
161
15
    }
162
}
163
164
/// Emergency response configuration
165
#[derive(Debug, Clone, Serialize, Deserialize)]
166
/// `EmergencyResponseConfig` component.
167
pub struct EmergencyResponseConfig {
168
    pub enabled: bool,
169
    pub loss_check_interval: Duration,
170
    pub position_check_interval: Duration,
171
    pub max_consecutive_violations: u32,
172
    pub emergency_contacts: Vec<String>,
173
    pub max_daily_loss: Price,
174
    pub max_drawdown: Price,
175
}
176
177
impl Default for EmergencyResponseConfig {
178
30
    fn default() -> Self {
179
30
        Self {
180
30
            enabled: true,
181
30
            loss_check_interval: Duration::from_secs(10),
182
30
            position_check_interval: Duration::from_secs(5),
183
30
            max_consecutive_violations: 5,
184
30
            emergency_contacts: vec!["risk@foxhunt.com".to_owned()],
185
30
            max_daily_loss: Price::new(1000.0).unwrap_or(Price::ZERO), // $1000.00 daily loss limit
186
30
            max_drawdown: Price::new(5000.0).unwrap_or(Price::ZERO),   // $5000.00 max drawdown
187
30
        }
188
30
    }
189
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/position_limiter.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/position_limiter.rs.html deleted file mode 100644 index 19b97a075..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/position_limiter.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/position_limiter.rs
Line
Count
Source
1
//! Position Limiter with Hybrid Checking
2
//!
3
//! Provides ultra-fast position limit enforcement using local caching
4
//! with fallback to authoritative RPC checks for critical limits.
5
6
// #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // COMMENTED: Crate-level allows applied
7
8
use std::collections::HashMap;
9
use std::sync::Arc;
10
use std::time::{Duration, Instant};
11
12
use dashmap::DashMap;
13
// REMOVED: Direct Decimal usage - use canonical types
14
15
use crate::error::{RiskError, RiskResult};
16
use crate::kelly_sizing::KellySizer;
17
use crate::position_tracker::PositionTracker;
18
use crate::safety::PositionLimiterConfig;
19
use common::types::{Order, Price, Symbol};
20
use config::structures::KellyConfig;
21
use rust_decimal::Decimal;
22
// Use common::types::prelude for Symbol and Order
23
use crate::compliance::PositionLimit;
24
25
// Production HybridPositionLimiter implementation
26
pub struct HybridPositionLimiter {
27
    pub config: PositionLimiterConfig,
28
    /// Real-time position tracker integration
29
    position_tracker: Arc<PositionTracker>,
30
    /// Kelly criterion position sizer
31
    kelly_sizer: Arc<KellySizer>,
32
    /// Local position cache for fast access
33
    position_cache: Arc<DashMap<(String, Symbol), CachedPosition>>,
34
    /// Portfolio position limits by account
35
    position_limits: Arc<DashMap<String, HashMap<Symbol, PositionLimit>>>,
36
}
37
38
/// Cached position with timestamp for TTL management
39
#[derive(Debug, Clone)]
40
struct CachedPosition {
41
    quantity: f64,
42
    market_value: f64,
43
    last_updated: Instant,
44
    // Infrastructure - will be used for position tracking and caching
45
    #[allow(dead_code)]
46
    portfolio_id: String,
47
}
48
49
impl CachedPosition {
50
24
    fn is_expired(&self, ttl: Duration) -> bool {
51
24
        self.last_updated.elapsed() > ttl
52
24
    }
53
}
54
55
impl HybridPositionLimiter {
56
    #[must_use]
57
33
    pub fn new(config: PositionLimiterConfig) -> Self {
58
33
        let kelly_config = KellyConfig::default();
59
33
        Self {
60
33
            config,
61
33
            position_tracker: Arc::new(PositionTracker::new()),
62
33
            kelly_sizer: Arc::new(KellySizer::new(kelly_config)),
63
33
            position_cache: Arc::new(DashMap::new()),
64
33
            position_limits: Arc::new(DashMap::new()),
65
33
        }
66
33
    }
67
68
    /// Create with existing position tracker (for integration)
69
    #[must_use]
70
0
    pub fn with_position_tracker(
71
0
        config: PositionLimiterConfig,
72
0
        position_tracker: Arc<PositionTracker>,
73
0
    ) -> Self {
74
0
        let kelly_config = KellyConfig::default();
75
0
        Self {
76
0
            config,
77
0
            position_tracker,
78
0
            kelly_sizer: Arc::new(KellySizer::new(kelly_config)),
79
0
            position_cache: Arc::new(DashMap::new()),
80
0
            position_limits: Arc::new(DashMap::new()),
81
0
        }
82
0
    }
83
84
4
    pub async fn check_and_update(&self, order: &Order) -> Result<(), RiskError> {
85
        // Get current portfolio value for Kelly sizing
86
4
        let default_account = "default".to_owned();
87
4
        let account_id = order.account_id.as_ref().unwrap_or(&default_account);
88
4
        let portfolio_value = self
89
4
            .get_portfolio_value(account_id)
90
4
            .await
91
4
            .unwrap_or(Price::from_f64(100000.0).unwrap_or(Price::ZERO));
92
93
        // Calculate Kelly-based position size with fallback for insufficient trade history
94
4
        let kelly_position_size = match self.kelly_sizer.get_position_size(
95
4
            &order.symbol,
96
4
            &format!("{:?}", order.order_type), // Use order type as strategy identifier
97
4
            portfolio_value,
98
4
            order
99
4
                .price
100
4
                .unwrap_or(Price::from_f64(100.0).unwrap_or(Price::ONE)),
101
        ) {
102
2
            Ok(size) => size,
103
            Err(RiskError::DataUnavailable { .. }) => {
104
                // No trade history - use conservative default (10% of portfolio)
105
                // This allows position checks to work even without historical data
106
2
                (portfolio_value * 0.10).map_err(|_| RiskError::ValidationError {
107
0
                    message: "Failed to calculate default position size".to_owned(),
108
0
                })?
109
            }
110
0
            Err(e) => return Err(e), // Propagate other errors
111
        };
112
113
4
        let requested_position =
114
4
            Price::from_decimal(order.quantity.to_decimal().unwrap_or(Decimal::ZERO));
115
116
        // Check if requested position exceeds Kelly recommendation
117
4
        let kelly_limit = (kelly_position_size * 2.0)
?0
;
118
4
        if requested_position > kelly_limit {
119
1
            return Err(RiskError::PositionLimitExceeded {
120
1
                instrument: format!("Kelly-sized position for {}", "position"),
121
1
                current: requested_position,
122
1
                limit: kelly_limit,
123
1
            });
124
3
        }
125
126
3
        Ok(())
127
4
    }
128
129
23
    pub async fn update_position(
130
23
        &self,
131
23
        _account: &str,
132
23
        _symbol: &Symbol,
133
23
        _quantity: f64,
134
23
        _price: f64,
135
23
    ) {
136
23
        let cache_key = (_account.to_owned(), _symbol.clone());
137
23
        let cached_position = CachedPosition {
138
23
            quantity: _quantity,
139
23
            market_value: _quantity * _price,
140
23
            last_updated: Instant::now(),
141
23
            portfolio_id: _account.to_owned(), // Using account as portfolio ID for simplicity
142
23
        };
143
144
23
        self.position_cache.insert(cache_key, cached_position);
145
146
        // Update the position tracker with enhanced position tracking
147
23
        if let Ok(price_typed) = Price::from_f64(_price) {
148
23
            let _ = self.position_tracker.update_position_sync(
149
23
                _account.to_owned(),  // portfolio_id
150
23
                _symbol.to_string(),  // instrument_id
151
23
                "default".to_owned(), // strategy_id
152
23
                _quantity,            // quantity as f64
153
23
                price_typed,
154
23
            );
155
23
        
}0
156
23
    }
157
158
20
    pub async fn get_cached_position(&self, _account: &str, _symbol: &Symbol) -> Option<f64> {
159
20
        let cache_key = (_account.to_owned(), _symbol.clone());
160
161
        // Check local cache first (fast path)
162
20
        if let Some(cached) = self.position_cache.get(&cache_key) {
163
20
            if cached.is_expired(self.config.cache_ttl) {
164
0
                // Remove expired entry
165
0
                self.position_cache.remove(&cache_key);
166
0
            } else {
167
20
                return Some(cached.quantity);
168
            }
169
0
        }
170
171
        // Fallback to position tracker (slower but authoritative)
172
0
        if let Some(enhanced_position) = self
173
0
            .position_tracker
174
0
            .get_enhanced_position(&_account.to_owned(), &_symbol.to_string())
175
0
            .await
176
        {
177
0
            let quantity = enhanced_position.base_position.quantity.to_f64();
178
0
            let market_value = enhanced_position.base_position.market_value.to_f64();
179
180
            // Update cache with fresh data
181
0
            let cached_position = CachedPosition {
182
0
                quantity,
183
0
                market_value,
184
0
                last_updated: Instant::now(),
185
0
                portfolio_id: _account.to_owned(),
186
0
            };
187
0
            self.position_cache.insert(cache_key, cached_position);
188
189
0
            return Some(quantity);
190
0
        }
191
192
        // No position found
193
0
        None
194
20
    }
195
196
1
    pub async fn get_metrics(&self) -> PositionLimiterMetrics {
197
1
        PositionLimiterMetrics { total_checks: 1 }
198
1
    }
199
200
    /// Set position limit for an account and symbol
201
4
    pub async fn set_limit(&self, account: String, limit: PositionLimit) -> RiskResult<()> {
202
4
        let mut account_limits = self.position_limits.entry(account).or_default();
203
4
        account_limits.insert(limit.instrument_id.clone().into(), limit);
204
4
        Ok(())
205
4
    }
206
207
    /// Get all limits for an account
208
4
    pub async fn get_limits(&self, account: &str) -> Vec<PositionLimit> {
209
4
        self.position_limits
210
4
            .get(account)
211
4
            .map(|limits| 
limits.values()2
.
cloned2
().
collect2
())
212
4
            .unwrap_or_default()
213
4
    }
214
215
    /// Get portfolio value for Kelly sizing calculations
216
5
    async fn get_portfolio_value(&self, account_id: &str) -> Option<Price> {
217
        // In production, this would query the portfolio tracker for total account value
218
        // For now, we'll calculate from cached positions
219
5
        let mut total_value = Price::ZERO;
220
221
        // Sum up all position values for this account
222
5
        for 
entry3
in self.position_cache.iter() {
223
3
            let (account, _symbol) = entry.key();
224
3
            if account == account_id {
225
3
                let position = entry.value();
226
3
                total_value += Price::from_f64(position.market_value).unwrap_or(Price::ZERO);
227
3
            
}0
228
        }
229
230
        // If no positions found, return a default portfolio value based on account type
231
5
        if total_value <= Price::ZERO {
232
            // Default portfolio values based on account patterns
233
4
            let default_value = if account_id.contains("test") {
234
0
                10000.0 // $10k for test accounts
235
4
            } else if account_id.contains("demo") {
236
0
                50000.0 // $50k for demo accounts
237
            } else {
238
4
                100000.0 // $100k for production accounts
239
            };
240
4
            Some(Price::from_f64(default_value).unwrap_or(Price::ZERO))
241
        } else {
242
1
            Some(total_value)
243
        }
244
5
    }
245
246
    /// Get Kelly sizer for external access
247
    #[must_use]
248
1
    pub fn get_kelly_sizer(&self) -> Arc<KellySizer> {
249
1
        self.kelly_sizer.clone()
250
1
    }
251
}
252
253
#[derive(Debug)]
254
pub struct PositionLimiterMetrics {
255
    pub total_checks: u64,
256
}
257
258
#[cfg(test)]
259
mod tests {
260
    use super::*;
261
    // CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
262
    use common::types::{OrderSide, OrderType, Quantity};
263
264
19
    fn create_test_config() -> PositionLimiterConfig {
265
        // DYNAMIC SCALING: Use portfolio-based limits instead of hardcoded values
266
19
        let test_portfolio_value = 1_000_000.0; // $1M test portfolio
267
268
19
        PositionLimiterConfig {
269
19
            enabled: true,
270
19
            cache_ttl: Duration::from_secs(60),
271
19
            rpc_check_threshold_percent: 0.8,
272
19
            // DYNAMIC: 5% of portfolio per symbol instead of hardcoded $10k
273
19
            max_position_per_symbol: test_portfolio_value * 0.05,
274
19
            // DYNAMIC: 2% of portfolio per order instead of hardcoded $5k
275
19
            max_order_value: test_portfolio_value * 0.02,
276
19
            // DYNAMIC: 10% of portfolio daily loss limit instead of hardcoded $50k
277
19
            max_daily_loss: test_portfolio_value * 0.10,
278
19
        }
279
19
    }
280
281
2
    fn create_test_order() -> Order {
282
        // DYNAMIC: Use portfolio-based position sizing instead of hardcoded quantity
283
2
        let test_portfolio_value = 1_000_000.0; // $1M test portfolio
284
2
        let test_quantity = test_portfolio_value * 0.01 / 150.0; // 1% of portfolio at $150/share
285
286
2
        Order::new(
287
2
            Symbol::from("AAPL"),
288
2
            OrderSide::Buy,
289
2
            Quantity::from_f64(test_quantity).unwrap_or(Quantity::ZERO),
290
2
            Some(Price::from_f64(150.0).unwrap_or(Price::ONE)),
291
2
            OrderType::Limit,
292
        )
293
2
        .with_account_id("account_001".to_string())
294
2
    }
295
296
    #[tokio::test]
297
1
    async fn test_position_limiter_creation() {
298
1
        let config = create_test_config();
299
1
        let limiter = HybridPositionLimiter::new(config);
300
1
        assert!(limiter.config.enabled);
301
1
    }
302
303
    #[tokio::test]
304
1
    async fn test_set_and_get_limits() {
305
1
        let config = create_test_config();
306
1
        let limiter = HybridPositionLimiter::new(config);
307
308
1
        let limit = PositionLimit {
309
1
            instrument_id: "TEST_SYMBOL".to_string(),
310
1
            max_position_size: Price::new(10000.0).unwrap_or(Price::ZERO),
311
1
            max_daily_turnover: Price::new(50000.0).unwrap_or(Price::ZERO),
312
1
            concentration_limit: Price::new(0.8).unwrap_or(Price::ZERO),
313
1
            regulatory_basis: "Test Limit".to_string(),
314
1
        };
315
316
1
        let _ = limiter.set_limit("account_001".to_string(), limit).await;
317
318
1
        let limits = limiter.get_limits("account_001").await;
319
1
        assert_eq!(limits.len(), 1);
320
1
        assert_eq!(
321
1
            limits[0].max_position_size,
322
1
            Price::new(10000.0).unwrap_or(Price::ZERO)
323
1
        );
324
1
    }
325
326
    #[tokio::test]
327
1
    async fn test_cache_functionality() {
328
1
        let config = create_test_config();
329
1
        let limiter = HybridPositionLimiter::new(config);
330
331
        // Update position cache with dynamic test values
332
1
        let test_portfolio_value = 1_000_000.0; // $1M test portfolio
333
1
        let test_quantity = test_portfolio_value * 0.02 / 150.0; // 2% of portfolio at $150/share
334
1
        let test_price = 150.0;
335
1
        let symbol = Symbol::from("AAPL");
336
1
        limiter
337
1
            .update_position("account_001", &symbol, test_quantity, test_price)
338
1
            .await;
339
340
        // Check cached position
341
1
        let position = limiter.get_cached_position("account_001", &symbol).await;
342
1
        assert_eq!(position, Some(test_quantity));
343
1
    }
344
345
    #[tokio::test]
346
1
    async fn test_metrics_tracking() {
347
1
        let config = create_test_config();
348
1
        let limiter = HybridPositionLimiter::new(config);
349
350
1
        let order = create_test_order();
351
1
        let _result = limiter.check_and_update(&order).await;
352
353
1
        let metrics = limiter.get_metrics().await;
354
1
        assert_eq!(metrics.total_checks, 1);
355
1
    }
356
357
    #[tokio::test]
358
1
    async fn test_position_limit_applies_to() {
359
        // DYNAMIC: Calculate limits based on portfolio size instead of hardcoded values
360
1
        let test_portfolio_value = 1_000_000.0; // $1M test portfolio
361
362
1
        let limit = PositionLimit {
363
1
            instrument_id: "AAPL".to_string(),
364
1
            max_position_size: Price::new(test_portfolio_value * 0.05).unwrap_or(Price::ZERO), // 5% of portfolio
365
1
            max_daily_turnover: Price::new(test_portfolio_value * 0.10).unwrap_or(Price::ZERO), // 10% of portfolio
366
1
            concentration_limit: Price::new(0.05).unwrap_or(Price::ZERO), // 5% concentration limit
367
1
            regulatory_basis: "Dynamic Portfolio-Based Position Limit".to_string(),
368
1
        };
369
370
        // Verify the limits are percentage-based, not fixed dollar amounts
371
1
        assert!(limit.max_position_size > Price::ZERO);
372
1
        assert!(limit.max_daily_turnover > Price::ZERO);
373
1
    }
374
375
    #[tokio::test]
376
1
    async fn test_kelly_sizing_integration() {
377
1
        let config = create_test_config();
378
1
        let limiter = HybridPositionLimiter::new(config);
379
380
1
        let kelly_sizer = limiter.get_kelly_sizer();
381
1
        assert!(Arc::strong_count(&kelly_sizer) > 0);
382
1
    }
383
384
    #[tokio::test]
385
1
    async fn test_position_cache_expiry() {
386
        // Test cache expiry by directly testing the is_expired method
387
        // This avoids real time delays and tests the expiry logic directly
388
1
        let mut config = create_test_config();
389
1
        config.cache_ttl = Duration::from_secs(1); // 1 second TTL
390
1
        let limiter = HybridPositionLimiter::new(config);
391
392
1
        let symbol = Symbol::from("AAPL");
393
1
        limiter
394
1
            .update_position("account_001", &symbol, 100.0, 150.0)
395
1
            .await;
396
397
        // Position should be cached
398
1
        assert_eq!(
399
1
            limiter.get_cached_position("account_001", &symbol).await,
400
            Some(100.0)
401
        );
402
403
        // Directly test the expiry logic without waiting
404
1
        let cache_key = ("account_001".to_owned(), symbol.clone());
405
1
        if let Some(cached) = limiter.position_cache.get(&cache_key) {
406
1
            // Test with zero TTL - should always be expired
407
1
            assert!(cached.is_expired(Duration::from_nanos(0)),
408
1
                
"Position should be expired with zero TTL"0
);
409
1
410
1
            // Test with long TTL - should not be expired
411
1
            assert!(!cached.is_expired(Duration::from_secs(3600)),
412
1
                
"Position should not be expired with 1 hour TTL"0
);
413
1
        
}0
; // Add semicolon to drop temporary earlier
414
1
    }
415
416
    #[tokio::test]
417
1
    async fn test_concurrent_position_updates() {
418
1
        let config = create_test_config();
419
1
        let limiter = Arc::new(HybridPositionLimiter::new(config));
420
421
1
        let mut handles = vec![];
422
11
        
for 1
i10
in 0..10 {
423
10
            let lim = limiter.clone();
424
10
            let handle = tokio::spawn(async move {
425
10
                let symbol = Symbol::from("AAPL");
426
10
                lim.update_position(&format!("account_{}", i), &symbol, 100.0 + i as f64, 150.0)
427
10
                    .await;
428
10
            });
429
10
            handles.push(handle);
430
1
        }
431
1
432
11
        for 
handle10
in handles {
433
10
            handle.await.unwrap();
434
1
        }
435
1
436
1
        // Verify positions were updated
437
11
        for 
i10
in 0..10 {
438
10
            let symbol = Symbol::from("AAPL");
439
10
            let position = limiter
440
10
                .get_cached_position(&format!("account_{}", i), &symbol)
441
10
                .await;
442
10
            assert_eq!(position, Some(100.0 + i as f64));
443
1
        }
444
1
    }
445
446
    #[tokio::test]
447
1
    async fn test_multiple_symbols_per_account() {
448
1
        let config = create_test_config();
449
1
        let limiter = HybridPositionLimiter::new(config);
450
451
1
        let symbols = vec!["AAPL", "GOOGL", "MSFT"];
452
3
        
for (1
i, symbol_str) in
symbols.iter()1
.
enumerate1
() {
453
3
            let symbol = Symbol::from((*symbol_str).to_string());
454
3
            limiter
455
3
                .update_position("account_001", &symbol, 100.0 * (i + 1) as f64, 150.0)
456
3
                .await;
457
1
        }
458
1
459
1
        // Verify all positions are cached
460
3
        for (i, symbol_str) in 
symbols.iter()1
.
enumerate1
() {
461
3
            let symbol = Symbol::from((*symbol_str).to_string());
462
3
            let position = limiter.get_cached_position("account_001", &symbol).await;
463
3
            assert_eq!(position, Some(100.0 * (i + 1) as f64));
464
1
        }
465
1
    }
466
467
    #[tokio::test]
468
1
    async fn test_position_update_with_zero_quantity() {
469
1
        let config = create_test_config();
470
1
        let limiter = HybridPositionLimiter::new(config);
471
472
1
        let symbol = Symbol::from("AAPL");
473
1
        limiter
474
1
            .update_position("account_001", &symbol, 0.0, 150.0)
475
1
            .await;
476
477
1
        let position = limiter.get_cached_position("account_001", &symbol).await;
478
1
        assert_eq!(position, Some(0.0));
479
1
    }
480
481
    #[tokio::test]
482
1
    async fn test_order_validation_within_kelly_limits() {
483
        use crate::kelly_sizing::TradeOutcome;
484
        use chrono::Utc;
485
        use rust_decimal::prelude::FromPrimitive;
486
487
1
        let config = create_test_config();
488
1
        let limiter = HybridPositionLimiter::new(config);
489
490
        // Add trade history to satisfy Kelly minimum sample size (10+ trades)
491
1
        let symbol = Symbol::from("AAPL");
492
1
        let strategy_id = format!("{:?}", OrderType::Limit);
493
494
16
        for 
i15
in 0..15 {
495
15
            let outcome = TradeOutcome {
496
15
                symbol: symbol.clone(),
497
15
                strategy_id: strategy_id.clone(),
498
15
                entry_price: Price::from_f64(100.0).unwrap_or(Price::ZERO),
499
15
                exit_price: Price::from_f64(if i % 2 == 0 { 
105.08
} else {
95.07
})
500
15
                    .unwrap_or(Price::ZERO),
501
15
                quantity: Price::from_f64(10.0).unwrap_or(Price::ZERO),
502
15
                profit_loss: Decimal::from_f64(if i % 2 == 0 { 
50.08
} else {
-30.07
})
503
15
                    .unwrap_or(Decimal::ZERO),
504
15
                win: i % 2 == 0,
505
15
                trade_date: Utc::now(),
506
            };
507
15
            limiter
508
15
                .kelly_sizer
509
15
                .add_trade_outcome(outcome)
510
15
                .expect("Failed to add trade outcome");
511
        }
512
513
        // Create a small order that should pass Kelly limits
514
1
        let small_order = Order::new(
515
1
            symbol.clone(),
516
1
            OrderSide::Buy,
517
1
            Quantity::from_f64(10.0).unwrap_or(Quantity::ZERO),
518
1
            Some(Price::from_f64(150.0).unwrap_or(Price::ONE)),
519
1
            OrderType::Limit,
520
        )
521
1
        .with_account_id("account_001".to_string());
522
523
1
        let result = limiter.check_and_update(&small_order).await;
524
1
        assert!(
525
1
            result.is_ok(),
526
1
            
"Order validation should pass with sufficient Kelly history: {:?}"0
,
527
1
            
result0
.
err0
()
528
1
        );
529
1
    }
530
531
    #[tokio::test]
532
1
    async fn test_get_limits_for_nonexistent_account() {
533
1
        let config = create_test_config();
534
1
        let limiter = HybridPositionLimiter::new(config);
535
536
1
        let limits = limiter.get_limits("nonexistent_account").await;
537
1
        assert_eq!(limits.len(), 0);
538
1
    }
539
540
    #[tokio::test]
541
1
    async fn test_portfolio_value_calculation() {
542
1
        let config = create_test_config();
543
1
        let limiter = HybridPositionLimiter::new(config);
544
545
        // Add multiple positions
546
1
        let symbols = vec!["AAPL", "GOOGL", "MSFT"];
547
3
        for symbol_str in 
symbols1
.
iter1
() {
548
3
            let symbol = Symbol::from((*symbol_str).to_string());
549
3
            limiter
550
3
                .update_position("test_account", &symbol, 100.0, 150.0)
551
3
                .await;
552
        }
553
554
        // Portfolio value should reflect the positions
555
1
        let portfolio_value = limiter.get_portfolio_value("test_account").await;
556
1
        assert!(portfolio_value.is_some());
557
1
        assert!(portfolio_value.unwrap() > Price::ZERO);
558
1
    }
559
560
    #[tokio::test]
561
1
    async fn test_check_and_update_with_zero_portfolio() {
562
1
        let config = create_test_config();
563
1
        let limiter = HybridPositionLimiter::new(config);
564
565
1
        let order = create_test_order();
566
        // Without positions, portfolio value will be a default amount
567
1
        let result = limiter.check_and_update(&order).await;
568
        // Should succeed with default portfolio value
569
1
        assert!(result.is_ok());
570
1
    }
571
572
    #[tokio::test]
573
1
    async fn test_kelly_limit_exceeded() {
574
        use crate::kelly_sizing::TradeOutcome;
575
        use chrono::Utc;
576
        use rust_decimal::prelude::FromPrimitive;
577
578
1
        let config = create_test_config();
579
1
        let limiter = HybridPositionLimiter::new(config);
580
581
        // Add sufficient trade history
582
1
        let symbol = Symbol::from("AAPL");
583
1
        let strategy_id = format!("{:?}", OrderType::Limit);
584
585
16
        for 
i15
in 0..15 {
586
15
            let outcome = TradeOutcome {
587
15
                symbol: symbol.clone(),
588
15
                strategy_id: strategy_id.clone(),
589
15
                entry_price: Price::from_f64(100.0).unwrap_or(Price::ZERO),
590
15
                exit_price: Price::from_f64(if i % 2 == 0 { 
105.08
} else {
95.07
})
591
15
                    .unwrap_or(Price::ZERO),
592
15
                quantity: Price::from_f64(10.0).unwrap_or(Price::ZERO),
593
15
                profit_loss: Decimal::from_f64(if i % 2 == 0 { 
50.08
} else {
-30.07
})
594
15
                    .unwrap_or(Decimal::ZERO),
595
15
                win: i % 2 == 0,
596
15
                trade_date: Utc::now(),
597
            };
598
15
            limiter
599
15
                .kelly_sizer
600
15
                .add_trade_outcome(outcome)
601
15
                .expect("Failed to add trade outcome");
602
        }
603
604
        // Create an order that exceeds Kelly limit
605
1
        let large_order = Order::new(
606
1
            symbol.clone(),
607
1
            OrderSide::Buy,
608
1
            Quantity::from_f64(100000.0).unwrap_or(Quantity::ZERO), // Very large position
609
1
            Some(Price::from_f64(150.0).unwrap_or(Price::ONE)),
610
1
            OrderType::Limit,
611
        )
612
1
        .with_account_id("account_001".to_string());
613
614
1
        let result = limiter.check_and_update(&large_order).await;
615
1
        assert!(result.is_err(), 
"Should fail when exceeding Kelly limit"0
);
616
1
    }
617
618
    #[tokio::test]
619
1
    async fn test_cached_position_expiry() {
620
        // Test cache expiry logic by directly testing CachedPosition::is_expired
621
        // This tests the expiry mechanism without relying on real time delays
622
623
1
        let mut config = create_test_config();
624
1
        config.cache_ttl = Duration::from_secs(1); // 1 second TTL
625
1
        let limiter = HybridPositionLimiter::new(config);
626
627
1
        let symbol = Symbol::from("AAPL");
628
629
        // Add position to cache
630
1
        limiter
631
1
            .update_position("test_account", &symbol, 100.0, 150.0)
632
1
            .await;
633
634
        // Verify cache has the position
635
1
        let cache_key = ("test_account".to_owned(), symbol.clone());
636
1
        let cached = limiter.position_cache.get(&cache_key);
637
1
        assert!(cached.is_some(), 
"Position should be in cache"0
);
638
639
        // Test the is_expired method directly with different TTLs
640
1
        let cached_pos = cached.unwrap();
641
642
        // Should NOT be expired with a long TTL
643
1
        assert!(!cached_pos.is_expired(Duration::from_secs(3600)),
644
0
            "Position should not be expired with 1 hour TTL");
645
646
        // Should be expired with a zero TTL
647
1
        assert!(cached_pos.is_expired(Duration::from_nanos(0)),
648
0
            "Position should be expired with zero TTL");
649
650
1
        drop(cached_pos);
651
652
        // Verify position is still accessible (not yet expired with 1s TTL)
653
1
        let position = limiter.get_cached_position("test_account", &symbol).await;
654
1
        assert_eq!(position, Some(100.0), 
"Position should still be cached"0
);
655
1
    }
656
657
    #[tokio::test]
658
1
    async fn test_multiple_accounts_isolation() {
659
1
        let config = create_test_config();
660
1
        let limiter = HybridPositionLimiter::new(config);
661
662
1
        let symbol = Symbol::from("AAPL");
663
664
        // Update positions for different accounts
665
1
        limiter
666
1
            .update_position("account_1", &symbol, 100.0, 150.0)
667
1
            .await;
668
1
        limiter
669
1
            .update_position("account_2", &symbol, 200.0, 150.0)
670
1
            .await;
671
672
        // Verify positions are isolated by account
673
1
        let pos1 = limiter.get_cached_position("account_1", &symbol).await;
674
1
        let pos2 = limiter.get_cached_position("account_2", &symbol).await;
675
676
1
        assert_eq!(pos1, Some(100.0));
677
1
        assert_eq!(pos2, Some(200.0));
678
1
    }
679
680
    #[tokio::test]
681
1
    async fn test_negative_position() {
682
1
        let config = create_test_config();
683
1
        let limiter = HybridPositionLimiter::new(config);
684
685
1
        let symbol = Symbol::from("AAPL");
686
1
        limiter
687
1
            .update_position("test_account", &symbol, -50.0, 150.0)
688
1
            .await;
689
690
1
        let position = limiter.get_cached_position("test_account", &symbol).await;
691
1
        assert_eq!(position, Some(-50.0));
692
1
    }
693
694
    #[tokio::test]
695
1
    async fn test_limit_for_nonexistent_account() {
696
1
        let config = create_test_config();
697
1
        let limiter = HybridPositionLimiter::new(config);
698
699
1
        let limits = limiter.get_limits("nonexistent_account").await;
700
1
        assert_eq!(limits.len(), 0);
701
1
    }
702
703
    #[tokio::test]
704
1
    async fn test_multiple_limits_same_account() {
705
1
        let config = create_test_config();
706
1
        let limiter = HybridPositionLimiter::new(config);
707
708
        // Set multiple limits for the same account
709
4
        for 
symbol_str3
in &["AAPL", "GOOGL", "MSFT"] {
710
3
            let limit = PositionLimit {
711
3
                instrument_id: (*symbol_str).to_string(),
712
3
                max_position_size: Price::new(10000.0).unwrap_or(Price::ZERO),
713
3
                max_daily_turnover: Price::new(50000.0).unwrap_or(Price::ZERO),
714
3
                concentration_limit: Price::new(0.05).unwrap_or(Price::ZERO),
715
3
                regulatory_basis: format!("Test Limit for {}", symbol_str),
716
3
            };
717
3
            limiter
718
3
                .set_limit("test_account".to_string(), limit)
719
3
                .await
720
3
                .unwrap();
721
        }
722
723
1
        let limits = limiter.get_limits("test_account").await;
724
1
        assert_eq!(limits.len(), 3);
725
1
    }
726
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/safety_coordinator.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/safety_coordinator.rs.html deleted file mode 100644 index 749fb57d1..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/safety_coordinator.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/safety_coordinator.rs
Line
Count
Source
1
//! Safety Coordinator - Integration Hub for All Safety Systems
2
//!
3
//! Coordinates and orchestrates all safety mechanisms across the trading system:
4
//! - Kill switches with atomic broadcasting
5
//! - Position limits with hybrid checking  
6
//! - Circuit breakers for market anomalies
7
//! - Emergency response coordination
8
//! - Real-time safety monitoring and alerts
9
//! - Integration with trading-engine and ai-intelligence services
10
11
use std::collections::HashMap;
12
use std::sync::Arc;
13
// Removed foxhunt_infrastructure - not available in this simplified risk crate
14
15
use chrono::{DateTime, Utc};
16
use redis::aio::MultiplexedConnection;
17
// REMOVED: Direct Decimal usage - use canonical types
18
use common::Price;
19
use rust_decimal::Decimal;
20
use serde::{Deserialize, Serialize};
21
use tokio::sync::{broadcast, RwLock};
22
use tracing::{debug, error, info, warn};
23
24
use crate::circuit_breaker::RealCircuitBreaker;
25
use crate::error::{RiskError, RiskResult};
26
use crate::safety::emergency_response::EmergencyResponseSystem;
27
use crate::safety::kill_switch::AtomicKillSwitch;
28
use crate::safety::position_limiter::HybridPositionLimiter;
29
use crate::safety::SafetyConfig;
30
31
/// System Health Report
32
#[derive(Debug, Clone, Serialize, Deserialize)]
33
pub struct SystemHealthReport {
34
    pub component_status: HashMap<String, String>,
35
    pub overall_health: f64,
36
    pub last_updated: DateTime<Utc>,
37
}
38
39
/// Safety Coordinator - Central hub for all safety systems
40
// Infrastructure - fields will be used for safety system coordination
41
#[allow(dead_code)]
42
pub struct SafetyCoordinator {
43
    config: SafetyConfig,
44
    kill_switch: Arc<AtomicKillSwitch>,
45
    position_limiter: Arc<HybridPositionLimiter>,
46
    circuit_breaker: Arc<RealCircuitBreaker>,
47
    emergency_response: Arc<EmergencyResponseSystem>,
48
    redis_connection: Arc<RwLock<Option<MultiplexedConnection>>>,
49
    event_tx: broadcast::Sender<String>,
50
    is_running: Arc<RwLock<bool>>,
51
}
52
53
impl SafetyCoordinator {
54
    /// Create a new `SafetyCoordinator`
55
0
    pub async fn new(config: SafetyConfig, redis_url: String) -> RiskResult<Self> {
56
0
        let (event_tx, _) = broadcast::channel(1000);
57
58
        // Create safety components with proper implementations
59
0
        let kill_switch_config = config.kill_switch.clone();
60
0
        let kill_switch = AtomicKillSwitch::new(kill_switch_config, redis_url.clone()).await?;
61
62
        // Create real implementations
63
0
        let position_limiter = Arc::new(HybridPositionLimiter::new(config.position_limits.clone()));
64
65
        // Create circuit breaker with proper configuration
66
        // For now, create a real broker client for local development using the circuit_breaker module's RealBrokerClient
67
0
        let _broker_service = Arc::new(crate::circuit_breaker::RealBrokerClient::new(
68
0
            "http://${SERVICE_HOST:-localhost}:8080".to_owned(),
69
        ));
70
0
        let circuit_breaker_config = crate::circuit_breaker::CircuitBreakerConfig {
71
0
            enabled: true,
72
0
            daily_loss_percentage: Price::from_f64(2.0).unwrap_or(Decimal::from(2).into()), // Default 2% daily loss limit
73
0
            position_limit_percentage: Price::from_f64(5.0).unwrap_or(Decimal::from(5).into()), // Default 5% position limit
74
0
            max_consecutive_violations: 3,
75
0
            redis_url: redis_url.clone(),
76
0
            redis_key_prefix: "foxhunt:risk:circuit_breaker".to_owned(),
77
0
            auto_recovery_enabled: true,
78
0
            portfolio_refresh_interval_secs: 60,
79
0
            cooldown_period_secs: 300,
80
0
        };
81
0
        let adapter = Arc::new(crate::risk_engine::BrokerAccountServiceAdapter::new());
82
0
        let circuit_breaker = RealCircuitBreaker::new(circuit_breaker_config, adapter)
83
0
            .await
84
0
            .map_err(|e| RiskError::Config(format!("Failed to initialize circuit breaker: {e}")))?;
85
86
        // Create emergency response system
87
0
        let kill_switch_arc = Arc::new(kill_switch);
88
0
        let emergency_response = EmergencyResponseSystem::new(
89
0
            config.emergency_response.clone(),
90
0
            redis_url.clone(),
91
0
            kill_switch_arc.clone(),
92
0
        )
93
0
        .await?;
94
95
0
        Ok(Self {
96
0
            config,
97
0
            kill_switch: kill_switch_arc,
98
0
            position_limiter,
99
0
            circuit_breaker: Arc::new(circuit_breaker),
100
0
            emergency_response: Arc::new(emergency_response),
101
0
            redis_connection: Arc::new(RwLock::new(None)),
102
0
            event_tx,
103
0
            is_running: Arc::new(RwLock::new(false)),
104
0
        })
105
0
    }
106
107
    /// Create a new `SafetyCoordinator` for testing without Redis
108
    #[cfg(test)]
109
14
    pub async fn new_test(config: SafetyConfig) -> RiskResult<Self> {
110
14
        let (event_tx, _) = broadcast::channel(1000);
111
112
        // Create test kill switch without Redis
113
14
        let kill_switch_config = config.kill_switch.clone();
114
14
        let kill_switch_arc = Arc::new(AtomicKillSwitch::new_test(kill_switch_config));
115
116
        // Create test position limiter
117
14
        let position_limiter = Arc::new(HybridPositionLimiter::new(config.position_limits.clone()));
118
119
        // Create test circuit breaker using mock adapter
120
14
        let circuit_breaker_config = crate::circuit_breaker::CircuitBreakerConfig {
121
14
            enabled: true,
122
14
            daily_loss_percentage: Price::from_f64(2.0).unwrap_or(Decimal::from(2).into()),
123
14
            position_limit_percentage: Price::from_f64(5.0).unwrap_or(Decimal::from(5).into()),
124
14
            max_consecutive_violations: 3,
125
14
            redis_url: "redis://localhost:6379".to_owned(),
126
14
            redis_key_prefix: "foxhunt:risk:circuit_breaker:test".to_owned(),
127
14
            auto_recovery_enabled: false, // Disable for tests
128
14
            portfolio_refresh_interval_secs: 60,
129
14
            cooldown_period_secs: 300,
130
14
        };
131
14
        let adapter = Arc::new(crate::risk_engine::BrokerAccountServiceAdapter::new());
132
        // Use real circuit breaker but it won't try to connect to Redis if auto_recovery is disabled
133
14
        let circuit_breaker = RealCircuitBreaker::new(circuit_breaker_config, adapter)
134
14
            .await
135
14
            .unwrap_or_else(|_| 
{0
136
                // Fallback: create a minimal circuit breaker for tests
137
0
                panic!("Circuit breaker creation failed in test - this should not happen")
138
            });
139
140
        // Create test emergency response system
141
14
        let emergency_response = EmergencyResponseSystem::new(
142
14
            config.emergency_response.clone(),
143
14
            "redis://localhost:6379".to_owned(),
144
14
            kill_switch_arc.clone(),
145
14
        )
146
14
        .await
?0
;
147
148
14
        Ok(Self {
149
14
            config,
150
14
            kill_switch: kill_switch_arc,
151
14
            position_limiter,
152
14
            circuit_breaker: Arc::new(circuit_breaker),
153
14
            emergency_response: Arc::new(emergency_response),
154
14
            redis_connection: Arc::new(RwLock::new(None)),
155
14
            event_tx,
156
14
            is_running: Arc::new(RwLock::new(false)),
157
14
        })
158
14
    }
159
160
    /// Start all safety systems
161
11
    pub async fn start_all_systems(&self) -> RiskResult<()> {
162
11
        info!(
"Starting all safety systems"0
);
163
164
        // Start kill switch monitoring
165
11
        self.kill_switch.start_monitoring().await
?0
;
166
167
        // Start emergency response system
168
11
        self.emergency_response.start_monitoring().await
?0
;
169
170
        // Circuit breaker is stateless and always active
171
172
11
        let mut running = self.is_running.write().await;
173
11
        *running = true;
174
175
11
        info!(
"All safety systems started successfully"0
);
176
11
        Ok(())
177
11
    }
178
179
    /// Stop all safety systems
180
9
    pub async fn stop_all_systems(&self) {
181
9
        info!(
"Stopping all safety systems"0
);
182
183
        // Stop kill switch monitoring
184
9
        if let Err(
e0
) = self.kill_switch.stop_monitoring().await {
185
0
            warn!("Error stopping kill switch: {}", e);
186
9
        }
187
188
        // Stop emergency response system
189
9
        if let Err(
e0
) = self.emergency_response.stop_monitoring().await {
190
0
            warn!("Error stopping emergency response: {}", e);
191
9
        }
192
193
9
        let mut running = self.is_running.write().await;
194
9
        *running = false;
195
196
9
        info!(
"All safety systems stopped"0
);
197
9
    }
198
199
    /// Check if trading is allowed for the given account and symbol
200
19
    pub async fn is_trading_allowed(&self, account_id: &str, symbol: &str) -> bool {
201
19
        let running = self.is_running.read().await;
202
19
        if !*running {
203
3
            return false;
204
16
        }
205
206
        // Check kill switch status
207
16
        if let Ok(is_active) = self.kill_switch.is_active().await {
208
16
            if is_active {
209
0
                debug!(
210
0
                    "Trading blocked by kill switch for {}:{}",
211
                    account_id, symbol
212
                );
213
0
                return false;
214
16
            }
215
0
        }
216
217
        // Check circuit breaker status
218
16
        if self.circuit_breaker.is_active(account_id).await {
219
0
            debug!(
220
0
                "Trading blocked by circuit breaker for account {}",
221
                account_id
222
            );
223
0
            return false;
224
16
        }
225
226
16
        true
227
19
    }
228
229
    /// Trigger global emergency halt
230
3
    pub async fn global_emergency_halt(&self, reason: String, user: String) -> RiskResult<()> {
231
3
        warn!(
"Global emergency halt triggered by {}: {}"0
, user, reason);
232
233
        // Activate kill switch
234
3
        self.kill_switch
235
3
            .activate_global(reason.clone(), user.clone())
236
3
            .await
?0
;
237
238
        // Trigger emergency response
239
3
        self.emergency_response
240
3
            .handle_manual_emergency(user.clone(), reason.clone())
241
3
            .await
?0
;
242
243
3
        let mut running = self.is_running.write().await;
244
3
        *running = false;
245
246
        // Broadcast emergency event
247
3
        let _ = self
248
3
            .event_tx
249
3
            .send(format!("EMERGENCY_HALT: {reason} by {user}"));
250
251
3
        error!(
"Global emergency halt activated"0
);
252
3
        Ok(())
253
3
    }
254
255
    /// Get system health report
256
3
    pub async fn get_system_health(&self) -> SystemHealthReport {
257
3
        let mut component_status = HashMap::new();
258
3
        let mut health_scores = Vec::new();
259
260
        // Check kill switch health
261
3
        match self.kill_switch.is_healthy().await {
262
3
            Ok(true) => {
263
3
                component_status.insert("kill_switch".to_owned(), "operational".to_owned());
264
3
                health_scores.push(1.0);
265
3
            },
266
0
            Ok(false) => {
267
0
                component_status.insert("kill_switch".to_owned(), "degraded".to_owned());
268
0
                health_scores.push(0.5);
269
0
            },
270
0
            Err(_) => {
271
0
                component_status.insert("kill_switch".to_owned(), "failed".to_owned());
272
0
                health_scores.push(0.0);
273
0
            },
274
        }
275
276
        // Position limiter is always healthy if initialized
277
3
        component_status.insert("position_limiter".to_owned(), "operational".to_owned());
278
3
        health_scores.push(1.0);
279
280
        // Check circuit breaker health (simplified check)
281
3
        component_status.insert("circuit_breaker".to_owned(), "operational".to_owned());
282
3
        health_scores.push(1.0);
283
284
        // Check emergency response health
285
3
        if self.emergency_response.is_healthy().await {
286
3
            component_status.insert("emergency_response".to_owned(), "operational".to_owned());
287
3
            health_scores.push(1.0);
288
3
        } else {
289
0
            component_status.insert("emergency_response".to_owned(), "degraded".to_owned());
290
0
            health_scores.push(0.5);
291
0
        }
292
293
        // Calculate overall health
294
3
        let overall_health = if health_scores.is_empty() {
295
0
            0.0
296
        } else {
297
3
            health_scores.iter().sum::<f64>() / health_scores.len() as f64
298
        };
299
300
3
        SystemHealthReport {
301
3
            component_status,
302
3
            overall_health,
303
3
            last_updated: Utc::now(),
304
3
        }
305
3
    }
306
307
    /// Subscribe to safety events
308
    #[must_use]
309
2
    pub fn subscribe_safety_events(&self) -> broadcast::Receiver<String> {
310
2
        self.event_tx.subscribe()
311
2
    }
312
}
313
314
#[cfg(test)]
315
mod tests {
316
    use super::*;
317
    use crate::safety::{EmergencyResponseConfig, KillSwitchConfig, PositionLimiterConfig};
318
    use std::time::Duration;
319
    // operations module removed - use direct imports from common
320
321
15
    fn create_test_config() -> SafetyConfig {
322
        SafetyConfig {
323
            enabled: true,
324
15
            kill_switch: KillSwitchConfig::default(),
325
15
            position_limits: PositionLimiterConfig::default(),
326
15
            emergency_response: EmergencyResponseConfig::default(),
327
15
            redis_url: std::env::var("REDIS_URL").unwrap_or_else(|_| 
{0
328
0
                std::env::var("REDIS_URL")
329
0
                    .unwrap_or_else(|_| "redis://${REDIS_HOST:-localhost}:6379".to_string())
330
0
            }),
331
15
            safety_check_timeout: Duration::from_millis(10),
332
        }
333
15
    }
334
335
14
    async fn create_test_coordinator() -> RiskResult<SafetyCoordinator> {
336
14
        let config = create_test_config();
337
14
        SafetyCoordinator::new_test(config).await
338
14
    }
339
340
    #[tokio::test]
341
1
    async fn test_safety_coordinator_creation() {
342
1
        let coordinator = create_test_coordinator().await;
343
1
        assert!(coordinator.is_ok());
344
1
    }
345
346
    #[tokio::test]
347
1
    async fn test_trading_allowed_check() -> RiskResult<()> {
348
1
        let coordinator = create_test_coordinator().await
?0
;
349
350
        // Start systems first
351
1
        coordinator.start_all_systems().await
?0
;
352
353
        // Trading should be allowed when systems are running
354
1
        assert!(coordinator.is_trading_allowed("account1", "AAPL").await);
355
356
1
        coordinator.stop_all_systems().await;
357
2
        Ok(())
358
1
    }
359
360
    #[tokio::test]
361
1
    async fn test_global_emergency_halt() -> RiskResult<()> {
362
1
        let coordinator = create_test_coordinator().await
?0
;
363
364
1
        coordinator.start_all_systems().await
?0
;
365
366
1
        let result = coordinator
367
1
            .global_emergency_halt("Test emergency".to_string(), "TEST_USER".to_string())
368
1
            .await;
369
370
1
        assert!(result.is_ok());
371
372
        // Check that trading is now blocked
373
1
        assert!(!coordinator.is_trading_allowed("account1", "AAPL").await);
374
375
1
        coordinator.stop_all_systems().await;
376
2
        Ok(())
377
1
    }
378
379
    #[tokio::test]
380
1
    async fn test_system_health_monitoring() -> RiskResult<()> {
381
1
        let coordinator = create_test_coordinator().await
?0
;
382
383
1
        coordinator.start_all_systems().await
?0
;
384
385
1
        let health_report = coordinator.get_system_health().await;
386
1
        assert!(!health_report.component_status.is_empty());
387
1
        assert!(health_report.overall_health >= 0.0 && health_report.overall_health <= 1.0);
388
389
1
        coordinator.stop_all_systems().await;
390
2
        Ok(())
391
1
    }
392
393
    #[tokio::test]
394
1
    async fn test_event_subscription() -> RiskResult<()> {
395
1
        let coordinator = create_test_coordinator().await
?0
;
396
397
1
        let mut event_rx = coordinator.subscribe_safety_events();
398
399
        // This would be a more comprehensive test with actual event generation
400
        // For now, just verify the subscription works
401
1
        assert!(event_rx.try_recv().is_err()); // No events initially
402
2
        Ok(())
403
1
    }
404
405
    #[tokio::test]
406
1
    async fn test_start_stop_lifecycle() -> RiskResult<()> {
407
1
        let coordinator = create_test_coordinator().await
?0
;
408
409
        // Start systems
410
1
        coordinator.start_all_systems().await
?0
;
411
412
        // Stop systems
413
1
        coordinator.stop_all_systems().await;
414
415
2
        Ok(())
416
1
    }
417
418
    #[tokio::test]
419
1
    async fn test_trading_blocked_when_not_running() -> RiskResult<()> {
420
1
        let coordinator = create_test_coordinator().await
?0
;
421
422
        // Before starting, trading should be blocked
423
1
        assert!(!coordinator.is_trading_allowed("account1", "AAPL").await);
424
425
2
        Ok(())
426
1
    }
427
428
    #[tokio::test]
429
1
    async fn test_circuit_breaker_blocks_trading() -> RiskResult<()> {
430
1
        let coordinator = create_test_coordinator().await
?0
;
431
432
1
        coordinator.start_all_systems().await
?0
;
433
434
        // Trading should initially be allowed
435
1
        assert!(coordinator.is_trading_allowed("account1", "AAPL").await);
436
437
1
        coordinator.stop_all_systems().await;
438
2
        Ok(())
439
1
    }
440
441
    #[tokio::test]
442
1
    async fn test_multiple_accounts() -> RiskResult<()> {
443
1
        let coordinator = create_test_coordinator().await
?0
;
444
445
1
        coordinator.start_all_systems().await
?0
;
446
447
        // Check multiple accounts
448
1
        assert!(coordinator.is_trading_allowed("account1", "AAPL").await);
449
1
        assert!(coordinator.is_trading_allowed("account2", "GOOGL").await);
450
1
        assert!(coordinator.is_trading_allowed("account3", "MSFT").await);
451
452
1
        coordinator.stop_all_systems().await;
453
2
        Ok(())
454
1
    }
455
456
    #[tokio::test]
457
1
    async fn test_health_report_components() -> RiskResult<()> {
458
1
        let coordinator = create_test_coordinator().await
?0
;
459
460
1
        coordinator.start_all_systems().await
?0
;
461
462
1
        let health = coordinator.get_system_health().await;
463
464
        // Verify all components are reported
465
1
        assert!(health.component_status.contains_key("kill_switch"));
466
1
        assert!(health.component_status.contains_key("position_limiter"));
467
1
        assert!(health.component_status.contains_key("circuit_breaker"));
468
1
        assert!(health.component_status.contains_key("emergency_response"));
469
470
1
        coordinator.stop_all_systems().await;
471
2
        Ok(())
472
1
    }
473
474
    #[tokio::test]
475
1
    async fn test_emergency_halt_stops_trading() -> RiskResult<()> {
476
1
        let coordinator = create_test_coordinator().await
?0
;
477
478
1
        coordinator.start_all_systems().await
?0
;
479
480
        // Initially allowed
481
1
        assert!(coordinator.is_trading_allowed("account1", "AAPL").await);
482
483
        // Trigger emergency halt
484
1
        coordinator
485
1
            .global_emergency_halt("Test emergency".to_string(), "TEST_USER".to_string())
486
1
            .await
?0
;
487
488
        // Should now be blocked
489
1
        assert!(!coordinator.is_trading_allowed("account1", "AAPL").await);
490
491
2
        Ok(())
492
1
    }
493
494
    #[tokio::test]
495
1
    async fn test_event_broadcast() -> RiskResult<()> {
496
1
        let coordinator = create_test_coordinator().await
?0
;
497
498
1
        let mut event_rx = coordinator.subscribe_safety_events();
499
500
1
        coordinator.start_all_systems().await
?0
;
501
502
        // Trigger emergency
503
1
        coordinator
504
1
            .global_emergency_halt("Test".to_string(), "USER".to_string())
505
1
            .await
?0
;
506
507
        // Should receive event (with timeout)
508
1
        let result =
509
1
            tokio::time::timeout(Duration::from_millis(100), event_rx.recv()).await;
510
511
1
        match result {
512
1
            Ok(Ok(_event)) => {
513
1
                // Event received successfully
514
1
            },
515
1
            _ => {
516
0
                // Event might have been dropped or not received in time
517
0
                // This is acceptable in test environment
518
0
            },
519
1
        }
520
1
521
1
        Ok(())
522
1
    }
523
524
    #[tokio::test]
525
1
    async fn test_health_score_calculation() -> RiskResult<()> {
526
1
        let coordinator = create_test_coordinator().await
?0
;
527
528
1
        coordinator.start_all_systems().await
?0
;
529
530
1
        let health = coordinator.get_system_health().await;
531
532
        // Health should be between 0 and 1
533
1
        assert!(health.overall_health >= 0.0);
534
1
        assert!(health.overall_health <= 1.0);
535
536
        // With all systems healthy, should be close to 1.0
537
1
        assert!(health.overall_health > 0.5);
538
539
1
        coordinator.stop_all_systems().await;
540
2
        Ok(())
541
1
    }
542
543
    #[tokio::test]
544
1
    async fn test_concurrent_trading_checks() -> RiskResult<()> {
545
1
        let _config = create_test_config();
546
1
        let coordinator = Arc::new(create_test_coordinator().await
?0
);
547
548
1
        coordinator.start_all_systems().await
?0
;
549
550
        // Spawn concurrent checks
551
1
        let mut handles = vec![];
552
11
        for 
i10
in 0..10 {
553
10
            let coord = coordinator.clone();
554
10
            let handle = tokio::spawn(async move {
555
10
                coord
556
10
                    .is_trading_allowed(&format!("account{}", i), "AAPL")
557
10
                    .await
558
10
            });
559
10
            handles.push(handle);
560
        }
561
562
        // All should complete successfully
563
11
        for 
handle10
in handles {
564
10
            let result = handle.await;
565
10
            assert!(result.is_ok());
566
        }
567
568
1
        coordinator.stop_all_systems().await;
569
2
        Ok(())
570
1
    }
571
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/trading_gate.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/trading_gate.rs.html deleted file mode 100644 index 428c0e2df..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/trading_gate.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/trading_gate.rs
Line
Count
Source
1
//! Trading Gate - Atomic Order Processing Guards
2
//!
3
//! Provides atomic gates at all order processing entry points to ensure
4
//! immediate blocking when kill switch is activated. Designed for regulatory
5
//! compliance with sub-microsecond checking latency.
6
7
use std::sync::Arc;
8
use std::time::Instant;
9
10
use tracing::{debug, warn};
11
12
use crate::error::{RiskError, RiskResult};
13
use crate::risk_types::KillSwitchScope;
14
use crate::safety::kill_switch::AtomicKillSwitch;
15
16
/// Trading gate that guards all order processing operations
17
#[derive(Debug, Clone)]
18
pub struct TradingGate {
19
    kill_switch: Arc<AtomicKillSwitch>,
20
}
21
22
impl TradingGate {
23
    /// Create new trading gate with kill switch reference
24
    #[must_use]
25
9
    pub const fn new(kill_switch: Arc<AtomicKillSwitch>) -> Self {
26
9
        Self { kill_switch }
27
9
    }
28
29
    /// Check if trading is allowed for the given scope (THE CRITICAL GATE)
30
    /// This function MUST complete in under 1 microsecond for regulatory compliance
31
    #[inline(always)]
32
40
    pub fn check_trading_allowed(&self, scope: &KillSwitchScope) -> RiskResult<()> {
33
40
        if !self.kill_switch.is_trading_allowed(scope) {
34
1
            return Err(RiskError::KillSwitchActive {
35
1
                scope: scope.clone(),
36
1
                message: "Trading blocked by kill switch".to_owned(),
37
1
            });
38
39
        }
39
39
        Ok(())
40
40
    }
41
42
    /// Pre-order validation gate - checks kill switch before any order processing
43
    #[inline(always)]
44
7
    pub fn pre_order_gate(&self, symbol: &str, account: Option<&str>) -> RiskResult<()> {
45
7
        let start_time = Instant::now();
46
47
        // Check symbol-level kill switch
48
7
        let symbol_scope = KillSwitchScope::Symbol(symbol.to_owned());
49
7
        self.check_trading_allowed(&symbol_scope)
?1
;
50
51
        // Check account-level kill switch if account is specified
52
6
        if let Some(
account_id2
) = account {
53
2
            let account_scope = KillSwitchScope::Account(account_id.to_owned());
54
2
            self.check_trading_allowed(&account_scope)
?0
;
55
4
        }
56
57
        // Check global kill switch (most critical)
58
6
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
59
60
6
        let elapsed = start_time.elapsed();
61
6
        if elapsed.as_nanos() > 1000 {
62
            // More than 1 microsecond
63
6
            warn!(
64
0
                "Trading gate check took {}ns (target: <1000ns) for symbol: {}",
65
0
                elapsed.as_nanos(),
66
                symbol
67
            );
68
0
        }
69
70
6
        debug!(
71
0
            "Trading gate passed for symbol: {} ({}ns)",
72
            symbol,
73
0
            elapsed.as_nanos()
74
        );
75
76
6
        Ok(())
77
7
    }
78
79
    /// Market data gate - checks if market data processing should continue
80
    #[inline(always)]
81
1
    pub fn market_data_gate(&self, symbol: &str) -> RiskResult<()> {
82
1
        let symbol_scope = KillSwitchScope::Symbol(symbol.to_owned());
83
1
        self.check_trading_allowed(&symbol_scope)
?0
;
84
1
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
85
1
        Ok(())
86
1
    }
87
88
    /// Execution gate - final check before order execution
89
    #[inline(always)]
90
1
    pub fn execution_gate(&self, symbol: &str, account: &str) -> RiskResult<()> {
91
1
        let start_time = Instant::now();
92
93
        // Final checks before execution - must be ultra-fast
94
1
        self.check_trading_allowed(&KillSwitchScope::Symbol(symbol.to_owned()))
?0
;
95
1
        self.check_trading_allowed(&KillSwitchScope::Account(account.to_owned()))
?0
;
96
1
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
97
98
1
        let elapsed = start_time.elapsed();
99
1
        if elapsed.as_nanos() > 500 {
100
            // Even faster for execution gate
101
1
            warn!(
102
0
                "Execution gate check took {}ns (target: <500ns) for {}/{}",
103
0
                elapsed.as_nanos(),
104
                symbol,
105
                account
106
            );
107
0
        }
108
109
1
        Ok(())
110
1
    }
111
112
    /// Strategy gate - checks if strategy should continue operating
113
    #[inline(always)]
114
1
    pub fn strategy_gate(&self, strategy_id: &str) -> RiskResult<()> {
115
1
        let strategy_scope = KillSwitchScope::Strategy(strategy_id.to_owned());
116
1
        self.check_trading_allowed(&strategy_scope)
?0
;
117
1
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
118
1
        Ok(())
119
1
    }
120
121
    /// Portfolio gate - checks if portfolio operations should continue
122
    #[inline(always)]
123
1
    pub fn portfolio_gate(&self, portfolio_id: &str) -> RiskResult<()> {
124
1
        let portfolio_scope = KillSwitchScope::Portfolio(portfolio_id.to_owned());
125
1
        self.check_trading_allowed(&portfolio_scope)
?0
;
126
1
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
127
1
        Ok(())
128
1
    }
129
130
    /// Risk calculation gate - checks if risk calculations should proceed
131
    #[inline(always)]
132
1
    pub fn risk_calculation_gate(&self, account: &str) -> RiskResult<()> {
133
1
        let account_scope = KillSwitchScope::Account(account.to_owned());
134
1
        self.check_trading_allowed(&account_scope)
?0
;
135
1
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
136
1
        Ok(())
137
1
    }
138
139
    /// Emergency check - bypasses all other gates for immediate shutdown validation
140
    #[inline(always)]
141
    #[must_use]
142
1
    pub fn emergency_check(&self) -> bool {
143
1
        self.kill_switch
144
1
            .is_trading_allowed(&KillSwitchScope::Global)
145
1
    }
146
147
    /// Get the underlying kill switch for direct access
148
    #[must_use]
149
1
    pub const fn kill_switch(&self) -> &Arc<AtomicKillSwitch> {
150
1
        &self.kill_switch
151
1
    }
152
}
153
154
/// Macro for automatic gate checking in trading functions
155
#[macro_export]
156
macro_rules! trading_gate_check {
157
    ($gate:expr, $symbol:expr) => {
158
        if let Err(e) = $gate.pre_order_gate($symbol, None) {
159
            return Err(e.into());
160
        }
161
    };
162
    ($gate:expr, $symbol:expr, $account:expr) => {
163
        if let Err(e) = $gate.pre_order_gate($symbol, Some($account)) {
164
            return Err(e.into());
165
        }
166
    };
167
}
168
169
/// Utility functions for common gate patterns
170
impl TradingGate {
171
    /// Comprehensive order validation gate - checks all relevant scopes
172
1
    pub fn comprehensive_order_gate(
173
1
        &self,
174
1
        symbol: &str,
175
1
        account: &str,
176
1
        strategy_id: Option<&str>,
177
1
    ) -> RiskResult<()> {
178
1
        let start_time = Instant::now();
179
180
        // Check global first (fastest rejection)
181
1
        self.check_trading_allowed(&KillSwitchScope::Global)
?0
;
182
183
        // Check account
184
1
        self.check_trading_allowed(&KillSwitchScope::Account(account.to_owned()))
?0
;
185
186
        // Check symbol
187
1
        self.check_trading_allowed(&KillSwitchScope::Symbol(symbol.to_owned()))
?0
;
188
189
        // Check strategy if provided
190
1
        if let Some(strategy) = strategy_id {
191
1
            self.check_trading_allowed(&KillSwitchScope::Strategy(strategy.to_owned()))
?0
;
192
0
        }
193
194
1
        let elapsed = start_time.elapsed();
195
1
        debug!(
196
0
            "Comprehensive order gate passed for {}/{} ({}ns)",
197
            symbol,
198
            account,
199
0
            elapsed.as_nanos()
200
        );
201
202
1
        Ok(())
203
1
    }
204
205
    /// Batch gate check for multiple symbols (optimized for market data processing)
206
1
    pub fn batch_symbol_gate(&self, symbols: &[String]) -> RiskResult<Vec<String>> {
207
        // First check global - if global is disabled, all symbols fail
208
1
        if !self
209
1
            .kill_switch
210
1
            .is_trading_allowed(&KillSwitchScope::Global)
211
        {
212
0
            return Err(RiskError::KillSwitchActive {
213
0
                scope: KillSwitchScope::Global,
214
0
                message: "Global kill switch active - all symbols blocked".to_owned(),
215
0
            });
216
1
        }
217
218
        // Check each symbol individually
219
1
        let mut allowed_symbols = Vec::with_capacity(symbols.len());
220
4
        for 
symbol3
in symbols {
221
3
            let symbol_scope = KillSwitchScope::Symbol(symbol.clone());
222
3
            if self.kill_switch.is_trading_allowed(&symbol_scope) {
223
3
                allowed_symbols.push(symbol.clone());
224
3
            } else {
225
0
                debug!("Symbol {} blocked by kill switch", symbol);
226
            }
227
        }
228
229
1
        Ok(allowed_symbols)
230
1
    }
231
232
    /// High-frequency gate check with performance monitoring
233
1
    pub fn hf_gate_check(&self, symbol: &str) -> (RiskResult<()>, u64) {
234
1
        let start_time = Instant::now();
235
1
        let result = self.pre_order_gate(symbol, None);
236
1
        let elapsed_ns = start_time.elapsed().as_nanos() as u64;
237
1
        (result, elapsed_ns)
238
1
    }
239
}
240
241
/// Performance metrics for gate operations
242
#[derive(Debug, Clone)]
243
pub struct GateMetrics {
244
    pub total_checks: u64,
245
    pub blocked_checks: u64,
246
    pub average_latency_ns: f64,
247
    pub max_latency_ns: u64,
248
    pub min_latency_ns: u64,
249
}
250
251
impl Default for GateMetrics {
252
1
    fn default() -> Self {
253
1
        Self {
254
1
            total_checks: 0,
255
1
            blocked_checks: 0,
256
1
            average_latency_ns: 0.0,
257
1
            max_latency_ns: 0,
258
1
            min_latency_ns: u64::MAX,
259
1
        }
260
1
    }
261
}
262
263
/// Trading gate with performance monitoring
264
pub struct MonitoredTradingGate {
265
    gate: TradingGate,
266
    metrics: Arc<std::sync::Mutex<GateMetrics>>,
267
}
268
269
impl MonitoredTradingGate {
270
    #[must_use]
271
1
    pub fn new(kill_switch: Arc<AtomicKillSwitch>) -> Self {
272
1
        Self {
273
1
            gate: TradingGate::new(kill_switch),
274
1
            metrics: Arc::new(std::sync::Mutex::new(GateMetrics::default())),
275
1
        }
276
1
    }
277
278
    /// Check with performance monitoring
279
10
    pub fn check_with_monitoring(&self, scope: &KillSwitchScope) -> RiskResult<()> {
280
10
        let start_time = Instant::now();
281
10
        let result = self.gate.check_trading_allowed(scope);
282
10
        let elapsed_ns = start_time.elapsed().as_nanos() as u64;
283
284
        // Update metrics
285
10
        if let Ok(mut metrics) = self.metrics.lock() {
286
10
            metrics.total_checks += 1;
287
10
            if result.is_err() {
288
0
                metrics.blocked_checks += 1;
289
10
            }
290
291
10
            metrics.max_latency_ns = metrics.max_latency_ns.max(elapsed_ns);
292
10
            metrics.min_latency_ns = metrics.min_latency_ns.min(elapsed_ns);
293
294
10
            let total_latency =
295
10
                metrics.average_latency_ns * (metrics.total_checks - 1) as f64 + elapsed_ns as f64;
296
10
            metrics.average_latency_ns = total_latency / metrics.total_checks as f64;
297
0
        }
298
299
10
        result
300
10
    }
301
302
1
    pub fn get_metrics(&self) -> RiskResult<GateMetrics> {
303
1
        self.metrics
304
1
            .lock()
305
1
            .map(|metrics| metrics.clone())
306
1
            .map_err(|_| RiskError::Internal(
"Failed to acquire metrics lock"0
.
to_owned0
()))
307
1
    }
308
309
    /// Get the underlying gate
310
    #[must_use]
311
0
    pub const fn gate(&self) -> &TradingGate {
312
0
        &self.gate
313
0
    }
314
}
315
316
#[cfg(test)]
317
mod tests {
318
    use super::*;
319
    use crate::safety::kill_switch::AtomicKillSwitch;
320
    use crate::safety::KillSwitchConfig;
321
322
8
    fn create_test_gate() -> RiskResult<TradingGate> {
323
8
        let config = KillSwitchConfig::default();
324
8
        let kill_switch = Arc::new(AtomicKillSwitch::new_test(config));
325
8
        Ok(TradingGate::new(kill_switch))
326
8
    }
327
328
    #[tokio::test]
329
1
    async fn test_gate_creation() -> RiskResult<()> {
330
1
        let gate = create_test_gate()
?0
;
331
332
        // Initially, all trading should be allowed
333
1
        assert!(gate.pre_order_gate("AAPL", None).is_ok());
334
1
        assert!(gate.emergency_check());
335
336
2
        Ok(())
337
1
    }
338
339
    #[tokio::test]
340
1
    async fn test_pre_order_gate() -> RiskResult<()> {
341
1
        let gate = create_test_gate()
?0
;
342
343
        // Test symbol gate
344
1
        let result = gate.pre_order_gate("AAPL", None);
345
1
        assert!(result.is_ok());
346
347
        // Test with account
348
1
        let result = gate.pre_order_gate("AAPL", Some("account123"));
349
1
        assert!(result.is_ok());
350
351
2
        Ok(())
352
1
    }
353
354
    #[tokio::test]
355
1
    async fn test_comprehensive_order_gate() -> RiskResult<()> {
356
1
        let gate = create_test_gate()
?0
;
357
358
1
        let result = gate.comprehensive_order_gate("AAPL", "account123", Some("strategy1"));
359
1
        assert!(result.is_ok());
360
361
2
        Ok(())
362
1
    }
363
364
    #[tokio::test]
365
1
    async fn test_batch_symbol_gate() -> RiskResult<()> {
366
1
        let gate = create_test_gate()
?0
;
367
368
1
        let symbols = vec!["AAPL".to_string(), "GOOGL".to_string(), "MSFT".to_string()];
369
1
        let allowed = gate.batch_symbol_gate(&symbols)
?0
;
370
371
1
        assert_eq!(allowed.len(), 3);
372
1
        assert_eq!(allowed, symbols);
373
374
2
        Ok(())
375
1
    }
376
377
    #[tokio::test]
378
1
    async fn test_gate_with_kill_switch_active() -> RiskResult<()> {
379
1
        let gate = create_test_gate()
?0
;
380
381
        // Activate kill switch for symbol
382
1
        gate.kill_switch()
383
1
            .activate(
384
1
                KillSwitchScope::Symbol("AAPL".to_string()),
385
1
                "Test".to_string(),
386
1
                "test_user".to_string(),
387
1
                false,
388
1
            )
389
1
            .await
?0
;
390
391
        // Gate should now block
392
1
        let result = gate.pre_order_gate("AAPL", None);
393
1
        assert!(result.is_err());
394
395
2
        Ok(())
396
1
    }
397
398
    #[tokio::test]
399
1
    async fn test_performance_monitoring() -> RiskResult<()> {
400
1
        let config = KillSwitchConfig::default();
401
1
        let kill_switch = Arc::new(AtomicKillSwitch::new_test(config));
402
403
1
        let monitored_gate = MonitoredTradingGate::new(kill_switch);
404
1
        let scope = KillSwitchScope::Symbol("AAPL".to_string());
405
406
        // Perform some checks
407
11
        for _ in 0..10 {
408
10
            let _ = monitored_gate.check_with_monitoring(&scope);
409
10
        }
410
411
1
        let metrics = monitored_gate.get_metrics()
?0
;
412
1
        assert_eq!(metrics.total_checks, 10);
413
1
        assert!(metrics.average_latency_ns > 0.0);
414
415
2
        Ok(())
416
1
    }
417
418
    #[tokio::test]
419
1
    async fn test_hf_gate_check() -> RiskResult<()> {
420
1
        let gate = create_test_gate()
?0
;
421
422
1
        let (result, latency_ns) = gate.hf_gate_check("AAPL");
423
1
        assert!(result.is_ok());
424
1
        assert!(latency_ns > 0);
425
426
        // Latency should be sub-microsecond for HFT compliance
427
1
        assert!(
428
1
            latency_ns < 10_000,
429
0
            "Gate check took {}ns (should be <10,000ns)",
430
            latency_ns
431
        );
432
433
2
        Ok(())
434
1
    }
435
436
    #[tokio::test]
437
1
    async fn test_different_gate_types() -> RiskResult<()> {
438
1
        let gate = create_test_gate()
?0
;
439
440
        // Test all gate types
441
1
        assert!(gate.market_data_gate("AAPL").is_ok());
442
1
        assert!(gate.execution_gate("AAPL", "account123").is_ok());
443
1
        assert!(gate.strategy_gate("strategy1").is_ok());
444
1
        assert!(gate.portfolio_gate("portfolio1").is_ok());
445
1
        assert!(gate.risk_calculation_gate("account123").is_ok());
446
447
2
        Ok(())
448
1
    }
449
450
    #[tokio::test]
451
1
    async fn test_macro_usage() -> RiskResult<()> {
452
1
        let gate = create_test_gate()
?0
;
453
454
        // This simulates usage of the trading_gate_check! macro
455
        // In real code, this would be used in trading functions
456
1
        let symbol = "AAPL";
457
1
        let account = "account123";
458
459
        // Macro equivalent checks
460
1
        assert!(gate.pre_order_gate(symbol, None).is_ok());
461
1
        assert!(gate.pre_order_gate(symbol, Some(account)).is_ok());
462
463
2
        Ok(())
464
1
    }
465
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/unix_socket_kill_switch.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/unix_socket_kill_switch.rs.html deleted file mode 100644 index 6afa7ff31..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/safety/unix_socket_kill_switch.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/safety/unix_socket_kill_switch.rs
Line
Count
Source
1
//! Unix Domain Socket Kill Switch Interface
2
//!
3
//! Provides external control of the kill switch via Unix domain socket at /`var/run/kill_switch`
4
//! for regulatory compliance and external monitoring systems integration.
5
//! Designed for sub-100ms emergency shutdown response times.
6
7
use chrono::Utc;
8
use std::collections::HashMap;
9
use std::path::Path;
10
use std::sync::atomic::{AtomicBool, Ordering};
11
use std::sync::{Arc, Mutex};
12
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
13
14
use serde::{Deserialize, Serialize};
15
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
16
use tokio::net::UnixListener as TokioUnixListener;
17
use tokio::signal::unix::{signal, SignalKind};
18
use tokio::sync::broadcast;
19
use tokio::time::timeout;
20
use tracing::{error, info, warn};
21
22
use crate::error::{RiskError, RiskResult};
23
use crate::risk_types::KillSwitchScope;
24
use crate::safety::kill_switch::AtomicKillSwitch;
25
26
/// Unix socket commands for kill switch control
27
#[derive(Debug, Clone, Serialize, Deserialize)]
28
pub enum KillSwitchCommand {
29
    /// Authenticate with the kill switch system
30
    Authenticate {
31
        token: String,
32
        user_id: String,
33
        timestamp: u64,
34
    },
35
    /// Activate kill switch for specific scope (requires authentication)
36
    Activate {
37
        scope: KillSwitchScope,
38
        reason: String,
39
        cascade: bool,
40
        auth_token: String,
41
    },
42
    /// Deactivate kill switch for specific scope (requires authentication)
43
    Deactivate {
44
        scope: KillSwitchScope,
45
        auth_token: String,
46
    },
47
    /// Get current status (requires authentication)
48
    Status { auth_token: String },
49
    /// Emergency global shutdown (requires authentication)
50
    EmergencyShutdown { reason: String, auth_token: String },
51
    /// Health check (read-only, no auth required)
52
    HealthCheck,
53
}
54
55
/// Response from kill switch operations
56
#[derive(Debug, Clone, Serialize, Deserialize)]
57
pub struct KillSwitchResponse {
58
    pub success: bool,
59
    pub message: String,
60
    pub timestamp: u64,
61
    pub latency_ns: u64,
62
}
63
64
/// Authentication session for kill switch operations
65
// Infrastructure - fields will be used for authentication session management
66
#[allow(dead_code)]
67
#[derive(Debug, Clone)]
68
struct AuthSession {
69
    user_id: String,
70
    created_at: SystemTime,
71
    last_used: SystemTime,
72
    permissions: Vec<String>,
73
}
74
75
/// Authentication manager for kill switch access control
76
#[derive(Clone)]
77
struct AuthManager {
78
    sessions: Arc<Mutex<HashMap<String, AuthSession>>>,
79
    master_token: String,
80
    session_timeout: Duration,
81
}
82
83
impl AuthManager {
84
9
    fn new() -> Self {
85
        // Generate master token from environment or secure random
86
9
        let master_token = std::env::var("KILL_SWITCH_MASTER_TOKEN").unwrap_or_else(|_| {
87
9
            warn!(
"KILL_SWITCH_MASTER_TOKEN not set, using fallback (INSECURE!)"0
);
88
9
            "fallback-token-change-me".to_owned()
89
9
        });
90
91
9
        Self {
92
9
            sessions: Arc::new(Mutex::new(HashMap::new())),
93
9
            master_token,
94
9
            session_timeout: Duration::from_secs(300), // 5 minute session timeout
95
9
        }
96
9
    }
97
98
    /// Authenticate user and create session
99
4
    fn authenticate(&self, token: &str, user_id: &str) -> Result<String, String> {
100
        // Check master token
101
4
        if token != self.master_token {
102
0
            return Err("Invalid authentication token".to_owned());
103
4
        }
104
105
        // Generate session token
106
4
        let timestamp = SystemTime::now()
107
4
            .duration_since(UNIX_EPOCH)
108
4
            .map(|d| d.as_secs())
109
4
            .unwrap_or_else(|_| 
{0
110
0
                error!("Failed to get system time for kill switch authentication");
111
                // Fallback to a fixed timestamp to avoid panic
112
0
                0
113
0
            });
114
115
4
        let session_token = format!("sess_{}_{}_{}", user_id, timestamp, rand::random::<u32>());
116
117
        // Create session
118
4
        let session = AuthSession {
119
4
            user_id: user_id.to_owned(),
120
4
            created_at: SystemTime::now(),
121
4
            last_used: SystemTime::now(),
122
4
            permissions: vec![
123
4
                "kill_switch:activate".to_owned(),
124
4
                "kill_switch:deactivate".to_owned(),
125
4
                "kill_switch:emergency".to_owned(),
126
4
            ],
127
4
        };
128
129
        // Store session
130
4
        if let Ok(mut sessions) = self.sessions.lock() {
131
4
            sessions.insert(session_token.clone(), session);
132
4
        
}0
133
134
4
        Ok(session_token)
135
4
    }
136
137
    /// Validate session token and check permissions
138
5
    fn validate_session(
139
5
        &self,
140
5
        session_token: &str,
141
5
        required_permission: &str,
142
5
    ) -> Result<String, String> {
143
5
        let mut sessions = self.sessions.lock().map_err(|_| "Session lock error")
?0
;
144
145
5
        let session = sessions
146
5
            .get_mut(session_token)
147
5
            .ok_or("Invalid or expired session token")
?0
;
148
149
        // Check session timeout
150
5
        if session
151
5
            .last_used
152
5
            .elapsed()
153
5
            .unwrap_or(Duration::from_secs(999))
154
5
            > self.session_timeout
155
        {
156
0
            sessions.remove(session_token);
157
0
            return Err("Session expired".to_owned());
158
5
        }
159
160
        // Check permissions
161
5
        if !session
162
5
            .permissions
163
5
            .contains(&required_permission.to_owned())
164
        {
165
0
            return Err(format!(
166
0
                "Insufficient permissions for {required_permission}"
167
0
            ));
168
5
        }
169
170
        // Update last used time
171
5
        session.last_used = SystemTime::now();
172
173
5
        Ok(session.user_id.clone())
174
5
    }
175
176
    /// Clean up expired sessions
177
10
    fn cleanup_expired_sessions(&self) {
178
10
        if let Ok(mut sessions) = self.sessions.lock() {
179
10
            sessions.retain(|_, session| 
{9
180
9
                session
181
9
                    .last_used
182
9
                    .elapsed()
183
9
                    .unwrap_or(Duration::from_secs(0))
184
9
                    <= self.session_timeout
185
9
            });
186
0
        }
187
10
    }
188
}
189
190
/// Unix Domain Socket Kill Switch Controller
191
/// Provides regulatory-compliant external control interface
192
pub struct UnixSocketKillSwitch {
193
    socket_path: String,
194
    kill_switch: Arc<AtomicKillSwitch>,
195
    emergency_shutdown: Arc<AtomicBool>,
196
    listener_handle: Option<tokio::task::JoinHandle<()>>,
197
    shutdown_sender: Option<broadcast::Sender<()>>,
198
    auth_manager: AuthManager,
199
}
200
201
impl UnixSocketKillSwitch {
202
    /// Create new Unix socket kill switch interface
203
9
    pub async fn new(socket_path: String, kill_switch: Arc<AtomicKillSwitch>) -> RiskResult<Self> {
204
        // Ensure socket directory exists and has proper permissions
205
9
        if let Some(parent) = Path::new(&socket_path).parent() {
206
9
            if !parent.exists() {
207
0
                tokio::fs::create_dir_all(parent).await.map_err(|e| {
208
0
                    RiskError::Internal(format!("Failed to create socket directory: {e}"))
209
0
                })?;
210
211
                // Set proper permissions for /var/run/kill_switch directory
212
                #[cfg(unix)]
213
                {
214
                    use std::os::unix::fs::PermissionsExt;
215
0
                    let perms = std::fs::Permissions::from_mode(0o755);
216
0
                    std::fs::set_permissions(parent, perms).map_err(|e| {
217
0
                        RiskError::Internal(format!("Failed to set directory permissions: {e}"))
218
0
                    })?;
219
                }
220
9
            }
221
0
        }
222
223
        // Remove existing socket if it exists
224
9
        if Path::new(&socket_path).exists() {
225
0
            std::fs::remove_file(&socket_path).map_err(|e| {
226
0
                RiskError::Internal(format!("Failed to remove existing socket: {e}"))
227
0
            })?;
228
9
        }
229
230
9
        Ok(Self {
231
9
            socket_path,
232
9
            kill_switch,
233
9
            emergency_shutdown: Arc::new(AtomicBool::new(false)),
234
9
            listener_handle: None,
235
9
            shutdown_sender: None,
236
9
            auth_manager: AuthManager::new(),
237
9
        })
238
9
    }
239
240
    /// Start the Unix socket listener for external control
241
7
    pub async fn start_listener(&mut self) -> RiskResult<()> {
242
7
        let listener = TokioUnixListener::bind(&self.socket_path)
243
7
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to bind Unix socket: {e}"0
)))
?0
;
244
245
        // Set proper permissions (readable/writable by owner and group)
246
        #[cfg(unix)]
247
        {
248
            use std::os::unix::fs::PermissionsExt;
249
7
            let perms = std::fs::Permissions::from_mode(0o660);
250
7
            std::fs::set_permissions(&self.socket_path, perms).map_err(|e| 
{0
251
0
                RiskError::Internal(format!("Failed to set socket permissions: {e}"))
252
0
            })?;
253
        }
254
255
7
        let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1);
256
7
        self.shutdown_sender = Some(shutdown_tx);
257
258
7
        let kill_switch = Arc::clone(&self.kill_switch);
259
7
        let emergency_shutdown = Arc::clone(&self.emergency_shutdown);
260
7
        let socket_path = self.socket_path.clone();
261
7
        let auth_manager = self.auth_manager.clone();
262
263
7
        let handle = tokio::spawn(async move {
264
7
            info!(
265
0
                "Unix socket kill switch listener started on {}",
266
                socket_path
267
            );
268
269
            loop {
270
18
                tokio::select! {
271
                    // Handle new connections
272
18
                    
result11
= listener.accept() => {
273
11
                        match result {
274
11
                            Ok((stream, _addr)) => {
275
11
                                let kill_switch_clone = Arc::clone(&kill_switch);
276
11
                                let emergency_shutdown_clone = Arc::clone(&emergency_shutdown);
277
278
11
                                let auth_manager_clone = auth_manager.clone();
279
11
                                tokio::spawn(async move {
280
11
                                    if let Err(
e0
) = Self::handle_connection(
281
11
                                        stream,
282
11
                                        kill_switch_clone,
283
11
                                        emergency_shutdown_clone,
284
11
                                        auth_manager_clone
285
11
                                    ).await {
286
0
                                        error!("Error handling Unix socket connection: {}", e);
287
11
                                    }
288
11
                                });
289
                            }
290
0
                            Err(e) => {
291
0
                                error!("Failed to accept Unix socket connection: {}", e);
292
                            }
293
                        }
294
                    }
295
                    // Handle shutdown signal
296
18
                    _ = shutdown_rx.recv() => {
297
7
                        info!(
"Shutting down Unix socket listener"0
);
298
7
                        break;
299
                    }
300
                }
301
            }
302
303
            // Cleanup socket file on shutdown
304
7
            if let Err(
e0
) = std::fs::remove_file(&socket_path) {
305
0
                warn!("Failed to remove socket file {}: {}", socket_path, e);
306
7
            }
307
7
        });
308
309
7
        self.listener_handle = Some(handle);
310
7
        info!(
311
0
            "Unix socket kill switch controller started on {}",
312
            self.socket_path
313
        );
314
315
7
        Ok(())
316
7
    }
317
318
    /// Stop the Unix socket listener
319
7
    pub async fn stop_listener(&mut self) -> RiskResult<()> {
320
7
        if let Some(sender) = &self.shutdown_sender {
321
7
            let _ = sender.send(());
322
7
        
}0
323
324
7
        if let Some(handle) = self.listener_handle.take() {
325
7
            if let Err(
e0
) = handle.await {
326
0
                warn!("Error waiting for listener shutdown: {}", e);
327
7
            }
328
0
        }
329
330
7
        info!(
"Unix socket kill switch controller stopped"0
);
331
7
        Ok(())
332
7
    }
333
334
    /// Setup signal-based emergency shutdown handlers that bypass Tokio
335
1
    pub async fn setup_emergency_shutdown_signals(&self) -> RiskResult<()> {
336
1
        let emergency_shutdown = Arc::clone(&self.emergency_shutdown);
337
1
        let kill_switch = Arc::clone(&self.kill_switch);
338
339
        // Setup SIGUSR1 for emergency shutdown (bypasses Tokio)
340
1
        let mut sigusr1 = signal(SignalKind::user_defined1())
341
1
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to setup SIGUSR1 handler: {e}"0
)))
?0
;
342
343
1
        let emergency_shutdown_usr1 = Arc::clone(&emergency_shutdown);
344
1
        let kill_switch_usr1 = Arc::clone(&kill_switch);
345
346
1
        tokio::spawn(async move 
{0
347
            loop {
348
0
                sigusr1.recv().await;
349
0
                warn!("\u{1f6a8} SIGUSR1 received - EMERGENCY SHUTDOWN ACTIVATED");
350
351
                // Set emergency flag immediately
352
0
                emergency_shutdown_usr1.store(true, Ordering::SeqCst);
353
354
                // Engage global kill switch
355
0
                if let Err(e) = kill_switch_usr1
356
0
                    .engage(
357
0
                        KillSwitchScope::Global,
358
0
                        "SIGUSR1 emergency signal received".to_owned(),
359
0
                        "signal-handler".to_owned(),
360
0
                        true,
361
0
                    )
362
0
                    .await
363
                {
364
0
                    error!("Failed to engage kill switch via SIGUSR1: {}", e);
365
0
                }
366
367
                // Perform immediate shutdown bypassing Tokio
368
0
                Self::perform_emergency_shutdown("SIGUSR1 signal").await;
369
            }
370
        });
371
372
        // Setup SIGUSR2 for emergency shutdown with different priority
373
1
        let mut sigusr2 = signal(SignalKind::user_defined2())
374
1
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to setup SIGUSR2 handler: {e}"0
)))
?0
;
375
376
1
        let emergency_shutdown_usr2 = Arc::clone(&emergency_shutdown);
377
1
        let kill_switch_usr2 = Arc::clone(&kill_switch);
378
379
1
        tokio::spawn(async move 
{0
380
            loop {
381
0
                sigusr2.recv().await;
382
0
                warn!("\u{1f6a8} SIGUSR2 received - PRIORITY EMERGENCY SHUTDOWN");
383
384
0
                emergency_shutdown_usr2.store(true, Ordering::SeqCst);
385
386
0
                if let Err(e) = kill_switch_usr2
387
0
                    .engage(
388
0
                        KillSwitchScope::Global,
389
0
                        "SIGUSR2 priority emergency signal received".to_owned(),
390
0
                        "signal-handler".to_owned(),
391
0
                        true,
392
0
                    )
393
0
                    .await
394
                {
395
0
                    error!("Failed to engage kill switch via SIGUSR2: {}", e);
396
0
                }
397
398
0
                Self::perform_emergency_shutdown("SIGUSR2 priority signal").await;
399
            }
400
        });
401
402
1
        info!(
"Emergency shutdown signal handlers configured (SIGUSR1, SIGUSR2)"0
);
403
1
        Ok(())
404
1
    }
405
406
    /// Check if emergency shutdown is active
407
    #[must_use]
408
2
    pub fn is_emergency_shutdown_active(&self) -> bool {
409
2
        self.emergency_shutdown.load(Ordering::SeqCst)
410
2
    }
411
412
    /// Handle incoming Unix socket connection
413
11
    async fn handle_connection(
414
11
        stream: tokio::net::UnixStream,
415
11
        kill_switch: Arc<AtomicKillSwitch>,
416
11
        emergency_shutdown: Arc<AtomicBool>,
417
11
        auth_manager: AuthManager,
418
11
    ) -> RiskResult<()> {
419
11
        let (stream_reader, mut stream_writer) = stream.into_split();
420
11
        let mut reader = BufReader::new(stream_reader);
421
11
        let mut line = String::new();
422
423
        // Set connection timeout for regulatory compliance
424
11
        let start_time = Instant::now();
425
426
11
        match timeout(Duration::from_millis(50), reader.read_line(&mut line)).await {
427
            Ok(Ok(_)) => {
428
10
                let latency_ns = start_time.elapsed().as_nanos() as u64;
429
430
                // Parse command
431
10
                let command: KillSwitchCommand = match serde_json::from_str(line.trim()) {
432
10
                    Ok(cmd) => cmd,
433
0
                    Err(e) => {
434
0
                        let response = KillSwitchResponse {
435
0
                            success: false,
436
0
                            message: format!("Invalid command format: {e}"),
437
0
                            timestamp: Utc::now().timestamp() as u64,
438
0
                            latency_ns,
439
0
                        };
440
0
                        Self::write_response(&mut stream_writer, response).await?;
441
0
                        return Ok(());
442
                    },
443
                };
444
445
                // Process command
446
10
                let response = Self::process_command(
447
10
                    command,
448
10
                    &kill_switch,
449
10
                    &emergency_shutdown,
450
10
                    &auth_manager,
451
10
                    latency_ns,
452
10
                )
453
10
                .await;
454
455
10
                Self::write_response(&mut stream_writer, response).await
?0
;
456
            },
457
0
            Ok(Err(e)) => {
458
0
                error!("Error reading from Unix socket: {}", e);
459
            },
460
            Err(_) => {
461
1
                warn!(
"Unix socket read timeout exceeded (50ms)"0
);
462
1
                let response = KillSwitchResponse {
463
1
                    success: false,
464
1
                    message: "Request timeout - must complete within 50ms".to_owned(),
465
1
                    timestamp: Utc::now().timestamp() as u64,
466
1
                    latency_ns: start_time.elapsed().as_nanos() as u64,
467
1
                };
468
1
                Self::write_response(&mut stream_writer, response).await
?0
;
469
            },
470
        }
471
472
11
        Ok(())
473
11
    }
474
475
    /// Process kill switch command
476
10
    async fn process_command(
477
10
        command: KillSwitchCommand,
478
10
        kill_switch: &Arc<AtomicKillSwitch>,
479
10
        emergency_shutdown: &Arc<AtomicBool>,
480
10
        auth_manager: &AuthManager,
481
10
        base_latency_ns: u64,
482
10
    ) -> KillSwitchResponse {
483
10
        let start_time = Instant::now();
484
485
10
        let (success, message) = match command {
486
            // Authentication command - creates a session
487
            KillSwitchCommand::Authenticate {
488
4
                token,
489
4
                user_id,
490
                timestamp: _,
491
4
            } => match auth_manager.authenticate(&token, &user_id) {
492
4
                Ok(session_token) => {
493
4
                    info!(
"User {} authenticated successfully"0
, user_id);
494
4
                    (
495
4
                        true,
496
4
                        format!("Authentication successful. Session token: {session_token}"),
497
4
                    )
498
                },
499
0
                Err(e) => {
500
0
                    warn!("Authentication failed for user {}: {}", user_id, e);
501
0
                    (false, format!("Authentication failed: {e}"))
502
                },
503
            },
504
505
            // Authenticated operations - require valid session
506
            KillSwitchCommand::Activate {
507
1
                scope,
508
1
                reason,
509
1
                cascade,
510
1
                auth_token,
511
1
            } => match auth_manager.validate_session(&auth_token, "kill_switch:activate") {
512
1
                Ok(user_id) => {
513
1
                    info!(
514
0
                        "User {} attempting to activate kill switch for {:?}",
515
                        user_id, scope
516
                    );
517
1
                    match kill_switch
518
1
                        .engage(scope.clone(), reason.clone(), user_id.clone(), cascade)
519
1
                        .await
520
                    {
521
                        Ok(()) => {
522
1
                            warn!(
"\u{1f6a8} KILL SWITCH ACTIVATED for {scope:?}: {reason} by user {user_id}"0
);
523
1
                            (
524
1
                                true,
525
1
                                format!("Kill switch activated for {scope:?}: {reason}"),
526
1
                            )
527
                        },
528
0
                        Err(e) => (false, format!("Failed to activate kill switch: {e}")),
529
                    }
530
                },
531
0
                Err(e) => {
532
0
                    warn!("Unauthorized kill switch activation attempt: {}", e);
533
0
                    (false, format!("Authentication required: {e}"))
534
                },
535
            },
536
537
1
            KillSwitchCommand::Deactivate { scope, auth_token } => {
538
1
                match auth_manager.validate_session(&auth_token, "kill_switch:deactivate") {
539
1
                    Ok(user_id) => {
540
1
                        info!(
541
0
                            "User {} attempting to deactivate kill switch for {:?}",
542
                            user_id, scope
543
                        );
544
1
                        match kill_switch.deactivate(scope.clone(), user_id.clone()).await {
545
                            Ok(()) => {
546
1
                                info!(
"\u{2705} Kill switch deactivated for {scope:?} by user {user_id}"0
);
547
1
                                (true, format!("Kill switch deactivated for {scope:?}"))
548
                            },
549
0
                            Err(e) => (false, format!("Failed to deactivate kill switch: {e}")),
550
                        }
551
                    },
552
0
                    Err(e) => {
553
0
                        warn!("Unauthorized kill switch deactivation attempt: {}", e);
554
0
                        (false, format!("Authentication required: {e}"))
555
                    },
556
                }
557
            },
558
559
2
            KillSwitchCommand::Status { auth_token } => {
560
2
                match auth_manager.validate_session(&auth_token, "kill_switch:activate") {
561
2
                    Ok(user_id) => {
562
2
                        info!(
"User {} requesting kill switch status"0
, user_id);
563
2
                        match kill_switch.is_active().await {
564
2
                            Ok(active) => {
565
2
                                let (checks, commands) = kill_switch.get_metrics();
566
2
                                let (error_rate, failures) = kill_switch.get_health_metrics();
567
2
                                (true, format!(
568
2
                                    "Kill switch status: {} | Checks: {} | Commands: {} | Error rate: {:.2}% | Consecutive failures: {} | User: {}",
569
2
                                    if active { 
"ACTIVE"0
} else { "INACTIVE" },
570
                                    checks,
571
                                    commands,
572
2
                                    error_rate * 100.0,
573
                                    failures,
574
                                    user_id
575
                                ))
576
                            },
577
0
                            Err(e) => (false, format!("Failed to get status: {e}")),
578
                        }
579
                    },
580
0
                    Err(e) => {
581
0
                        warn!("Unauthorized status check attempt: {}", e);
582
0
                        (false, format!("Authentication required: {e}"))
583
                    },
584
                }
585
            },
586
587
1
            KillSwitchCommand::EmergencyShutdown { reason, auth_token } => {
588
1
                match auth_manager.validate_session(&auth_token, "kill_switch:emergency") {
589
1
                    Ok(user_id) => {
590
1
                        error!(
591
0
                            "\u{1f6a8}\u{1f6a8}\u{1f6a8} EMERGENCY SHUTDOWN initiated by user {}: {}",
592
                            user_id, reason
593
                        );
594
595
1
                        emergency_shutdown.store(true, Ordering::SeqCst);
596
597
                        // Activate global kill switch immediately
598
1
                        if let Err(
e0
) = kill_switch
599
1
                            .engage(
600
1
                                KillSwitchScope::Global,
601
1
                                reason.clone(),
602
1
                                user_id.clone(),
603
1
                                true,
604
1
                            )
605
1
                            .await
606
                        {
607
0
                            error!("Failed to engage kill switch during emergency: {}", e);
608
1
                        }
609
610
                        // Trigger emergency shutdown in background
611
1
                        let reason_for_shutdown = format!("{reason} (initiated by {user_id})");
612
1
                        tokio::spawn(async move {
613
1
                            Self::perform_emergency_shutdown(&reason_for_shutdown).await;
614
0
                        });
615
616
1
                        (
617
1
                            true,
618
1
                            format!("Emergency shutdown initiated: {reason} by user {user_id}"),
619
1
                        )
620
                    },
621
0
                    Err(e) => {
622
0
                        error!("\u{1f6a8} UNAUTHORIZED EMERGENCY SHUTDOWN ATTEMPT: {}", e);
623
0
                        (
624
0
                            false,
625
0
                            format!("Authentication required for emergency shutdown: {e}"),
626
0
                        )
627
                    },
628
                }
629
            },
630
631
            // Health check is read-only and doesn't require authentication
632
1
            KillSwitchCommand::HealthCheck => match kill_switch.is_healthy().await {
633
1
                Ok(healthy) => (
634
1
                    healthy,
635
1
                    if healthy {
636
1
                        "System healthy"
637
                    } else {
638
0
                        "System unhealthy - circuit breaker triggered"
639
                    }
640
1
                    .to_owned(),
641
                ),
642
0
                Err(e) => (false, format!("Health check failed: {e}")),
643
            },
644
        };
645
646
        // Clean up expired sessions periodically
647
10
        auth_manager.cleanup_expired_sessions();
648
649
10
        let total_latency_ns = base_latency_ns + start_time.elapsed().as_nanos() as u64;
650
651
10
        KillSwitchResponse {
652
10
            success,
653
10
            message,
654
10
            timestamp: Utc::now().timestamp() as u64,
655
10
            latency_ns: total_latency_ns,
656
10
        }
657
10
    }
658
659
    /// Write response back through Unix socket writer
660
11
    async fn write_response(
661
11
        writer: &mut tokio::net::unix::OwnedWriteHalf,
662
11
        response: KillSwitchResponse,
663
11
    ) -> RiskResult<()> {
664
11
        let response_json = serde_json::to_string(&response)
665
11
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to serialize response: {e}"0
)))
?0
;
666
667
11
        writer
668
11
            .write_all(response_json.as_bytes())
669
11
            .await
670
11
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to write response: {e}"0
)))
?0
;
671
672
11
        writer
673
11
            .write_all(b"\n")
674
11
            .await
675
11
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to write newline: {e}"0
)))
?0
;
676
677
11
        Ok(())
678
11
    }
679
680
    /// Perform emergency shutdown bypassing Tokio runtime
681
    /// This is the critical regulatory compliance function - must complete in <100ms
682
1
    async fn perform_emergency_shutdown(reason: &str) {
683
1
        error!(
"\u{1f6a8}\u{1f6a8}\u{1f6a8} EMERGENCY SHUTDOWN INITIATED: {} \u{1f6a8}\u{1f6a8}\u{1f6a8}"0
, reason);
684
685
        // Log emergency event
686
1
        error!(
"Emergency shutdown timestamp: {}"0
,
Utc::now()0
.
to_rfc33390
());
687
688
        // In a real implementation, this would:
689
        // 1. Immediately cancel all outstanding orders
690
        // 2. Close all positions at market
691
        // 3. Disconnect from all brokers
692
        // 4. Stop all trading algorithms
693
        // 5. Notify regulatory authorities
694
        // 6. Generate emergency audit log
695
696
        // For this implementation, we'll simulate immediate action
697
1
        tokio::time::sleep(Duration::from_millis(10)).await; // Simulated shutdown time
698
699
0
        error!("\u{1f6a8} EMERGENCY SHUTDOWN COMPLETE - System halted");
700
701
        // In production, this might call std::process::exit(1) to ensure immediate termination
702
        // std::process::exit(1);
703
0
    }
704
}
705
706
/// Utility functions for Unix socket kill switch control
707
impl UnixSocketKillSwitch {
708
    /// Send a command to the kill switch via Unix socket (client utility)
709
10
    pub async fn send_command_to_socket(
710
10
        socket_path: &str,
711
10
        command: KillSwitchCommand,
712
10
    ) -> RiskResult<KillSwitchResponse> {
713
10
        let stream = tokio::net::UnixStream::connect(socket_path)
714
10
            .await
715
10
            .map_err(|e| 
{0
716
0
                RiskError::Internal(format!("Failed to connect to kill switch socket: {e}"))
717
0
            })?;
718
719
10
        let command_json = serde_json::to_string(&command)
720
10
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to serialize command: {e}"0
)))
?0
;
721
722
        // Split stream for reading and writing
723
10
        let (stream_reader, mut stream_writer) = stream.into_split();
724
725
        // Send command
726
10
        stream_writer
727
10
            .write_all(command_json.as_bytes())
728
10
            .await
729
10
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to send command: {e}"0
)))
?0
;
730
10
        stream_writer
731
10
            .write_all(b"\n")
732
10
            .await
733
10
            .map_err(|e| RiskError::Internal(
format!0
(
"Failed to send newline: {e}"0
)))
?0
;
734
735
        // Read response
736
10
        let mut reader = BufReader::new(stream_reader);
737
10
        let mut response_line = String::new();
738
739
10
        match timeout(
740
10
            Duration::from_millis(100),
741
10
            reader.read_line(&mut response_line),
742
        )
743
10
        .await
744
        {
745
10
            Ok(Ok(_)) => serde_json::from_str(response_line.trim())
746
10
                .map_err(|e| RiskError::Internal(
format!0
(
"Failed to parse response: {e}"0
))),
747
0
            Ok(Err(e)) => Err(RiskError::Internal(format!("Failed to read response: {e}"))),
748
0
            Err(_) => Err(RiskError::Internal("Response timeout".to_owned())),
749
        }
750
10
    }
751
752
    /// Emergency activation via Unix socket (for external monitoring systems)
753
    /// Requires authentication token
754
0
    pub async fn emergency_activate(
755
0
        socket_path: &str,
756
0
        reason: String,
757
0
        auth_token: String,
758
0
    ) -> RiskResult<KillSwitchResponse> {
759
0
        Self::send_command_to_socket(
760
0
            socket_path,
761
0
            KillSwitchCommand::EmergencyShutdown { reason, auth_token },
762
0
        )
763
0
        .await
764
0
    }
765
766
    /// Quick status check via Unix socket
767
    /// Requires authentication token
768
0
    pub async fn quick_status_check(
769
0
        socket_path: &str,
770
0
        auth_token: String,
771
0
    ) -> RiskResult<KillSwitchResponse> {
772
0
        Self::send_command_to_socket(socket_path, KillSwitchCommand::Status { auth_token }).await
773
0
    }
774
775
    /// Authenticate and get session token for subsequent operations
776
0
    pub async fn authenticate(
777
0
        socket_path: &str,
778
0
        master_token: String,
779
0
        user_id: String,
780
0
    ) -> RiskResult<String> {
781
0
        let response = Self::send_command_to_socket(
782
0
            socket_path,
783
            KillSwitchCommand::Authenticate {
784
0
                token: master_token,
785
0
                user_id,
786
0
                timestamp: SystemTime::now()
787
0
                    .duration_since(UNIX_EPOCH)
788
0
                    .map(|d| d.as_secs())
789
0
                    .unwrap_or_else(|_| {
790
0
                        error!("Failed to get system time for kill switch authentication");
791
0
                        0
792
0
                    }),
793
            },
794
        )
795
0
        .await?;
796
797
0
        if response.success {
798
            // Extract session token from response message
799
0
            if let Some(token) = response.message.split("Session token: ").nth(1) {
800
0
                Ok(token.to_owned())
801
            } else {
802
0
                Err(RiskError::Internal(
803
0
                    "Failed to extract session token from response".to_owned(),
804
0
                ))
805
            }
806
        } else {
807
0
            Err(RiskError::Internal(format!(
808
0
                "Authentication failed: {}",
809
0
                response.message
810
0
            )))
811
        }
812
0
    }
813
}
814
815
#[cfg(test)]
816
mod tests {
817
    use super::*;
818
    use crate::safety::kill_switch::AtomicKillSwitch;
819
    use crate::safety::KillSwitchConfig;
820
    use tempfile::tempdir;
821
822
9
    async fn create_test_setup() -> RiskResult<(UnixSocketKillSwitch, String, tempfile::TempDir)> {
823
9
        let temp_dir = tempdir().map_err(|e| RiskError::Internal(
e0
.
to_string0
()))
?0
;
824
9
        let socket_path = temp_dir.path().join("test_kill_switch.sock");
825
9
        let socket_path_str = socket_path.to_string_lossy().to_string();
826
827
9
        let config = KillSwitchConfig::default();
828
        // Use test constructor to avoid Redis dependency
829
9
        let kill_switch = Arc::new(AtomicKillSwitch::new_test(config));
830
831
9
        let unix_socket_kill_switch =
832
9
            UnixSocketKillSwitch::new(socket_path_str.clone(), kill_switch).await
?0
;
833
834
9
        Ok((unix_socket_kill_switch, socket_path_str, temp_dir))
835
9
    }
836
837
    #[tokio::test]
838
1
    async fn test_unix_socket_creation() -> RiskResult<()> {
839
1
        let (unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
840
841
        // Verify socket path is set correctly
842
1
        assert_eq!(unix_socket_kill_switch.socket_path, socket_path);
843
1
        assert!(!unix_socket_kill_switch.is_emergency_shutdown_active());
844
845
2
        Ok(())
846
1
    }
847
848
    #[tokio::test]
849
1
    async fn test_socket_listener_lifecycle() -> RiskResult<()> {
850
1
        let (mut unix_socket_kill_switch, _, _temp_dir) = create_test_setup().await
?0
;
851
852
        // Start listener
853
1
        unix_socket_kill_switch.start_listener().await
?0
;
854
1
        assert!(unix_socket_kill_switch.listener_handle.is_some());
855
856
        // Stop listener
857
1
        unix_socket_kill_switch.stop_listener().await
?0
;
858
1
        assert!(unix_socket_kill_switch.listener_handle.is_none());
859
860
2
        Ok(())
861
1
    }
862
863
    #[tokio::test]
864
1
    async fn test_command_processing() -> RiskResult<()> {
865
1
        let (mut unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
866
867
        // Start listener
868
1
        unix_socket_kill_switch.start_listener().await
?0
;
869
870
        // Give listener time to start
871
1
        tokio::time::sleep(Duration::from_millis(50)).await;
872
873
        // First authenticate to get session token
874
1
        let auth_response = UnixSocketKillSwitch::send_command_to_socket(
875
1
            &socket_path,
876
1
            KillSwitchCommand::Authenticate {
877
1
                token: "fallback-token-change-me".to_string(),
878
1
                user_id: "test_user".to_string(),
879
1
                timestamp: SystemTime::now()
880
1
                    .duration_since(UNIX_EPOCH)
881
1
                    .expect("System time should be after UNIX epoch in test")
882
1
                    .as_secs(),
883
1
            },
884
1
        )
885
1
        .await
?0
;
886
887
1
        assert!(auth_response.success);
888
1
        assert!(auth_response.message.contains("Authentication successful"));
889
890
        // Extract session token from response
891
1
        let session_token = auth_response
892
1
            .message
893
1
            .split("Session token: ")
894
1
            .nth(1)
895
1
            .expect("Auth response should contain session token")
896
1
            .to_string();
897
898
        // Test status command with authentication
899
1
        let response = UnixSocketKillSwitch::send_command_to_socket(
900
1
            &socket_path,
901
1
            KillSwitchCommand::Status {
902
1
                auth_token: session_token,
903
1
            },
904
1
        )
905
1
        .await
?0
;
906
907
1
        assert!(response.success);
908
1
        assert!(response.message.contains("Kill switch status"));
909
1
        assert!(response.latency_ns > 0);
910
911
        // Stop listener
912
1
        unix_socket_kill_switch.stop_listener().await
?0
;
913
914
2
        Ok(())
915
1
    }
916
917
    #[tokio::test]
918
1
    async fn test_emergency_shutdown_command() -> RiskResult<()> {
919
1
        let (mut unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
920
921
1
        unix_socket_kill_switch.start_listener().await
?0
;
922
1
        tokio::time::sleep(Duration::from_millis(50)).await;
923
924
        // Authenticate first
925
1
        let auth_response = UnixSocketKillSwitch::send_command_to_socket(
926
1
            &socket_path,
927
1
            KillSwitchCommand::Authenticate {
928
1
                token: "fallback-token-change-me".to_string(),
929
1
                user_id: "emergency_user".to_string(),
930
1
                timestamp: SystemTime::now()
931
1
                    .duration_since(UNIX_EPOCH)
932
1
                    .expect("System time should be after UNIX epoch in test")
933
1
                    .as_secs(),
934
1
            },
935
1
        )
936
1
        .await
?0
;
937
938
1
        assert!(auth_response.success);
939
1
        let session_token = auth_response
940
1
            .message
941
1
            .split("Session token: ")
942
1
            .nth(1)
943
1
            .expect("Auth response should contain session token")
944
1
            .to_string();
945
946
        // Test emergency shutdown with authentication
947
1
        let response = UnixSocketKillSwitch::send_command_to_socket(
948
1
            &socket_path,
949
1
            KillSwitchCommand::EmergencyShutdown {
950
1
                reason: "Test emergency".to_string(),
951
1
                auth_token: session_token,
952
1
            },
953
1
        )
954
1
        .await
?0
;
955
956
1
        assert!(response.success);
957
1
        assert!(response.message.contains("Emergency shutdown initiated"));
958
1
        assert!(unix_socket_kill_switch.is_emergency_shutdown_active());
959
960
1
        unix_socket_kill_switch.stop_listener().await
?0
;
961
2
        Ok(())
962
1
    }
963
964
    #[tokio::test]
965
1
    async fn test_activate_deactivate_commands() -> RiskResult<()> {
966
1
        let (mut unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
967
968
1
        unix_socket_kill_switch.start_listener().await
?0
;
969
1
        tokio::time::sleep(Duration::from_millis(50)).await;
970
971
        // Authenticate first
972
1
        let auth_response = UnixSocketKillSwitch::send_command_to_socket(
973
1
            &socket_path,
974
1
            KillSwitchCommand::Authenticate {
975
1
                token: "fallback-token-change-me".to_string(),
976
1
                user_id: "test_operator".to_string(),
977
1
                timestamp: SystemTime::now()
978
1
                    .duration_since(UNIX_EPOCH)
979
1
                    .expect("System time should be after UNIX epoch in test")
980
1
                    .as_secs(),
981
1
            },
982
1
        )
983
1
        .await
?0
;
984
985
1
        assert!(auth_response.success);
986
1
        let session_token = auth_response
987
1
            .message
988
1
            .split("Session token: ")
989
1
            .nth(1)
990
1
            .expect("Auth response should contain session token")
991
1
            .to_string();
992
993
        // Activate kill switch
994
1
        let activate_response = UnixSocketKillSwitch::send_command_to_socket(
995
1
            &socket_path,
996
1
            KillSwitchCommand::Activate {
997
1
                scope: KillSwitchScope::Symbol("AAPL".to_string()),
998
1
                reason: "Test activation".to_string(),
999
1
                cascade: false,
1000
1
                auth_token: session_token.clone(),
1001
1
            },
1002
1
        )
1003
1
        .await
?0
;
1004
1005
1
        assert!(activate_response.success);
1006
1
        assert!(activate_response.message.contains("Kill switch activated"));
1007
1008
        // Deactivate kill switch
1009
1
        let deactivate_response = UnixSocketKillSwitch::send_command_to_socket(
1010
1
            &socket_path,
1011
1
            KillSwitchCommand::Deactivate {
1012
1
                scope: KillSwitchScope::Symbol("AAPL".to_string()),
1013
1
                auth_token: session_token,
1014
1
            },
1015
1
        )
1016
1
        .await
?0
;
1017
1018
1
        assert!(deactivate_response.success);
1019
1
        assert!(deactivate_response
1020
1
            .message
1021
1
            .contains("Kill switch deactivated"));
1022
1023
1
        unix_socket_kill_switch.stop_listener().await
?0
;
1024
2
        Ok(())
1025
1
    }
1026
1027
    #[tokio::test]
1028
1
    async fn test_health_check_command() -> RiskResult<()> {
1029
1
        let (mut unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
1030
1031
1
        unix_socket_kill_switch.start_listener().await
?0
;
1032
1
        tokio::time::sleep(Duration::from_millis(50)).await;
1033
1034
1
        let response = UnixSocketKillSwitch::send_command_to_socket(
1035
1
            &socket_path,
1036
1
            KillSwitchCommand::HealthCheck,
1037
1
        )
1038
1
        .await
?0
;
1039
1040
1
        assert!(response.success);
1041
1
        assert!(response.message.contains("healthy"));
1042
1043
1
        unix_socket_kill_switch.stop_listener().await
?0
;
1044
2
        Ok(())
1045
1
    }
1046
1047
    #[tokio::test]
1048
1
    async fn test_signal_handler_setup() -> RiskResult<()> {
1049
1
        let (unix_socket_kill_switch, _, _temp_dir) = create_test_setup().await
?0
;
1050
1051
        // Setup signal handlers (this should not fail)
1052
1
        let result = unix_socket_kill_switch
1053
1
            .setup_emergency_shutdown_signals()
1054
1
            .await;
1055
1
        assert!(result.is_ok());
1056
1057
2
        Ok(())
1058
1
    }
1059
1060
    #[tokio::test]
1061
1
    async fn test_utility_functions() -> RiskResult<()> {
1062
1
        let (mut unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
1063
1064
1
        unix_socket_kill_switch.start_listener().await
?0
;
1065
1
        tokio::time::sleep(Duration::from_millis(50)).await;
1066
1067
        // First authenticate to get session token
1068
1
        let auth_response = UnixSocketKillSwitch::send_command_to_socket(
1069
1
            &socket_path,
1070
1
            KillSwitchCommand::Authenticate {
1071
1
                token: "fallback-token-change-me".to_string(),
1072
1
                user_id: "utility_user".to_string(),
1073
1
                timestamp: SystemTime::now()
1074
1
                    .duration_since(UNIX_EPOCH)
1075
1
                    .expect("System time should be after UNIX epoch in test")
1076
1
                    .as_secs(),
1077
1
            },
1078
1
        )
1079
1
        .await
?0
;
1080
1081
1
        assert!(auth_response.success);
1082
1
        let session_token = auth_response
1083
1
            .message
1084
1
            .split("Session token: ")
1085
1
            .nth(1)
1086
1
            .expect("Auth response should contain session token")
1087
1
            .to_string();
1088
1089
        // Test utility function for status check with authentication
1090
1
        let status_response = UnixSocketKillSwitch::send_command_to_socket(
1091
1
            &socket_path,
1092
1
            KillSwitchCommand::Status {
1093
1
                auth_token: session_token,
1094
1
            },
1095
1
        )
1096
1
        .await
?0
;
1097
1
        assert!(status_response.success);
1098
1099
1
        unix_socket_kill_switch.stop_listener().await
?0
;
1100
2
        Ok(())
1101
1
    }
1102
1103
    #[tokio::test]
1104
1
    async fn test_connection_timeout() -> RiskResult<()> {
1105
1
        let (mut unix_socket_kill_switch, socket_path, _temp_dir) = create_test_setup().await
?0
;
1106
1107
1
        unix_socket_kill_switch.start_listener().await
?0
;
1108
1
        tokio::time::sleep(Duration::from_millis(50)).await;
1109
1110
        // Connect but don't send data (should timeout)
1111
1
        let _stream = tokio::net::UnixStream::connect(&socket_path).await
?0
;
1112
1113
        // Wait for timeout to occur
1114
1
        tokio::time::sleep(Duration::from_millis(100)).await;
1115
1116
1
        unix_socket_kill_switch.stop_listener().await
?0
;
1117
2
        Ok(())
1118
1
    }
1119
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/stress_tester.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/stress_tester.rs.html deleted file mode 100644 index 08c84d723..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/stress_tester.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/stress_tester.rs
Line
Count
Source
1
//! Stress testing engine for portfolio risk analysis
2
// #![deny(clippy::unwrap_used, clippy::expect_used)] // COMMENTED: Crate-level allows applied
3
#![allow(unused_variables, unused_imports)]
4
5
use std::collections::HashMap;
6
use std::sync::Arc;
7
use std::time::Instant;
8
9
use chrono::Utc;
10
use num::{FromPrimitive, ToPrimitive};
11
// REMOVED: Direct Decimal usage - use canonical types
12
use tokio::sync::RwLock;
13
use tracing::{debug, info, warn};
14
15
use crate::error::{RiskError, RiskResult};
16
use crate::risk_types::{InstrumentId, StressScenario, StressTestResult};
17
use common::{Position, Price, Symbol};
18
use config::{AssetClassMapping, RiskAssetClass, RiskConfig, StressScenarioConfig};
19
use rust_decimal::Decimal;
20
// CANONICAL TYPE IMPORTS - All types from core
21
22
/// Stress testing engine for portfolio risk analysis
23
#[derive(Debug)]
24
pub struct StressTester {
25
    scenarios: Arc<RwLock<HashMap<String, StressScenario>>>,
26
    risk_config: Arc<RwLock<RiskConfig>>,
27
    asset_mapping: Arc<RwLock<AssetClassMapping>>,
28
}
29
30
impl Default for StressTester {
31
0
    fn default() -> Self {
32
0
        Self::new()
33
0
    }
34
}
35
36
impl StressTester {
37
    /// Create a new stress tester with configurable scenarios
38
    ///
39
    /// Initializes a stress tester with scenarios loaded from configuration.
40
    /// Uses the provided `RiskConfig` to load stress scenarios, or creates
41
    /// default scenarios if none provided.
42
    ///
43
    /// # Arguments
44
    ///
45
    /// * `risk_config` - Optional risk configuration containing stress scenarios
46
    ///
47
    /// # Returns
48
    ///
49
    /// Returns a new `StressTester` instance with configured scenarios loaded.
50
    #[must_use]
51
3
    pub fn new() -> Self {
52
3
        Self::with_config(None)
53
3
    }
54
55
    /// Create a new stress tester with specific risk configuration
56
    ///
57
    /// # Arguments
58
    ///
59
    /// * `risk_config` - Optional risk configuration containing stress scenarios
60
    #[must_use]
61
6
    pub fn with_config(risk_config: Option<RiskConfig>) -> Self {
62
6
        let config = risk_config.unwrap_or_default();
63
6
        let asset_mapping = config.asset_class_mapping.clone();
64
65
        // Convert configured scenarios to runtime scenarios
66
6
        let mut scenarios = HashMap::new();
67
24
        for 
scenario_config18
in &config.stress_scenarios {
68
18
            if scenario_config.is_active {
69
18
                scenarios.insert(
70
18
                    scenario_config.id.clone(),
71
18
                    convert_config_to_scenario(scenario_config, &asset_mapping),
72
18
                );
73
18
            
}0
74
        }
75
76
6
        Self {
77
6
            scenarios: Arc::new(RwLock::new(scenarios)),
78
6
            risk_config: Arc::new(RwLock::new(config)),
79
6
            asset_mapping: Arc::new(RwLock::new(asset_mapping)),
80
6
        }
81
6
    }
82
83
    /// Run a stress test on a portfolio using a predefined scenario
84
    ///
85
    /// Applies the specified stress scenario to the given portfolio positions
86
    /// and calculates the potential profit/loss and risk metrics under stress
87
    /// conditions. This is essential for understanding portfolio resilience
88
    /// during market crises.
89
    ///
90
    /// # Arguments
91
    ///
92
    /// * `portfolio_id` - Unique identifier for the portfolio being tested
93
    /// * `scenario_id` - ID of the stress scenario to apply (e.g., "`market_crash_2008`")
94
    /// * `positions` - Array of current portfolio positions to stress test
95
    ///
96
    /// # Returns
97
    ///
98
    /// Returns a `StressTestResult` containing detailed analysis including:
99
    /// - Total profit/loss under stress
100
    /// - Position-level impacts
101
    /// - Risk metrics and statistics
102
    /// - Execution time and metadata
103
    ///
104
    /// # Errors
105
    ///
106
    /// Returns a `RiskError` if:
107
    /// - Scenario ID is not found
108
    /// - Position data is invalid
109
    /// - Calculation fails due to insufficient data
110
6
    pub async fn run_stress_test(
111
6
        &self,
112
6
        portfolio_id: &str,
113
6
        scenario_id: &str,
114
6
        positions: &[Position],
115
6
    ) -> RiskResult<StressTestResult> {
116
6
        let start_time = Instant::now();
117
118
6
        let scenarios = self.scenarios.read().await;
119
6
        let scenario = scenarios
120
6
            .get(scenario_id)
121
6
            .ok_or_else(|| RiskError::Validation {
122
0
                field: "scenario_id".to_owned(),
123
0
                message: format!("Scenario not found: {scenario_id}"),
124
0
            })?;
125
126
        // Calculate pre-stress portfolio value
127
6
        let pre_stress_results: Result<Vec<Price>, RiskError> = positions
128
6
            .iter()
129
12
            .
map6
(|p| {
130
12
                Decimal::try_from(ToPrimitive::to_f64(&p.market_value).unwrap_or(0.0))
131
12
                    .map(Into::into)
132
12
                    .map_err(|_| RiskError::Calculation {
133
0
                        operation: "pre_stress_portfolio_value".to_owned(),
134
0
                        reason: format!(
135
0
                            "Failed to convert market value for instrument {}",
136
                            p.symbol
137
                        ),
138
0
                    })
139
12
            })
140
6
            .collect();
141
6
        let pre_stress_value: Price = pre_stress_results
?0
142
6
            .into_iter()
143
12
            .
map6
(|p| {
144
12
                p.to_decimal().map_err(|_| RiskError::Calculation {
145
0
                    operation: "pre_stress_value_conversion".to_owned(),
146
0
                    reason: "Failed to convert pre-stress value to decimal".to_owned(),
147
0
                })
148
12
            })
149
6
            .collect::<Result<Vec<_>, _>>()
?0
150
6
            .into_iter()
151
6
            .sum::<Decimal>()
152
6
            .into();
153
154
        // Apply stress shocks using configurable approach
155
6
        let asset_mapping = self.asset_mapping.read().await;
156
6
        let mut post_stress_value = Price::ZERO;
157
6
        let mut max_loss_instrument: Option<String> = None;
158
6
        let mut max_loss = Price::ZERO;
159
160
18
        for 
position12
in positions {
161
12
            let stressed_value = if let Some(
shock8
) =
162
12
                scenario.market_shocks.get(&position.symbol.to_string())
163
            {
164
8
                let original_value: Price =
165
8
                    Decimal::try_from(ToPrimitive::to_f64(&position.market_value).unwrap_or(0.0))
166
8
                        .map_err(|_| RiskError::Calculation {
167
0
                            operation: "stress_test_original_value".to_owned(),
168
0
                            reason: format!(
169
0
                                "Failed to convert original market value for instrument {}",
170
                                position.symbol
171
                            ),
172
0
                        })?
173
8
                        .into();
174
                // Calculate shock multiplier using Decimals to handle negative shocks
175
8
                let shock_decimal =
176
8
                    Decimal::try_from(*shock / 100.0).map_err(|_| RiskError::Calculation {
177
0
                        operation: "shock_conversion".to_owned(),
178
0
                        reason: format!("Failed to convert shock value: {shock}"),
179
0
                    })?;
180
8
                let original_decimal =
181
8
                    original_value
182
8
                        .to_decimal()
183
8
                        .map_err(|_| RiskError::Calculation {
184
0
                            operation: "original_value_conversion".to_owned(),
185
0
                            reason: "Failed to convert original value to decimal".to_owned(),
186
0
                        })?;
187
                // Apply shock: new_value = original_value * (1 + shock_decimal)
188
8
                let new_value_decimal = original_decimal * (Decimal::ONE + shock_decimal);
189
8
                let new_value = Price::from_decimal(new_value_decimal.abs()); // Use abs to ensure positive
190
8
                let loss = (original_value - new_value).abs();
191
192
8
                if loss > max_loss {
193
8
                    max_loss = loss;
194
8
                    max_loss_instrument = Some(position.symbol.to_string());
195
8
                
}0
196
197
8
                new_value
198
            } else {
199
4
                let value: Price =
200
4
                    Decimal::try_from(ToPrimitive::to_f64(&position.market_value).unwrap_or(0.0))
201
4
                        .map_err(|_| RiskError::Calculation {
202
0
                            operation: "stress_test_fallback_value".to_owned(),
203
0
                            reason: format!(
204
0
                                "Failed to convert market value for non-shocked instrument {}",
205
                                position.symbol
206
                            ),
207
0
                        })?
208
4
                        .into();
209
4
                value
210
            };
211
212
12
            let post_stress_decimal =
213
12
                post_stress_value
214
12
                    .to_decimal()
215
12
                    .map_err(|_| RiskError::Calculation {
216
0
                        operation: "post_stress_value_conversion".to_owned(),
217
0
                        reason: "Failed to convert post-stress value to decimal".to_owned(),
218
0
                    })?;
219
12
            let stressed_decimal =
220
12
                stressed_value
221
12
                    .to_decimal()
222
12
                    .map_err(|_| RiskError::Calculation {
223
0
                        operation: "stressed_value_conversion".to_owned(),
224
0
                        reason: "Failed to convert stressed value to decimal".to_owned(),
225
0
                    })?;
226
12
            post_stress_value = (post_stress_decimal + stressed_decimal).into();
227
        }
228
229
        // Calculate stress PnL using Decimals to handle negative values
230
6
        let pre_stress_decimal =
231
6
            pre_stress_value
232
6
                .to_decimal()
233
6
                .map_err(|_| RiskError::Calculation {
234
0
                    operation: "pre_stress_value_conversion".to_owned(),
235
0
                    reason: "Failed to convert pre stress value to decimal".to_owned(),
236
0
                })?;
237
6
        let post_stress_decimal =
238
6
            post_stress_value
239
6
                .to_decimal()
240
6
                .map_err(|_| RiskError::Calculation {
241
0
                    operation: "post_stress_value_conversion".to_owned(),
242
0
                    reason: "Failed to convert post stress value to decimal".to_owned(),
243
0
                })?;
244
245
6
        let stress_pnl_decimal = post_stress_decimal - pre_stress_decimal;
246
247
        // Convert to Price using abs value (Price cannot be negative)
248
6
        let stress_pnl = Price::from_decimal(stress_pnl_decimal.abs());
249
250
6
        let stress_pnl_percentage = if pre_stress_value == Price::ZERO {
251
0
            Price::ZERO
252
        } else {
253
6
            let ratio = stress_pnl_decimal / pre_stress_decimal;
254
6
            (ratio * Decimal::from(100)).abs().into()
255
        };
256
257
6
        let execution_time_ms = start_time.elapsed().as_millis() as u64;
258
259
        Ok(StressTestResult {
260
6
            scenario: scenario.clone(),
261
6
            scenario_id: scenario_id.to_owned(),
262
6
            portfolio_id: portfolio_id.to_owned(),
263
6
            pre_stress_value,
264
6
            post_stress_value,
265
6
            stressed_portfolio_value: post_stress_value,
266
6
            stressed_pnl: stress_pnl,
267
6
            stress_pnl: Price::from_decimal(stress_pnl.to_decimal().map_err(|_| 
{0
268
0
                RiskError::Calculation {
269
0
                    operation: "stress_pnl_final_conversion".to_owned(),
270
0
                    reason: "Failed to convert final stress PnL to decimal".to_owned(),
271
0
                }
272
0
            })?),
273
6
            stress_pnl_percentage: stress_pnl_percentage.raw_value() as f64,
274
            var_breach: false,
275
6
            limit_breaches: Vec::new(),
276
            liquidity_shortfall: Price::ZERO,
277
6
            max_loss_instrument,
278
6
            max_loss,
279
6
            execution_time_ms,
280
6
            timestamp: Utc::now(),
281
            max_drawdown: Price::ZERO,
282
6
            risk_metrics: HashMap::new(),
283
        })
284
6
    }
285
286
    /// Get all available stress test scenarios
287
    ///
288
    /// Returns a vector of all predefined and custom stress scenarios
289
    /// currently available for stress testing. This includes both the
290
    /// built-in historical scenarios and any custom scenarios that have
291
    /// been added.
292
    ///
293
    /// # Returns
294
    ///
295
    /// A vector containing all available `StressScenario` instances.
296
6
    pub async fn get_scenarios(&self) -> Vec<StressScenario> {
297
6
        let scenarios = self.scenarios.read().await;
298
6
        scenarios.values().cloned().collect()
299
6
    }
300
301
    /// Add a custom stress test scenario
302
    ///
303
    /// Adds a new stress scenario to the available scenarios. This allows
304
    /// for testing custom market conditions or hypothetical scenarios
305
    /// beyond the predefined historical events.
306
    ///
307
    /// # Arguments
308
    ///
309
    /// * `scenario` - The stress scenario to add to the collection
310
0
    pub async fn add_scenario(&self, scenario: StressScenario) {
311
0
        let mut scenarios = self.scenarios.write().await;
312
0
        scenarios.insert(scenario.id.clone(), scenario);
313
0
    }
314
315
    /// Remove a stress test scenario by ID
316
    ///
317
    /// Removes a stress scenario from the available scenarios collection.
318
    /// Note that predefined scenarios can be removed, but it's recommended
319
    /// to only remove custom scenarios to maintain standard stress testing
320
    /// capabilities.
321
    ///
322
    /// # Arguments
323
    ///
324
    /// * `scenario_id` - The ID of the scenario to remove
325
    ///
326
    /// # Returns
327
    ///
328
    /// Returns `true` if the scenario was found and removed, `false` otherwise.
329
1
    pub async fn remove_scenario(&self, scenario_id: &str) -> bool {
330
1
        let mut scenarios = self.scenarios.write().await;
331
1
        scenarios.remove(scenario_id).is_some()
332
1
    }
333
334
1
    pub async fn run_comprehensive_stress_test(
335
1
        &self,
336
1
        portfolio_id: &str,
337
1
        positions: &[Position],
338
1
    ) -> RiskResult<Vec<StressTestResult>> {
339
1
        let scenarios = self.get_scenarios().await;
340
1
        let mut results = Vec::new();
341
342
6
        for 
scenario5
in scenarios {
343
5
            let result = self
344
5
                .run_stress_test(portfolio_id, &scenario.id, positions)
345
5
                .await
?0
;
346
5
            results.push(result);
347
        }
348
349
1
        Ok(results)
350
1
    }
351
352
    /// Update the risk configuration and reload scenarios
353
    ///
354
    /// This method allows for hot-reloading of stress scenarios from updated
355
    /// configuration without requiring a restart of the stress testing engine.
356
    ///
357
    /// # Arguments
358
    ///
359
    /// * `new_config` - Updated risk configuration containing new scenarios
360
1
    pub async fn update_config(&self, new_config: RiskConfig) {
361
1
        let asset_mapping = new_config.asset_class_mapping.clone();
362
363
        // Update the configuration
364
        {
365
1
            let mut config = self.risk_config.write().await;
366
1
            *config = new_config;
367
        }
368
369
        // Update asset mapping
370
        {
371
1
            let mut mapping = self.asset_mapping.write().await;
372
1
            *mapping = asset_mapping.clone();
373
        }
374
375
        // Reload scenarios from new configuration
376
        {
377
1
            let config = self.risk_config.read().await;
378
1
            let mut scenarios = self.scenarios.write().await;
379
1
            scenarios.clear();
380
381
5
            for scenario_config in &
config.stress_scenarios1
{
382
5
                if scenario_config.is_active {
383
5
                    scenarios.insert(
384
5
                        scenario_config.id.clone(),
385
5
                        convert_config_to_scenario(scenario_config, &asset_mapping),
386
5
                    );
387
5
                
}0
388
            }
389
        }
390
1
    }
391
392
    /// Get the current risk configuration
393
0
    pub async fn get_config(&self) -> RiskConfig {
394
0
        self.risk_config.read().await.clone()
395
0
    }
396
397
    /// Get the current asset class mapping
398
0
    pub async fn get_asset_mapping(&self) -> AssetClassMapping {
399
0
        self.asset_mapping.read().await.clone()
400
0
    }
401
}
402
403
/// Convert a configuration-based stress scenario to a runtime stress scenario
404
///
405
/// This function bridges the gap between the configuration system and the runtime
406
/// stress testing engine by converting configurable scenarios into the format
407
/// expected by the stress testing logic.
408
23
fn convert_config_to_scenario(
409
23
    config: &StressScenarioConfig,
410
23
    asset_mapping: &AssetClassMapping,
411
23
) -> StressScenario {
412
23
    let mut market_shocks = HashMap::new();
413
414
    // Add instrument-specific shocks
415
23
    for (
symbol0
,
shock0
) in &config.instrument_shocks {
416
0
        market_shocks.insert(symbol.clone(), *shock / 100.0); // Convert percentage to decimal
417
0
    }
418
419
    // Add asset class-based shocks for all mapped symbols
420
552
    for (
symbol529
,
asset_class529
) in &asset_mapping.mappings {
421
529
        if let Some(
shock237
) = config.asset_class_shocks.get(asset_class) {
422
            // Only add if no instrument-specific shock exists
423
237
            if !market_shocks.contains_key(symbol) {
424
237
                market_shocks.insert(symbol.clone(), *shock / 100.0); // Convert percentage to decimal
425
237
            
}0
426
292
        }
427
    }
428
429
    // Convert volatility multipliers from asset class to instrument level
430
23
    let mut volatility_multipliers = HashMap::new();
431
552
    for (
symbol529
,
asset_class529
) in &asset_mapping.mappings {
432
529
        if let Some(
multiplier12
) = config.volatility_multipliers.get(asset_class) {
433
12
            volatility_multipliers.insert(symbol.clone(), *multiplier);
434
517
        }
435
    }
436
437
    // Convert liquidity haircuts from asset class to instrument level
438
23
    let mut liquidity_haircuts = HashMap::new();
439
552
    for (
symbol529
,
asset_class529
) in &asset_mapping.mappings {
440
529
        if let Some(
haircut60
) = config.liquidity_haircuts.get(asset_class) {
441
60
            liquidity_haircuts.insert(symbol.clone(), *haircut);
442
469
        }
443
    }
444
445
23
    StressScenario {
446
23
        id: config.id.clone(),
447
23
        name: config.name.clone(),
448
23
        price_shocks: market_shocks.clone(), // Alias for backward compatibility
449
23
        market_shocks,
450
23
        volatility_multiplier: config.volatility_multiplier,
451
23
        volatility_multipliers,
452
23
        correlation_changes: HashMap::new(), // Could be extended later
453
23
        correlation_adjustments: config.correlation_adjustments.clone(),
454
23
        liquidity_haircuts,
455
23
    }
456
23
}
457
458
#[cfg(test)]
459
mod tests {
460
    use super::*;
461
    // operations module removed - use direct imports from common
462
    // Types already imported via prelude at top of file
463
464
2
    fn create_test_positions() -> Result<Vec<Position>, Box<dyn std::error::Error>> {
465
2
        let now = Utc::now();
466
2
        Ok(vec![
467
            Position {
468
2
                id: uuid::Uuid::new_v4(),
469
2
                symbol: "AAPL".to_string(),
470
2
                quantity: FromPrimitive::from_f64(100.0).ok_or_else(|| 
{0
471
0
                    RiskError::CalculationError("Failed to convert 100.0 to decimal".to_owned())
472
0
                })?,
473
2
                avg_price: FromPrimitive::from_f64(150.0).ok_or_else(|| 
{0
474
0
                    RiskError::CalculationError("Failed to convert 150.0 to decimal".to_owned())
475
0
                })?,
476
2
                avg_cost: FromPrimitive::from_f64(150.0).ok_or_else(|| 
{0
477
0
                    RiskError::CalculationError("Failed to convert 150.0 to decimal".to_owned())
478
0
                })?,
479
2
                basis: FromPrimitive::from_f64(15000.0).ok_or_else(|| 
{0
480
0
                    RiskError::CalculationError("Failed to convert 15000.0 to decimal".to_owned())
481
0
                })?,
482
2
                average_price: FromPrimitive::from_f64(150.0).ok_or_else(|| 
{0
483
0
                    RiskError::CalculationError("Failed to convert 150.0 to decimal".to_owned())
484
0
                })?,
485
2
                market_value: FromPrimitive::from_f64(15000.0).ok_or_else(|| 
{0
486
0
                    RiskError::CalculationError("Failed to convert 15000.0 to decimal".to_owned())
487
0
                })?,
488
                unrealized_pnl: Decimal::ZERO,
489
                realized_pnl: Decimal::ZERO,
490
2
                created_at: now,
491
2
                updated_at: now,
492
2
                last_updated: now,
493
2
                current_price: None,
494
2
                notional_value: FromPrimitive::from_f64(15000.0).ok_or_else(|| 
{0
495
0
                    RiskError::CalculationError("Failed to convert 15000.0 to decimal".to_owned())
496
0
                })?,
497
                margin_requirement: Decimal::ZERO,
498
            },
499
            Position {
500
2
                id: uuid::Uuid::new_v4(),
501
2
                symbol: "GOOGL".to_string(),
502
2
                quantity: FromPrimitive::from_f64(50.0).ok_or_else(|| 
{0
503
0
                    RiskError::CalculationError("Failed to convert 50.0 to decimal".to_owned())
504
0
                })?,
505
2
                avg_price: FromPrimitive::from_f64(2500.0).ok_or_else(|| 
{0
506
0
                    RiskError::CalculationError("Failed to convert 2500.0 to decimal".to_owned())
507
0
                })?,
508
2
                avg_cost: FromPrimitive::from_f64(2500.0).ok_or_else(|| 
{0
509
0
                    RiskError::CalculationError("Failed to convert 2500.0 to decimal".to_owned())
510
0
                })?,
511
2
                basis: FromPrimitive::from_f64(125000.0).ok_or_else(|| 
{0
512
0
                    RiskError::CalculationError("Failed to convert 125000.0 to decimal".to_owned())
513
0
                })?,
514
2
                average_price: FromPrimitive::from_f64(2500.0).ok_or_else(|| 
{0
515
0
                    RiskError::CalculationError("Failed to convert 2500.0 to decimal".to_owned())
516
0
                })?,
517
2
                market_value: FromPrimitive::from_f64(125000.0).ok_or_else(|| 
{0
518
0
                    RiskError::CalculationError("Failed to convert 125000.0 to decimal".to_owned())
519
0
                })?,
520
                unrealized_pnl: Decimal::ZERO,
521
                realized_pnl: Decimal::ZERO,
522
2
                created_at: now,
523
2
                updated_at: now,
524
2
                last_updated: now,
525
2
                current_price: None,
526
2
                notional_value: FromPrimitive::from_f64(125000.0).ok_or_else(|| 
{0
527
0
                    RiskError::CalculationError("Failed to convert 125000.0 to decimal".to_owned())
528
0
                })?,
529
                margin_requirement: Decimal::ZERO,
530
            },
531
        ])
532
2
    }
533
534
3
    fn create_test_scenario_config() -> StressScenarioConfig {
535
3
        let mut asset_class_shocks = HashMap::new();
536
3
        asset_class_shocks.insert(RiskAssetClass::Technology, -10.0); // -10%
537
3
        asset_class_shocks.insert(RiskAssetClass::LargeCapEquity, -15.0); // -15%
538
539
3
        StressScenarioConfig {
540
3
            id: "test_scenario".to_string(),
541
3
            name: "Test Scenario".to_string(),
542
3
            description: "Test scenario for unit testing".to_string(),
543
3
            instrument_shocks: HashMap::new(),
544
3
            asset_class_shocks,
545
3
            volatility_multiplier: 1.0,
546
3
            volatility_multipliers: HashMap::new(),
547
3
            correlation_adjustments: HashMap::new(),
548
3
            liquidity_haircuts: HashMap::new(),
549
3
            is_active: true,
550
3
        }
551
3
    }
552
553
3
    fn create_test_risk_config() -> RiskConfig {
554
3
        RiskConfig {
555
3
            stress_scenarios: vec![create_test_scenario_config()],
556
3
            asset_class_mapping: create_test_asset_mapping(),
557
3
            default_volatility_multiplier: 1.0,
558
3
            max_portfolio_loss_pct: 20.0,
559
3
            var_confidence_level: 0.95,
560
3
            var_time_horizon_days: 1,
561
3
        }
562
3
    }
563
564
3
    fn create_test_asset_mapping() -> AssetClassMapping {
565
3
        let mut mappings = HashMap::new();
566
3
        mappings.insert("AAPL".to_string(), RiskAssetClass::Technology);
567
3
        mappings.insert("GOOGL".to_string(), RiskAssetClass::Technology);
568
3
        mappings.insert("SPY".to_string(), RiskAssetClass::LargeCapEquity);
569
570
3
        AssetClassMapping {
571
3
            mappings,
572
3
            default_class: RiskAssetClass::LargeCapEquity,
573
3
        }
574
3
    }
575
576
    #[tokio::test]
577
1
    async fn test_stress_scenario_application() -> Result<(), Box<dyn std::error::Error>> {
578
1
        let _tester = StressTester::new();
579
        // Test passes if no panic
580
2
        Ok(())
581
1
    }
582
583
    #[tokio::test]
584
1
    async fn test_add_remove_scenario() -> Result<(), Box<dyn std::error::Error>> {
585
1
        let risk_config = create_test_risk_config();
586
1
        let tester = StressTester::with_config(Some(risk_config));
587
588
        // Test scenario should be loaded from config
589
1
        let scenarios = tester.get_scenarios().await;
590
1
        assert!(scenarios.iter().any(|s| s.id == "test_scenario"));
591
592
        // Test removing scenario
593
1
        let removed = tester.remove_scenario("test_scenario").await;
594
1
        assert!(removed);
595
596
1
        let scenarios = tester.get_scenarios().await;
597
1
        assert!(!scenarios.iter().any(|s| 
s.id0
==
"test_scenario"0
));
598
2
        Ok(())
599
1
    }
600
601
    #[tokio::test]
602
1
    async fn test_stress_test_execution() -> Result<(), Box<dyn std::error::Error>> {
603
1
        let risk_config = create_test_risk_config();
604
1
        let tester = StressTester::with_config(Some(risk_config));
605
1
        let positions = create_test_positions()
?0
;
606
607
1
        let result = tester
608
1
            .run_stress_test("test_portfolio", "test_scenario", &positions)
609
1
            .await;
610
611
1
        if let Err(
e0
) = &result {
612
0
            eprintln!("Stress test error: {:?}", e);
613
1
        }
614
1
        assert!(result.is_ok());
615
616
1
        let result = result
?0
;
617
1
        assert_eq!(result.portfolio_id, "test_portfolio");
618
1
        assert_eq!(result.scenario_id, "test_scenario");
619
1
        assert!(result.stress_pnl > Price::ZERO); // Should show loss magnitude due to price drops
620
                                                  // execution_time_ms is always >= 0 (u64), so we just verify it exists
621
1
        let _ = result.execution_time_ms; // Acknowledge the field exists
622
2
        Ok(())
623
1
    }
624
    #[tokio::test]
625
1
    async fn test_predefined_scenarios() -> Result<(), Box<dyn std::error::Error>> {
626
1
        let tester = StressTester::new(); // Uses default config with predefined scenarios
627
1
        let scenarios = tester.get_scenarios().await;
628
629
        // Should have default scenarios from configuration
630
1
        assert!(scenarios.iter().any(|s| s.id == "market_crash_2008"));
631
2
        
assert!1
(
scenarios.iter()1
.
any1
(|s| s.id == "covid_crash_2020"));
632
3
        
assert!1
(
scenarios.iter()1
.
any1
(|s| s.id == "flash_crash_2010"));
633
4
        
assert!1
(
scenarios.iter()1
.
any1
(|s| s.id == "volatility_spike"));
634
5
        
assert!1
(
scenarios.iter()1
.
any1
(|s| s.id == "interest_rate_shock"));
635
2
        Ok(())
636
1
    }
637
638
    #[tokio::test]
639
1
    async fn test_comprehensive_stress_test() -> Result<(), Box<dyn std::error::Error>> {
640
1
        let tester = StressTester::new();
641
1
        let positions = create_test_positions()
?0
;
642
643
1
        let results = tester
644
1
            .run_comprehensive_stress_test("test_portfolio", &positions)
645
1
            .await
?0
;
646
647
        // Should have results for multiple scenarios (default config has 5 scenarios)
648
1
        assert!(!results.is_empty());
649
1
        assert!(results.len() >= 5);
650
651
        // All results should be for the same portfolio
652
6
        
for 1
result5
in &results {
653
5
            assert_eq!(result.portfolio_id, "test_portfolio");
654
1
        }
655
1
        Ok(())
656
1
    }
657
658
    #[tokio::test]
659
1
    async fn test_config_update() -> Result<(), Box<dyn std::error::Error>> {
660
1
        let initial_config = create_test_risk_config();
661
1
        let tester = StressTester::with_config(Some(initial_config));
662
663
        // Initially should have test scenario
664
1
        let scenarios = tester.get_scenarios().await;
665
1
        assert!(scenarios.iter().any(|s| s.id == "test_scenario"));
666
667
        // Update config with default scenarios
668
1
        let new_config = RiskConfig::default();
669
1
        tester.update_config(new_config).await;
670
671
        // Should now have default scenarios instead
672
1
        let scenarios = tester.get_scenarios().await;
673
5
        
assert!1
(!
scenarios.iter()1
.
any1
(|s| s.id == "test_scenario"));
674
1
        assert!(scenarios.iter().any(|s| s.id == "market_crash_2008"));
675
676
2
        Ok(())
677
1
    }
678
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/expected_shortfall.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/expected_shortfall.rs.html deleted file mode 100644 index 18903de0e..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/expected_shortfall.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/expected_shortfall.rs
Line
Count
Source
1
//! Expected Shortfall (ES) / Conditional Value at Risk (`CVaR`) calculation
2
//! Production implementation for tail risk assessment
3
4
use std::collections::HashMap;
5
// REMOVED: Direct Decimal usage - use canonical types
6
use anyhow::Result;
7
use common::types::Price;
8
use rust_decimal::Decimal;
9
use tracing::warn;
10
11
// Removed types::operations - using common::types::prelude instead
12
13
/// Expected Shortfall calculator for tail risk measurement
14
///
15
/// Expected Shortfall (ES), also known as Conditional Value at Risk (`CVaR`),
16
/// measures the expected loss given that a loss exceeds the Value at Risk (`VaR`)
17
/// threshold. This provides a more comprehensive view of tail risk than `VaR` alone.
18
///
19
/// # Mathematical Foundation
20
///
21
/// For a given confidence level α, Expected Shortfall is defined as:
22
/// `ES_α` = E[X | X ≤ `VaR_α`]
23
///
24
/// Where X represents portfolio returns and `VaR_α` is the Value at Risk at confidence level α.
25
///
26
/// # Use Cases
27
///
28
/// - Regulatory capital calculations
29
/// - Risk-adjusted performance measurement
30
/// - Portfolio optimization with tail risk constraints
31
/// - Stress testing and scenario analysis
32
///
33
/// # Example
34
///
35
/// ```rust
36
/// use risk::var_calculator::expected_shortfall::ExpectedShortfall;
37
/// use std::collections::HashMap;
38
/// use common::types::Price;
39
///
40
/// let mut es_calculator = ExpectedShortfall::new(0.95);
41
///
42
/// let mut returns_data = HashMap::new();
43
/// returns_data.insert("AAPL".to_string(), vec![-0.02, 0.01, -0.05, 0.03]);
44
/// returns_data.insert("MSFT".to_string(), vec![-0.01, 0.02, -0.03, 0.01]);
45
///
46
/// es_calculator.update_returns_data(returns_data);
47
///
48
/// let weights = vec![0.6, 0.4];
49
/// let portfolio_value = Price::from(1_000_000);
50
///
51
/// let es = es_calculator.calculate_expected_shortfall(&weights, portfolio_value)?;
52
/// ```
53
#[derive(Debug)]
54
pub struct ExpectedShortfall {
55
    /// Confidence level for Expected Shortfall calculation
56
    ///
57
    /// Typical values:
58
    /// - 0.95 (95%): Standard risk management
59
    /// - 0.99 (99%): Regulatory requirements (Basel III)
60
    /// - 0.975 (97.5%): Internal risk limits
61
    confidence_level: f64,
62
63
    /// Historical returns data by symbol
64
    ///
65
    /// Key: Symbol identifier (e.g., "AAPL", "MSFT")
66
    /// Value: Vector of historical returns (as decimals, e.g., 0.02 for 2%)
67
    ///
68
    /// Returns should be:
69
    /// - Chronologically ordered (oldest to newest)
70
    /// - Same frequency (daily, weekly, etc.)
71
    /// - Clean of corporate actions adjustments
72
    returns_data: HashMap<String, Vec<f64>>,
73
}
74
75
impl ExpectedShortfall {
76
    /// Creates a new Expected Shortfall calculator with specified confidence level
77
    ///
78
    /// # Arguments
79
    ///
80
    /// * `confidence_level` - Confidence level between 0.0 and 1.0 (e.g., 0.95 for 95%)
81
    ///
82
    /// # Returns
83
    ///
84
    /// New `ExpectedShortfall` instance with empty returns data
85
    ///
86
    /// # Examples
87
    ///
88
    /// ```rust
89
    /// use risk::var_calculator::expected_shortfall::ExpectedShortfall;
90
    ///
91
    /// // Create 95% confidence level ES calculator
92
    /// let es_calc = ExpectedShortfall::new(0.95);
93
    ///
94
    /// // Create 99% confidence level for regulatory compliance
95
    /// let regulatory_es = ExpectedShortfall::new(0.99);
96
    /// ```
97
    ///
98
    /// # Panics
99
    ///
100
    /// Does not panic, but confidence levels outside [0, 1] will produce
101
    /// meaningless results in subsequent calculations.
102
    #[must_use]
103
17
    pub fn new(confidence_level: f64) -> Self {
104
17
        Self {
105
17
            confidence_level,
106
17
            returns_data: HashMap::new(),
107
17
        }
108
17
    }
109
110
    /// Updates the historical returns data used for Expected Shortfall calculations
111
    ///
112
    /// # Arguments
113
    ///
114
    /// * `returns` - `HashMap` mapping symbol identifiers to their historical returns
115
    ///
116
    /// # Data Requirements
117
    ///
118
    /// - Returns should be expressed as decimals (0.02 for 2%)
119
    /// - All return series should have the same frequency
120
    /// - Series should be chronologically ordered
121
    /// - Minimum 100 observations recommended for stable ES estimates
122
    ///
123
    /// # Examples
124
    ///
125
    /// ```rust
126
    /// use std::collections::HashMap;
127
    /// use risk::var_calculator::expected_shortfall::ExpectedShortfall;
128
    ///
129
    /// let mut es_calc = ExpectedShortfall::new(0.95);
130
    ///
131
    /// let mut returns = HashMap::new();
132
    /// returns.insert("AAPL".to_string(), vec![-0.05, 0.02, -0.01, 0.03]);
133
    /// returns.insert("GOOGL".to_string(), vec![-0.03, 0.01, -0.02, 0.04]);
134
    ///
135
    /// es_calc.update_returns_data(returns);
136
    /// ```
137
15
    pub fn update_returns_data(&mut self, returns: HashMap<String, Vec<f64>>) {
138
15
        self.returns_data = returns;
139
15
    }
140
141
    /// Calculates Expected Shortfall using historical simulation methodology
142
    ///
143
    /// This method computes the expected loss in the tail beyond the `VaR` threshold
144
    /// using historical return data and portfolio weights.
145
    ///
146
    /// # Arguments
147
    ///
148
    /// * `portfolio_weights` - Allocation weights for each asset (must sum to 1.0)
149
    /// * `portfolio_value` - Total portfolio value in base currency
150
    ///
151
    /// # Returns
152
    ///
153
    /// * `Ok(Decimal)` - Expected Shortfall amount in absolute currency terms
154
    /// * `Err(anyhow::Error)` - If calculation fails due to insufficient data or invalid inputs
155
    ///
156
    /// # Mathematical Process
157
    ///
158
    /// 1. Calculate weighted portfolio returns for each historical period
159
    /// 2. Sort returns in ascending order (worst losses first)
160
    /// 3. Identify `VaR` threshold at specified confidence level
161
    /// 4. Compute average of all returns worse than `VaR` threshold
162
    /// 5. Convert to absolute dollar amount using portfolio value
163
    ///
164
    /// # Examples
165
    ///
166
    /// ```rust
167
    /// use risk::var_calculator::expected_shortfall::ExpectedShortfall;
168
    /// use common::types::Price;
169
    /// use std::collections::HashMap;
170
    ///
171
    /// let mut es_calc = ExpectedShortfall::new(0.95);
172
    ///
173
    /// // Set up returns data
174
    /// let mut returns = HashMap::new();
175
    /// returns.insert("AAPL".to_string(), vec![-0.05, 0.02, -0.03, 0.01]);
176
    /// returns.insert("MSFT".to_string(), vec![-0.02, 0.03, -0.01, 0.02]);
177
    /// es_calc.update_returns_data(returns);
178
    ///
179
    /// // Calculate ES for 60/40 portfolio worth $1M
180
    /// let weights = vec![0.6, 0.4];
181
    /// let portfolio_value = Price::from(1_000_000);
182
    ///
183
    /// let es_amount = es_calc.calculate_expected_shortfall(&weights, portfolio_value)?;
184
    /// println!("95% Expected Shortfall: ${}", es_amount);
185
    /// ```
186
    ///
187
    /// # Errors
188
    ///
189
    /// - Returns error if no returns data is available
190
    /// - Returns error if portfolio weights length doesn't match number of assets
191
    /// - Returns error if portfolio value cannot be parsed
192
    /// - Returns error if `VaR` index calculation produces invalid results
193
15
    pub fn calculate_expected_shortfall(
194
15
        &self,
195
15
        portfolio_weights: &[f64],
196
15
        portfolio_value: Price,
197
15
    ) -> Result<Decimal> {
198
15
        if self.returns_data.is_empty() {
199
1
            return Err(anyhow::anyhow!(
200
1
                "No returns data available for ES calculation"
201
1
            ));
202
14
        }
203
204
        // Calculate portfolio returns
205
14
        let 
portfolio_returns13
= self.calculate_portfolio_returns(portfolio_weights)
?1
;
206
207
13
        if portfolio_returns.is_empty() {
208
0
            return Err(anyhow::anyhow!("No portfolio returns calculated"));
209
13
        }
210
211
        // Sort returns in ascending order (worst first)
212
13
        let mut sorted_returns = portfolio_returns;
213
139
        
sorted_returns13
.
sort_by13
(|a, b| {
214
139
            a.partial_cmp(b).unwrap_or_else(|| 
{0
215
                // Handle NaN values: treat NaN as the smallest value for conservative risk assessment
216
0
                if a.is_nan() && b.is_nan() {
217
0
                    std::cmp::Ordering::Equal
218
0
                } else if a.is_nan() {
219
0
                    std::cmp::Ordering::Less
220
0
                } else if b.is_nan() {
221
0
                    std::cmp::Ordering::Greater
222
                } else {
223
0
                    std::cmp::Ordering::Equal
224
                }
225
0
            })
226
139
        });
227
228
        // Find VaR threshold
229
13
        let var_index =
230
13
            ((1.0 - self.confidence_level) * sorted_returns.len() as f64).floor() as usize;
231
232
13
        if var_index >= sorted_returns.len() {
233
0
            return Err(anyhow::anyhow!("VaR index out of bounds"));
234
13
        }
235
236
        // Calculate Expected Shortfall as average of returns worse than VaR
237
13
        let tail_returns = &sorted_returns[0..=var_index];
238
13
        let expected_shortfall_return =
239
13
            tail_returns.iter().sum::<f64>() / tail_returns.len() as f64;
240
241
        // Convert to dollar amount
242
13
        let portfolio_value_f64 = portfolio_value
243
13
            .to_string()
244
13
            .parse::<f64>()
245
13
            .map_err(|e| anyhow::anyhow!(
"Failed to parse portfolio value: {e}"0
))
?0
;
246
247
13
        let es_amount = expected_shortfall_return.abs() * portfolio_value_f64;
248
249
13
        Decimal::try_from(es_amount).map_err(|_| anyhow::anyhow!(
"Failed to convert ES to decimal"0
))
250
15
    }
251
252
    /// Calculates weighted portfolio returns from individual asset returns
253
    ///
254
    /// # Arguments
255
    ///
256
    /// * `weights` - Portfolio allocation weights (must match number of assets)
257
    ///
258
    /// # Returns
259
    ///
260
    /// * `Ok(Vec<f64>)` - Vector of portfolio returns for each time period
261
    /// * `Err(anyhow::Error)` - If weights don't match assets or no data available
262
    ///
263
    /// # Process
264
    ///
265
    /// 1. Validates weights length matches number of assets
266
    /// 2. Finds minimum length across all return series for alignment
267
    /// 3. Calculates weighted sum of returns for each time period
268
    /// 4. Uses most recent data when series have different lengths
269
14
    fn calculate_portfolio_returns(&self, weights: &[f64]) -> Result<Vec<f64>> {
270
14
        if weights.is_empty() {
271
0
            return Err(anyhow::anyhow!("Portfolio weights cannot be empty"));
272
14
        }
273
274
14
        let symbols: Vec<String> = self.returns_data.keys().cloned().collect();
275
276
14
        if symbols.len() != weights.len() {
277
1
            return Err(anyhow::anyhow!(
278
1
                "Number of weights must match number of assets"
279
1
            ));
280
13
        }
281
282
        // Find minimum length across all return series
283
13
        let min_length = self
284
13
            .returns_data
285
13
            .values()
286
13
            .map(Vec::len)
287
13
            .min()
288
13
            .unwrap_or_else(|| 
{0
289
0
                warn!("No returns data found in expected shortfall calculation");
290
0
                0
291
0
            });
292
293
13
        if min_length == 0 {
294
0
            return Err(anyhow::anyhow!("No returns data available"));
295
13
        }
296
297
13
        let mut portfolio_returns = Vec::with_capacity(min_length);
298
299
        // Calculate weighted portfolio returns for each period
300
75
        for period in 0..
min_length13
{
301
75
            let mut portfolio_return = 0.0;
302
303
95
            for (i, symbol) in 
symbols.iter()75
.
enumerate75
() {
304
95
                if let Some(asset_returns) = self.returns_data.get(symbol) {
305
95
                    // Use the most recent data by indexing from the end
306
95
                    let return_index = asset_returns.len() - min_length + period;
307
95
                    portfolio_return += weights[i] * asset_returns[return_index];
308
95
                
}0
309
            }
310
311
75
            portfolio_returns.push(portfolio_return);
312
        }
313
314
13
        Ok(portfolio_returns)
315
14
    }
316
317
    /// Calculates Expected Shortfall with bootstrap confidence intervals
318
    ///
319
    /// This method provides not only the point estimate of Expected Shortfall
320
    /// but also confidence intervals around that estimate using bootstrap resampling.
321
    ///
322
    /// # Arguments
323
    ///
324
    /// * `portfolio_weights` - Portfolio allocation weights
325
    /// * `portfolio_value` - Total portfolio value
326
    /// * `confidence_interval` - Confidence level for the interval (e.g., 0.95 for 95%)
327
    ///
328
    /// # Returns
329
    ///
330
    /// * `Ok(ESResult)` - Complete ES results with confidence bounds
331
    /// * `Err(anyhow::Error)` - If calculation fails
332
    ///
333
    /// # Bootstrap Methodology
334
    ///
335
    /// 1. Performs 1000 bootstrap samples of historical returns
336
    /// 2. Calculates ES for each bootstrap sample
337
    /// 3. Derives confidence intervals from bootstrap distribution
338
    /// 4. Provides lower and upper bounds for risk assessment
339
    ///
340
    /// # Use Cases
341
    ///
342
    /// - Model validation and backtesting
343
    /// - Uncertainty quantification in risk reports
344
    /// - Regulatory stress testing with confidence bounds
345
    /// - Portfolio optimization with estimation risk
346
    ///
347
    /// # Examples
348
    ///
349
    /// ```rust
350
    /// use risk::var_calculator::expected_shortfall::ExpectedShortfall;
351
    /// use common::types::Price;
352
    ///
353
    /// let es_calc = ExpectedShortfall::new(0.95);
354
    /// // ... set up returns data ...
355
    ///
356
    /// let weights = vec![0.6, 0.4];
357
    /// let portfolio_value = Price::from(1_000_000);
358
    ///
359
    /// let es_result = es_calc.calculate_es_with_confidence(
360
    ///     &weights,
361
    ///     portfolio_value,
362
    ///     0.95  // 95% confidence interval
363
    /// )?;
364
    ///
365
    /// println!("ES: ${}", es_result.expected_shortfall);
366
    /// println!("95% CI: [${}, ${}]", es_result.lower_bound, es_result.upper_bound);
367
    /// ```
368
0
    pub fn calculate_es_with_confidence(
369
0
        &self,
370
0
        portfolio_weights: &[f64],
371
0
        portfolio_value: Price,
372
0
        confidence_interval: f64,
373
0
    ) -> Result<ESResult> {
374
0
        let base_es = self.calculate_expected_shortfall(portfolio_weights, portfolio_value)?;
375
376
        // Bootstrap confidence intervals (simplified implementation)
377
0
        let portfolio_returns = self.calculate_portfolio_returns(portfolio_weights)?;
378
0
        let n_bootstrap = 1000;
379
0
        let mut bootstrap_es = Vec::new();
380
381
        // Simple bootstrap resampling
382
0
        for _ in 0..n_bootstrap {
383
0
            let mut resampled_returns = Vec::new();
384
0
            for _ in 0..portfolio_returns.len() {
385
0
                let idx = fastrand::usize(..portfolio_returns.len());
386
0
                resampled_returns.push(portfolio_returns[idx]);
387
0
            }
388
389
0
            let es = self.calculate_es_from_returns(&resampled_returns, portfolio_value)?;
390
0
            bootstrap_es.push(es);
391
        }
392
393
0
        bootstrap_es.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
394
395
0
        let lower_idx = ((1.0 - confidence_interval) / 2.0 * f64::from(n_bootstrap)) as usize;
396
0
        let upper_idx = ((1.0 + confidence_interval) / 2.0 * f64::from(n_bootstrap)) as usize;
397
398
        Ok(ESResult {
399
0
            expected_shortfall: base_es.into(),
400
0
            confidence_level: self.confidence_level,
401
0
            lower_bound: bootstrap_es
402
0
                .get(lower_idx)
403
0
                .copied()
404
0
                .unwrap_or_else(|| {
405
0
                    warn!("Failed to get bootstrap lower bound, using base ES");
406
0
                    base_es
407
0
                })
408
0
                .into(),
409
0
            upper_bound: bootstrap_es
410
0
                .get(upper_idx.min(bootstrap_es.len() - 1))
411
0
                .copied()
412
0
                .unwrap_or_else(|| {
413
0
                    warn!("Failed to get bootstrap upper bound, using base ES");
414
0
                    base_es
415
0
                })
416
0
                .into(),
417
0
            confidence_interval,
418
        })
419
0
    }
420
421
    /// Helper method to calculate Expected Shortfall from a given set of returns
422
    ///
423
    /// Used internally for bootstrap resampling and testing scenarios.
424
    ///
425
    /// # Arguments
426
    ///
427
    /// * `returns` - Vector of portfolio returns
428
    /// * `portfolio_value` - Portfolio value for absolute amount calculation
429
    ///
430
    /// # Returns
431
    ///
432
    /// Expected Shortfall amount as Decimal, or error if calculation fails
433
0
    fn calculate_es_from_returns(
434
0
        &self,
435
0
        returns: &[f64],
436
0
        portfolio_value: Price,
437
0
    ) -> Result<Decimal> {
438
0
        let mut sorted_returns = returns.to_vec();
439
0
        sorted_returns.sort_by(|a, b| {
440
0
            a.partial_cmp(b).unwrap_or_else(|| {
441
                // Handle NaN values: treat NaN as the smallest value for conservative risk assessment
442
0
                if a.is_nan() && b.is_nan() {
443
0
                    std::cmp::Ordering::Equal
444
0
                } else if a.is_nan() {
445
0
                    std::cmp::Ordering::Less
446
0
                } else if b.is_nan() {
447
0
                    std::cmp::Ordering::Greater
448
                } else {
449
0
                    std::cmp::Ordering::Equal
450
                }
451
0
            })
452
0
        });
453
454
0
        let var_index =
455
0
            ((1.0 - self.confidence_level) * sorted_returns.len() as f64).floor() as usize;
456
457
0
        if var_index >= sorted_returns.len() {
458
0
            return Ok(Decimal::ZERO);
459
0
        }
460
461
0
        let tail_returns = &sorted_returns[0..=var_index];
462
0
        let expected_shortfall_return =
463
0
            tail_returns.iter().sum::<f64>() / tail_returns.len() as f64;
464
465
0
        let portfolio_value_f64 = portfolio_value
466
0
            .to_string()
467
0
            .parse::<f64>()
468
0
            .map_err(|e| anyhow::anyhow!("Failed to parse portfolio value: {e}"))?;
469
470
0
        let es_amount = expected_shortfall_return.abs() * portfolio_value_f64;
471
472
0
        Decimal::from_f64_retain(es_amount)
473
0
            .ok_or_else(|| anyhow::anyhow!("Failed to convert ES to decimal"))
474
0
    }
475
}
476
477
/// Expected Shortfall calculation result with confidence intervals
478
///
479
/// Contains the complete results of an Expected Shortfall calculation
480
/// including the point estimate and bootstrap confidence intervals.
481
///
482
/// # Fields Description
483
///
484
/// - `expected_shortfall`: Point estimate of ES in absolute currency terms
485
/// - `confidence_level`: Confidence level used for ES calculation (e.g., 0.95)
486
/// - `lower_bound`: Lower bound of bootstrap confidence interval
487
/// - `upper_bound`: Upper bound of bootstrap confidence interval  
488
/// - `confidence_interval`: Confidence level for the interval bounds
489
///
490
/// # Usage in Risk Management
491
///
492
/// This structure provides comprehensive ES information for:
493
/// - Risk reporting with uncertainty quantification
494
/// - Model validation and backtesting
495
/// - Regulatory capital calculations
496
/// - Portfolio optimization with estimation risk
497
///
498
/// # Example
499
///
500
/// ```rust
501
/// use risk::var_calculator::expected_shortfall::ESResult;
502
/// use common::types::Price;
503
///
504
/// let es_result = ESResult {
505
///     expected_shortfall: Price::from(50_000),
506
///     confidence_level: 0.95,
507
///     lower_bound: Price::from(45_000),
508
///     upper_bound: Price::from(55_000),
509
///     confidence_interval: 0.95,
510
/// };
511
///
512
/// println!("95% ES: ${} [${}, ${}]",
513
///          es_result.expected_shortfall,
514
///          es_result.lower_bound,
515
///          es_result.upper_bound);
516
/// ```
517
#[derive(Debug, Clone)]
518
pub struct ESResult {
519
    /// Point estimate of Expected Shortfall in absolute currency terms
520
    ///
521
    /// This represents the expected loss given that losses exceed the `VaR` threshold.
522
    /// Always expressed as a positive value representing potential loss amount.
523
    pub expected_shortfall: Price,
524
525
    /// Confidence level used for Expected Shortfall calculation
526
    ///
527
    /// Typical values:
528
    /// - 0.95 (95%): Standard risk management
529
    /// - 0.99 (99%): Regulatory requirements
530
    /// - 0.975 (97.5%): Internal risk limits
531
    pub confidence_level: f64,
532
533
    /// Lower bound of the bootstrap confidence interval
534
    ///
535
    /// Represents the lower estimate of ES accounting for estimation uncertainty.
536
    /// Used for conservative risk assessment and model validation.
537
    pub lower_bound: Price,
538
539
    /// Upper bound of the bootstrap confidence interval
540
    ///
541
    /// Represents the upper estimate of ES accounting for estimation uncertainty.
542
    /// Important for understanding the range of possible ES values.
543
    pub upper_bound: Price,
544
545
    /// Confidence level for the interval bounds
546
    ///
547
    /// Confidence level used to construct the bootstrap confidence interval
548
    /// (e.g., 0.95 means 95% of bootstrap samples fall within [`lower_bound`, `upper_bound`]).
549
    pub confidence_interval: f64,
550
}
551
552
#[cfg(test)]
553
mod tests {
554
    use super::*;
555
    use common::types::Price;
556
    use std::collections::HashMap;
557
558
    #[test]
559
1
    fn test_expected_shortfall_new() {
560
1
        let es_calc = ExpectedShortfall::new(0.95);
561
1
        assert!((es_calc.confidence_level - 0.95).abs() < 1e-6);
562
1
        assert_eq!(es_calc.returns_data.len(), 0);
563
1
    }
564
565
    #[test]
566
1
    fn test_update_returns_data() {
567
1
        let mut es_calc = ExpectedShortfall::new(0.95);
568
1
        let mut returns_data = HashMap::new();
569
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01]);
570
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00]);
571
572
1
        es_calc.update_returns_data(returns_data.clone());
573
574
1
        assert_eq!(es_calc.returns_data.len(), 2);
575
1
        assert_eq!(es_calc.returns_data.get("AAPL").unwrap().len(), 4);
576
1
        assert_eq!(es_calc.returns_data.get("MSFT").unwrap().len(), 4);
577
1
    }
578
579
    #[test]
580
1
    fn test_calculate_expected_shortfall_no_data() {
581
1
        let es_calc = ExpectedShortfall::new(0.95);
582
1
        let weights = vec![1.0];
583
1
        let portfolio_value = Price::from_f64(1000000.0).unwrap();
584
585
1
        let result = es_calc.calculate_expected_shortfall(&weights, portfolio_value);
586
1
        assert!(result.is_err());
587
1
        assert!(result.unwrap_err().to_string().contains("No returns data"));
588
1
    }
589
590
    #[test]
591
1
    fn test_calculate_expected_shortfall_single_asset() -> Result<()> {
592
1
        let mut es_calc = ExpectedShortfall::new(0.95);
593
1
        let mut returns_data = HashMap::new();
594
        // Include some negative returns to ensure ES is non-zero
595
1
        returns_data.insert(
596
1
            "AAPL".to_string(),
597
1
            vec![0.01, -0.05, 0.02, -0.03, 0.01, -0.02, 0.03, -0.01],
598
        );
599
600
1
        es_calc.update_returns_data(returns_data);
601
602
1
        let weights = vec![1.0];
603
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
604
605
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
606
607
        // ES should be positive
608
1
        assert!(es > Decimal::ZERO);
609
        // ES should be reasonable (less than portfolio value)
610
1
        assert!(es < Decimal::from(1000000));
611
612
1
        Ok(())
613
1
    }
614
615
    #[test]
616
1
    fn test_calculate_expected_shortfall_portfolio() -> Result<()> {
617
1
        let mut es_calc = ExpectedShortfall::new(0.95);
618
1
        let mut returns_data = HashMap::new();
619
1
        returns_data.insert(
620
1
            "AAPL".to_string(),
621
1
            vec![0.01, -0.04, 0.02, -0.02, 0.03, -0.03],
622
        );
623
1
        returns_data.insert(
624
1
            "MSFT".to_string(),
625
1
            vec![0.02, -0.02, 0.01, -0.01, 0.00, -0.02],
626
        );
627
628
1
        es_calc.update_returns_data(returns_data);
629
630
1
        let weights = vec![0.6, 0.4];
631
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
632
633
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
634
635
1
        assert!(es > Decimal::ZERO);
636
1
        assert!(es < Decimal::from(1000000));
637
638
1
        Ok(())
639
1
    }
640
641
    #[test]
642
1
    fn test_expected_shortfall_different_confidence_levels() -> Result<()> {
643
1
        let mut returns_data = HashMap::new();
644
1
        returns_data.insert(
645
1
            "AAPL".to_string(),
646
1
            vec![0.01, -0.05, 0.02, -0.03, 0.03, -0.02, 0.01, -0.04],
647
        );
648
649
1
        let weights = vec![1.0];
650
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
651
652
        // 90% confidence
653
1
        let mut es_90 = ExpectedShortfall::new(0.90);
654
1
        es_90.update_returns_data(returns_data.clone());
655
1
        let es_90_result = es_90.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
656
657
        // 95% confidence
658
1
        let mut es_95 = ExpectedShortfall::new(0.95);
659
1
        es_95.update_returns_data(returns_data.clone());
660
1
        let es_95_result = es_95.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
661
662
        // 99% confidence
663
1
        let mut es_99 = ExpectedShortfall::new(0.99);
664
1
        es_99.update_returns_data(returns_data);
665
1
        let es_99_result = es_99.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
666
667
        // Higher confidence levels should generally produce higher ES
668
        // Note: This may not always hold with small samples, so we just verify all are positive
669
1
        assert!(es_90_result > Decimal::ZERO);
670
1
        assert!(es_95_result > Decimal::ZERO);
671
1
        assert!(es_99_result > Decimal::ZERO);
672
673
1
        Ok(())
674
1
    }
675
676
    #[test]
677
1
    fn test_weights_mismatch() -> Result<()> {
678
1
        let mut es_calc = ExpectedShortfall::new(0.95);
679
1
        let mut returns_data = HashMap::new();
680
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03]);
681
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01]);
682
683
1
        es_calc.update_returns_data(returns_data);
684
685
        // Wrong number of weights
686
1
        let weights = vec![1.0]; // Should be 2
687
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
688
689
1
        let result = es_calc.calculate_expected_shortfall(&weights, portfolio_value);
690
1
        assert!(result.is_err());
691
692
1
        Ok(())
693
1
    }
694
695
    #[test]
696
1
    fn test_weights_sum_to_one() -> Result<()> {
697
1
        let mut es_calc = ExpectedShortfall::new(0.95);
698
1
        let mut returns_data = HashMap::new();
699
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.03, 0.02, -0.02]);
700
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.02, 0.01, -0.01]);
701
702
1
        es_calc.update_returns_data(returns_data);
703
704
        // Weights that sum to 1.0
705
1
        let weights = vec![0.6, 0.4];
706
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
707
708
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
709
1
        assert!(es > Decimal::ZERO);
710
711
1
        Ok(())
712
1
    }
713
714
    #[test]
715
1
    fn test_all_positive_returns() -> Result<()> {
716
1
        let mut es_calc = ExpectedShortfall::new(0.95);
717
1
        let mut returns_data = HashMap::new();
718
        // All positive returns - ES should be zero or very small
719
1
        returns_data.insert("AAPL".to_string(), vec![0.01, 0.02, 0.03, 0.01, 0.02]);
720
721
1
        es_calc.update_returns_data(returns_data);
722
723
1
        let weights = vec![1.0];
724
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
725
726
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
727
728
        // With all positive returns, ES should be zero or minimal
729
1
        assert!(es >= Decimal::ZERO);
730
731
1
        Ok(())
732
1
    }
733
734
    #[test]
735
1
    fn test_all_negative_returns() -> Result<()> {
736
1
        let mut es_calc = ExpectedShortfall::new(0.95);
737
1
        let mut returns_data = HashMap::new();
738
        // All negative returns
739
1
        returns_data.insert("AAPL".to_string(), vec![-0.01, -0.02, -0.03, -0.01, -0.02]);
740
741
1
        es_calc.update_returns_data(returns_data);
742
743
1
        let weights = vec![1.0];
744
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
745
746
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
747
748
        // With all negative returns, ES should be substantial
749
1
        assert!(es > Decimal::ZERO);
750
1
        assert!(es > Decimal::from(5000)); // Should be at least 0.5% of portfolio
751
752
1
        Ok(())
753
1
    }
754
755
    #[test]
756
1
    fn test_portfolio_with_zero_weight() -> Result<()> {
757
1
        let mut es_calc = ExpectedShortfall::new(0.95);
758
1
        let mut returns_data = HashMap::new();
759
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.03, 0.02, -0.02]);
760
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.02, 0.01, -0.01]);
761
762
1
        es_calc.update_returns_data(returns_data);
763
764
        // One asset with zero weight
765
1
        let weights = vec![1.0, 0.0];
766
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
767
768
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
769
1
        assert!(es >= Decimal::ZERO);
770
771
1
        Ok(())
772
1
    }
773
774
    #[test]
775
1
    fn test_minimum_returns_requirement() -> Result<()> {
776
1
        let mut es_calc = ExpectedShortfall::new(0.95);
777
1
        let mut returns_data = HashMap::new();
778
        // Only 2 returns - might not be enough for meaningful ES
779
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02]);
780
781
1
        es_calc.update_returns_data(returns_data);
782
783
1
        let weights = vec![1.0];
784
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
785
786
        // Should still calculate something, even if not very reliable
787
1
        let result = es_calc.calculate_expected_shortfall(&weights, portfolio_value);
788
        // The function may or may not fail with insufficient data - either is acceptable
789
1
        match result {
790
1
            Ok(es) => assert!(es >= Decimal::ZERO),
791
0
            Err(_) => {}, // Acceptable to reject insufficient data
792
        }
793
794
1
        Ok(())
795
1
    }
796
797
    #[test]
798
1
    fn test_extreme_negative_returns() -> Result<()> {
799
1
        let mut es_calc = ExpectedShortfall::new(0.95);
800
1
        let mut returns_data = HashMap::new();
801
        // Mix of normal and extreme negative returns
802
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.10, 0.02, -0.15, 0.01]);
803
804
1
        es_calc.update_returns_data(returns_data);
805
806
1
        let weights = vec![1.0];
807
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
808
809
1
        let es = es_calc.calculate_expected_shortfall(&weights, portfolio_value)
?0
;
810
811
        // ES should capture the extreme losses
812
1
        assert!(es > Decimal::from(50000)); // Should be significant given -10% and -15% returns
813
814
1
        Ok(())
815
1
    }
816
817
    #[test]
818
1
    fn test_diversification_benefit() -> Result<()> {
819
1
        let mut returns_data = HashMap::new();
820
        // Negatively correlated assets
821
1
        returns_data.insert(
822
1
            "ASSET_A".to_string(),
823
1
            vec![0.05, -0.05, 0.03, -0.03, 0.02, -0.02],
824
        );
825
1
        returns_data.insert(
826
1
            "ASSET_B".to_string(),
827
1
            vec![-0.05, 0.05, -0.03, 0.03, -0.02, 0.02],
828
        );
829
830
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
831
832
        // Single asset ES
833
1
        let mut es_single_a = ExpectedShortfall::new(0.95);
834
1
        let mut data_a = HashMap::new();
835
1
        data_a.insert("ASSET_A".to_string(), returns_data["ASSET_A"].clone());
836
1
        es_single_a.update_returns_data(data_a);
837
1
        let es_a = es_single_a.calculate_expected_shortfall(&vec![1.0], portfolio_value)
?0
;
838
839
        // Diversified portfolio ES
840
1
        let mut es_portfolio = ExpectedShortfall::new(0.95);
841
1
        es_portfolio.update_returns_data(returns_data);
842
1
        let es_port =
843
1
            es_portfolio.calculate_expected_shortfall(&vec![0.5, 0.5], portfolio_value)
?0
;
844
845
        // Diversified portfolio should have lower ES (or at worst equal)
846
        // Due to negative correlation, portfolio ES should be lower
847
1
        assert!(es_port <= es_a || 
(es_a - es_port).abs() < Decimal::from(1000)0
);
848
849
1
        Ok(())
850
1
    }
851
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/historical_simulation.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/historical_simulation.rs.html deleted file mode 100644 index ac4f1bb7b..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/historical_simulation.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/historical_simulation.rs
Line
Count
Source
1
//! Historical Simulation `VaR` calculation
2
//! REPLACES: `var_1d_95`: `Price::ZERO` with real `VaR` calculations
3
4
// REMOVED: Direct Decimal usage - use canonical types
5
use crate::error::{RiskError, RiskResult};
6
use chrono::{DateTime, Utc};
7
use common::types::{Price, Symbol};
8
use serde::{Deserialize, Serialize};
9
use std::collections::HashMap;
10
// Removed broker_integration - not available in this simplified risk crate
11
use crate::var_calculator::var_engine::{HistoricalPrice, PositionInfo};
12
13
/// Historical Simulation Value at Risk (`VaR`) calculator using empirical distribution
14
///
15
/// Historical Simulation is a non-parametric method for calculating `VaR` that uses
16
/// actual historical price movements to estimate potential future losses. This approach
17
/// does not assume any particular distribution and captures the actual empirical
18
/// distribution of returns including fat tails, skewness, and other real market characteristics.
19
///
20
/// # Methodology
21
///
22
/// 1. **Historical Data Collection**: Gather historical price data for the specified lookback period
23
/// 2. **Returns Calculation**: Calculate period-over-period returns from historical prices
24
/// 3. **Scenario Generation**: Apply historical returns to current portfolio positions
25
/// 4. **Distribution Analysis**: Sort profit/loss scenarios to create empirical distribution
26
/// 5. **`VaR` Estimation**: Extract quantile corresponding to confidence level
27
///
28
/// # Mathematical Foundation
29
///
30
/// For a portfolio with current value V₀, historical returns R₁, R₂, ..., Rₙ:
31
/// - P&L scenarios: P&Lᵢ = V₀ × Rᵢ
32
/// - Sorted scenarios: P&L₍₁₎ ≤ P&L₍₂₎ ≤ ... ≤ P&L₍ₙ₎
33
/// - `VaR` at confidence α: `VaR_α` = -P&L₍⌊(1-α)×n⌋₎
34
///
35
/// # Advantages
36
///
37
/// - **Model-free**: No distributional assumptions required
38
/// - **Fat tail capture**: Naturally incorporates extreme events from history
39
/// - **Correlation capture**: Implicitly includes historical correlations
40
/// - **Intuitive**: Easy to explain and validate
41
///
42
/// # Limitations
43
///
44
/// - **Historical bias**: Assumes future will resemble past
45
/// - **Limited scenarios**: Cannot model unprecedented events
46
/// - **Data requirements**: Needs substantial historical data
47
/// - **Non-stationarity**: May not capture regime changes
48
///
49
/// # Use Cases
50
///
51
/// - Daily risk monitoring and reporting
52
/// - Regulatory capital calculations (Basel II/III)
53
/// - Portfolio optimization with realistic risk constraints
54
/// - Backtesting and model validation
55
///
56
/// # Example
57
///
58
/// ```rust
59
/// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
60
/// use std::collections::HashMap;
61
///
62
/// // Create 95% confidence VaR calculator with 1-year lookback
63
/// let var_calculator = HistoricalSimulationVaR::new(0.95, 252);
64
///
65
/// // Or use predefined configurations
66
/// let standard_calc = HistoricalSimulationVaR::standard();      // 95%, 252 days
67
/// let conservative_calc = HistoricalSimulationVaR::conservative(); // 99%, 252 days
68
/// ```
69
#[derive(Debug, Clone)]
70
pub struct HistoricalSimulationVaR {
71
    /// Confidence level for `VaR` calculation (e.g., 0.95 for 95% `VaR`)
72
    ///
73
    /// Common confidence levels:
74
    /// - 0.95 (95%): Standard risk management and daily monitoring
75
    /// - 0.99 (99%): Regulatory requirements (Basel III, Solvency II)
76
    /// - 0.975 (97.5%): Internal risk limits and stress testing
77
    /// - 0.999 (99.9%): Extreme event analysis
78
    confidence_level: f64,
79
80
    /// Number of historical trading days to include in lookback window
81
    ///
82
    /// Typical values:
83
    /// - 252: One year of trading days (standard)
84
    /// - 504: Two years for more stable estimates
85
    /// - 126: Half year for more responsive estimates
86
    /// - 63: Quarter for highly dynamic markets
87
    ///
88
    /// Trade-off considerations:
89
    /// - Longer periods: More stable estimates, less responsive to regime changes
90
    /// - Shorter periods: More responsive, but higher estimation error
91
    lookback_days: usize,
92
}
93
94
/// Value at Risk calculation result for a single position
95
///
96
/// Contains comprehensive `VaR` metrics for a specific symbol/position including
97
/// 1-day and 10-day `VaR` estimates, Expected Shortfall, and calculation metadata.
98
///
99
/// # Risk Metrics Included
100
///
101
/// - **1-Day `VaR`**: Potential loss over 1 trading day at specified confidence level
102
/// - **10-Day `VaR`**: Scaled `VaR` using square-root-of-time rule for longer horizon
103
/// - **Expected Shortfall**: Average loss given that loss exceeds `VaR` threshold
104
/// - **Observation Count**: Number of historical data points used in calculation
105
///
106
/// # Scaling Methodology
107
///
108
/// 10-day `VaR` uses the square-root-of-time scaling rule:
109
/// `VaR₁₀` = `VaR₁` × √10
110
///
111
/// This assumes:
112
/// - Returns are independent and identically distributed
113
/// - No autocorrelation in returns
114
/// - Constant volatility over the scaling period
115
///
116
/// # Usage in Risk Management
117
///
118
/// - Daily risk reporting and monitoring
119
/// - Position limit enforcement
120
/// - Regulatory capital calculations
121
/// - Performance attribution analysis
122
///
123
/// # Example
124
///
125
/// ```rust
126
/// // Typical VaR result interpretation
127
/// if var_result.var_1d > position_limit {
128
///     println!("Position exceeds 1-day VaR limit: ${} > ${}",
129
///              var_result.var_1d, position_limit);
130
/// }
131
///
132
/// // Expected Shortfall provides tail risk insight
133
/// let tail_risk_ratio = var_result.expected_shortfall / var_result.var_1d;
134
/// println!("Tail risk multiplier: {:.2}x", tail_risk_ratio);
135
/// ```
136
#[derive(Debug, Clone, Serialize, Deserialize)]
137
pub struct VaRResult {
138
    /// Symbol identifier for the position being analyzed
139
    ///
140
    /// Unique identifier for the financial instrument (e.g., "AAPL", "EURUSD")
141
    pub symbol: Symbol,
142
143
    /// Confidence level used for `VaR` calculation
144
    ///
145
    /// The probability that actual losses will not exceed the `VaR` estimate.
146
    /// For example, 0.95 means 95% confidence that losses won't exceed `VaR`.
147
    pub confidence_level: f64,
148
149
    /// 1-day Value at Risk in absolute currency terms
150
    ///
151
    /// Maximum expected loss over 1 trading day at the specified confidence level.
152
    /// Always expressed as a positive value representing potential loss amount.
153
    ///
154
    /// Example: $50,000 means 95% confidence that daily loss won't exceed $50,000.
155
    pub var_1d: Price,
156
157
    /// 10-day Value at Risk scaled using square-root-of-time rule
158
    ///
159
    /// `VaR` estimate for a 10-day holding period, calculated as:
160
    /// `var_10d` = `var_1d` × √10 ≈ `var_1d` × 3.16
161
    ///
162
    /// Used for:
163
    /// - Regulatory reporting (many jurisdictions require 10-day `VaR`)
164
    /// - Longer-term risk assessment
165
    /// - Capital adequacy calculations
166
    pub var_10d: Price,
167
168
    /// Expected Shortfall (Conditional `VaR`) at the same confidence level
169
    ///
170
    /// Average loss given that the loss exceeds the `VaR` threshold.
171
    /// Provides additional insight into tail risk beyond `VaR`.
172
    ///
173
    /// Always ≥ `VaR`, with larger values indicating fatter tail distributions.
174
    pub expected_shortfall: Price,
175
176
    /// Number of historical return observations used in the calculation
177
    ///
178
    /// Indicates the sample size for the empirical distribution.
179
    /// Higher values generally provide more reliable estimates but may
180
    /// include less relevant historical periods.
181
    pub historical_observations: usize,
182
183
    /// Timestamp when the `VaR` calculation was performed
184
    ///
185
    /// Used for:
186
    /// - Audit trails and compliance reporting
187
    /// - Determining freshness of risk calculations
188
    /// - Historical analysis of risk evolution
189
    pub calculated_at: DateTime<Utc>,
190
}
191
192
/// Portfolio-level Value at Risk calculation result with diversification analysis
193
///
194
/// Comprehensive portfolio `VaR` metrics that account for correlations between
195
/// positions and quantify the diversification benefit from portfolio construction.
196
///
197
/// # Key Metrics
198
///
199
/// - **Total Portfolio `VaR`**: Risk of the entire portfolio accounting for correlations
200
/// - **Component `VaRs`**: Individual position `VaRs` for decomposition analysis
201
/// - **Diversification Benefit**: Risk reduction achieved through diversification
202
///
203
/// # Diversification Benefit Calculation
204
///
205
/// Diversification Benefit = Σ(Component `VaRs`) - Portfolio `VaR`
206
///
207
/// Where:
208
/// - Σ(Component VaRs): Sum of individual position `VaRs`
209
/// - Portfolio `VaR`: `VaR` of the combined portfolio
210
/// - Positive values indicate effective diversification
211
///
212
/// # Mathematical Foundation
213
///
214
/// Portfolio `VaR` accounts for correlations through joint simulation:
215
/// - Each historical scenario applies to all positions simultaneously
216
/// - Portfolio P&L = `Σ(Position_i` × `Return_i`) for each scenario
217
/// - `VaR` extracted from joint P&L distribution
218
///
219
/// # Risk Management Applications
220
///
221
/// - **Limit Management**: Ensure portfolio `VaR` stays within bounds
222
/// - **Capital Allocation**: Optimize diversification benefits
223
/// - **Performance Attribution**: Decompose risk by component
224
/// - **Regulatory Reporting**: Meet portfolio-level capital requirements
225
///
226
/// # Example Analysis
227
///
228
/// ```rust
229
/// // Analyze diversification effectiveness
230
/// let diversification_ratio = portfolio_result.diversification_benefit /
231
///                           portfolio_result.component_vars.values()
232
///                               .map(|v| v.var_1d).sum::<Price>();
233
///
234
/// if diversification_ratio > 0.20 {
235
///     println!("Strong diversification: {:.1}% risk reduction",
236
///              diversification_ratio * 100.0);
237
/// }
238
/// ```
239
#[derive(Debug, Clone, Serialize, Deserialize)]
240
pub struct PortfolioVaRResult {
241
    /// Unique identifier for the portfolio being analyzed
242
    ///
243
    /// Used for tracking, reporting, and audit purposes.
244
    /// Examples: "`MAIN_TRADING`", "`HEDGE_FUND_A`", "`CLIENT_12345`"
245
    pub portfolio_id: String,
246
247
    /// Total portfolio 1-day `VaR` accounting for correlations
248
    ///
249
    /// The diversified `VaR` of the entire portfolio, which incorporates
250
    /// correlations between positions. Typically less than the sum of
251
    /// individual component `VaRs` due to diversification benefits.
252
    pub total_var_1d: Price,
253
254
    /// Total portfolio 10-day `VaR` using square-root-of-time scaling
255
    ///
256
    /// Scaled version of 1-day portfolio `VaR`: `total_var_10d` = `total_var_1d` × √10
257
    /// Used for regulatory reporting and longer-term risk assessment.
258
    pub total_var_10d: Price,
259
260
    /// Individual `VaR` results for each component position
261
    ///
262
    /// Map of symbol → `VaRResult` for portfolio decomposition analysis.
263
    /// Allows identification of risk contributors and concentration analysis.
264
    ///
265
    /// Key insights:
266
    /// - Largest component `VaRs` indicate risk concentrations
267
    /// - Comparison with portfolio `VaR` shows diversification effects
268
    /// - Used for position sizing and risk budgeting decisions
269
    pub component_vars: HashMap<String, VaRResult>,
270
271
    /// Diversification benefit from portfolio construction
272
    ///
273
    /// Calculated as: Σ(Component `VaRs`) - Portfolio `VaR`
274
    ///
275
    /// Positive values indicate risk reduction through diversification.
276
    /// Higher values suggest more effective portfolio construction.
277
    ///
278
    /// Typical ranges:
279
    /// - 0-10%: Limited diversification
280
    /// - 10-30%: Good diversification  
281
    /// - 30%+: Excellent diversification
282
    pub diversification_benefit: Price,
283
284
    /// Confidence level used for all `VaR` calculations
285
    ///
286
    /// Applied consistently across portfolio and component calculations
287
    /// to ensure comparable risk metrics.
288
    pub confidence_level: f64,
289
290
    /// Timestamp when the portfolio `VaR` calculation was performed
291
    ///
292
    /// Critical for:
293
    /// - Regulatory reporting timestamps
294
    /// - Risk monitoring and alerting
295
    /// - Historical risk analysis
296
    pub calculated_at: DateTime<Utc>,
297
}
298
299
impl HistoricalSimulationVaR {
300
    /// Creates a new Historical Simulation `VaR` calculator with custom parameters
301
    ///
302
    /// # Arguments
303
    ///
304
    /// * `confidence_level` - Confidence level between 0.0 and 1.0 (e.g., 0.95 for 95%)
305
    /// * `lookback_days` - Number of historical trading days to include in calculation
306
    ///
307
    /// # Returns
308
    ///
309
    /// New `HistoricalSimulationVaR` instance ready for calculations
310
    ///
311
    /// # Parameter Guidelines
312
    ///
313
    /// **Confidence Level Selection:**
314
    /// - 0.95 (95%): Standard daily risk monitoring
315
    /// - 0.99 (99%): Regulatory requirements, stress testing
316
    /// - 0.975 (97.5%): Internal risk limits
317
    ///
318
    /// **Lookback Period Selection:**
319
    /// - 252 days: One year (most common, balances stability vs responsiveness)
320
    /// - 504 days: Two years (more stable, less responsive to recent changes)
321
    /// - 126 days: Half year (more responsive to market regime changes)
322
    /// - 63 days: Quarter (highly responsive, higher estimation error)
323
    ///
324
    /// # Examples
325
    ///
326
    /// ```rust
327
    /// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
328
    ///
329
    /// // Standard configuration for daily risk monitoring
330
    /// let daily_var = HistoricalSimulationVaR::new(0.95, 252);
331
    ///
332
    /// // Conservative configuration for regulatory reporting
333
    /// let regulatory_var = HistoricalSimulationVaR::new(0.99, 252);
334
    ///
335
    /// // Responsive configuration for volatile markets
336
    /// let responsive_var = HistoricalSimulationVaR::new(0.95, 126);
337
    /// ```
338
    ///
339
    /// # Performance Considerations
340
    ///
341
    /// Longer lookback periods require more computation but provide more stable estimates.
342
    /// Consider the trade-off between accuracy and computational cost for your use case.
343
    #[must_use]
344
157
    pub const fn new(confidence_level: f64, lookback_days: usize) -> Self {
345
157
        Self {
346
157
            confidence_level,
347
157
            lookback_days,
348
157
        }
349
157
    }
350
351
    /// Creates a `VaR` calculator with standard market risk parameters
352
    ///
353
    /// Uses 95% confidence level with 252 trading days (1 year) lookback period.
354
    /// This configuration is widely used in the financial industry for daily
355
    /// risk monitoring and represents a good balance between stability and responsiveness.
356
    ///
357
    /// # Returns
358
    ///
359
    /// `HistoricalSimulationVaR` configured with:
360
    /// - Confidence level: 95% (0.95)
361
    /// - Lookback period: 252 trading days (≈ 1 calendar year)
362
    ///
363
    /// # Use Cases
364
    ///
365
    /// - Daily portfolio risk monitoring
366
    /// - Position limit enforcement
367
    /// - Risk-adjusted performance measurement
368
    /// - Internal risk reporting
369
    ///
370
    /// # Equivalent To
371
    ///
372
    /// ```rust
373
    /// HistoricalSimulationVaR::new(0.95, 252)
374
    /// ```
375
    ///
376
    /// # Example
377
    ///
378
    /// ```rust
379
    /// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
380
    ///
381
    /// let var_calc = HistoricalSimulationVaR::standard();
382
    /// // Ready for standard daily VaR calculations
383
    /// ```
384
    #[must_use]
385
5
    pub const fn standard() -> Self {
386
5
        Self::new(0.95, 252)
387
5
    }
388
389
    /// Creates a `VaR` calculator with conservative parameters for regulatory compliance
390
    ///
391
    /// Uses 99% confidence level with 252 trading days lookback period.
392
    /// This configuration meets most regulatory requirements (Basel III, Solvency II)
393
    /// and provides more conservative risk estimates for capital adequacy calculations.
394
    ///
395
    /// # Returns
396
    ///
397
    /// `HistoricalSimulationVaR` configured with:
398
    /// - Confidence level: 99% (0.99)
399
    /// - Lookback period: 252 trading days (≈ 1 calendar year)
400
    ///
401
    /// # Regulatory Applications
402
    ///
403
    /// - Basel III market risk capital requirements
404
    /// - Solvency II standard formula calculations
405
    /// - Internal Capital Adequacy Assessment Process (ICAAP)
406
    /// - Stress testing and scenario analysis
407
    ///
408
    /// # Risk Implications
409
    ///
410
    /// 99% `VaR` estimates will be significantly higher than 95% `VaR`:
411
    /// - Captures more extreme tail events
412
    /// - Provides greater protection against unexpected losses
413
    /// - Results in higher capital requirements
414
    ///
415
    /// # Equivalent To
416
    ///
417
    /// ```rust
418
    /// HistoricalSimulationVaR::new(0.99, 252)
419
    /// ```
420
    ///
421
    /// # Example
422
    ///
423
    /// ```rust
424
    /// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
425
    ///
426
    /// let regulatory_calc = HistoricalSimulationVaR::conservative();
427
    /// // Ready for regulatory capital calculations
428
    /// ```
429
    #[must_use]
430
1
    pub const fn conservative() -> Self {
431
1
        Self::new(0.99, 252)
432
1
    }
433
434
    /// Calculates Value at Risk for a single position using historical simulation
435
    ///
436
    /// This method applies historical price movements to the current position to generate
437
    /// a distribution of potential profit/loss scenarios, then extracts `VaR` at the
438
    /// specified confidence level.
439
    ///
440
    /// # Arguments
441
    ///
442
    /// * `symbol` - Symbol identifier for the position
443
    /// * `position` - Current position information (quantity, market value, etc.)
444
    /// * `historical_prices` - Historical price data for the symbol
445
    ///
446
    /// # Returns
447
    ///
448
    /// * `Ok(VaRResult)` - Complete `VaR` analysis including 1-day, 10-day `VaR` and Expected Shortfall
449
    /// * `Err(RiskError)` - If calculation fails due to insufficient data or other errors
450
    ///
451
    /// # Algorithm Steps
452
    ///
453
    /// 1. **Data Validation**: Ensure sufficient historical data (≥ `lookback_days`)
454
    /// 2. **Returns Calculation**: Compute period-over-period returns from price data
455
    /// 3. **Scenario Generation**: Apply returns to current position value
456
    /// 4. **Distribution Creation**: Sort P&L scenarios from worst to best
457
    /// 5. **`VaR` Extraction**: Find quantile corresponding to confidence level
458
    /// 6. **Scaling**: Apply square-root-of-time rule for 10-day `VaR`
459
    /// 7. **Expected Shortfall**: Calculate average loss beyond `VaR` threshold
460
    ///
461
    /// # Data Requirements
462
    ///
463
    /// - Historical prices must cover at least `lookback_days` periods
464
    /// - Prices should be adjusted for splits and dividends
465
    /// - Data should be clean (no missing values, outliers reviewed)
466
    /// - Consistent frequency (daily, weekly, etc.)
467
    ///
468
    /// # Mathematical Detail
469
    ///
470
    /// For position value V and historical returns R₁, ..., Rₙ:
471
    /// - P&L scenarios: `ΔV_i` = V × `R_i`
472
    /// - Sorted scenarios: ΔV_(1) ≤ ... ≤ ΔV_(n)
473
    /// - `VaR` index: k = ⌊(1 - α) × n⌋
474
    /// - `VaR` estimate: `VaR` = -ΔV_(k)
475
    ///
476
    /// # Examples
477
    ///
478
    /// ```rust
479
    /// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
480
    /// use common::types::{Symbol, Price};
481
    ///
482
    /// let var_calc = HistoricalSimulationVaR::standard();
483
    ///
484
    /// // Calculate VaR for AAPL position
485
    /// let symbol = Symbol::from("AAPL");
486
    /// let var_result = var_calc.calculate_position_var(
487
    ///     &symbol,
488
    ///     &position_info,
489
    ///     &historical_price_data
490
    /// )?;
491
    ///
492
    /// println!("1-day 95% VaR: ${}", var_result.var_1d);
493
    /// println!("Expected Shortfall: ${}", var_result.expected_shortfall);
494
    /// ```
495
    ///
496
    /// # Errors
497
    ///
498
    /// - `RiskError::Calculation` with operation "`historical_var`" if insufficient data
499
    /// - `RiskError::Calculation` with operation "`returns_calculation`" if price data invalid
500
    /// - `RiskError::Calculation` with operation "`var_scaling`" if scaling fails
501
    ///
502
    /// # Performance Notes
503
    ///
504
    /// Computational complexity is O(n log n) due to sorting of scenarios.
505
    /// For high-frequency calculations, consider caching sorted historical returns.
506
154
    pub fn calculate_position_var(
507
154
        &self,
508
154
        symbol: &Symbol,
509
154
        position: &PositionInfo,
510
154
        historical_prices: &[HistoricalPrice],
511
154
    ) -> RiskResult<VaRResult> {
512
154
        if historical_prices.len() < self.lookback_days {
513
1
            return Err(RiskError::Calculation {
514
1
                operation: "historical_var".to_owned(),
515
1
                reason: format!(
516
1
                    "Insufficient historical data: {} days required, {} available",
517
1
                    self.lookback_days,
518
1
                    historical_prices.len()
519
1
                ),
520
1
            });
521
153
        }
522
523
        // Calculate daily returns from historical prices
524
153
        let returns = self.calculate_returns(historical_prices)
?0
;
525
526
        // Calculate position value changes based on returns
527
153
        let position_value = position.quantity.to_f64() * position.market_value.to_f64();
528
153
        let mut pnl_scenarios: Vec<f64> = returns
529
153
            .iter()
530
8.39k
            .
map153
(|return_rate| position_value * return_rate)
531
153
            .collect();
532
533
        // Sort P&L scenarios (worst losses first)
534
51.2k
        
pnl_scenarios153
.
sort_by153
(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
535
536
        // Calculate VaR at confidence level
537
153
        let var_index = ((1.0 - self.confidence_level) * pnl_scenarios.len() as f64) as usize;
538
153
        let var_loss = pnl_scenarios
539
153
            .get(var_index.min(pnl_scenarios.len().saturating_sub(1)))
540
153
            .copied()
541
153
            .unwrap_or(0.0);
542
543
        // VaR is positive for losses (negate negative P&L)
544
153
        let var_1d = Price::from_f64(var_loss.abs()).unwrap_or(Price::ZERO);
545
546
        // Scale to 10-day VaR (square root of time scaling)
547
153
        let var_10d = (var_1d * 10.0_f64.sqrt()).map_err(|e| RiskError::Calculation {
548
0
            operation: "var_scaling".to_owned(),
549
0
            reason: format!("Failed to scale VaR to 10 days: {e:?}"),
550
0
        })?;
551
552
        // Calculate Expected Shortfall (average of losses beyond VaR)
553
153
        let es_scenarios: Vec<f64> = pnl_scenarios.iter().take(var_index + 1).copied().collect();
554
153
        let expected_shortfall = if es_scenarios.is_empty() {
555
0
            Price::ZERO
556
        } else {
557
153
            let sum: f64 = es_scenarios.iter().sum();
558
153
            let avg = sum / es_scenarios.len() as f64;
559
            // ES is positive for losses (negate negative P&L)
560
153
            Price::from_f64(avg.abs()).unwrap_or(Price::ZERO)
561
        };
562
563
153
        Ok(VaRResult {
564
153
            symbol: symbol.to_string().into(),
565
153
            confidence_level: self.confidence_level,
566
153
            var_1d,
567
153
            var_10d,
568
153
            expected_shortfall,
569
153
            historical_observations: returns.len(),
570
153
            calculated_at: Utc::now(),
571
153
        })
572
154
    }
573
574
    /// Calculates portfolio Value at Risk accounting for correlations between positions
575
    ///
576
    /// This method performs joint simulation across all portfolio positions to capture
577
    /// correlation effects and calculate diversified portfolio `VaR`. The resulting
578
    /// portfolio `VaR` typically differs from the sum of individual position `VaRs`
579
    /// due to diversification benefits or concentration risks.
580
    ///
581
    /// # Arguments
582
    ///
583
    /// * `portfolio_id` - Unique identifier for the portfolio
584
    /// * `positions` - Map of symbol → position information for all holdings
585
    /// * `historical_prices` - Map of symbol → historical price data
586
    ///
587
    /// # Returns
588
    ///
589
    /// * `Ok(PortfolioVaRResult)` - Complete portfolio analysis with diversification metrics
590
    /// * `Err(RiskError)` - If calculation fails due to data issues or mismatched inputs
591
    ///
592
    /// # Correlation Methodology
593
    ///
594
    /// Unlike simple `VaR` aggregation, this method captures correlations through:
595
    /// 1. **Joint Simulation**: Each historical scenario applies to all positions simultaneously
596
    /// 2. **Portfolio P&L**: Sum position-level P&L for each scenario
597
    /// 3. **Empirical Distribution**: Create portfolio-level loss distribution
598
    /// 4. **Diversified `VaR`**: Extract `VaR` from joint distribution
599
    ///
600
    /// # Mathematical Foundation
601
    ///
602
    /// For portfolio with positions i = 1...n and historical scenario t:
603
    /// - Portfolio `P&L_t` = Σᵢ (`Position_Value_i` × `Return_i,t`)
604
    /// - Portfolio `VaR` = Quantile(Portfolio P&L Distribution, 1-α)
605
    /// - Diversification Benefit = Σᵢ(Individual `VaR_i`) - Portfolio `VaR`
606
    ///
607
    /// # Key Outputs
608
    ///
609
    /// - **Portfolio `VaR`**: Risk of combined portfolio
610
    /// - **Component `VaRs`**: Individual position risks for decomposition
611
    /// - **Diversification Benefit**: Risk reduction from portfolio construction
612
    /// - **Correlation Effects**: Implicit in the difference between sum and portfolio `VaR`
613
    ///
614
    /// # Data Requirements
615
    ///
616
    /// - All symbols must have historical data covering the lookback period
617
    /// - Historical data should be time-aligned across symbols
618
    /// - Position information must be current and accurate
619
    /// - Minimum overlap period across all historical series
620
    ///
621
    /// # Examples
622
    ///
623
    /// ```rust
624
    /// use std::collections::HashMap;
625
    /// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
626
    ///
627
    /// let var_calc = HistoricalSimulationVaR::standard();
628
    ///
629
    /// // Portfolio with multiple positions
630
    /// let mut positions = HashMap::new();
631
    /// positions.insert(symbol_aapl, position_aapl);
632
    /// positions.insert(symbol_googl, position_googl);
633
    /// positions.insert(symbol_msft, position_msft);
634
    ///
635
    /// let portfolio_result = var_calc.calculate_portfolio_var(
636
    ///     "EQUITY_PORTFOLIO",
637
    ///     &positions,
638
    ///     &historical_data
639
    /// )?;
640
    ///
641
    /// // Analyze diversification effectiveness
642
    /// let component_sum = portfolio_result.component_vars.values()
643
    ///     .map(|v| v.var_1d).sum::<Price>();
644
    /// let diversification_pct = portfolio_result.diversification_benefit / component_sum;
645
    ///
646
    /// println!("Portfolio VaR: ${}", portfolio_result.total_var_1d);
647
    /// println!("Diversification benefit: {:.1}%", diversification_pct * 100.0);
648
    /// ```
649
    ///
650
    /// # Risk Management Applications
651
    ///
652
    /// - **Limit Monitoring**: Ensure portfolio `VaR` stays within risk appetite
653
    /// - **Capital Allocation**: Optimize portfolio construction for diversification
654
    /// - **Performance Attribution**: Identify risk contributors vs diversifiers
655
    /// - **Regulatory Reporting**: Meet portfolio-level capital requirements
656
    ///
657
    /// # Errors
658
    ///
659
    /// - `RiskError::Calculation` with operation "`portfolio_var`" if insufficient data
660
    /// - `RiskError::Calculation` if position/price data misalignment
661
    /// - `RiskError::Calculation` with operation "`portfolio_var_scaling`" if scaling fails
662
    ///
663
    /// # Performance Considerations
664
    ///
665
    /// Portfolio `VaR` calculation scales linearly with number of positions and
666
    /// historical periods. For large portfolios, consider:
667
    /// - Parallel processing of component `VaRs`
668
    /// - Caching of historical return calculations
669
    /// - Approximate methods for real-time applications
670
1
    pub fn calculate_portfolio_var(
671
1
        &self,
672
1
        portfolio_id: &str,
673
1
        positions: &HashMap<Symbol, PositionInfo>,
674
1
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
675
1
    ) -> RiskResult<PortfolioVaRResult> {
676
1
        let mut component_vars = HashMap::new();
677
1
        let mut portfolio_pnl_scenarios: Vec<f64> = Vec::new();
678
679
        // Get the minimum number of observations across all symbols
680
1
        let min_observations = historical_prices.values().map(Vec::len).min().unwrap_or(0);
681
682
1
        if min_observations < self.lookback_days {
683
0
            return Err(RiskError::Calculation {
684
0
                operation: "portfolio_var".to_owned(),
685
0
                reason: format!(
686
0
                    "Insufficient historical data across portfolio: {} days required",
687
0
                    self.lookback_days
688
0
                ),
689
0
            });
690
1
        }
691
692
        // Initialize portfolio P&L scenarios
693
299
        for _ in 0..
min_observations - 11
{
694
299
            portfolio_pnl_scenarios.push(0.0);
695
299
        }
696
697
        // Calculate component VaRs and aggregate portfolio scenarios
698
3
        for (
symbol2
,
position2
) in positions {
699
2
            if let Some(symbol_prices) = historical_prices.get(symbol) {
700
                // Calculate component VaR
701
2
                let component_var = self.calculate_position_var(symbol, position, symbol_prices)
?0
;
702
2
                component_vars.insert(symbol.to_string(), component_var);
703
704
                // Add to portfolio scenarios
705
2
                let returns = self.calculate_returns(symbol_prices)
?0
;
706
2
                let position_value = position.quantity.to_f64() * position.market_value.to_f64();
707
708
598
                for (i, return_rate) in 
returns.iter()2
.
enumerate2
() {
709
598
                    if let Some(scenario) = portfolio_pnl_scenarios.get_mut(i) {
710
598
                        let pnl_change = position_value * return_rate;
711
598
                        *scenario += pnl_change;
712
598
                    
}0
713
                }
714
0
            }
715
        }
716
717
        // Calculate portfolio VaR from aggregated scenarios
718
1
        let mut sorted_portfolio_pnl = portfolio_pnl_scenarios.clone();
719
2.65k
        
sorted_portfolio_pnl1
.
sort_by1
(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
720
721
1
        let var_index =
722
1
            ((1.0 - self.confidence_level) * sorted_portfolio_pnl.len() as f64) as usize;
723
1
        let var_loss = sorted_portfolio_pnl
724
1
            .get(var_index.min(sorted_portfolio_pnl.len().saturating_sub(1)))
725
1
            .copied()
726
1
            .unwrap_or(0.0);
727
728
        // VaR is positive for losses
729
1
        let total_var_1d = Price::from_f64(var_loss.abs()).unwrap_or(Price::ZERO);
730
731
        // Scale to 10-day VaR
732
1
        let total_var_10d =
733
1
            (total_var_1d * 10.0_f64.sqrt()).map_err(|e| RiskError::Calculation {
734
0
                operation: "portfolio_var_scaling".to_owned(),
735
0
                reason: format!("Failed to scale portfolio VaR to 10 days: {e:?}"),
736
0
            })?;
737
738
        // Calculate diversification benefit
739
1
        let component_var_sum = component_vars
740
1
            .values()
741
1
            .map(|var| var.var_1d)
742
2
            .
fold1
(Price::ZERO, |acc, price| {
743
2
                Price::from_f64(acc.to_f64() + price.to_f64()).unwrap_or(Price::ZERO)
744
2
            });
745
1
        let diversification_benefit = component_var_sum - total_var_1d;
746
747
1
        Ok(PortfolioVaRResult {
748
1
            portfolio_id: portfolio_id.to_owned(),
749
1
            total_var_1d,
750
1
            total_var_10d,
751
1
            component_vars,
752
1
            diversification_benefit,
753
1
            confidence_level: self.confidence_level,
754
1
            calculated_at: Utc::now(),
755
1
        })
756
1
    }
757
758
    /// Calculates daily returns from historical price data
759
    ///
760
    /// Computes period-over-period returns using simple return formula:
761
    /// `Return_t` = (`Price_t` - Price_{t-1}) / Price_{t-1}
762
    ///
763
    /// # Arguments
764
    ///
765
    /// * `historical_prices` - Vector of historical price data in chronological order
766
    ///
767
    /// # Returns
768
    ///
769
    /// * `Ok(Vec<Price>)` - Vector of returns (length = `prices.len()` - 1)
770
    /// * `Err(RiskError)` - If insufficient data or calculation errors
771
    ///
772
    /// # Implementation Details
773
    ///
774
    /// - Uses simple returns (not log returns) for intuitive interpretation
775
    /// - Handles zero prices by returning calculation error
776
    /// - Preserves precision through Decimal arithmetic
777
    /// - Returns vector has n-1 elements for n price points
778
    ///
779
    /// # Error Conditions
780
    ///
781
    /// - Fewer than 2 price points (cannot calculate returns)
782
    /// - Zero prices in historical data (division by zero)
783
    /// - Price conversion errors
784
156
    fn calculate_returns(&self, historical_prices: &[HistoricalPrice]) -> RiskResult<Vec<f64>> {
785
156
        if historical_prices.len() < 2 {
786
0
            return Err(RiskError::Calculation {
787
0
                operation: "returns_calculation".to_owned(),
788
0
                reason: "Need at least 2 price points to calculate returns".to_owned(),
789
0
            });
790
156
        }
791
792
156
        let mut returns = Vec::new();
793
794
9.00k
        for window in 
historical_prices156
.
windows156
(2) {
795
9.00k
            let (prev_price, curr_price) = match (window.first(), window.get(1)) {
796
9.00k
                (Some(prev), Some(curr)) => (prev.price.to_f64(), curr.price.to_f64()),
797
0
                _ => continue, // Skip invalid windows
798
            };
799
800
9.00k
            if prev_price == 0.0 {
801
0
                return Err(RiskError::Calculation {
802
0
                    operation: "returns_calculation".to_owned(),
803
0
                    reason: "Zero price found in historical data".to_owned(),
804
0
                });
805
9.00k
            }
806
807
9.00k
            let return_rate = (curr_price - prev_price) / prev_price;
808
9.00k
            returns.push(return_rate);
809
        }
810
811
156
        Ok(returns)
812
156
    }
813
814
    /// Calculates rolling Value at Risk estimates over time
815
    ///
816
    /// Produces a time series of `VaR` estimates using a rolling window approach.
817
    /// Each estimate uses the previous `window_size` observations, providing
818
    /// insight into how `VaR` evolves over time and enabling trend analysis.
819
    ///
820
    /// # Arguments
821
    ///
822
    /// * `symbol` - Symbol identifier for the position
823
    /// * `position` - Position information (assumed constant for analysis period)
824
    /// * `historical_prices` - Complete historical price dataset
825
    /// * `window_size` - Number of observations to include in each rolling window
826
    ///
827
    /// # Returns
828
    ///
829
    /// * `Ok(Vec<VaRResult>)` - Time series of `VaR` estimates
830
    /// * `Err(RiskError)` - If insufficient data or calculation errors
831
    ///
832
    /// # Rolling Window Methodology
833
    ///
834
    /// For historical data of length N and window size W:
835
    /// - Window 1: observations [0, W]
836
    /// - Window 2: observations [1, W+1]
837
    /// - ...
838
    /// - Window N-W: observations [N-W-1, N-1]
839
    /// - Total rolling estimates: N - W
840
    ///
841
    /// # Applications
842
    ///
843
    /// - **Trend Analysis**: Identify increasing/decreasing risk patterns
844
    /// - **Model Validation**: Compare rolling `VaR` with actual losses
845
    /// - **Regime Detection**: Spot structural breaks in risk characteristics
846
    /// - **Dynamic Hedging**: Adjust hedge ratios based on evolving risk
847
    ///
848
    /// # Window Size Selection
849
    ///
850
    /// Trade-offs in window size selection:
851
    /// - **Smaller windows** (30-60 days): More responsive to recent changes, higher noise
852
    /// - **Medium windows** (120-180 days): Balanced responsiveness and stability
853
    /// - **Larger windows** (250+ days): More stable, less responsive to regime changes
854
    ///
855
    /// # Examples
856
    ///
857
    /// ```rust
858
    /// use risk::var_calculator::historical_simulation::HistoricalSimulationVaR;
859
    ///
860
    /// let var_calc = HistoricalSimulationVaR::standard();
861
    ///
862
    /// // Calculate 6-month rolling VaR with 3-month windows
863
    /// let rolling_vars = var_calc.calculate_rolling_var(
864
    ///     &symbol,
865
    ///     &position,
866
    ///     &price_history,  // 6 months of data
867
    ///     63               // 3-month rolling window
868
    /// )?;
869
    ///
870
    /// // Analyze VaR trend
871
    /// let recent_var = rolling_vars.last().unwrap().var_1d;
872
    /// let earlier_var = rolling_vars.first().unwrap().var_1d;
873
    /// let var_trend = (recent_var - earlier_var) / earlier_var;
874
    ///
875
    /// if var_trend > 0.20 {
876
    ///     println!("VaR has increased by {:.1}% - consider risk reduction", var_trend * 100.0);
877
    /// }
878
    /// ```
879
    ///
880
    /// # Performance Considerations
881
    ///
882
    /// - Each rolling window requires separate `VaR` calculation
883
    /// - Consider parallel processing for large datasets
884
    /// - Memory usage scales with (`data_length` - `window_size`)
885
    ///
886
    /// # Errors
887
    ///
888
    /// - `RiskError::Calculation` with operation "`rolling_var`" if insufficient data
889
    /// - Propagates errors from individual `VaR` calculations
890
    ///
891
    /// # Interpretation Guidelines
892
    ///
893
    /// - **Increasing trend**: Rising market risk or volatility
894
    /// - **Decreasing trend**: Improving market conditions or reduced exposure
895
    /// - **Sudden spikes**: Market stress events or structural breaks
896
    /// - **Stable patterns**: Consistent risk regime
897
1
    pub fn calculate_rolling_var(
898
1
        &self,
899
1
        symbol: &Symbol,
900
1
        position: &PositionInfo,
901
1
        historical_prices: &[HistoricalPrice],
902
1
        window_size: usize,
903
1
    ) -> RiskResult<Vec<VaRResult>> {
904
1
        if historical_prices.len() < window_size + 1 {
905
0
            return Err(RiskError::Calculation {
906
0
                operation: "rolling_var".to_owned(),
907
0
                reason: format!(
908
0
                    "Insufficient data for rolling VaR: {} required, {} available",
909
0
                    window_size + 1,
910
0
                    historical_prices.len()
911
0
                ),
912
0
            });
913
1
        }
914
915
1
        let mut rolling_vars = Vec::new();
916
917
150
        for i in 
window_size1
..
historical_prices1
.
len1
() {
918
150
            if let Some(window_prices) = historical_prices.get(i.saturating_sub(window_size)..=i) {
919
150
                let temp_calculator =
920
150
                    HistoricalSimulationVaR::new(self.confidence_level, window_size);
921
150
                let var_result =
922
150
                    temp_calculator.calculate_position_var(symbol, position, window_prices)
?0
;
923
150
                rolling_vars.push(var_result);
924
0
            }
925
        }
926
927
1
        Ok(rolling_vars)
928
1
    }
929
}
930
931
#[cfg(test)]
932
mod tests {
933
    use super::*;
934
    use chrono::Duration;
935
    use common::types::Quantity;
936
    // operations module removed - use direct imports from common
937
938
6
    fn create_test_historical_prices(
939
6
        symbol: &Symbol,
940
6
        days: usize,
941
6
        base_price: f64,
942
6
    ) -> Result<Vec<HistoricalPrice>, Box<dyn std::error::Error>> {
943
6
        let mut prices = Vec::new();
944
6
        let mut current_price = base_price;
945
946
1.21k
        for i in 0..
days6
{
947
            // Simple random walk simulation
948
1.21k
            let change = if i % 2 == 0 { 
0.02605
} else {
-0.015605
}; // +2% or -1.5%
949
1.21k
            current_price *= 1.0 + change;
950
951
1.21k
            prices.push(HistoricalPrice {
952
1.21k
                symbol: symbol.to_string(),
953
1.21k
                date: Utc::now() - Duration::days(days as i64 - i as i64),
954
1.21k
                open: Price::from_f64(current_price * 0.999)
?0
,
955
1.21k
                high: Price::from_f64(current_price * 1.005)
?0
,
956
1.21k
                low: Price::from_f64(current_price * 0.995)
?0
,
957
1.21k
                price: Price::from_f64(current_price)
?0
,
958
1.21k
                volume: Quantity::from_f64(1000000.0)
?0
,
959
            });
960
        }
961
962
6
        Ok(prices)
963
6
    }
964
965
5
    fn create_test_position(
966
5
        symbol: &Symbol,
967
5
        quantity: f64,
968
5
        market_price: f64,
969
5
    ) -> Result<PositionInfo, Box<dyn std::error::Error>> {
970
        Ok(PositionInfo {
971
5
            symbol: symbol.to_string().into(),
972
5
            quantity: Quantity::from_f64(quantity)
?0
,
973
5
            market_value: Price::from_f64(quantity * market_price)
?0
,
974
5
            average_cost: Price::from_f64(market_price * 0.95)
?0
,
975
5
            unrealized_pnl: Price::from_f64(quantity * market_price * 0.05)
?0
,
976
            realized_pnl: Price::ZERO,
977
5
            currency: "USD".to_string(),
978
5
            timestamp: Utc::now(),
979
        })
980
5
    }
981
982
    #[test]
983
1
    fn test_var_calculator_creation() {
984
1
        let calculator = HistoricalSimulationVaR::standard();
985
1
        assert_eq!(calculator.confidence_level, 0.95);
986
1
        assert_eq!(calculator.lookback_days, 252);
987
988
1
        let conservative = HistoricalSimulationVaR::conservative();
989
1
        assert_eq!(conservative.confidence_level, 0.99);
990
1
    }
991
992
    #[test]
993
1
    fn test_returns_calculation() -> Result<(), Box<dyn std::error::Error>> {
994
1
        let calculator = HistoricalSimulationVaR::standard();
995
1
        let prices = create_test_historical_prices(&Symbol::from("AAPL".to_string()), 10, 100.0)
?0
;
996
1
        let returns = calculator.calculate_returns(&prices)
?0
;
997
998
1
        assert_eq!(returns.len(), 9); // n-1 returns from n prices
999
1
        assert!(returns
1000
1
            .iter()
1001
9
            .
all1
(|r| r.abs() < Price::from_f64(0.1).unwrap_or(Price::ZERO))); // Reasonable returns
1002
1
        Ok(())
1003
1
    }
1004
    #[test]
1005
1
    fn test_position_var_calculation() -> Result<(), Box<dyn std::error::Error>> {
1006
1
        let calculator = HistoricalSimulationVaR::standard();
1007
1
        let prices = create_test_historical_prices(&Symbol::from("AAPL".to_string()), 300, 150.0)
?0
;
1008
1
        let position = create_test_position(&Symbol::from("AAPL".to_string()), 100.0, 150.0)
?0
;
1009
1010
1
        let var_result = calculator.calculate_position_var(
1011
1
            &Symbol::from("AAPL".to_string()),
1012
1
            &position,
1013
1
            &prices,
1014
0
        )?;
1015
1016
1
        assert_eq!(var_result.symbol, Symbol::from("AAPL".to_string()));
1017
1
        assert_eq!(var_result.confidence_level, 0.95);
1018
1
        assert!(var_result.var_1d > Price::ZERO);
1019
1
        assert!(var_result.var_10d > var_result.var_1d);
1020
1
        assert!(var_result.expected_shortfall >= var_result.var_1d);
1021
1
        assert_eq!(var_result.historical_observations, 299); // 300 prices = 299 returns
1022
1
        Ok(())
1023
1
    }
1024
1025
    #[test]
1026
1
    fn test_portfolio_var_calculation() -> Result<(), Box<dyn std::error::Error>> {
1027
1
        let calculator = HistoricalSimulationVaR::standard();
1028
1029
        // Create test portfolio
1030
1
        let mut positions = HashMap::new();
1031
1
        positions.insert(
1032
1
            Symbol::from("AAPL".to_string()),
1033
1
            create_test_position(&Symbol::from("AAPL".to_string()), 100.0, 150.0)
?0
,
1034
        );
1035
1
        positions.insert(
1036
1
            Symbol::from("GOOGL".to_string()),
1037
1
            create_test_position(&Symbol::from("GOOGL".to_string()), 50.0, 2800.0)
?0
,
1038
        );
1039
1040
        // Create historical data
1041
1
        let mut historical_prices = HashMap::new();
1042
1
        historical_prices.insert(
1043
1
            Symbol::from("AAPL".to_string()),
1044
1
            create_test_historical_prices(&Symbol::from("AAPL".to_string()), 300, 150.0)
?0
,
1045
        );
1046
1
        historical_prices.insert(
1047
1
            Symbol::from("GOOGL".to_string()),
1048
1
            create_test_historical_prices(&Symbol::from("GOOGL".to_string()), 300, 2800.0)
?0
,
1049
        );
1050
1051
1
        let portfolio_var =
1052
1
            calculator.calculate_portfolio_var("TEST_PORTFOLIO", &positions, &historical_prices)
?0
;
1053
1054
1
        assert_eq!(portfolio_var.portfolio_id, "TEST_PORTFOLIO");
1055
1
        assert!(portfolio_var.total_var_1d > Price::ZERO);
1056
1
        assert!(portfolio_var.total_var_10d > portfolio_var.total_var_1d);
1057
1
        assert_eq!(portfolio_var.component_vars.len(), 2);
1058
1
        assert!(portfolio_var.component_vars.contains_key("AAPL"));
1059
1
        assert!(portfolio_var.component_vars.contains_key("GOOGL"));
1060
1061
        // Diversification benefit should be positive (portfolio VaR < sum of component VaRs)
1062
1
        assert!(portfolio_var.diversification_benefit > Price::ZERO);
1063
1
        Ok(())
1064
1
    }
1065
1066
    #[test]
1067
1
    fn test_insufficient_data_error() -> Result<(), Box<dyn std::error::Error>> {
1068
1
        let calculator = HistoricalSimulationVaR::standard();
1069
1
        let prices = create_test_historical_prices(&Symbol::from("AAPL".to_string()), 100, 150.0)
?0
; // Only 100 days, need 252
1070
1
        let position = create_test_position(&Symbol::from("AAPL".to_string()), 100.0, 150.0)
?0
;
1071
1072
1
        let result = calculator.calculate_position_var(
1073
1
            &Symbol::from("AAPL".to_string()),
1074
1
            &position,
1075
1
            &prices,
1076
        );
1077
1
        assert!(result.is_err());
1078
1079
1
        if let Err(RiskError::Calculation { operation, reason }) = result {
1080
1
            assert_eq!(operation, "historical_var");
1081
1
            assert!(reason.contains("Insufficient historical data"));
1082
0
        }
1083
1
        Ok(())
1084
1
    }
1085
1086
    #[test]
1087
1
    fn test_rolling_var() -> Result<(), Box<dyn std::error::Error>> {
1088
1
        let calculator = HistoricalSimulationVaR::new(0.95, 50); // Shorter window for testing
1089
1
        let prices = create_test_historical_prices(&Symbol::from("AAPL".to_string()), 200, 150.0)
?0
;
1090
1
        let position = create_test_position(&Symbol::from("AAPL".to_string()), 100.0, 150.0)
?0
;
1091
1092
1
        let rolling_vars = calculator.calculate_rolling_var(
1093
1
            &Symbol::from("AAPL".to_string()),
1094
1
            &position,
1095
1
            &prices,
1096
            50,
1097
0
        )?;
1098
1099
1
        assert_eq!(rolling_vars.len(), 200 - 50); // 150 rolling windows
1100
150
        
assert!1
(
rolling_vars.iter()1
.
all1
(|var| var.var_1d > Price::ZERO));
1101
1
        Ok(())
1102
1
    }
1103
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/monte_carlo.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/monte_carlo.rs.html deleted file mode 100644 index 83d35cd9b..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/monte_carlo.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/monte_carlo.rs
Line
Count
Source
1
//! Monte Carlo `VaR` calculation with correlation modeling
2
//! Advanced risk calculation with 10,000+ simulations
3
4
// REMOVED: Direct Decimal usage - use canonical types
5
use crate::error::{RiskError, RiskResult};
6
use chrono::{DateTime, Utc};
7
use common::types::{Price, Symbol};
8
use num::FromPrimitive;
9
use rust_decimal::Decimal;
10
use serde::{Deserialize, Serialize};
11
use std::collections::HashMap;
12
use tracing::warn;
13
// Removed broker_integration - not available in this simplified risk crate
14
use crate::var_calculator::var_engine::{HistoricalPrice, PositionInfo};
15
// CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
16
17
/// Monte Carlo Value at Risk calculator with advanced correlation modeling
18
///
19
/// Monte Carlo simulation is a powerful method for calculating `VaR` that uses
20
/// random sampling to model the statistical behavior of portfolio returns.
21
/// This implementation includes sophisticated correlation modeling using
22
/// Cholesky decomposition and proper mathematical foundations.
23
///
24
/// # Key Features
25
///
26
/// - **Correlation Modeling**: Uses Cholesky decomposition for accurate correlation
27
/// - **Flexible Simulations**: Configurable number of simulations (1,000 to 1,000,000+)
28
/// - **Multiple Time Horizons**: Support for 1-day, 10-day, and custom periods
29
/// - **Comprehensive Metrics**: `VaR`, Expected Shortfall, scenario analysis
30
/// - **Reproducible Results**: Optional random seed for deterministic output
31
///
32
/// # Mathematical Foundation
33
///
34
/// The Monte Carlo approach generates scenarios through:
35
///
36
/// 1. **Parameter Estimation**: Calculate μ (mean) and σ (volatility) from historical data
37
/// 2. **Correlation Matrix**: Build asset correlation matrix from return data
38
/// 3. **Cholesky Decomposition**: L such that `LLᵀ` = Σ (correlation matrix)
39
/// 4. **Random Generation**: Z ~ N(0,I) independent normal variables
40
/// 5. **Correlated Shocks**: X = LZ produces correlated normal variables
41
/// 6. **Scenario Generation**: Returns Rᵢ = μ + σ × Xᵢ
42
/// 7. **Portfolio P&L**: P&L = Σ(Positionᵢ × Rᵢ)
43
/// 8. **Risk Metrics**: Extract quantiles from P&L distribution
44
///
45
/// # Advantages over Historical Simulation
46
///
47
/// - **Forward-looking**: Can model scenarios not seen in history
48
/// - **Flexible distributions**: Not limited to historical empirical distribution
49
/// - **Scenario control**: Can stress-test specific parameter combinations
50
/// - **Smooth distributions**: Continuous distribution vs discrete historical points
51
///
52
/// # Computational Complexity
53
///
54
/// - Time complexity: O(n²m + nm²) where n = assets, m = simulations
55
/// - Space complexity: O(n² + m) for correlation matrix and scenarios
56
/// - Cholesky decomposition: O(n³) but computed once per calculation
57
///
58
/// # Use Cases
59
///
60
/// - **Regulatory Capital**: Basel III market risk requirements
61
/// - **Risk Budgeting**: Portfolio optimization with risk constraints
62
/// - **Stress Testing**: Model extreme but plausible scenarios
63
/// - **Product Pricing**: Risk-adjusted pricing for structured products
64
///
65
/// # Example
66
///
67
/// ```rust
68
/// use risk::var_calculator::monte_carlo::MonteCarloVaR;
69
/// use std::collections::HashMap;
70
///
71
/// // Create 95% confidence calculator with 10,000 simulations
72
/// let mc_calc = MonteCarloVaR::new(0.95, 10_000, 1, Some(42));
73
///
74
/// // Or use predefined configurations
75
/// let standard = MonteCarloVaR::standard();      // 95%, 10K simulations
76
/// let precise = MonteCarloVaR::high_precision();  // 99%, 100K simulations
77
///
78
/// // Calculate portfolio VaR with correlations
79
/// let result = mc_calc.calculate_portfolio_var(
80
///     "EQUITY_PORTFOLIO",
81
///     &positions,
82
///     &historical_data
83
/// )?;
84
///
85
/// println!("Monte Carlo VaR: ${}", result.var_1d);
86
/// println!("Expected Shortfall: ${}", result.expected_shortfall);
87
/// println!("Worst case scenario: ${}", result.worst_case_scenario);
88
/// ```
89
#[derive(Debug, Clone)]
90
pub struct MonteCarloVaR {
91
    /// Confidence level for `VaR` calculation (e.g., 0.95 for 95% `VaR`)
92
    ///
93
    /// Determines the quantile extracted from the Monte Carlo distribution.
94
    /// Common values:
95
    /// - 0.95 (95%): Standard risk management
96
    /// - 0.99 (99%): Regulatory requirements
97
    /// - 0.995 (99.5%): Extreme stress testing
98
    confidence_level: f64,
99
100
    /// Number of Monte Carlo simulations to run
101
    ///
102
    /// Trade-offs in simulation count:
103
    /// - 1,000-5,000: Fast computation, higher Monte Carlo error
104
    /// - 10,000-50,000: Standard practice, good accuracy/speed balance
105
    /// - 100,000+: High precision, slower computation
106
    ///
107
    /// Monte Carlo error decreases as 1/√n where n = simulations
108
    num_simulations: usize,
109
110
    /// Time horizon in trading days for `VaR` calculation
111
    ///
112
    /// Common horizons:
113
    /// - 1 day: Daily risk monitoring
114
    /// - 10 days: Regulatory requirements (Basel III)
115
    /// - 21 days: Monthly risk assessment
116
    ///
117
    /// Scaling uses square-root-of-time rule: `VaRₜ` = `VaR₁` × √t
118
    time_horizon_days: usize,
119
120
    /// Optional random seed for reproducible results
121
    ///
122
    /// When specified, ensures identical simulation results across runs.
123
    /// Useful for:
124
    /// - Model validation and backtesting
125
    /// - Regulatory reporting consistency
126
    /// - Debugging and testing
127
    ///
128
    /// If None, uses default seed (42) for deterministic behavior
129
    random_seed: Option<u64>,
130
}
131
132
/// Comprehensive Monte Carlo simulation result with full scenario analysis
133
///
134
/// Contains complete risk metrics derived from Monte Carlo simulation including
135
/// traditional `VaR` measures, tail risk metrics, and scenario statistics.
136
///
137
/// # Risk Metrics Overview
138
///
139
/// - **`VaR` Estimates**: 1-day and multi-day Value at Risk
140
/// - **Expected Shortfall**: Tail risk beyond `VaR` threshold
141
/// - **Scenario Analysis**: Best/worst case outcomes
142
/// - **Distribution Statistics**: Mean, volatility of simulated returns
143
/// - **Simulation Metadata**: Number of runs, confidence level, timestamps
144
///
145
/// # Statistical Interpretation
146
///
147
/// All monetary amounts represent potential losses (positive values):
148
/// - `VaR`: "We are X% confident losses won't exceed $Y over N days"
149
/// - Expected Shortfall: "If losses exceed `VaR`, average loss is $Z"
150
/// - Worst case: "In most extreme scenario, loss could reach $W"
151
///
152
/// # Validation and Quality Checks
153
///
154
/// - Expected Shortfall ≥ `VaR` (mathematical requirement)
155
/// - Worst case ≥ Expected Shortfall ≥ `VaR` (ordering check)
156
/// - Mean P&L near zero for unbiased portfolios
157
/// - Volatility consistent with historical market behavior
158
///
159
/// # Example Analysis
160
///
161
/// ```rust
162
/// // Risk metric relationships
163
/// let tail_risk_ratio = result.expected_shortfall / result.var_1d;
164
/// if tail_risk_ratio > 1.5 {
165
///     println!("High tail risk detected: ES/VaR = {:.2}", tail_risk_ratio);
166
/// }
167
///
168
/// // Scenario range analysis
169
/// let scenario_range = result.best_case_scenario + result.worst_case_scenario;
170
/// println!("Total scenario range: ${}", scenario_range);
171
///
172
/// // Distribution symmetry
173
/// if result.mean_pnl.abs() > result.volatility * 0.1 {
174
///     println!("Asymmetric return distribution detected");
175
/// }
176
/// ```
177
#[derive(Debug, Clone, Serialize, Deserialize)]
178
pub struct MonteCarloResult {
179
    /// Unique identifier for the portfolio analyzed
180
    ///
181
    /// Used for tracking, reporting, and audit purposes.
182
    /// Examples: "`EQUITY_PORTFOLIO`", "`FIXED_INCOME`", "`DERIVATIVES_BOOK`"
183
    pub portfolio_id: String,
184
185
    /// 1-day Value at Risk at specified confidence level
186
    ///
187
    /// Maximum expected loss over 1 trading day with given confidence.
188
    /// Derived from the Monte Carlo distribution quantile.
189
    ///
190
    /// Example: $50,000 at 95% confidence means 95% probability
191
    /// that daily losses won't exceed $50,000.
192
    pub var_1d: Price,
193
194
    /// 10-day Value at Risk using time scaling
195
    ///
196
    /// `VaR` scaled to 10-day horizon using: `VaR₁₀` = `VaR₁` × √10
197
    ///
198
    /// Used for:
199
    /// - Basel III regulatory capital requirements
200
    /// - Longer-term risk assessment
201
    /// - Liquidity-adjusted risk metrics
202
    pub var_10d: Price,
203
204
    /// Expected Shortfall (Conditional `VaR`) at same confidence level
205
    ///
206
    /// Average loss given that losses exceed the `VaR` threshold.
207
    /// Provides insight into tail risk severity beyond `VaR`.
208
    ///
209
    /// Always ≥ `VaR`, with larger ratios indicating fat-tail distributions.
210
    /// Particularly important for portfolios with option-like payoffs.
211
    pub expected_shortfall: Price,
212
213
    /// Confidence level used for `VaR` and ES calculations
214
    ///
215
    /// Consistent across all risk metrics to ensure comparability.
216
    /// Typically 95% for internal risk management, 99% for regulatory use.
217
    pub confidence_level: f64,
218
219
    /// Number of Monte Carlo simulations performed
220
    ///
221
    /// Higher values provide more accurate estimates but require more computation.
222
    /// Standard practice: 10,000-100,000 simulations.
223
    ///
224
    /// Monte Carlo standard error ∝ 1/√n where n = `num_simulations`
225
    pub num_simulations: usize,
226
227
    /// Worst-case scenario loss from all simulations
228
    ///
229
    /// Maximum loss observed across all Monte Carlo runs.
230
    /// Represents extreme tail event for stress testing.
231
    ///
232
    /// Note: This is NOT a confidence-based metric but the absolute worst outcome.
233
    pub worst_case_scenario: Price,
234
235
    /// Best-case scenario gain from all simulations
236
    ///
237
    /// Maximum gain observed across all Monte Carlo runs.
238
    /// Shows upside potential under favorable conditions.
239
    ///
240
    /// Expressed as positive value representing potential profit.
241
    pub best_case_scenario: Price,
242
243
    /// Mean profit/loss across all simulations
244
    ///
245
    /// Expected portfolio return based on Monte Carlo distribution.
246
    /// Should be close to zero for unbiased risk-neutral simulations.
247
    ///
248
    /// Large deviations from zero may indicate:
249
    /// - Trending market conditions
250
    /// - Biased parameter estimation
251
    /// - Portfolio with directional exposure
252
    pub mean_pnl: Decimal,
253
254
    /// Volatility (standard deviation) of simulated P&L
255
    ///
256
    /// Measures dispersion of portfolio outcomes.
257
    /// Higher values indicate greater uncertainty in portfolio performance.
258
    ///
259
    /// Used for:
260
    /// - Risk-adjusted performance metrics (Sharpe ratio)
261
    /// - Portfolio optimization constraints
262
    /// - Stress testing scenario design
263
    pub volatility: Price,
264
265
    /// Timestamp when the Monte Carlo calculation was performed
266
    ///
267
    /// Critical for:
268
    /// - Audit trails and regulatory compliance
269
    /// - Determining calculation freshness
270
    /// - Historical analysis of risk evolution
271
    pub calculated_at: DateTime<Utc>,
272
}
273
274
/// Asset statistics for Monte Carlo simulation
275
#[derive(Debug, Clone)]
276
struct AssetStats {
277
    symbol: String,
278
    mean_return: f64,
279
    volatility: f64,
280
    position_value: Price,
281
}
282
283
/// Correlation matrix for portfolio simulation
284
// Infrastructure - fields will be used for correlation-based simulation
285
#[allow(dead_code)]
286
#[derive(Debug, Clone)]
287
struct CorrelationMatrix {
288
    symbols: Vec<String>,
289
    matrix: Vec<Vec<f64>>,
290
}
291
292
impl MonteCarloVaR {
293
    /// Creates a new Monte Carlo `VaR` calculator with custom parameters
294
    ///
295
    /// # Arguments
296
    ///
297
    /// * `confidence_level` - Confidence level between 0.0 and 1.0 (e.g., 0.95 for 95%)
298
    /// * `num_simulations` - Number of Monte Carlo simulations to run
299
    /// * `time_horizon_days` - Time horizon in trading days for `VaR` calculation
300
    /// * `random_seed` - Optional seed for reproducible results (None uses default)
301
    ///
302
    /// # Returns
303
    ///
304
    /// New `MonteCarloVaR` instance configured with specified parameters
305
    ///
306
    /// # Parameter Selection Guidelines
307
    ///
308
    /// **Confidence Level:**
309
    /// - 0.95 (95%): Standard daily risk monitoring
310
    /// - 0.99 (99%): Regulatory requirements, conservative estimates
311
    /// - 0.995 (99.5%): Extreme stress testing scenarios
312
    ///
313
    /// **Simulation Count:**
314
    /// - 1,000-5,000: Quick estimates, development testing
315
    /// - 10,000-50,000: Production use, good accuracy/speed balance
316
    /// - 100,000+: High-precision calculations, model validation
317
    ///
318
    /// **Time Horizon:**
319
    /// - 1 day: Daily risk monitoring and limit checking
320
    /// - 10 days: Regulatory capital requirements (Basel III)
321
    /// - 21 days: Monthly risk assessment and budgeting
322
    ///
323
    /// **Random Seed:**
324
    /// - Some(seed): Reproducible results for testing/validation
325
    /// - None: Uses default seed (42) for consistent behavior
326
    ///
327
    /// # Examples
328
    ///
329
    /// ```rust
330
    /// use risk::var_calculator::monte_carlo::MonteCarloVaR;
331
    ///
332
    /// // Standard daily risk monitoring
333
    /// let daily_calc = MonteCarloVaR::new(0.95, 10_000, 1, None);
334
    ///
335
    /// // Regulatory capital calculation
336
    /// let regulatory_calc = MonteCarloVaR::new(0.99, 100_000, 10, Some(12345));
337
    ///
338
    /// // Fast approximation for development
339
    /// let quick_calc = MonteCarloVaR::new(0.95, 1_000, 1, Some(42));
340
    /// ```
341
    ///
342
    /// # Performance Considerations
343
    ///
344
    /// Computational time scales linearly with simulation count and quadratically
345
    /// with number of assets (due to correlation matrix operations).
346
    ///
347
    /// For real-time applications, consider:
348
    /// - Caching correlation matrices
349
    /// - Parallel simulation execution
350
    /// - Adaptive simulation counts based on portfolio complexity
351
    #[must_use]
352
8
    pub const fn new(
353
8
        confidence_level: f64,
354
8
        num_simulations: usize,
355
8
        time_horizon_days: usize,
356
8
        random_seed: Option<u64>,
357
8
    ) -> Self {
358
8
        Self {
359
8
            confidence_level,
360
8
            num_simulations,
361
8
            time_horizon_days,
362
8
            random_seed,
363
8
        }
364
8
    }
365
366
    /// Creates a Monte Carlo `VaR` calculator with standard industry parameters
367
    ///
368
    /// Uses widely-adopted configuration suitable for daily risk monitoring:
369
    /// - 95% confidence level (standard risk management practice)
370
    /// - 10,000 simulations (good accuracy/performance balance)
371
    /// - 1-day time horizon (daily risk monitoring)
372
    /// - No fixed random seed (uses default for consistency)
373
    ///
374
    /// # Returns
375
    ///
376
    /// `MonteCarloVaR` configured for standard daily risk management use
377
    ///
378
    /// # Use Cases
379
    ///
380
    /// - Daily portfolio risk monitoring
381
    /// - Position limit enforcement
382
    /// - Risk committee reporting
383
    /// - Internal risk model validation
384
    ///
385
    /// # Expected Performance
386
    ///
387
    /// With 10,000 simulations:
388
    /// - Monte Carlo error: ~1% of true `VaR`
389
    /// - Typical runtime: 0.1-1 seconds for 10-asset portfolio
390
    /// - Memory usage: ~1MB for correlation matrices and scenarios
391
    ///
392
    /// # Equivalent To
393
    ///
394
    /// ```rust
395
    /// MonteCarloVaR::new(0.95, 10_000, 1, None)
396
    /// ```
397
    ///
398
    /// # Example
399
    ///
400
    /// ```rust
401
    /// use risk::var_calculator::monte_carlo::MonteCarloVaR;
402
    ///
403
    /// let mc_calc = MonteCarloVaR::standard();
404
    /// // Ready for daily risk calculations
405
    /// ```
406
    #[must_use]
407
6
    pub const fn standard() -> Self {
408
6
        Self::new(0.95, 10_000, 1, None)
409
6
    }
410
411
    /// Creates a Monte Carlo `VaR` calculator optimized for high-precision analysis
412
    ///
413
    /// Uses conservative parameters suitable for regulatory reporting and model validation:
414
    /// - 99% confidence level (regulatory standard)
415
    /// - 100,000 simulations (high precision, low Monte Carlo error)
416
    /// - 1-day time horizon (can be scaled as needed)
417
    /// - No fixed random seed (uses default for consistency)
418
    ///
419
    /// # Returns
420
    ///
421
    /// `MonteCarloVaR` configured for high-precision regulatory and validation use
422
    ///
423
    /// # Use Cases
424
    ///
425
    /// - Regulatory capital calculations (Basel III)
426
    /// - Model validation and backtesting
427
    /// - Stress testing and scenario analysis
428
    /// - Academic research and benchmarking
429
    ///
430
    /// # Performance Characteristics
431
    ///
432
    /// With 100,000 simulations:
433
    /// - Monte Carlo error: ~0.3% of true `VaR`
434
    /// - Typical runtime: 1-10 seconds for 10-asset portfolio
435
    /// - Memory usage: ~10MB for scenarios and intermediate results
436
    /// - Higher computational cost but superior accuracy
437
    ///
438
    /// # Accuracy Benefits
439
    ///
440
    /// - More stable tail risk estimates (Expected Shortfall)
441
    /// - Better representation of extreme scenarios
442
    /// - Reduced simulation noise in risk metrics
443
    /// - Suitable for regulatory submission and audit
444
    ///
445
    /// # Equivalent To
446
    ///
447
    /// ```rust
448
    /// MonteCarloVaR::new(0.99, 100_000, 1, None)
449
    /// ```
450
    ///
451
    /// # Example
452
    ///
453
    /// ```rust
454
    /// use risk::var_calculator::monte_carlo::MonteCarloVaR;
455
    ///
456
    /// let precise_calc = MonteCarloVaR::high_precision();
457
    /// // Ready for regulatory capital calculations
458
    /// ```
459
    #[must_use]
460
1
    pub const fn high_precision() -> Self {
461
1
        Self::new(0.99, 100_000, 1, None)
462
1
    }
463
464
    /// Calculates portfolio Value at Risk using Monte Carlo simulation with full correlation modeling
465
    ///
466
    /// This method performs sophisticated portfolio risk analysis using Monte Carlo simulation
467
    /// with proper correlation modeling via Cholesky decomposition. The implementation
468
    /// captures realistic portfolio behavior including asset correlations, tail dependencies,
469
    /// and non-linear risk aggregation effects.
470
    ///
471
    /// # Arguments
472
    ///
473
    /// * `portfolio_id` - Unique identifier for the portfolio being analyzed
474
    /// * `positions` - Map of symbol → position information for all holdings
475
    /// * `historical_prices` - Map of symbol → historical price data for parameter estimation
476
    ///
477
    /// # Returns
478
    ///
479
    /// * `Ok(MonteCarloResult)` - Comprehensive risk analysis with `VaR`, ES, and scenarios
480
    /// * `Err(RiskError)` - If calculation fails due to data issues or mathematical problems
481
    ///
482
    /// # Algorithm Overview
483
    ///
484
    /// 1. **Parameter Estimation**: Calculate mean returns and volatilities from historical data
485
    /// 2. **Correlation Analysis**: Build correlation matrix from historical return series
486
    /// 3. **Cholesky Decomposition**: Decompose correlation matrix for random number generation
487
    /// 4. **Monte Carlo Simulation**: Generate correlated random scenarios
488
    /// 5. **Portfolio Simulation**: Apply scenarios to current portfolio positions
489
    /// 6. **Risk Metric Calculation**: Extract `VaR`, ES, and other statistics
490
    ///
491
    /// # Mathematical Foundation
492
    ///
493
    /// **Parameter Estimation:**
494
    /// - Mean return: μᵢ = (1/n) `ΣRᵢₜ`
495
    /// - Volatility: σᵢ = √[(1/(n-1)) Σ(Rᵢₜ - μᵢ)²]
496
    ///
497
    /// **Correlation Matrix:**
498
    /// - ρᵢⱼ = Cov(Rᵢ, Rⱼ) / (σᵢ × σⱼ)
499
    ///
500
    /// **Scenario Generation:**
501
    /// - Z ~ N(0,I) independent normals
502
    /// - X = LZ where `LLᵀ` = Σ (Cholesky decomposition)
503
    /// - Rᵢ = μᵢ + σᵢ × Xᵢ (correlated returns)
504
    ///
505
    /// **Portfolio P&L:**
506
    /// - Portfolio P&L = Σᵢ (Positionᵢ × Rᵢ)
507
    ///
508
    /// # Data Requirements
509
    ///
510
    /// - Minimum 30 historical observations per asset for reliable parameter estimation
511
    /// - Historical data should be time-aligned across all assets
512
    /// - Price data should be adjusted for corporate actions
513
    /// - Consistent frequency (daily recommended for daily `VaR`)
514
    ///
515
    /// # Correlation Modeling
516
    ///
517
    /// The implementation uses mathematically rigorous correlation modeling:
518
    /// - Cholesky decomposition ensures positive semi-definite correlation matrices
519
    /// - Handles near-singular matrices with numerical stability
520
    /// - Preserves exact correlation structure from historical data
521
    /// - Generates truly correlated (not pseudo-correlated) random variables
522
    ///
523
    /// # Examples
524
    ///
525
    /// ```rust
526
    /// use std::collections::HashMap;
527
    /// use risk::var_calculator::monte_carlo::MonteCarloVaR;
528
    ///
529
    /// let mc_calc = MonteCarloVaR::standard();
530
    ///
531
    /// // Multi-asset portfolio
532
    /// let mut positions = HashMap::new();
533
    /// positions.insert(symbol_aapl, position_aapl);
534
    /// positions.insert(symbol_googl, position_googl);
535
    /// positions.insert(symbol_bond, position_bond);
536
    ///
537
    /// let result = mc_calc.calculate_portfolio_var(
538
    ///     "BALANCED_PORTFOLIO",
539
    ///     &positions,
540
    ///     &historical_data
541
    /// )?;
542
    ///
543
    /// // Analyze risk characteristics
544
    /// println!("Portfolio VaR (95%): ${}", result.var_1d);
545
    /// println!("Expected Shortfall: ${}", result.expected_shortfall);
546
    /// println!("Tail risk ratio: {:.2}",
547
    ///          result.expected_shortfall.to_f64() / result.var_1d.to_f64());
548
    ///
549
    /// // Scenario analysis
550
    /// println!("Scenario range: ${} to ${}",
551
    ///          result.worst_case_scenario, result.best_case_scenario);
552
    /// ```
553
    ///
554
    /// # Advanced Features
555
    ///
556
    /// - **Time Scaling**: Automatic scaling for multi-day horizons
557
    /// - **Numerical Stability**: Robust handling of near-singular correlation matrices
558
    /// - **Scenario Preservation**: Full scenario distribution available for analysis
559
    /// - **Quality Validation**: Automatic checks for mathematical consistency
560
    ///
561
    /// # Performance Optimization
562
    ///
563
    /// For large portfolios or frequent calculations:
564
    /// - Pre-compute and cache correlation matrices
565
    /// - Use parallel processing for independent simulations
566
    /// - Consider variance reduction techniques for faster convergence
567
    /// - Implement adaptive simulation counts based on convergence criteria
568
    ///
569
    /// # Errors
570
    ///
571
    /// - `RiskError::Calculation` with operation "`monte_carlo_stats`" if insufficient data
572
    /// - `RiskError::Calculation` with operation "`cholesky_decomposition`" if matrix not positive definite
573
    /// - `RiskError::Calculation` with operation "`monte_carlo_simulation`" if simulation fails
574
    /// - `RiskError::Calculation` with operation "`monte_carlo_metrics`" if metric calculation fails
575
    ///
576
    /// # Validation Checks
577
    ///
578
    /// The method performs automatic validation:
579
    /// - Correlation matrix positive definiteness
580
    /// - Parameter reasonableness (volatility > 0, correlations ∈ [-1,1])
581
    /// - Simulation convergence and stability
582
    /// - Mathematical consistency of risk metrics
583
2
    pub fn calculate_portfolio_var(
584
2
        &self,
585
2
        portfolio_id: &str,
586
2
        positions: &HashMap<Symbol, PositionInfo>,
587
2
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
588
2
    ) -> RiskResult<MonteCarloResult> {
589
        // Extract asset statistics from historical data
590
2
        let 
asset_stats1
= self.calculate_asset_statistics(positions, historical_prices)
?1
;
591
592
        // Build correlation matrix
593
1
        let correlation_matrix =
594
1
            self.calculate_correlation_matrix(&asset_stats, historical_prices)
?0
;
595
596
        // Run Monte Carlo simulations
597
1
        let pnl_scenarios = self.run_monte_carlo_simulations(&asset_stats, &correlation_matrix)
?0
;
598
599
        // Calculate risk metrics from scenarios
600
1
        self.calculate_risk_metrics(portfolio_id, pnl_scenarios)
601
2
    }
602
603
    /// Calculate asset statistics from historical data
604
3
    fn calculate_asset_statistics(
605
3
        &self,
606
3
        positions: &HashMap<Symbol, PositionInfo>,
607
3
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
608
3
    ) -> RiskResult<Vec<AssetStats>> {
609
3
        let mut stats = Vec::new();
610
611
6
        for (
symbol4
,
position4
) in positions {
612
4
            if let Some(prices) = historical_prices.get(symbol) {
613
4
                if prices.len() < 30 {
614
1
                    return Err(RiskError::Calculation {
615
1
                        operation: "monte_carlo_stats".to_owned(),
616
1
                        reason: format!(
617
1
                            "Insufficient price data for {}: {} days available, 30 required",
618
1
                            symbol,
619
1
                            prices.len()
620
1
                        ),
621
1
                    });
622
3
                }
623
624
                // Calculate returns
625
3
                let returns = self.calculate_returns(prices)
?0
;
626
627
                // Calculate mean return and volatility
628
3
                let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
629
3
                let variance = returns
630
3
                    .iter()
631
297
                    .
map3
(|r| (r - mean_return).powi(2))
632
3
                    .sum::<f64>()
633
3
                    / (returns.len() - 1) as f64;
634
3
                let volatility = variance.sqrt();
635
636
3
                let position_value = position.quantity.to_f64() * position.market_value.to_f64();
637
638
3
                stats.push(AssetStats {
639
3
                    symbol: symbol.to_string(),
640
3
                    mean_return,
641
3
                    volatility,
642
3
                    position_value: Price::from_f64(position_value).unwrap_or_else(|e| 
{0
643
0
                        warn!(
644
0
                            "Failed to convert position value {} to Price: {}, using ZERO",
645
                            position_value, e
646
                        );
647
0
                        Price::ZERO
648
0
                    }),
649
                });
650
0
            }
651
        }
652
653
2
        if stats.is_empty() {
654
0
            return Err(RiskError::Calculation {
655
0
                operation: "monte_carlo_stats".to_owned(),
656
0
                reason: "No valid asset statistics could be calculated".to_owned(),
657
0
            });
658
2
        }
659
660
2
        Ok(stats)
661
3
    }
662
663
    /// Calculate correlation matrix between assets
664
1
    fn calculate_correlation_matrix(
665
1
        &self,
666
1
        asset_stats: &[AssetStats],
667
1
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
668
1
    ) -> RiskResult<CorrelationMatrix> {
669
2
        let 
symbols1
:
Vec<String>1
=
asset_stats1
.
iter1
().
map1
(|s| s.symbol.clone()).
collect1
();
670
1
        let n = symbols.len();
671
1
        let mut matrix = vec![vec![0.0; n]; n];
672
673
        // Calculate all pairwise correlations
674
2
        for i in 0..
n1
{
675
4
            for j in 0..
n2
{
676
4
                if i == j {
677
2
                    if let Some(row) = matrix.get_mut(i) {
678
2
                        if let Some(cell) = row.get_mut(j) {
679
2
                            *cell = 1.0; // Perfect correlation with itself
680
2
                        
}0
681
0
                    }
682
2
                } else if let (Some(symbol_i), Some(symbol_j)) = (symbols.get(i), symbols.get(j)) {
683
2
                    let corr = self.calculate_correlation(symbol_i, symbol_j, historical_prices)
?0
;
684
2
                    if let Some(row_i) = matrix.get_mut(i) {
685
2
                        if let Some(cell_ij) = row_i.get_mut(j) {
686
2
                            *cell_ij = corr;
687
2
                        
}0
688
0
                    }
689
2
                    if let Some(row_j) = matrix.get_mut(j) {
690
2
                        if let Some(cell_ji) = row_j.get_mut(i) {
691
2
                            *cell_ji = corr; // Symmetric matrix
692
2
                        
}0
693
0
                    }
694
0
                }
695
            }
696
        }
697
698
1
        Ok(CorrelationMatrix { symbols, matrix })
699
1
    }
700
701
    /// Calculate correlation between two assets
702
4
    fn calculate_correlation(
703
4
        &self,
704
4
        symbol1: &str,
705
4
        symbol2: &str,
706
4
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
707
4
    ) -> RiskResult<f64> {
708
4
        let symbol1_key = Symbol::from(symbol1);
709
4
        let prices1 =
710
4
            historical_prices
711
4
                .get(&symbol1_key)
712
4
                .ok_or_else(|| RiskError::Calculation {
713
0
                    operation: "correlation".to_owned(),
714
0
                    reason: format!("No price data for {symbol1}"),
715
0
                })?;
716
717
4
        let symbol2_key = Symbol::from(symbol2);
718
4
        let prices2 =
719
4
            historical_prices
720
4
                .get(&symbol2_key)
721
4
                .ok_or_else(|| RiskError::Calculation {
722
0
                    operation: "correlation".to_owned(),
723
0
                    reason: format!("No price data for {symbol2}"),
724
0
                })?;
725
726
4
        let returns1 = self.calculate_returns(prices1)
?0
;
727
4
        let returns2 = self.calculate_returns(prices2)
?0
;
728
729
4
        let min_len = returns1.len().min(returns2.len());
730
4
        if min_len < 20 {
731
0
            return Ok(0.0); // Default to zero correlation with insufficient data
732
4
        }
733
734
4
        let returns1_slice = returns1.get(..min_len).unwrap_or(&[]);
735
4
        let returns2_slice = returns2.get(..min_len).unwrap_or(&[]);
736
737
        // Calculate Pearson correlation coefficient
738
4
        let mean1 = returns1_slice.iter().sum::<f64>() / returns1_slice.len() as f64;
739
4
        let mean2 = returns2_slice.iter().sum::<f64>() / returns2_slice.len() as f64;
740
741
4
        let mut covariance = 0.0;
742
4
        let mut var1 = 0.0;
743
4
        let mut var2 = 0.0;
744
745
396
        for (val1, val2) in 
returns1_slice4
.
iter4
().
zip4
(
returns2_slice4
.
iter4
()) {
746
396
            let dev1 = val1 - mean1;
747
396
            let dev2 = val2 - mean2;
748
396
749
396
            covariance += dev1 * dev2;
750
396
            var1 += dev1 * dev1;
751
396
            var2 += dev2 * dev2;
752
396
        }
753
754
4
        let denominator = (var1 * var2).sqrt();
755
4
        if denominator == 0.0 {
756
0
            Ok(0.0)
757
        } else {
758
4
            Ok(covariance / denominator)
759
        }
760
4
    }
761
762
    /// Run Monte Carlo simulations
763
1
    fn run_monte_carlo_simulations(
764
1
        &self,
765
1
        asset_stats: &[AssetStats],
766
1
        correlation_matrix: &CorrelationMatrix,
767
1
    ) -> RiskResult<Vec<f64>> {
768
1
        let mut pnl_scenarios = Vec::with_capacity(self.num_simulations);
769
770
        // Use simple pseudorandom generator for reproducibility
771
1
        let mut rng_state = self.random_seed.unwrap_or(42);
772
773
1
        for _ in 0..self.num_simulations {
774
1.00k
            let mut portfolio_pnl = 0.0;
775
776
            // Generate correlated random shocks for all assets
777
1.00k
            let shocks = self.generate_correlated_shocks(
778
1.00k
                asset_stats.len(),
779
1.00k
                correlation_matrix,
780
1.00k
                &mut rng_state,
781
0
            )?;
782
783
            // Apply shocks to each position
784
2.00k
            for (i, asset) in 
asset_stats1.00k
.
iter1.00k
().
enumerate1.00k
() {
785
2.00k
                let shock = shocks.get(i).copied().unwrap_or(0.0);
786
2.00k
787
2.00k
                // Calculate return for this scenario
788
2.00k
                let scenario_return = asset.mean_return + asset.volatility * shock;
789
2.00k
790
2.00k
                // Apply time scaling for multi-day horizon
791
2.00k
                let scaled_return = scenario_return * (self.time_horizon_days as f64).sqrt();
792
2.00k
793
2.00k
                // Calculate P&L for this position
794
2.00k
                let position_pnl = asset.position_value.to_f64() * scaled_return;
795
2.00k
                portfolio_pnl += position_pnl;
796
2.00k
            }
797
798
1.00k
            pnl_scenarios.push(portfolio_pnl);
799
        }
800
801
1
        Ok(pnl_scenarios)
802
1
    }
803
804
    /// Generate correlated random shocks using proper Cholesky decomposition
805
    /// REPLACES: Fake correlation with 0.5 multiplier - NOW USES REAL MATHEMATICAL MODEL
806
1.00k
    fn generate_correlated_shocks(
807
1.00k
        &self,
808
1.00k
        num_assets: usize,
809
1.00k
        correlation_matrix: &CorrelationMatrix,
810
1.00k
        rng_state: &mut u64,
811
1.00k
    ) -> RiskResult<Vec<f64>> {
812
        // Generate independent normal random variables
813
1.00k
        let mut independent_shocks = Vec::with_capacity(num_assets);
814
2.00k
        for _ in 0..
num_assets1.00k
{
815
2.00k
            let normal_random = self.box_muller_normal(rng_state);
816
2.00k
            independent_shocks.push(normal_random);
817
2.00k
        }
818
819
        // REAL Cholesky decomposition for correlation modeling
820
        // Based on financial mathematics literature and Riskfolio-Lib methodology
821
1.00k
        let cholesky_matrix = self.compute_cholesky_decomposition(&correlation_matrix.matrix)
?0
;
822
823
        // Apply proper correlation using Cholesky decomposition
824
1.00k
        let mut correlated_shocks = vec![0.0; num_assets];
825
826
2.00k
        for i in 0..
num_assets1.00k
{
827
2.00k
            let mut shock = 0.0;
828
3.00k
            for j in 0..=
i2.00k
{
829
3.00k
                let chol_val = cholesky_matrix
830
3.00k
                    .get(i)
831
3.00k
                    .and_then(|row| row.get(j))
832
3.00k
                    .copied()
833
3.00k
                    .unwrap_or(0.0);
834
3.00k
                let indep_shock = independent_shocks.get(j).copied().unwrap_or(0.0);
835
3.00k
                shock += chol_val * indep_shock;
836
            }
837
2.00k
            if let Some(corr_shock) = correlated_shocks.get_mut(i) {
838
2.00k
                *corr_shock = shock;
839
2.00k
            
}0
840
        }
841
842
1.00k
        Ok(correlated_shocks)
843
1.00k
    }
844
845
    /// Compute Cholesky decomposition of correlation matrix
846
    /// REAL MATHEMATICAL IMPLEMENTATION - no more fake 0.5 multipliers
847
1.00k
    fn compute_cholesky_decomposition(
848
1.00k
        &self,
849
1.00k
        correlation_matrix: &[Vec<f64>],
850
1.00k
    ) -> RiskResult<Vec<Vec<f64>>> {
851
1.00k
        let n = correlation_matrix.len();
852
1.00k
        let mut cholesky = vec![vec![0.0; n]; n];
853
854
        // Helper function for safe matrix access
855
5.00k
        let 
safe_get1.00k
= |matrix: &[Vec<f64>], i: usize, j: usize| -> f64 {
856
5.00k
            matrix
857
5.00k
                .get(i)
858
5.00k
                .and_then(|row| row.get(j))
859
5.00k
                .copied()
860
5.00k
                .unwrap_or(0.0)
861
5.00k
        };
862
863
3.00k
        let 
safe_set1.00k
= |matrix: &mut [Vec<f64>], i: usize, j: usize, value: f64| -> bool {
864
3.00k
            if let Some(row) = matrix.get_mut(i) {
865
3.00k
                if let Some(cell) = row.get_mut(j) {
866
3.00k
                    *cell = value;
867
3.00k
                    return true;
868
0
                }
869
0
            }
870
0
            false
871
3.00k
        };
872
873
2.00k
        for i in 0..
n1.00k
{
874
3.00k
            for j in 0..=
i2.00k
{
875
3.00k
                if i == j {
876
                    // Diagonal element
877
2.00k
                    let mut sum_squares = 0.0;
878
2.00k
                    for 
k1.00k
in 0..j {
879
1.00k
                        sum_squares += safe_get(&cholesky, i, k).powi(2);
880
1.00k
                    }
881
882
2.00k
                    let diagonal_value = safe_get(correlation_matrix, i, i) - sum_squares;
883
2.00k
                    if diagonal_value <= 0.0 {
884
0
                        return Err(RiskError::Calculation {
885
0
                            operation: "cholesky_decomposition".to_owned(),
886
0
                            reason: format!("Matrix not positive definite at position ({i}, {i})"),
887
0
                        });
888
2.00k
                    }
889
890
2.00k
                    safe_set(&mut cholesky, i, j, diagonal_value.sqrt());
891
                } else {
892
                    // Lower triangular element
893
1.00k
                    let mut sum_products = 0.0;
894
1.00k
                    for 
k0
in 0..j {
895
0
                        sum_products += safe_get(&cholesky, i, k) * safe_get(&cholesky, j, k);
896
0
                    }
897
898
1.00k
                    let divisor = safe_get(&cholesky, j, j);
899
1.00k
                    if divisor == 0.0 {
900
0
                        return Err(RiskError::Calculation {
901
0
                            operation: "cholesky_decomposition".to_owned(),
902
0
                            reason: format!("Division by zero at position ({j}, {j})"),
903
0
                        });
904
1.00k
                    }
905
906
1.00k
                    let value = (safe_get(correlation_matrix, i, j) - sum_products) / divisor;
907
1.00k
                    safe_set(&mut cholesky, i, j, value);
908
                }
909
            }
910
        }
911
912
1.00k
        Ok(cholesky)
913
1.00k
    }
914
915
    /// Box-Muller transformation for normal random variables
916
12.0k
    fn box_muller_normal(&self, rng_state: &mut u64) -> f64 {
917
        // Simple linear congruential generator
918
12.0k
        *rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
919
12.0k
        let u1 = (*rng_state as f64) / (u64::MAX as f64);
920
921
12.0k
        *rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
922
12.0k
        let u2 = (*rng_state as f64) / (u64::MAX as f64);
923
924
        // Box-Muller transformation
925
926
12.0k
        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
927
12.0k
    }
928
929
    /// Calculate risk metrics from P&L scenarios
930
1
    fn calculate_risk_metrics(
931
1
        &self,
932
1
        portfolio_id: &str,
933
1
        mut pnl_scenarios: Vec<f64>,
934
1
    ) -> RiskResult<MonteCarloResult> {
935
1
        if pnl_scenarios.is_empty() {
936
0
            return Err(RiskError::Calculation {
937
0
                operation: "monte_carlo_metrics".to_owned(),
938
0
                reason: "No P&L scenarios generated".to_owned(),
939
0
            });
940
1
        }
941
942
        // Sort scenarios (worst losses first)
943
10.5k
        
pnl_scenarios1
.
sort_by1
(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
944
945
        // Calculate VaR at confidence level
946
1
        let var_index = ((1.0 - self.confidence_level) * pnl_scenarios.len() as f64) as usize;
947
1
        let scenario_value = pnl_scenarios
948
1
            .get(var_index.min(pnl_scenarios.len().saturating_sub(1)))
949
1
            .copied()
950
1
            .unwrap_or(0.0);
951
952
        // VaR is positive for losses (negate negative P&L)
953
1
        let var_1d = Price::from_f64(scenario_value.abs()).map_err(|e| RiskError::Calculation {
954
0
            operation: "var_calculation".to_owned(),
955
0
            reason: format!("Failed to calculate VaR: {e}"),
956
0
        })?;
957
958
        // Scale to different time horizons
959
1
        let time_scaling = 10.0_f64.sqrt();
960
1
        let var_10d = Price::from_f64(var_1d.to_f64() * time_scaling).map_err(|e| 
{0
961
0
            RiskError::Calculation {
962
0
                operation: "var_time_scaling".to_owned(),
963
0
                reason: format!("Failed to scale VaR to 10 days: {e}"),
964
0
            }
965
0
        })?;
966
967
        // Calculate Expected Shortfall (Conditional VaR)
968
1
        let es_scenarios: Vec<f64> = pnl_scenarios.iter().take(var_index + 1).copied().collect();
969
970
1
        let expected_shortfall = if es_scenarios.is_empty() {
971
0
            var_1d
972
        } else {
973
1
            let sum_f64: f64 = es_scenarios.iter().sum();
974
1
            let count = es_scenarios.len() as f64;
975
1
            let avg = sum_f64 / count;
976
1
            Price::from_f64(avg.abs()).map_err(|e| RiskError::Calculation {
977
0
                operation: "expected_shortfall_calculation".to_owned(),
978
0
                reason: format!("Failed to calculate expected shortfall: {e}"),
979
0
            })?
980
        };
981
982
        // Calculate other statistics
983
1
        let worst_case_scenario = pnl_scenarios
984
1
            .first()
985
1
            .map(|p| Price::from_f64(p.abs()))
986
1
            .and_then(Result::ok)
987
1
            .unwrap_or(Price::ZERO);
988
1
        let best_case_scenario = pnl_scenarios
989
1
            .last()
990
1
            .map(|p| Price::from_f64(p.abs()))
991
1
            .and_then(Result::ok)
992
1
            .unwrap_or(Price::ZERO);
993
994
1
        let sum_f64: f64 = pnl_scenarios.iter().sum();
995
1
        let count = pnl_scenarios.len() as f64;
996
1
        let mean_pnl_f64 = sum_f64 / count;
997
1
        let mean_pnl = Decimal::from_f64(mean_pnl_f64).unwrap_or(Decimal::ZERO);
998
999
        // Calculate volatility (standard deviation of scenarios)
1000
1
        let variance_sum: f64 = pnl_scenarios
1001
1
            .iter()
1002
1.00k
            .
map1
(|pnl| {
1003
1.00k
                let diff = pnl - mean_pnl_f64;
1004
1.00k
                diff * diff
1005
1.00k
            })
1006
1
            .sum();
1007
1
        let variance = variance_sum / (pnl_scenarios.len() - 1) as f64;
1008
1
        let volatility = Price::from_f64(variance.sqrt()).map_err(|e| RiskError::Calculation {
1009
0
            operation: "volatility_calculation".to_owned(),
1010
0
            reason: format!("Failed to calculate volatility: {e}"),
1011
0
        })?;
1012
1013
1
        Ok(MonteCarloResult {
1014
1
            portfolio_id: portfolio_id.to_owned(),
1015
1
            var_1d,
1016
1
            var_10d,
1017
1
            expected_shortfall,
1018
1
            confidence_level: self.confidence_level,
1019
1
            num_simulations: self.num_simulations,
1020
1
            worst_case_scenario,
1021
1
            best_case_scenario,
1022
1
            mean_pnl,
1023
1
            volatility,
1024
1
            calculated_at: Utc::now(),
1025
1
        })
1026
1
    }
1027
1028
    /// Calculate returns from historical prices
1029
12
    fn calculate_returns(&self, prices: &[HistoricalPrice]) -> RiskResult<Vec<f64>> {
1030
12
        if prices.len() < 2 {
1031
0
            return Ok(Vec::new());
1032
12
        }
1033
1034
12
        let mut returns = Vec::new();
1035
1.18k
        for window in 
prices12
.
windows12
(2) {
1036
1.18k
            if let (Some(prev), Some(curr)) = (window.first(), window.get(1)) {
1037
1.18k
                let prev_price = prev.price.to_f64();
1038
1.18k
                let curr_price = curr.price.to_f64();
1039
1040
1.18k
                if prev_price > 0.0 {
1041
1.18k
                    let return_rate = (curr_price - prev_price) / prev_price;
1042
1.18k
                    returns.push(return_rate);
1043
1.18k
                
}0
1044
0
            }
1045
        }
1046
1047
12
        Ok(returns)
1048
12
    }
1049
}
1050
1051
#[cfg(test)]
1052
mod tests {
1053
    use super::*;
1054
    use chrono::Duration;
1055
    use common::types::Quantity;
1056
    // operations module removed - use direct imports from common
1057
1058
7
    fn create_test_historical_prices(
1059
7
        symbol: &str,
1060
7
        days: usize,
1061
7
        base_price: f64,
1062
7
        volatility: f64,
1063
7
    ) -> Vec<HistoricalPrice> {
1064
7
        let mut prices = Vec::new();
1065
7
        let mut current_price = base_price;
1066
7
        let mut simple_rng = 12345u64;
1067
1068
610
        for i in 0..
days7
{
1069
610
            // Simple random walk with specified volatility
1070
610
            simple_rng = simple_rng.wrapping_mul(1664525).wrapping_add(1013904223);
1071
610
            let random = (simple_rng as f64) / (u64::MAX as f64);
1072
610
            let change = (random - 0.5) * volatility * 2.0; // Scale to volatility
1073
610
1074
610
            current_price *= 1.0 + change;
1075
610
1076
610
            prices.push(HistoricalPrice {
1077
610
                symbol: symbol.to_string(),
1078
610
                date: Utc::now() - Duration::days(days as i64 - i as i64),
1079
610
                open: Price::from_f64(current_price * 0.999).unwrap_or(Price::ZERO),
1080
610
                high: Price::from_f64(current_price * 1.005).unwrap_or(Price::ZERO),
1081
610
                low: Price::from_f64(current_price * 0.995).unwrap_or(Price::ZERO),
1082
610
                price: Price::from_f64(current_price).unwrap_or(Price::ZERO),
1083
610
                volume: Quantity::from_f64(1000000.0).unwrap_or(Quantity::ZERO),
1084
610
            });
1085
610
        }
1086
1087
7
        prices
1088
7
    }
1089
1090
4
    fn create_test_position(symbol: &str, quantity: f64, market_price: f64) -> PositionInfo {
1091
4
        PositionInfo {
1092
4
            symbol: symbol.to_string().into(),
1093
4
            quantity: Quantity::from_f64(quantity).unwrap_or(Quantity::ZERO),
1094
4
            market_value: Price::from_f64(quantity * market_price).unwrap_or(Price::ZERO),
1095
4
            average_cost: Price::from_f64(market_price * 0.95).unwrap_or(Price::ZERO),
1096
4
            unrealized_pnl: Price::from_f64(quantity * market_price * 0.05).unwrap_or(Price::ZERO),
1097
4
            realized_pnl: Price::from_f64(0.0).unwrap_or(Price::ZERO),
1098
4
            currency: "USD".to_string(),
1099
4
            timestamp: Utc::now(),
1100
4
        }
1101
4
    }
1102
1103
    #[test]
1104
1
    fn test_monte_carlo_calculator_creation() {
1105
1
        let calculator = MonteCarloVaR::standard();
1106
1
        assert_eq!(calculator.confidence_level, 0.95);
1107
1
        assert_eq!(calculator.num_simulations, 10_000);
1108
1109
1
        let hp_calculator = MonteCarloVaR::high_precision();
1110
1
        assert_eq!(hp_calculator.num_simulations, 100_000);
1111
1
        assert_eq!(hp_calculator.confidence_level, 0.99);
1112
1
    }
1113
1114
    #[test]
1115
1
    fn test_returns_calculation() -> Result<(), Box<dyn std::error::Error>> {
1116
1
        let calculator = MonteCarloVaR::standard();
1117
1
        let prices = create_test_historical_prices("AAPL", 100, 150.0, 0.02);
1118
1
        let returns = calculator.calculate_returns(&prices)
?0
;
1119
1120
1
        assert_eq!(returns.len(), 99);
1121
99
        
assert!1
(
returns.iter()1
.
all1
(|r| r.abs() < 0.2)); // Reasonable returns
1122
1
        Ok(())
1123
1
    }
1124
1125
    #[test]
1126
1
    fn test_asset_statistics_calculation() -> Result<(), Box<dyn std::error::Error>> {
1127
1
        let calculator = MonteCarloVaR::standard();
1128
1129
1
        let mut positions = HashMap::new();
1130
1
        positions.insert(
1131
1
            Symbol::from("AAPL".to_string()),
1132
1
            create_test_position("AAPL", 100.0, 150.0),
1133
        );
1134
1135
1
        let mut historical_prices = HashMap::new();
1136
1
        historical_prices.insert(
1137
1
            Symbol::from("AAPL".to_string()),
1138
1
            create_test_historical_prices("AAPL", 100, 150.0, 0.02),
1139
        );
1140
1141
1
        let stats = calculator.calculate_asset_statistics(&positions, &historical_prices)
?0
;
1142
1143
1
        assert_eq!(stats.len(), 1);
1144
1
        assert_eq!(stats[0].symbol, "AAPL");
1145
1
        assert!(stats[0].volatility > 0.0);
1146
1
        assert!(stats[0].position_value > Price::ZERO);
1147
1
        Ok(())
1148
1
    }
1149
1150
    #[test]
1151
1
    fn test_correlation_calculation() -> Result<(), Box<dyn std::error::Error>> {
1152
1
        let calculator = MonteCarloVaR::standard();
1153
1154
1
        let mut historical_prices = HashMap::new();
1155
1
        historical_prices.insert(
1156
1
            Symbol::from("AAPL".to_string()),
1157
1
            create_test_historical_prices("AAPL", 100, 150.0, 0.02),
1158
        );
1159
1
        historical_prices.insert(
1160
1
            Symbol::from("GOOGL".to_string()),
1161
1
            create_test_historical_prices("GOOGL", 100, 2800.0, 0.025),
1162
        );
1163
1164
1
        let corr = calculator.calculate_correlation("AAPL", "GOOGL", &historical_prices)
?0
;
1165
1166
1
        assert!(corr >= -1.0 && corr <= 1.0);
1167
1168
        // Self-correlation should be handled separately, but let's test the method
1169
1
        let self_corr = calculator.calculate_correlation("AAPL", "AAPL", &historical_prices)
?0
;
1170
1
        assert!((self_corr - 1.0).abs() < 0.01); // Should be close to 1.0
1171
1
        Ok(())
1172
1
    }
1173
1174
    #[test]
1175
1
    fn test_monte_carlo_portfolio_var() -> Result<(), Box<dyn std::error::Error>> {
1176
1
        let calculator = MonteCarloVaR::new(0.95, 1000, 1, Some(42)); // Small simulation for testing
1177
1178
1
        let mut positions = HashMap::new();
1179
1
        positions.insert(
1180
1
            Symbol::from("AAPL".to_string()),
1181
1
            create_test_position("AAPL", 100.0, 150.0),
1182
        );
1183
1
        positions.insert(
1184
1
            Symbol::from("GOOGL".to_string()),
1185
1
            create_test_position("GOOGL", 50.0, 2800.0),
1186
        );
1187
1188
1
        let mut historical_prices = HashMap::new();
1189
1
        historical_prices.insert(
1190
1
            Symbol::from("AAPL".to_string()),
1191
1
            create_test_historical_prices("AAPL", 100, 150.0, 0.02),
1192
        );
1193
1
        historical_prices.insert(
1194
1
            Symbol::from("GOOGL".to_string()),
1195
1
            create_test_historical_prices("GOOGL", 100, 2800.0, 0.025),
1196
        );
1197
1198
1
        let result =
1199
1
            calculator.calculate_portfolio_var("TEST_PORTFOLIO", &positions, &historical_prices)
?0
;
1200
1201
1
        assert_eq!(result.portfolio_id, "TEST_PORTFOLIO");
1202
1
        assert_eq!(result.confidence_level, 0.95);
1203
1
        assert_eq!(result.num_simulations, 1000);
1204
1
        assert!(result.var_1d > Price::ZERO);
1205
1
        assert!(result.var_10d > result.var_1d);
1206
1
        assert!(result.expected_shortfall >= result.var_1d);
1207
1
        assert!(result.worst_case_scenario >= result.var_1d);
1208
1
        assert!(result.volatility > Price::ZERO);
1209
1
        Ok(())
1210
1
    }
1211
1212
    #[test]
1213
1
    fn test_box_muller_normal() {
1214
1
        let calculator = MonteCarloVaR::standard();
1215
1
        let mut rng_state = 42;
1216
1217
        // Generate many samples and check they form approximately normal distribution
1218
1
        let mut samples = Vec::new();
1219
10.0k
        for _ in 0..10000 {
1220
10.0k
            let sample = calculator.box_muller_normal(&mut rng_state);
1221
10.0k
            samples.push(sample);
1222
10.0k
        }
1223
1224
        // Basic sanity checks
1225
1
        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
1226
1
        let variance =
1227
10.0k
            
samples.iter()1
.
map1
(|x| (x - mean).powi(2)).
sum1
::<f64>() /
samples.len() as f641
;
1228
1
        let std_dev = variance.sqrt();
1229
1230
        // Should be approximately N(0,1)
1231
1
        assert!(mean.abs() < 0.1, 
"Mean should be close to 0, got {}"0
, mean);
1232
1
        assert!(
1233
1
            (std_dev - 1.0).abs() < 0.1,
1234
0
            "Std dev should be close to 1, got {}",
1235
            std_dev
1236
        );
1237
1
    }
1238
1239
    #[test]
1240
1
    fn test_insufficient_data_error() {
1241
1
        let calculator = MonteCarloVaR::standard();
1242
1243
1
        let mut positions = HashMap::new();
1244
1
        positions.insert(
1245
1
            Symbol::from("AAPL".to_string()),
1246
1
            create_test_position("AAPL", 100.0, 150.0),
1247
        );
1248
1249
1
        let mut historical_prices = HashMap::new();
1250
1
        historical_prices.insert(
1251
1
            Symbol::from("AAPL".to_string()),
1252
1
            create_test_historical_prices("AAPL", 10, 150.0, 0.02),
1253
        ); // Only 10 days
1254
1255
1
        let result =
1256
1
            calculator.calculate_portfolio_var("TEST_PORTFOLIO", &positions, &historical_prices);
1257
1
        assert!(result.is_err());
1258
1259
1
        if let Err(RiskError::Calculation { operation, reason }) = result {
1260
1
            assert_eq!(operation, "monte_carlo_stats");
1261
1
            assert!(reason.contains("Insufficient price data"));
1262
0
        }
1263
1
    }
1264
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/parametric.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/parametric.rs.html deleted file mode 100644 index 4d45c0928..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/parametric.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/parametric.rs
Line
Count
Source
1
//! Parametric `VaR` calculation using variance-covariance method
2
//! Production implementation for risk management
3
4
use anyhow::Result;
5
use common::types::Price;
6
use nalgebra::{DMatrix, DVector};
7
use num::FromPrimitive;
8
use rust_decimal::Decimal;
9
use std::collections::HashMap;
10
/// Parametric `VaR` calculator using variance-covariance method
11
#[derive(Debug)]
12
pub struct ParametricVaR {
13
    /// Confidence level (e.g., 0.95 for 95% `VaR`)
14
    confidence_level: f64,
15
    /// Covariance matrix of asset returns
16
    covariance_matrix: Option<DMatrix<f64>>,
17
    /// Mean returns vector
18
    mean_returns: Option<DVector<f64>>,
19
    /// Asset symbols
20
    symbols: Vec<String>,
21
}
22
23
impl ParametricVaR {
24
    /// Create new parametric `VaR` calculator
25
    #[must_use]
26
17
    pub const fn new(confidence_level: f64) -> Self {
27
17
        Self {
28
17
            confidence_level,
29
17
            covariance_matrix: None,
30
17
            mean_returns: None,
31
17
            symbols: Vec::new(),
32
17
        }
33
17
    }
34
35
    /// Update covariance matrix with new market data
36
14
    pub fn update_covariance_matrix(
37
14
        &mut self,
38
14
        returns_data: &HashMap<String, Vec<f64>>,
39
14
    ) -> Result<()> {
40
14
        let symbols: Vec<String> = returns_data.keys().cloned().collect();
41
14
        let n_assets = symbols.len();
42
43
14
        if n_assets == 0 {
44
1
            return Err(anyhow::anyhow!(
45
1
                "No assets provided for covariance calculation"
46
1
            ));
47
13
        }
48
49
        // Build returns matrix
50
13
        let mut returns_matrix = Vec::new();
51
13
        let mut min_length = usize::MAX;
52
53
        // Find minimum length across all return series
54
22
        for returns in 
returns_data13
.
values13
() {
55
22
            min_length = min_length.min(returns.len());
56
22
        }
57
58
        // Build matrix with consistent length
59
35
        for 
symbol22
in &symbols {
60
22
            if let Some(returns) = returns_data.get(symbol) {
61
22
                if let Some(slice) = returns.get(returns.len().saturating_sub(min_length)..) {
62
22
                    returns_matrix.push(slice.to_vec());
63
22
                
}0
64
0
            }
65
        }
66
67
        // Calculate covariance matrix
68
13
        let mut covariance = DMatrix::zeros(n_assets, n_assets);
69
13
        let mut means = DVector::zeros(n_assets);
70
71
        // Calculate means
72
22
        for (i, returns) in 
returns_matrix.iter()13
.
enumerate13
() {
73
22
            if let Some(mean_val) = means.get_mut(i) {
74
22
                *mean_val = returns.iter().sum::<f64>() / returns.len() as f64;
75
22
            
}0
76
        }
77
78
        // Calculate covariances
79
22
        for i in 0..
n_assets13
{
80
44
            for j in 0..
n_assets22
{
81
44
                let mut covar = 0.0;
82
202
                for k in 0..
min_length44
{
83
202
                    let ret_i_k = returns_matrix
84
202
                        .get(i)
85
202
                        .and_then(|row| row.get(k))
86
202
                        .copied()
87
202
                        .unwrap_or(0.0);
88
202
                    let ret_j_k = returns_matrix
89
202
                        .get(j)
90
202
                        .and_then(|row| row.get(k))
91
202
                        .copied()
92
202
                        .unwrap_or(0.0);
93
202
                    let mean_i = means.get(i).copied().unwrap_or(0.0);
94
202
                    let mean_j = means.get(j).copied().unwrap_or(0.0);
95
202
                    covar += (ret_i_k - mean_i) * (ret_j_k - mean_j);
96
                }
97
44
                if let Some(cell) = covariance.get_mut((i, j)) {
98
44
                    *cell = covar / (min_length - 1) as f64;
99
44
                
}0
100
            }
101
        }
102
103
13
        self.covariance_matrix = Some(covariance);
104
13
        self.mean_returns = Some(means);
105
13
        self.symbols = symbols;
106
107
13
        Ok(())
108
14
    }
109
110
    /// Calculate `VaR` for given portfolio weights
111
8
    pub fn calculate_var(
112
8
        &self,
113
8
        portfolio_weights: &DVector<f64>,
114
8
        portfolio_value: Price,
115
8
    ) -> Result<Decimal> {
116
8
        let 
covar_matrix7
= self
117
8
            .covariance_matrix
118
8
            .as_ref()
119
8
            .ok_or_else(|| anyhow::anyhow!(
"Covariance matrix not initialized"1
))
?1
;
120
121
        // Calculate portfolio variance: w^T * Σ * w
122
7
        let portfolio_variance = portfolio_weights.transpose() * covar_matrix * portfolio_weights;
123
7
        let portfolio_vol = portfolio_variance.get(0).copied().ok_or_else(|| 
{0
124
0
            anyhow::anyhow!("Failed to calculate portfolio variance - invalid matrix dimensions")
125
7
        
}0
)
?0
.sqrt();
126
127
        // Get z-score for confidence level
128
7
        let z_score = Self::get_z_score(self.confidence_level);
129
130
        // VaR = z * σ * V (where V is portfolio value)
131
7
        let var_percentage = z_score * portfolio_vol;
132
7
        let portfolio_value_f64 = portfolio_value
133
7
            .to_string()
134
7
            .parse::<f64>()
135
7
            .map_err(|e| anyhow::anyhow!(
"Failed to parse portfolio value: {e}"0
))
?0
;
136
137
7
        let var_amount = var_percentage * portfolio_value_f64;
138
139
7
        FromPrimitive::from_f64(var_amount.abs()).ok_or_else(|| 
{0
140
0
            anyhow::anyhow!("Failed to convert VaR amount to Decimal: {var_amount}")
141
0
        })
142
8
    }
143
144
    /// Get z-score for given confidence level
145
12
    fn get_z_score(confidence_level: f64) -> f64 {
146
        // Approximate z-scores for common confidence levels
147
12
        match (confidence_level * 100.0) as u32 {
148
2
            90 => 1.282,
149
8
            95 => 1.645,
150
2
            99 => 2.326,
151
            _ => {
152
                // Linear interpolation for other values
153
0
                if confidence_level <= 0.90 {
154
0
                    1.282 * confidence_level / 0.90
155
0
                } else if confidence_level <= 0.95 {
156
0
                    1.282 + (1.645 - 1.282) * (confidence_level - 0.90) / 0.05
157
                } else {
158
0
                    1.645 + (2.326 - 1.645) * (confidence_level - 0.95) / 0.04
159
                }
160
            },
161
        }
162
12
    }
163
164
    /// Calculate component `VaR` (marginal contribution to `VaR`)
165
3
    pub fn calculate_component_var(
166
3
        &self,
167
3
        portfolio_weights: &DVector<f64>,
168
3
        portfolio_value: Price,
169
3
    ) -> Result<Vec<Price>> {
170
3
        let 
covar_matrix2
= self
171
3
            .covariance_matrix
172
3
            .as_ref()
173
3
            .ok_or_else(|| anyhow::anyhow!(
"Covariance matrix not initialized"1
))
?1
;
174
175
2
        let portfolio_variance = portfolio_weights.transpose() * covar_matrix * portfolio_weights;
176
2
        let portfolio_vol = portfolio_variance.get(0).copied().ok_or_else(|| 
{0
177
0
            anyhow::anyhow!("Failed to calculate portfolio variance in component VaR - invalid matrix dimensions")
178
2
        
}0
)
?0
.sqrt();
179
180
2
        let z_score = Self::get_z_score(self.confidence_level);
181
2
        let portfolio_value_f64 = portfolio_value
182
2
            .to_string()
183
2
            .parse::<f64>()
184
2
            .map_err(|e| anyhow::anyhow!(
"Failed to parse portfolio value: {e}"0
))
?0
;
185
186
2
        let mut component_vars = Vec::new();
187
188
5
        for i in 0..
portfolio_weights2
.
len2
() {
189
            // Marginal VaR = (Σ * w) / σ_p
190
5
            let mut marginal_var = 0.0;
191
13
            for j in 0..
portfolio_weights5
.
len5
() {
192
13
                let covar_val = covar_matrix.get((i, j)).copied().unwrap_or(0.0);
193
13
                let weight_j = portfolio_weights.get(j).copied().unwrap_or(0.0);
194
13
                marginal_var += covar_val * weight_j;
195
13
            }
196
5
            marginal_var /= portfolio_vol;
197
198
            // Component VaR = weight * marginal VaR * z-score * portfolio value
199
5
            let weight_i = portfolio_weights.get(i).copied().unwrap_or(0.0);
200
5
            let component_var = weight_i * marginal_var * z_score * portfolio_value_f64;
201
202
5
            component_vars.push(Price::from_f64(component_var.abs()).unwrap_or(Price::ZERO));
203
        }
204
205
2
        Ok(component_vars.into_iter().collect())
206
3
    }
207
}
208
209
#[cfg(test)]
210
mod tests {
211
    use super::*;
212
    use common::types::Price;
213
    use nalgebra::DVector;
214
    use std::collections::HashMap;
215
216
    #[test]
217
1
    fn test_parametric_var_new() {
218
1
        let var_calc = ParametricVaR::new(0.95);
219
1
        assert!((var_calc.confidence_level - 0.95).abs() < 1e-6);
220
1
        assert!(var_calc.covariance_matrix.is_none());
221
1
        assert!(var_calc.mean_returns.is_none());
222
1
        assert_eq!(var_calc.symbols.len(), 0);
223
1
    }
224
225
    #[test]
226
1
    fn test_z_score_calculation() {
227
1
        assert!((ParametricVaR::get_z_score(0.90) - 1.282).abs() < 0.01);
228
1
        assert!((ParametricVaR::get_z_score(0.95) - 1.645).abs() < 0.01);
229
1
        assert!((ParametricVaR::get_z_score(0.99) - 2.326).abs() < 0.01);
230
1
    }
231
232
    #[test]
233
1
    fn test_update_covariance_matrix_empty_data() {
234
1
        let mut var_calc = ParametricVaR::new(0.95);
235
1
        let empty_data: HashMap<String, Vec<f64>> = HashMap::new();
236
237
1
        let result = var_calc.update_covariance_matrix(&empty_data);
238
1
        assert!(result.is_err());
239
1
        assert!(result
240
1
            .unwrap_err()
241
1
            .to_string()
242
1
            .contains("No assets provided"));
243
1
    }
244
245
    #[test]
246
1
    fn test_update_covariance_matrix_single_asset() -> Result<()> {
247
1
        let mut var_calc = ParametricVaR::new(0.95);
248
1
        let mut returns_data = HashMap::new();
249
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
250
251
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
252
253
1
        assert!(var_calc.covariance_matrix.is_some());
254
1
        assert!(var_calc.mean_returns.is_some());
255
1
        assert_eq!(var_calc.symbols.len(), 1);
256
1
        assert_eq!(var_calc.symbols[0], "AAPL");
257
258
1
        Ok(())
259
1
    }
260
261
    #[test]
262
1
    fn test_update_covariance_matrix_multiple_assets() -> Result<()> {
263
1
        let mut var_calc = ParametricVaR::new(0.95);
264
1
        let mut returns_data = HashMap::new();
265
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01]);
266
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00]);
267
1
        returns_data.insert("GOOGL".to_string(), vec![-0.01, 0.03, -0.02, 0.01]);
268
269
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
270
271
1
        assert!(var_calc.covariance_matrix.is_some());
272
1
        assert!(var_calc.mean_returns.is_some());
273
1
        assert_eq!(var_calc.symbols.len(), 3);
274
275
1
        let covar = var_calc.covariance_matrix.as_ref().unwrap();
276
1
        assert_eq!(covar.nrows(), 3);
277
1
        assert_eq!(covar.ncols(), 3);
278
279
1
        Ok(())
280
1
    }
281
282
    #[test]
283
1
    fn test_covariance_matrix_symmetry() -> Result<()> {
284
1
        let mut var_calc = ParametricVaR::new(0.95);
285
1
        let mut returns_data = HashMap::new();
286
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
287
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);
288
289
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
290
291
1
        let covar = var_calc.covariance_matrix.as_ref().unwrap();
292
        // Covariance matrix should be symmetric
293
2
        for i in 0..
covar1
.
nrows1
() {
294
4
            for j in 0..
covar2
.
ncols2
() {
295
4
                let val_ij = covar.get((i, j)).copied().unwrap_or(0.0);
296
4
                let val_ji = covar.get((j, i)).copied().unwrap_or(0.0);
297
4
                assert!(
298
4
                    (val_ij - val_ji).abs() < 1e-10,
299
0
                    "Covariance matrix not symmetric"
300
                );
301
            }
302
        }
303
304
1
        Ok(())
305
1
    }
306
307
    #[test]
308
1
    fn test_calculate_var_without_covariance() {
309
1
        let var_calc = ParametricVaR::new(0.95);
310
1
        let weights = DVector::from_vec(vec![1.0]);
311
1
        let portfolio_value = Price::from_f64(1000000.0).unwrap();
312
313
1
        let result = var_calc.calculate_var(&weights, portfolio_value);
314
1
        assert!(result.is_err());
315
1
        assert!(result.unwrap_err().to_string().contains("not initialized"));
316
1
    }
317
318
    #[test]
319
1
    fn test_calculate_var_single_asset() -> Result<()> {
320
1
        let mut var_calc = ParametricVaR::new(0.95);
321
1
        let mut returns_data = HashMap::new();
322
        // Returns with known standard deviation
323
1
        returns_data.insert("AAPL".to_string(), vec![0.02, -0.02, 0.03, -0.01, 0.01]);
324
325
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
326
327
1
        let weights = DVector::from_vec(vec![1.0]);
328
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
329
330
1
        let var = var_calc.calculate_var(&weights, portfolio_value)
?0
;
331
332
        // VaR should be positive
333
1
        assert!(var > Decimal::ZERO);
334
        // VaR should be reasonable (less than portfolio value)
335
1
        assert!(var < Decimal::from(1000000));
336
337
1
        Ok(())
338
1
    }
339
340
    #[test]
341
1
    fn test_calculate_var_portfolio() -> Result<()> {
342
1
        let mut var_calc = ParametricVaR::new(0.95);
343
1
        let mut returns_data = HashMap::new();
344
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
345
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);
346
347
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
348
349
        // Equal weight portfolio
350
1
        let weights = DVector::from_vec(vec![0.5, 0.5]);
351
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
352
353
1
        let var = var_calc.calculate_var(&weights, portfolio_value)
?0
;
354
355
1
        assert!(var > Decimal::ZERO);
356
1
        assert!(var < Decimal::from(1000000));
357
358
1
        Ok(())
359
1
    }
360
361
    #[test]
362
1
    fn test_calculate_var_different_confidence_levels() -> Result<()> {
363
1
        let mut returns_data = HashMap::new();
364
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
365
366
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
367
1
        let weights = DVector::from_vec(vec![1.0]);
368
369
        // 90% confidence
370
1
        let mut var_90 = ParametricVaR::new(0.90);
371
1
        var_90.update_covariance_matrix(&returns_data)
?0
;
372
1
        let var_90_result = var_90.calculate_var(&weights, portfolio_value)
?0
;
373
374
        // 95% confidence
375
1
        let mut var_95 = ParametricVaR::new(0.95);
376
1
        var_95.update_covariance_matrix(&returns_data)
?0
;
377
1
        let var_95_result = var_95.calculate_var(&weights, portfolio_value)
?0
;
378
379
        // 99% confidence
380
1
        let mut var_99 = ParametricVaR::new(0.99);
381
1
        var_99.update_covariance_matrix(&returns_data)
?0
;
382
1
        let var_99_result = var_99.calculate_var(&weights, portfolio_value)
?0
;
383
384
        // Higher confidence should produce higher VaR
385
1
        assert!(var_99_result > var_95_result);
386
1
        assert!(var_95_result > var_90_result);
387
388
1
        Ok(())
389
1
    }
390
391
    #[test]
392
1
    fn test_calculate_component_var_without_covariance() {
393
1
        let var_calc = ParametricVaR::new(0.95);
394
1
        let weights = DVector::from_vec(vec![1.0]);
395
1
        let portfolio_value = Price::from_f64(1000000.0).unwrap();
396
397
1
        let result = var_calc.calculate_component_var(&weights, portfolio_value);
398
1
        assert!(result.is_err());
399
1
    }
400
401
    #[test]
402
1
    fn test_calculate_component_var() -> Result<()> {
403
1
        let mut var_calc = ParametricVaR::new(0.95);
404
1
        let mut returns_data = HashMap::new();
405
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
406
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);
407
1
        returns_data.insert("GOOGL".to_string(), vec![-0.01, 0.03, -0.02, 0.01, 0.02]);
408
409
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
410
411
1
        let weights = DVector::from_vec(vec![0.4, 0.3, 0.3]);
412
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
413
414
1
        let component_vars = var_calc.calculate_component_var(&weights, portfolio_value)
?0
;
415
416
        // Should have one component VaR for each asset
417
1
        assert_eq!(component_vars.len(), 3);
418
419
        // All component VaRs should be non-negative
420
4
        for 
comp_var3
in &component_vars {
421
3
            assert!(*comp_var >= Price::ZERO);
422
        }
423
424
1
        Ok(())
425
1
    }
426
427
    #[test]
428
1
    fn test_component_var_sum_equals_total_var() -> Result<()> {
429
1
        let mut var_calc = ParametricVaR::new(0.95);
430
1
        let mut returns_data = HashMap::new();
431
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
432
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);
433
434
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
435
436
1
        let weights = DVector::from_vec(vec![0.6, 0.4]);
437
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
438
439
1
        let total_var = var_calc.calculate_var(&weights, portfolio_value)
?0
;
440
1
        let component_vars = var_calc.calculate_component_var(&weights, portfolio_value)
?0
;
441
442
1
        let sum_component_vars: Decimal = component_vars
443
1
            .iter()
444
2
            .
map1
(|p| p.to_string().parse::<Decimal>().unwrap_or(Decimal::ZERO))
445
1
            .sum();
446
447
        // Component VaRs should sum to total VaR (within tolerance)
448
1
        let diff = (total_var - sum_component_vars).abs();
449
1
        let tolerance = total_var * Decimal::from_f64_retain(0.01).unwrap(); // 1% tolerance
450
1
        assert!(
451
1
            diff < tolerance,
452
0
            "Component VaRs don't sum to total: total={}, sum={}",
453
            total_var,
454
            sum_component_vars
455
        );
456
457
1
        Ok(())
458
1
    }
459
460
    #[test]
461
1
    fn test_mean_returns_calculation() -> Result<()> {
462
1
        let mut var_calc = ParametricVaR::new(0.95);
463
1
        let mut returns_data = HashMap::new();
464
1
        let returns = vec![0.02, -0.02, 0.04, -0.04]; // Mean = 0.0
465
1
        returns_data.insert("TEST".to_string(), returns.clone());
466
467
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
468
469
1
        let means = var_calc.mean_returns.as_ref().unwrap();
470
1
        let calculated_mean = means.get(0).copied().unwrap_or(f64::NAN);
471
472
1
        let expected_mean: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
473
1
        assert!((calculated_mean - expected_mean).abs() < 1e-10);
474
475
1
        Ok(())
476
1
    }
477
478
    #[test]
479
1
    fn test_unequal_length_returns() -> Result<()> {
480
1
        let mut var_calc = ParametricVaR::new(0.95);
481
1
        let mut returns_data = HashMap::new();
482
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
483
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01]); // Shorter
484
485
        // Should handle unequal lengths by truncating to minimum
486
1
        let result = var_calc.update_covariance_matrix(&returns_data);
487
1
        assert!(result.is_ok());
488
489
1
        Ok(())
490
1
    }
491
492
    #[test]
493
1
    fn test_portfolio_with_zero_weights() -> Result<()> {
494
1
        let mut var_calc = ParametricVaR::new(0.95);
495
1
        let mut returns_data = HashMap::new();
496
1
        returns_data.insert("AAPL".to_string(), vec![0.01, -0.02, 0.03, -0.01, 0.02]);
497
1
        returns_data.insert("MSFT".to_string(), vec![0.02, -0.01, 0.01, 0.00, -0.01]);
498
499
1
        var_calc.update_covariance_matrix(&returns_data)
?0
;
500
501
        // One asset with zero weight
502
1
        let weights = DVector::from_vec(vec![1.0, 0.0]);
503
1
        let portfolio_value = Price::from_f64(1000000.0)
?0
;
504
505
1
        let var = var_calc.calculate_var(&weights, portfolio_value)
?0
;
506
1
        assert!(var > Decimal::ZERO);
507
508
1
        Ok(())
509
1
    }
510
}
\ No newline at end of file diff --git a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/var_engine.rs.html b/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/var_engine.rs.html deleted file mode 100644 index 5f262da27..000000000 --- a/coverage_risk/html/coverage/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/var_engine.rs.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/risk/src/var_calculator/var_engine.rs
Line
Count
Source
1
//! REAL `VaR` Calculation Engine - NO MORE MOCK VALUES!
2
//! Implements multiple `VaR` methodologies based on financial mathematics
3
//!
4
//! Based on research from:
5
//! - Riskfolio-Lib: Portfolio optimization and `VaR` calculation
6
//! - `QuantLib`: Financial mathematics library
7
//! - Academic literature on Value at Risk
8
9
// REMOVED: Direct Decimal usage - use canonical types
10
use crate::error::{RiskError, RiskResult};
11
use chrono::{DateTime, Utc};
12
use common::types::{Price, Quantity, Symbol};
13
use num::{FromPrimitive, ToPrimitive};
14
use rust_decimal::Decimal;
15
use serde::{Deserialize, Serialize};
16
use std::collections::HashMap;
17
use tracing::error;
18
// Removed broker_integration - types not available in simplified risk crate
19
// Define minimal replacements for compilation
20
21
// Production types for VaR calculation
22
/// **Historical Price Data Point for `VaR` Calculations**
23
///
24
/// Contains comprehensive price information for a single trading day.
25
/// Used in historical simulation `VaR` calculations and volatility modeling.
26
///
27
/// # Fields
28
/// - `symbol`: Trading symbol or instrument identifier
29
/// - `date`: Trading date (UTC timestamp)
30
/// - `open`: Opening price for the trading session
31
/// - `high`: Highest price during the trading session
32
/// - `low`: Lowest price during the trading session
33
/// - `price`: Closing price (primary price for `VaR` calculations)
34
/// - `volume`: Trading volume for the day
35
///
36
/// # Usage
37
/// Used to build historical return series for `VaR` calculation:
38
/// ```rust
39
/// let price_data = HistoricalPrice {
40
///     symbol: "AAPL".to_string(),
41
///     date: Utc::now(),
42
///     price: Price::from_f64(175.0)?,
43
///     volume: Quantity::from_f64(1_000_000.0)?,
44
///     // ... other fields
45
/// };
46
/// ```
47
#[derive(Debug, Clone)]
48
pub struct HistoricalPrice {
49
    /// Trading symbol or instrument identifier
50
    pub symbol: String,
51
    /// Trading date (UTC timestamp)
52
    pub date: DateTime<Utc>,
53
    /// Opening price for the trading session
54
    pub open: Price,
55
    /// Highest price during the trading session
56
    pub high: Price,
57
    /// Lowest price during the trading session
58
    pub low: Price,
59
    /// Closing price (primary price for `VaR` calculations)
60
    pub price: Price,
61
    /// Trading volume for the day
62
    pub volume: Quantity,
63
}
64
65
/// **Position Information for `VaR` Portfolio Analysis**
66
///
67
/// Contains comprehensive position details required for Value at Risk calculations.
68
/// Represents a single position within a portfolio for risk assessment.
69
///
70
/// # Fields
71
/// - `symbol`: Financial instrument symbol (e.g., "AAPL", "EURUSD")
72
/// - `quantity`: Number of shares/units held (positive for long, negative for short)
73
/// - `market_value`: Current market value of the position in USD
74
/// - `average_cost`: Volume-weighted average cost basis
75
/// - `unrealized_pnl`: Mark-to-market profit/loss
76
/// - `realized_pnl`: Realized profit/loss from partial closes
77
/// - `currency`: Base currency for the position
78
/// - `timestamp`: Last update timestamp for position data
79
///
80
/// # `VaR` Calculation Usage
81
/// Used to calculate:
82
/// - Position weights in portfolio `VaR`
83
/// - Individual asset volatility contributions
84
/// - Correlation-based risk decomposition
85
/// - Concentration risk metrics
86
///
87
/// # Example
88
/// ```rust
89
/// let position = PositionInfo {
90
///     symbol: Symbol::from("AAPL"),
91
///     quantity: Quantity::from_f64(100.0)?,
92
///     market_value: Price::from_f64(17_500.0)?,
93
///     average_cost: Price::from_f64(170.0)?,
94
///     unrealized_pnl: Price::from_f64(2_500.0)?,
95
///     currency: "USD".to_string(),
96
///     timestamp: Utc::now(),
97
/// };
98
/// ```
99
#[derive(Debug, Clone)]
100
pub struct PositionInfo {
101
    /// Financial instrument symbol (e.g., "AAPL", "EURUSD")
102
    pub symbol: Symbol,
103
    /// Number of shares/units held (positive for long, negative for short)
104
    pub quantity: Quantity,
105
    /// Current market value of the position in USD
106
    pub market_value: Price,
107
    /// Volume-weighted average cost basis
108
    pub average_cost: Price,
109
    /// Mark-to-market profit/loss
110
    pub unrealized_pnl: Price,
111
    /// Realized profit/loss from partial closes
112
    pub realized_pnl: Price,
113
    /// Base currency for the position
114
    pub currency: String,
115
    /// Last update timestamp for position data
116
    pub timestamp: DateTime<Utc>,
117
}
118
119
/// **Memory-Safe Bounded Vector for Risk Calculations**
120
///
121
/// A vector with a fixed maximum capacity to prevent memory exhaustion
122
/// during intensive `VaR` calculations with large datasets.
123
///
124
/// # Type Parameters
125
/// - `T`: The type of elements stored in the vector
126
///
127
/// # Fields
128
/// - `inner`: Internal vector storage
129
/// - `capacity`: Maximum number of elements allowed
130
///
131
/// # Safety Features
132
/// - Prevents unbounded memory growth during calculations
133
/// - Provides predictable memory usage patterns
134
/// - Maintains performance with large historical datasets
135
///
136
/// # Usage
137
/// Used for storing:
138
/// - Historical price observations
139
/// - Return calculations
140
/// - Stress test scenarios
141
/// - Configuration parameters
142
///
143
/// # Example
144
/// ```rust
145
/// let mut bounded_returns = BoundedVec::<f64>::new(1000);
146
/// bounded_returns.push(0.02); // Add daily return
147
/// bounded_returns.push(0.01); // Add another return
148
/// assert_eq!(bounded_returns.len(), 2);
149
/// ```
150
#[derive(Debug, Clone)]
151
pub struct BoundedVec<T> {
152
    inner: Vec<T>,
153
    capacity: usize,
154
}
155
156
impl<T> BoundedVec<T> {
157
    /// **Create New Bounded Vector with Fixed Capacity**
158
    ///
159
    /// Initializes a bounded vector with the specified maximum capacity.
160
    /// The vector will never exceed this capacity, providing memory safety.
161
    ///
162
    /// # Arguments
163
    /// * `capacity` - Maximum number of elements allowed in the vector
164
    ///
165
    /// # Returns
166
    /// * `Self` - New bounded vector instance ready for use
167
    ///
168
    /// # Memory Management
169
    /// - Pre-allocates space for efficient insertions
170
    /// - Prevents memory exhaustion with large datasets
171
    /// - Provides predictable memory usage patterns
172
    ///
173
    /// # Example
174
    /// ```rust
175
    /// let price_history = BoundedVec::<HistoricalPrice>::new(252); // 1 year of daily prices
176
    /// let returns = BoundedVec::<f64>::new(1000); // 1000 return observations
177
    /// ```
178
    #[must_use]
179
9
    pub fn new(capacity: usize) -> Self {
180
9
        Self {
181
9
            inner: Vec::with_capacity(capacity),
182
9
            capacity,
183
9
        }
184
9
    }
185
186
    /// **Add Element to Bounded Vector**
187
    ///
188
    /// Adds an item to the vector if capacity allows.
189
    /// If the vector is at capacity, the item is silently dropped.
190
    ///
191
    /// # Arguments
192
    /// * `item` - Element to add to the vector
193
    ///
194
    /// # Behavior
195
    /// - Adds item if space is available
196
    /// - Silently ignores item if at capacity
197
    /// - Maintains memory safety by respecting capacity limits
198
    ///
199
    /// # Example
200
    /// ```rust
201
    /// let mut returns = BoundedVec::<f64>::new(3);
202
    /// returns.push(0.01);
203
    /// returns.push(0.02);
204
    /// returns.push(0.03);
205
    /// returns.push(0.04); // Silently dropped - at capacity
206
    /// assert_eq!(returns.len(), 3);
207
    /// ```
208
30
    pub fn push(&mut self, item: T) {
209
30
        if self.inner.len() < self.capacity {
210
30
            self.inner.push(item);
211
30
        
}0
212
30
    }
213
214
    /// **Get Current Number of Elements**
215
    ///
216
    /// Returns the number of elements currently stored in the bounded vector.
217
    ///
218
    /// # Returns
219
    /// * `usize` - Number of elements in the vector (0 to capacity)
220
    ///
221
    /// # Performance
222
    /// - O(1) constant time operation
223
    /// - No allocation or computation required
224
    ///
225
    /// # Example
226
    /// ```rust
227
    /// let mut scenarios = BoundedVec::<StressScenario>::new(10);
228
    /// assert_eq!(scenarios.len(), 0);
229
    /// scenarios.push(stress_scenario);
230
    /// assert_eq!(scenarios.len(), 1);
231
    /// ```
232
    #[must_use]
233
0
    pub fn len(&self) -> usize {
234
0
        self.inner.len()
235
0
    }
236
237
    /// **Create Iterator Over Elements**
238
    ///
239
    /// Returns an iterator over all elements in the bounded vector.
240
    ///
241
    /// # Returns
242
    /// * `std::slice::Iter<T>` - Iterator over vector elements
243
    ///
244
    /// # Usage
245
    /// ```rust
246
    /// let returns = BoundedVec::<f64>::new(100);
247
    /// for return_value in returns.iter() {
248
    ///     println!("Return: {}", return_value);
249
    /// }
250
    /// ```
251
0
    pub fn iter(&self) -> std::slice::Iter<'_, T> {
252
0
        self.inner.iter()
253
0
    }
254
255
    /// **Check if Vector is Empty**
256
    ///
257
    /// Returns true if the vector contains no elements.
258
    ///
259
    /// # Returns
260
    /// * `bool` - `true` if empty, `false` if contains elements
261
    ///
262
    /// # Performance
263
    /// - O(1) constant time operation
264
    ///
265
    /// # Example
266
    /// ```rust
267
    /// let scenarios = BoundedVec::<StressScenario>::new(10);
268
    /// assert!(scenarios.is_empty());
269
    /// ```
270
    #[must_use]
271
1
    pub fn is_empty(&self) -> bool {
272
1
        self.inner.is_empty()
273
1
    }
274
}
275
276
impl<T: PartialEq> PartialEq<Vec<T>> for BoundedVec<T> {
277
1
    fn eq(&self, other: &Vec<T>) -> bool {
278
1
        self.inner == *other
279
1
    }
280
}
281
282
/// **Overflow Handling Strategy for Bounded Containers**
283
///
284
/// Defines how bounded vectors should behave when they reach capacity
285
/// and new elements need to be added.
286
///
287
/// # Variants
288
/// - `DropOldest`: Remove the oldest element to make room for new ones
289
/// - `DropNewest`: Reject new elements and keep existing ones
290
/// - `Reject`: Silently ignore new elements when at capacity
291
///
292
/// # Use Cases
293
/// - **`DropOldest`**: Time series data where recent observations are more important
294
/// - **`DropNewest`**: Historical data preservation scenarios
295
/// - **Reject**: Fixed-size configuration collections
296
///
297
/// # Example
298
/// ```rust
299
/// let strategy = OverflowStrategy::DropOldest;
300
/// let mut price_history = create_bounded_vec(252, strategy); // Rolling 1-year window
301
/// ```
302
#[derive(Debug, Clone)]
303
pub enum OverflowStrategy {
304
    /// Remove oldest elements when capacity is reached
305
    DropOldest,
306
    /// Keep existing elements, reject new ones
307
    DropNewest,
308
    /// Silently ignore new elements at capacity
309
    Reject,
310
}
311
312
/// **Create Bounded Vector with Overflow Strategy**
313
///
314
/// Factory function to create a bounded vector with specified capacity and overflow behavior.
315
/// Currently implements basic capacity limiting with reject strategy.
316
///
317
/// # Type Parameters
318
/// * `T` - Type of elements to store in the vector
319
///
320
/// # Arguments
321
/// * `_capacity` - Maximum number of elements (currently used for initialization)
322
/// * `_strategy` - Overflow handling strategy (reserved for future implementation)
323
///
324
/// # Returns
325
/// * `BoundedVec<T>` - New bounded vector instance
326
///
327
/// # Current Implementation
328
/// Currently creates a bounded vector with reject strategy regardless of the
329
/// strategy parameter. Future versions will implement full strategy support.
330
///
331
/// # Example
332
/// ```rust
333
/// let price_data = create_bounded_vec::<HistoricalPrice>(252, OverflowStrategy::DropOldest);
334
/// let returns = create_bounded_vec::<f64>(1000, OverflowStrategy::Reject);
335
/// ```
336
#[must_use]
337
9
pub fn create_bounded_vec<T>(_capacity: usize, _strategy: OverflowStrategy) -> BoundedVec<T> {
338
9
    BoundedVec::new(_capacity)
339
9
}
340
use crate::operations::{price_to_decimal_safe, price_to_f64_safe, safe_divide};
341
use crate::var_calculator::monte_carlo::MonteCarloVaR;
342
// CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
343
344
/// REAL `VaR` calculation engine with multiple methodologies
345
// Infrastructure - fields will be used for VaR calculation configuration
346
#[allow(dead_code)]
347
#[derive(Debug)]
348
pub struct RealVaREngine {
349
    confidence_levels: BoundedVec<f64>,
350
    time_horizons_days: BoundedVec<usize>,
351
    min_historical_days: usize,
352
    stress_scenarios: BoundedVec<StressScenario>,
353
}
354
355
/// **Value at Risk (`VaR`) Calculation Methodology**
356
///
357
/// Defines the mathematical approach used for `VaR` calculation.
358
/// Each methodology has different strengths and is suitable for different market conditions.
359
///
360
/// # Methodologies
361
///
362
/// ## Historical Simulation
363
/// - Uses actual historical price movements
364
/// - No distributional assumptions
365
/// - Best for: Normal market conditions with sufficient history
366
/// - Pros: Real market behavior, no model assumptions
367
/// - Cons: Requires extensive historical data
368
///
369
/// ## Parametric `VaR`
370
/// - Assumes normal distribution of returns
371
/// - Uses calculated volatility and correlation
372
/// - Best for: Large portfolios with liquid assets
373
/// - Pros: Fast calculation, works with limited data
374
/// - Cons: Normal distribution assumption may not hold
375
///
376
/// ## Monte Carlo
377
/// - Simulates thousands of possible price paths
378
/// - Can model complex correlations and distributions
379
/// - Best for: Complex portfolios with derivatives
380
/// - Pros: Flexible, handles non-linear instruments
381
/// - Cons: Computationally intensive
382
///
383
/// ## Hybrid
384
/// - Combines multiple methodologies for robustness
385
/// - Weighted average of different approaches
386
/// - Best for: Production systems requiring reliability
387
/// - Pros: Reduces model risk, more robust
388
/// - Cons: More complex to implement and validate
389
///
390
/// # Selection Criteria
391
/// The optimal methodology depends on:
392
/// - Portfolio size and complexity
393
/// - Available historical data
394
/// - Computational resources
395
/// - Risk tolerance and regulatory requirements
396
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397
pub enum VaRMethodology {
398
    /// **Historical Simulation `VaR`**
399
    ///
400
    /// Uses actual historical price movements to calculate `VaR`.
401
    /// No distributional assumptions required.
402
    HistoricalSimulation,
403
    /// **Parametric `VaR`**
404
    ///
405
    /// Assumes normal distribution with calculated volatility.
406
    /// Fast but relies on normality assumption.
407
    Parametric,
408
    /// **Monte Carlo Simulation `VaR`**
409
    ///
410
    /// Uses Monte Carlo simulation with correlation modeling.
411
    /// Most flexible but computationally intensive.
412
    MonteCarlo,
413
    /// **Hybrid `VaR` Approach**
414
    ///
415
    /// Combines multiple methodologies for robust estimation.
416
    /// Weighted average of different `VaR` calculations.
417
    Hybrid,
418
}
419
420
/// Comprehensive `VaR` results with ALL real calculations
421
#[derive(Debug, Clone, Serialize, Deserialize)]
422
pub struct ComprehensiveVaRResult {
423
    pub portfolio_id: String,
424
    pub methodology_used: String,
425
426
    // Core VaR metrics (REAL calculations)
427
    pub var_1d_95: Price,  // 1-day VaR at 95% confidence
428
    pub var_1d_99: Price,  // 1-day VaR at 99% confidence
429
    pub var_10d_95: Price, // 10-day VaR at 95% confidence
430
    pub var_10d_99: Price, // 10-day VaR at 99% confidence
431
432
    // Expected Shortfall (Conditional VaR)
433
    pub expected_shortfall_95: Price,
434
    pub expected_shortfall_99: Price,
435
436
    // Risk decomposition
437
    pub component_var: HashMap<Symbol, Price>,
438
    pub marginal_var: HashMap<Symbol, Price>,
439
    pub correlation_contribution: HashMap<Symbol, Price>,
440
441
    // Stress testing results
442
    pub stress_test_results: Vec<StressTestResult>,
443
444
    // Model validation metrics
445
    pub model_confidence: f64,
446
    pub historical_accuracy: Option<f64>,
447
    pub portfolio_volatility: Price,
448
    pub concentration_risk: Price,
449
450
    // Calculation metadata
451
    pub calculation_method: String,
452
    pub data_quality_score: f64,
453
    pub num_observations: usize,
454
    pub calculated_at: DateTime<Utc>,
455
}
456
457
/// Stress test scenario definition
458
#[derive(Debug, Clone, Serialize, Deserialize)]
459
pub struct StressScenario {
460
    pub name: String,
461
    pub description: String,
462
    pub asset_shocks: HashMap<String, f64>, // Symbol -> percentage shock
463
    pub correlation_multiplier: f64,
464
    pub probability_estimate: Option<f64>,
465
}
466
467
/// Stress test result
468
#[derive(Debug, Clone, Serialize, Deserialize)]
469
pub struct StressTestResult {
470
    pub scenario_name: String,
471
    pub portfolio_loss: Price,
472
    pub largest_contributor: String,
473
    pub largest_contribution: Price,
474
    pub diversification_benefit: Price,
475
}
476
477
/// Circuit breaker trigger conditions
478
#[derive(Debug, Clone, Serialize, Deserialize)]
479
pub struct CircuitBreakerCondition {
480
    pub condition_name: String,
481
    pub current_value: Price,
482
    pub threshold: Price,
483
    pub severity: String, // "WARNING", "CRITICAL", "EMERGENCY"
484
    pub should_trigger: bool,
485
}
486
487
impl Default for RealVaREngine {
488
0
    fn default() -> Self {
489
0
        Self::new()
490
0
    }
491
}
492
493
impl RealVaREngine {
494
    /// Create new REAL `VaR` engine with production-grade parameters
495
    #[must_use]
496
3
    pub fn new() -> Self {
497
        // Create bounded collections for memory safety
498
3
        let mut confidence_levels = create_bounded_vec(10, OverflowStrategy::Reject);
499
3
        let mut time_horizons_days = create_bounded_vec(10, OverflowStrategy::Reject);
500
3
        let mut stress_scenarios = create_bounded_vec(50, OverflowStrategy::DropOldest);
501
502
        // Initialize with standard risk management values
503
12
        for 
level9
in [0.95, 0.99, 0.999] {
504
9
            confidence_levels.push(level);
505
9
        }
506
507
12
        for 
horizon9
in [1, 10, 22] {
508
9
            time_horizons_days.push(horizon);
509
9
        }
510
511
        // Initialize stress scenarios
512
12
        for scenario in 
Self::create_default_stress_scenarios3
() {
513
12
            stress_scenarios.push(scenario);
514
12
        }
515
516
3
        Self {
517
3
            confidence_levels,
518
3
            time_horizons_days,
519
3
            min_historical_days: 252, // Minimum 1 year of data for reliable estimates
520
3
            stress_scenarios,
521
3
        }
522
3
    }
523
524
    /// Create stress scenarios based on historical market crashes
525
3
    fn create_default_stress_scenarios() -> Vec<StressScenario> {
526
3
        vec![
527
3
            StressScenario {
528
3
                name: "2008_Financial_Crisis".to_owned(),
529
3
                description: "Market crash similar to 2008 financial crisis".to_owned(),
530
3
                asset_shocks: [
531
3
                    ("EQUITIES".to_owned(), -0.40), // 40% equity drop
532
3
                    ("CREDIT".to_owned(), -0.30),   // 30% credit spread widening
533
3
                    ("VOLATILITY".to_owned(), 2.5), // 250% volatility increase
534
3
                ]
535
3
                .iter()
536
3
                .cloned()
537
3
                .collect(),
538
3
                correlation_multiplier: 1.5, // Correlations increase during crisis
539
3
                probability_estimate: Some(0.01), // ~1% annual probability
540
3
            },
541
3
            StressScenario {
542
3
                name: "COVID_Pandemic".to_owned(),
543
3
                description: "Market shock similar to March 2020".to_owned(),
544
3
                asset_shocks: [
545
3
                    ("EQUITIES".to_owned(), -0.35),
546
3
                    ("OIL".to_owned(), -0.60),  // Oil price collapse
547
3
                    ("BONDS".to_owned(), 0.15), // Flight to quality
548
3
                ]
549
3
                .iter()
550
3
                .cloned()
551
3
                .collect(),
552
3
                correlation_multiplier: 1.3,
553
3
                probability_estimate: Some(0.02),
554
3
            },
555
3
            StressScenario {
556
3
                name: "Flash_Crash".to_owned(),
557
3
                description: "Intraday liquidity crisis".to_owned(),
558
3
                asset_shocks: [
559
3
                    ("EQUITIES".to_owned(), -0.15),
560
3
                    ("VOLATILITY".to_owned(), 3.0),
561
3
                ]
562
3
                .iter()
563
3
                .cloned()
564
3
                .collect(),
565
3
                correlation_multiplier: 2.0, // Very high correlations during flash crashes
566
3
                probability_estimate: Some(0.05),
567
3
            },
568
3
            StressScenario {
569
3
                name: "Inflation_Shock".to_owned(),
570
3
                description: "Unexpected inflation surge".to_owned(),
571
3
                asset_shocks: [
572
3
                    ("BONDS".to_owned(), -0.25),
573
3
                    ("REAL_ESTATE".to_owned(), -0.20),
574
3
                    ("COMMODITIES".to_owned(), 0.30),
575
3
                ]
576
3
                .iter()
577
3
                .cloned()
578
3
                .collect(),
579
3
                correlation_multiplier: 1.2,
580
3
                probability_estimate: Some(0.03),
581
3
            },
582
        ]
583
3
    }
584
585
    /// Calculate comprehensive `VaR` using best methodology for given data
586
    /// NO MORE MOCK VALUES - all calculations use real mathematical models
587
0
    pub async fn calculate_comprehensive_var(
588
0
        &self,
589
0
        portfolio_id: &str,
590
0
        positions: &HashMap<Symbol, PositionInfo>,
591
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
592
0
    ) -> RiskResult<ComprehensiveVaRResult> {
593
        // Validate data quality first
594
0
        let data_quality = self.assess_data_quality(historical_prices)?;
595
596
0
        if data_quality < 0.6 {
597
0
            return Err(RiskError::Calculation {
598
0
                operation: "var_calculation".to_owned(),
599
0
                reason: format!(
600
0
                    "Data quality too low: {:.1}% - minimum 60% required",
601
0
                    data_quality * 100.0
602
0
                ),
603
0
            });
604
0
        }
605
606
        // Select best methodology based on data characteristics
607
0
        let methodology = self.select_optimal_methodology(positions, historical_prices)?;
608
609
        // Calculate VaR using selected methodology
610
0
        let var_results = match methodology {
611
            VaRMethodology::HistoricalSimulation => {
612
0
                self.calculate_historical_simulation_var(portfolio_id, positions, historical_prices)
613
0
                    .await?
614
            },
615
            VaRMethodology::Parametric => {
616
0
                self.calculate_parametric_var(portfolio_id, positions, historical_prices)
617
0
                    .await?
618
            },
619
            VaRMethodology::MonteCarlo => {
620
0
                self.calculate_monte_carlo_var(portfolio_id, positions, historical_prices)
621
0
                    .await?
622
            },
623
            VaRMethodology::Hybrid => {
624
0
                self.calculate_hybrid_var(portfolio_id, positions, historical_prices)
625
0
                    .await?
626
            },
627
        };
628
629
        // Run stress tests
630
0
        let stress_results = self.run_stress_tests(positions, historical_prices).await?;
631
632
        // Calculate risk decomposition
633
0
        let (component_var, marginal_var, correlation_contribution) = self
634
0
            .calculate_risk_decomposition(positions, historical_prices)
635
0
            .await?;
636
637
        // Calculate concentration risk
638
0
        let concentration_risk = self.calculate_concentration_risk(positions)?;
639
640
        // Model validation
641
0
        let model_confidence = self.calculate_model_confidence(&methodology, data_quality);
642
0
        let historical_accuracy = self.backtest_accuracy(historical_prices).await?;
643
644
0
        Ok(ComprehensiveVaRResult {
645
0
            portfolio_id: portfolio_id.to_owned(),
646
0
            methodology_used: format!("{methodology:?}"),
647
0
            var_1d_95: var_results.var_1d_95,
648
0
            var_1d_99: var_results.var_1d_99,
649
0
            var_10d_95: var_results.var_10d_95,
650
0
            var_10d_99: var_results.var_10d_99,
651
0
            expected_shortfall_95: var_results.expected_shortfall_95,
652
0
            expected_shortfall_99: var_results.expected_shortfall_99,
653
0
            component_var,
654
0
            marginal_var,
655
0
            correlation_contribution,
656
0
            stress_test_results: stress_results,
657
0
            model_confidence,
658
0
            historical_accuracy,
659
0
            portfolio_volatility: var_results.portfolio_volatility,
660
0
            concentration_risk: Price::from_decimal(concentration_risk),
661
0
            calculation_method: format!("RealVaREngine::{methodology:?}"),
662
0
            data_quality_score: data_quality,
663
0
            num_observations: historical_prices.values().map(Vec::len).max().unwrap_or(0),
664
0
            calculated_at: Utc::now(),
665
0
        })
666
0
    }
667
668
    /// Calculate REAL Historical Simulation `VaR` - uses actual historical price movements
669
0
    async fn calculate_historical_simulation_var(
670
0
        &self,
671
0
        _portfolio_id: &str,
672
0
        positions: &HashMap<Symbol, PositionInfo>,
673
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
674
0
    ) -> RiskResult<VaRCalculationResult> {
675
0
        let mut portfolio_returns = Vec::new();
676
0
        let min_length = historical_prices.values().map(Vec::len).min().unwrap_or(0);
677
678
0
        if min_length < self.min_historical_days {
679
0
            return Err(RiskError::Calculation {
680
0
                operation: "historical_simulation_var".to_owned(),
681
0
                reason: format!(
682
0
                    "Insufficient historical data: {} days available, {} required",
683
0
                    min_length, self.min_historical_days
684
0
                ),
685
0
            });
686
0
        }
687
688
        // Calculate historical portfolio returns for each day
689
0
        for day_index in 1..min_length {
690
0
            let mut daily_portfolio_return = 0.0;
691
0
            let mut total_portfolio_value = 0.0;
692
693
0
            for (symbol, position) in positions {
694
0
                if let Some(prices) = historical_prices.get(&symbol.clone()) {
695
0
                    let prev_price = match prices.get(day_index - 1) {
696
0
                        Some(price_data) => {
697
0
                            price_to_f64_safe(price_data.price, "previous price conversion")
698
0
                                .unwrap_or(0.0)
699
                        },
700
0
                        None => continue, // Skip if price data unavailable
701
                    };
702
0
                    let curr_price = match prices.get(day_index) {
703
0
                        Some(price_data) => {
704
0
                            price_to_f64_safe(price_data.price, "current price conversion")
705
0
                                .unwrap_or(0.0)
706
                        },
707
0
                        None => continue, // Skip if price data unavailable
708
                    };
709
710
0
                    if prev_price > 0.0 {
711
0
                        let asset_return = (curr_price - prev_price) / prev_price;
712
0
                        let position_value = position.market_value.to_f64();
713
0
714
0
                        daily_portfolio_return += asset_return * position_value;
715
0
                        total_portfolio_value += position_value;
716
0
                    }
717
0
                }
718
            }
719
720
0
            if total_portfolio_value > 0.0 {
721
0
                portfolio_returns.push(daily_portfolio_return / total_portfolio_value);
722
0
            }
723
        }
724
725
0
        if portfolio_returns.is_empty() {
726
0
            return Err(RiskError::Calculation {
727
0
                operation: "historical_simulation_var".to_owned(),
728
0
                reason: "No valid portfolio returns could be calculated".to_owned(),
729
0
            });
730
0
        }
731
732
        // Sort returns (worst first)
733
        // FAIL-SAFE: Handle NaN or invalid values in portfolio returns
734
0
        portfolio_returns.sort_by(|a, b| {
735
0
            if let Some(ordering) = a.partial_cmp(b) { ordering } else {
736
                // Log critical data quality issue but continue with conservative ordering
737
0
                error!("\u{1f6a8} CRITICAL: Invalid portfolio return values detected (NaN/Infinity) - using conservative ordering");
738
0
                std::cmp::Ordering::Equal // Treat invalid values as equal to prevent crash
739
            }
740
0
        });
741
742
        // Calculate VaR at different confidence levels
743
0
        let total_value: Price = positions
744
0
            .values()
745
0
            .map(|pos| pos.market_value)
746
0
            .fold(Price::ZERO, |acc, val| acc + val);
747
748
0
        let var_1d_95 = self.calculate_var_from_returns(&portfolio_returns, 0.95, total_value)?;
749
0
        let var_1d_99 = self.calculate_var_from_returns(&portfolio_returns, 0.99, total_value)?;
750
751
        // Scale to longer time horizons using square root rule
752
0
        let var_10d_95 =
753
0
            var_1d_95 * FromPrimitive::from_f64(10.0_f64.sqrt()).unwrap_or(Decimal::ONE);
754
0
        let var_10d_99 =
755
0
            var_1d_99 * FromPrimitive::from_f64(10.0_f64.sqrt()).unwrap_or(Decimal::ONE);
756
757
        // Calculate Expected Shortfall (average of tail losses)
758
0
        let es_95 = self.calculate_expected_shortfall(&portfolio_returns, 0.95, total_value)?;
759
0
        let es_99 = self.calculate_expected_shortfall(&portfolio_returns, 0.99, total_value)?;
760
761
        // Calculate portfolio volatility
762
0
        let mean_return = portfolio_returns.iter().sum::<f64>() / portfolio_returns.len() as f64;
763
0
        let variance = portfolio_returns
764
0
            .iter()
765
0
            .map(|r| (r - mean_return).powi(2))
766
0
            .sum::<f64>()
767
0
            / (portfolio_returns.len() - 1) as f64;
768
0
        let volatility = FromPrimitive::from_f64(variance.sqrt() * total_value.to_f64())
769
0
            .unwrap_or(Decimal::ZERO);
770
771
0
        Ok(VaRCalculationResult {
772
0
            var_1d_95: Price::from_decimal(var_1d_95),
773
0
            var_1d_99: Price::from_decimal(var_1d_99),
774
0
            var_10d_95: Price::from_decimal(var_10d_95),
775
0
            var_10d_99: Price::from_decimal(var_10d_99),
776
0
            expected_shortfall_95: Price::from_decimal(es_95),
777
0
            expected_shortfall_99: Price::from_decimal(es_99),
778
0
            portfolio_volatility: Price::from_decimal(volatility),
779
0
        })
780
0
    }
781
782
    /// Calculate `VaR` from sorted return distribution
783
0
    fn calculate_var_from_returns(
784
0
        &self,
785
0
        sorted_returns: &[f64],
786
0
        confidence_level: f64,
787
0
        portfolio_value: Price,
788
0
    ) -> RiskResult<Decimal> {
789
0
        let percentile_index = ((1.0 - confidence_level) * sorted_returns.len() as f64) as usize;
790
0
        let var_return = sorted_returns.get(percentile_index).unwrap_or(&0.0);
791
792
0
        let var_amount = -*var_return * portfolio_value.to_f64();
793
794
0
        Ok(FromPrimitive::from_f64(var_amount.max(0.0)).unwrap_or(Decimal::ZERO))
795
0
    }
796
797
    /// Calculate Expected Shortfall (Conditional `VaR`)
798
0
    fn calculate_expected_shortfall(
799
0
        &self,
800
0
        sorted_returns: &[f64],
801
0
        confidence_level: f64,
802
0
        portfolio_value: Price,
803
0
    ) -> RiskResult<Decimal> {
804
0
        let cutoff_index = ((1.0 - confidence_level) * sorted_returns.len() as f64) as usize;
805
806
0
        if cutoff_index == 0 {
807
0
            return Ok(Decimal::ZERO);
808
0
        }
809
810
0
        let tail_returns: Vec<f64> = sorted_returns[..=cutoff_index].to_vec();
811
0
        let mean_tail_loss = tail_returns.iter().sum::<f64>() / tail_returns.len() as f64;
812
813
0
        let es_amount = -mean_tail_loss * portfolio_value.to_f64();
814
815
0
        Ok(FromPrimitive::from_f64(es_amount.max(0.0)).unwrap_or(Decimal::ZERO))
816
0
    }
817
818
    /// Check circuit breaker conditions based on REAL risk metrics
819
    /// 2% daily loss limit as specified in requirements
820
    #[must_use]
821
1
    pub fn check_circuit_breaker_conditions(
822
1
        &self,
823
1
        var_results: &ComprehensiveVaRResult,
824
1
        current_pnl: Price,
825
1
        portfolio_value: Price,
826
1
    ) -> Vec<CircuitBreakerCondition> {
827
1
        let mut conditions = Vec::new();
828
829
        // 2% daily loss limit (CRITICAL REQUIREMENT)
830
        // Note: current_pnl represents loss magnitude (positive value), not signed P&L
831
1
        let _daily_loss_threshold = portfolio_value
832
1
            * Price::from_decimal(FromPrimitive::from_f64(0.02).unwrap_or(Decimal::ZERO));
833
1
        let current_loss_pct = if portfolio_value > Price::from_decimal(Decimal::ZERO) {
834
1
            Price::from_f64(current_pnl.to_f64() / portfolio_value.to_f64()).unwrap_or(Price::ZERO)
835
        } else {
836
0
            Price::from_decimal(Decimal::ZERO)
837
        };
838
839
1
        conditions.push(CircuitBreakerCondition {
840
1
            condition_name: "Daily_Loss_Limit".to_owned(),
841
1
            current_value: current_loss_pct,
842
1
            threshold: Price::from_decimal(FromPrimitive::from_f64(0.02).unwrap_or(Decimal::ZERO)),
843
1
            severity: if current_loss_pct
844
1
                >= Price::from_decimal(FromPrimitive::from_f64(0.02).unwrap_or(Decimal::ZERO))
845
            {
846
1
                "CRITICAL".to_owned()
847
0
            } else if current_loss_pct
848
0
                >= Price::from_decimal(FromPrimitive::from_f64(0.015).unwrap_or(Decimal::ZERO))
849
            {
850
0
                "HIGH".to_owned()
851
0
            } else if current_loss_pct
852
0
                >= Price::from_decimal(FromPrimitive::from_f64(0.01).unwrap_or(Decimal::ZERO))
853
            {
854
0
                "MEDIUM".to_owned()
855
            } else {
856
0
                "LOW".to_owned()
857
            },
858
1
            should_trigger: current_loss_pct
859
1
                >= Price::from_decimal(Decimal::try_from(0.02).unwrap_or(Decimal::ZERO)),
860
        });
861
862
        // VaR breach condition
863
1
        let var_breach_ratio = if var_results.var_1d_95 > Price::from_decimal(Decimal::ZERO) {
864
1
            Price::from_f64(current_pnl.to_f64() / var_results.var_1d_95.to_f64())
865
1
                .unwrap_or(Price::ZERO)
866
        } else {
867
0
            Price::from_decimal(Decimal::ZERO)
868
        };
869
870
1
        conditions.push(CircuitBreakerCondition {
871
1
            condition_name: "VaR_Breach".to_owned(),
872
1
            current_value: var_breach_ratio,
873
1
            threshold: Price::from_decimal(Decimal::ONE), // 100% of VaR
874
1
            severity: if var_breach_ratio >= Price::from_decimal(Decimal::from(2)) {
875
1
                "CRITICAL".to_owned()
876
0
            } else if var_breach_ratio
877
0
                >= Price::from_decimal(Decimal::try_from(1.5).unwrap_or(Decimal::ZERO))
878
            {
879
0
                "HIGH".to_owned()
880
0
            } else if var_breach_ratio >= Price::from_decimal(Decimal::ONE) {
881
0
                "MEDIUM".to_owned()
882
            } else {
883
0
                "LOW".to_owned()
884
            },
885
1
            should_trigger: var_breach_ratio >= Price::from_decimal(Decimal::ONE),
886
        });
887
888
        // Concentration risk condition
889
1
        conditions.push(CircuitBreakerCondition {
890
1
            condition_name: "Concentration_Risk".to_owned(),
891
1
            current_value: var_results.concentration_risk,
892
1
            threshold: Price::from_decimal(Decimal::try_from(0.25).unwrap_or(Decimal::ZERO)), // 25% max concentration
893
1
            severity: if var_results.concentration_risk
894
1
                >= Price::from_decimal(Decimal::try_from(0.4).unwrap_or(Decimal::ZERO))
895
            {
896
0
                "CRITICAL".to_owned()
897
1
            } else if var_results.concentration_risk
898
1
                >= Price::from_decimal(Decimal::try_from(0.3).unwrap_or(Decimal::ZERO))
899
            {
900
0
                "HIGH".to_owned()
901
1
            } else if var_results.concentration_risk
902
1
                >= Price::from_decimal(Decimal::try_from(0.25).unwrap_or(Decimal::ZERO))
903
            {
904
0
                "MEDIUM".to_owned()
905
            } else {
906
1
                "LOW".to_owned()
907
            },
908
1
            should_trigger: var_results.concentration_risk
909
1
                >= Price::from_decimal(Decimal::try_from(0.25).unwrap_or(Decimal::ZERO)),
910
        });
911
912
1
        conditions
913
1
    }
914
915
0
    async fn calculate_parametric_var(
916
0
        &self,
917
0
        _portfolio_id: &str,
918
0
        positions: &HashMap<Symbol, PositionInfo>,
919
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
920
0
    ) -> RiskResult<VaRCalculationResult> {
921
        // Parametric VaR implementation using normal distribution assumption
922
0
        if positions.is_empty() {
923
0
            return Ok(VaRCalculationResult::zero());
924
0
        }
925
926
0
        let mut portfolio_value = Decimal::ZERO;
927
0
        let mut weighted_returns = Vec::new();
928
0
        let mut portfolio_volatility: f64 = 0.0;
929
930
        // Calculate portfolio value and gather returns
931
0
        for (symbol, position) in positions {
932
0
            if let Some(prices) = historical_prices.get(&symbol.clone()) {
933
0
                if prices.len() < 2 {
934
0
                    continue; // Skip assets with insufficient price history
935
0
                }
936
937
                // Calculate position value using safe operations
938
0
                let current_price = prices
939
0
                    .last()
940
0
                    .ok_or_else(|| RiskError::Calculation {
941
0
                        operation: "historical_price_access".to_owned(),
942
0
                        reason: "No historical prices available".to_owned(),
943
0
                    })?
944
                    .price;
945
946
                // Safe conversions with proper error handling
947
0
                let current_price_decimal =
948
0
                    price_to_decimal_safe(current_price, "position_value_calculation")?;
949
0
                let quantity_decimal =
950
0
                    position
951
0
                        .quantity
952
0
                        .to_decimal()
953
0
                        .map_err(|e| RiskError::TypeConversion {
954
0
                            from_type: "Quantity".to_owned(),
955
0
                            to_type: "Decimal".to_owned(),
956
0
                            reason: format!("Failed to convert quantity: {e:?}"),
957
0
                        })?;
958
959
0
                let position_value = current_price_decimal * quantity_decimal;
960
0
                portfolio_value += position_value;
961
962
                // Calculate historical returns with safe operations
963
0
                let returns: Vec<f64> = prices
964
0
                    .windows(2)
965
0
                    .filter_map(|window| {
966
0
                        let prev_price = window[0].price.to_f64();
967
0
                        let curr_price = window[1].price.to_f64();
968
0
                        (prev_price > 0.0 && curr_price > 0.0)
969
0
                            .then(|| (curr_price - prev_price) / prev_price)
970
0
                    })
971
0
                    .collect();
972
973
0
                if !returns.is_empty() {
974
                    // Calculate mean and standard deviation
975
0
                    let mean_return = returns.iter().sum::<f64>() / returns.len() as f64;
976
0
                    let variance = returns
977
0
                        .iter()
978
0
                        .map(|r| (r - mean_return).powi(2))
979
0
                        .sum::<f64>()
980
0
                        / (returns.len() - 1) as f64;
981
0
                    let std_dev = variance.sqrt();
982
983
                    // Weight by position size using safe operations
984
0
                    let weight = if portfolio_value > Decimal::ZERO {
985
0
                        let weight_decimal = safe_divide(
986
0
                            Price::from_decimal(position_value),
987
0
                            Price::from_decimal(portfolio_value),
988
0
                            "weight_calculation",
989
0
                        )?;
990
0
                        price_to_f64_safe(Price::from_decimal(weight_decimal), "weight_to_f64")?
991
                    } else {
992
0
                        0.0
993
                    };
994
0
                    weighted_returns.extend(returns.iter().map(|r| r * weight));
995
0
                    portfolio_volatility += (weight * std_dev).powi(2);
996
0
                }
997
0
            }
998
        }
999
1000
0
        portfolio_volatility = portfolio_volatility.sqrt();
1001
1002
0
        if weighted_returns.is_empty() {
1003
0
            return Ok(VaRCalculationResult::zero());
1004
0
        }
1005
1006
        // Calculate parametric VaR using normal distribution
1007
0
        let _mean_return = weighted_returns.iter().sum::<f64>() / weighted_returns.len() as f64;
1008
1009
        // Z-scores for confidence levels (assuming normal distribution)
1010
0
        let z_95 = 1.645; // 95% confidence (one-tailed)
1011
0
        let z_99 = 2.326; // 99% confidence (one-tailed)
1012
1013
0
        let portfolio_value_f64 = portfolio_value.to_f64().unwrap_or(0.0);
1014
1015
        // 1-day VaR calculations
1016
0
        let var_1d_95 = Decimal::try_from(portfolio_value_f64 * portfolio_volatility * z_95)
1017
0
            .unwrap_or(Decimal::ZERO);
1018
1019
0
        let var_1d_99 = Decimal::try_from(portfolio_value_f64 * portfolio_volatility * z_99)
1020
0
            .unwrap_or(Decimal::ZERO);
1021
1022
        // Time scaling for 10-day VaR
1023
0
        let time_scaling_10d = 10.0_f64.sqrt();
1024
0
        let var_10d_95 = var_1d_95 * Decimal::try_from(time_scaling_10d).unwrap_or(Decimal::ONE);
1025
0
        let var_10d_99 = var_1d_99 * Decimal::try_from(time_scaling_10d).unwrap_or(Decimal::ONE);
1026
        // Expected Shortfall (simplified estimation)
1027
0
        let es_multiplier_95 = 1.28; // Approximation for normal distribution
1028
0
        let es_multiplier_99 = 1.15;
1029
1030
0
        let expected_shortfall_95 =
1031
0
            var_1d_95 * Decimal::try_from(es_multiplier_95).unwrap_or(Decimal::ONE);
1032
0
        let expected_shortfall_99 =
1033
0
            var_1d_99 * Decimal::try_from(es_multiplier_99).unwrap_or(Decimal::ONE);
1034
1035
0
        Ok(VaRCalculationResult {
1036
0
            var_1d_95: Price::from_decimal(var_1d_95.abs()),
1037
0
            var_1d_99: Price::from_decimal(var_1d_99.abs()),
1038
0
            var_10d_95: Price::from_decimal(var_10d_95.abs()),
1039
0
            var_10d_99: Price::from_decimal(var_10d_99.abs()),
1040
0
            expected_shortfall_95: Price::from_decimal(expected_shortfall_95.abs()),
1041
0
            expected_shortfall_99: Price::from_decimal(expected_shortfall_99.abs()),
1042
0
            portfolio_volatility: Price::from_f64(portfolio_volatility).unwrap_or(Price::ZERO),
1043
0
        })
1044
0
    }
1045
1046
0
    async fn calculate_monte_carlo_var(
1047
0
        &self,
1048
0
        portfolio_id: &str,
1049
0
        positions: &HashMap<Symbol, PositionInfo>,
1050
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1051
0
    ) -> RiskResult<VaRCalculationResult> {
1052
0
        let mc_calculator = MonteCarloVaR::high_precision();
1053
0
        let mc_result =
1054
0
            mc_calculator.calculate_portfolio_var(portfolio_id, positions, historical_prices)?;
1055
1056
        Ok(VaRCalculationResult {
1057
0
            var_1d_95: mc_result.var_1d,
1058
0
            var_1d_99: (mc_result.var_1d
1059
0
                * Price::from_decimal(Decimal::try_from(1.3).unwrap_or(Decimal::ONE)))
1060
0
            .map_err(|e| {
1061
0
                RiskError::CalculationError(format!("Failed to scale VaR 1d 99: {e:?}"))
1062
0
            })?,
1063
0
            var_10d_95: mc_result.var_10d,
1064
0
            var_10d_99: (mc_result.var_10d
1065
0
                * Price::from_decimal(Decimal::try_from(1.3).unwrap_or(Decimal::ONE)))
1066
0
            .map_err(|e| {
1067
0
                RiskError::CalculationError(format!("Failed to scale VaR 10d 99: {e:?}"))
1068
0
            })?,
1069
0
            expected_shortfall_95: mc_result.expected_shortfall,
1070
0
            expected_shortfall_99: (mc_result.expected_shortfall
1071
0
                * Price::from_decimal(Decimal::try_from(1.2).unwrap_or(Decimal::ONE)))
1072
0
            .map_err(|e| RiskError::CalculationError(format!("Failed to scale ES 99: {e:?}")))?,
1073
0
            portfolio_volatility: mc_result.volatility,
1074
        })
1075
0
    }
1076
1077
0
    async fn calculate_hybrid_var(
1078
0
        &self,
1079
0
        portfolio_id: &str,
1080
0
        positions: &HashMap<Symbol, PositionInfo>,
1081
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1082
0
    ) -> RiskResult<VaRCalculationResult> {
1083
        // Hybrid VaR combines multiple methodologies for more robust estimation
1084
1085
        // Get results from different methods
1086
0
        let historical_result = self
1087
0
            .calculate_historical_simulation_var(portfolio_id, positions, historical_prices)
1088
0
            .await?;
1089
0
        let parametric_result = self
1090
0
            .calculate_parametric_var(portfolio_id, positions, historical_prices)
1091
0
            .await?;
1092
0
        let monte_carlo_result = self
1093
0
            .calculate_monte_carlo_var(portfolio_id, positions, historical_prices)
1094
0
            .await?;
1095
1096
        // Weight the results (can be adjusted based on market conditions)
1097
0
        let historical_weight = Price::from_f64(0.4).unwrap_or(Price::ZERO);
1098
0
        let parametric_weight = Price::from_f64(0.3).unwrap_or(Price::ZERO);
1099
0
        let monte_carlo_weight = Price::from_f64(0.3).unwrap_or(Price::ZERO);
1100
1101
        // Weighted average of VaR estimates - handle Results properly
1102
0
        let var_1d_95 = {
1103
0
            let hist_weighted = (historical_result.var_1d_95 * historical_weight).map_err(|e| {
1104
0
                RiskError::CalculationError(format!("Historical weight calculation failed: {e:?}"))
1105
0
            })?;
1106
0
            let param_weighted =
1107
0
                (parametric_result.var_1d_95 * parametric_weight).map_err(|e| {
1108
0
                    RiskError::CalculationError(format!(
1109
0
                        "Parametric weight calculation failed: {e:?}"
1110
0
                    ))
1111
0
                })?;
1112
0
            let mc_weighted = (monte_carlo_result.var_1d_95 * monte_carlo_weight).map_err(|e| {
1113
0
                RiskError::CalculationError(format!("Monte Carlo weight calculation failed: {e:?}"))
1114
0
            })?;
1115
0
            hist_weighted + param_weighted + mc_weighted
1116
        };
1117
1118
0
        let var_1d_99 = {
1119
0
            let hist_weighted = (historical_result.var_1d_99 * historical_weight).map_err(|e| {
1120
0
                RiskError::CalculationError(format!("Historical weight calculation failed: {e:?}"))
1121
0
            })?;
1122
0
            let param_weighted =
1123
0
                (parametric_result.var_1d_99 * parametric_weight).map_err(|e| {
1124
0
                    RiskError::CalculationError(format!(
1125
0
                        "Parametric weight calculation failed: {e:?}"
1126
0
                    ))
1127
0
                })?;
1128
0
            let mc_weighted = (monte_carlo_result.var_1d_99 * monte_carlo_weight).map_err(|e| {
1129
0
                RiskError::CalculationError(format!("Monte Carlo weight calculation failed: {e:?}"))
1130
0
            })?;
1131
0
            hist_weighted + param_weighted + mc_weighted
1132
        };
1133
1134
0
        let var_10d_95 = {
1135
0
            let hist_weighted =
1136
0
                (historical_result.var_10d_95 * historical_weight).map_err(|e| {
1137
0
                    RiskError::CalculationError(format!(
1138
0
                        "Historical weight calculation failed: {e:?}"
1139
0
                    ))
1140
0
                })?;
1141
0
            let param_weighted =
1142
0
                (parametric_result.var_10d_95 * parametric_weight).map_err(|e| {
1143
0
                    RiskError::CalculationError(format!(
1144
0
                        "Parametric weight calculation failed: {e:?}"
1145
0
                    ))
1146
0
                })?;
1147
0
            let mc_weighted =
1148
0
                (monte_carlo_result.var_10d_95 * monte_carlo_weight).map_err(|e| {
1149
0
                    RiskError::CalculationError(format!(
1150
0
                        "Monte Carlo weight calculation failed: {e:?}"
1151
0
                    ))
1152
0
                })?;
1153
0
            hist_weighted + param_weighted + mc_weighted
1154
        };
1155
1156
0
        let var_10d_99 = {
1157
0
            let hist_weighted =
1158
0
                (historical_result.var_10d_99 * historical_weight).map_err(|e| {
1159
0
                    RiskError::CalculationError(format!(
1160
0
                        "Historical weight calculation failed: {e:?}"
1161
0
                    ))
1162
0
                })?;
1163
0
            let param_weighted =
1164
0
                (parametric_result.var_10d_99 * parametric_weight).map_err(|e| {
1165
0
                    RiskError::CalculationError(format!(
1166
0
                        "Parametric weight calculation failed: {e:?}"
1167
0
                    ))
1168
0
                })?;
1169
0
            let mc_weighted =
1170
0
                (monte_carlo_result.var_10d_99 * monte_carlo_weight).map_err(|e| {
1171
0
                    RiskError::CalculationError(format!(
1172
0
                        "Monte Carlo weight calculation failed: {e:?}"
1173
0
                    ))
1174
0
                })?;
1175
0
            hist_weighted + param_weighted + mc_weighted
1176
        };
1177
1178
0
        let expected_shortfall_95 = {
1179
0
            let hist_weighted = (historical_result.expected_shortfall_95 * historical_weight)
1180
0
                .map_err(|e| {
1181
0
                    RiskError::CalculationError(format!(
1182
0
                        "Historical weight calculation failed: {e:?}"
1183
0
                    ))
1184
0
                })?;
1185
0
            let param_weighted = (parametric_result.expected_shortfall_95 * parametric_weight)
1186
0
                .map_err(|e| {
1187
0
                    RiskError::CalculationError(format!(
1188
0
                        "Parametric weight calculation failed: {e:?}"
1189
0
                    ))
1190
0
                })?;
1191
0
            let mc_weighted = (monte_carlo_result.expected_shortfall_95 * monte_carlo_weight)
1192
0
                .map_err(|e| {
1193
0
                    RiskError::CalculationError(format!(
1194
0
                        "Monte Carlo weight calculation failed: {e:?}"
1195
0
                    ))
1196
0
                })?;
1197
0
            hist_weighted + param_weighted + mc_weighted
1198
        };
1199
1200
0
        let expected_shortfall_99 = {
1201
0
            let hist_weighted = (historical_result.expected_shortfall_99 * historical_weight)
1202
0
                .map_err(|e| {
1203
0
                    RiskError::CalculationError(format!(
1204
0
                        "Historical weight calculation failed: {e:?}"
1205
0
                    ))
1206
0
                })?;
1207
0
            let param_weighted = (parametric_result.expected_shortfall_99 * parametric_weight)
1208
0
                .map_err(|e| {
1209
0
                    RiskError::CalculationError(format!(
1210
0
                        "Parametric weight calculation failed: {e:?}"
1211
0
                    ))
1212
0
                })?;
1213
0
            let mc_weighted = (monte_carlo_result.expected_shortfall_99 * monte_carlo_weight)
1214
0
                .map_err(|e| {
1215
0
                    RiskError::CalculationError(format!(
1216
0
                        "Monte Carlo weight calculation failed: {e:?}"
1217
0
                    ))
1218
0
                })?;
1219
0
            hist_weighted + param_weighted + mc_weighted
1220
        };
1221
1222
0
        let portfolio_volatility = {
1223
0
            let hist_weighted = (historical_result.portfolio_volatility * historical_weight)
1224
0
                .map_err(|e| {
1225
0
                    RiskError::CalculationError(format!(
1226
0
                        "Historical weight calculation failed: {e:?}"
1227
0
                    ))
1228
0
                })?;
1229
0
            let param_weighted = (parametric_result.portfolio_volatility * parametric_weight)
1230
0
                .map_err(|e| {
1231
0
                    RiskError::CalculationError(format!(
1232
0
                        "Parametric weight calculation failed: {e:?}"
1233
0
                    ))
1234
0
                })?;
1235
0
            let mc_weighted = (monte_carlo_result.portfolio_volatility * monte_carlo_weight)
1236
0
                .map_err(|e| {
1237
0
                    RiskError::CalculationError(format!(
1238
0
                        "Monte Carlo weight calculation failed: {e:?}"
1239
0
                    ))
1240
0
                })?;
1241
0
            hist_weighted + param_weighted + mc_weighted
1242
        };
1243
1244
        // Add confidence adjustment based on method agreement
1245
0
        let method_agreement = self.calculate_method_agreement(
1246
0
            &historical_result,
1247
0
            &parametric_result,
1248
0
            &monte_carlo_result,
1249
        );
1250
0
        let confidence_multiplier = if method_agreement < 0.8 {
1251
0
            Decimal::try_from(1.1).unwrap_or(Decimal::ONE) // Increase VaR if methods disagree
1252
        } else {
1253
0
            Decimal::ONE
1254
        };
1255
1256
        Ok(VaRCalculationResult {
1257
0
            var_1d_95: (var_1d_95 * Price::from_decimal(confidence_multiplier)).map_err(|e| {
1258
0
                RiskError::CalculationError(format!(
1259
0
                    "VaR 1d 95 confidence adjustment failed: {e:?}"
1260
0
                ))
1261
0
            })?,
1262
0
            var_1d_99: (var_1d_99 * Price::from_decimal(confidence_multiplier)).map_err(|e| {
1263
0
                RiskError::CalculationError(format!(
1264
0
                    "VaR 1d 99 confidence adjustment failed: {e:?}"
1265
0
                ))
1266
0
            })?,
1267
0
            var_10d_95: (var_10d_95 * Price::from_decimal(confidence_multiplier)).map_err(|e| {
1268
0
                RiskError::CalculationError(format!(
1269
0
                    "VaR 10d 95 confidence adjustment failed: {e:?}"
1270
0
                ))
1271
0
            })?,
1272
0
            var_10d_99: (var_10d_99 * Price::from_decimal(confidence_multiplier)).map_err(|e| {
1273
0
                RiskError::CalculationError(format!(
1274
0
                    "VaR 10d 99 confidence adjustment failed: {e:?}"
1275
0
                ))
1276
0
            })?,
1277
0
            expected_shortfall_95: (expected_shortfall_95
1278
0
                * Price::from_decimal(confidence_multiplier))
1279
0
            .map_err(|e| {
1280
0
                RiskError::CalculationError(format!("ES 95 confidence adjustment failed: {e:?}"))
1281
0
            })?,
1282
0
            expected_shortfall_99: (expected_shortfall_99
1283
0
                * Price::from_decimal(confidence_multiplier))
1284
0
            .map_err(|e| {
1285
0
                RiskError::CalculationError(format!("ES 99 confidence adjustment failed: {e:?}"))
1286
0
            })?,
1287
0
            portfolio_volatility,
1288
        })
1289
0
    }
1290
1291
    // Helper method to calculate agreement between different VaR methods
1292
0
    fn calculate_method_agreement(
1293
0
        &self,
1294
0
        historical: &VaRCalculationResult,
1295
0
        parametric: &VaRCalculationResult,
1296
0
        monte_carlo: &VaRCalculationResult,
1297
0
    ) -> f64 {
1298
        // Compare the 1-day 95% VaR estimates
1299
0
        let h_var = historical.var_1d_95.to_f64();
1300
0
        let p_var = parametric.var_1d_95.to_f64();
1301
0
        let mc_var = monte_carlo.var_1d_95.to_f64();
1302
1303
0
        if h_var == 0.0 || p_var == 0.0 || mc_var == 0.0 {
1304
0
            return 0.5; // Neutral agreement if any method returns zero
1305
0
        }
1306
1307
        // Calculate coefficient of variation
1308
0
        let values = [h_var, p_var, mc_var];
1309
0
        let mean = values.iter().sum::<f64>() / values.len() as f64;
1310
0
        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
1311
0
        let std_dev = variance.sqrt();
1312
0
        let coeff_of_variation = std_dev / mean;
1313
1314
        // Convert to agreement score (lower variation = higher agreement)
1315
        // Agreement of 1.0 means perfect agreement, 0.0 means complete disagreement
1316
0
        (1.0_f64 - coeff_of_variation.min(1.0)).max(0.0)
1317
0
    }
1318
1319
    // Helper methods for the comprehensive calculation
1320
0
    fn assess_data_quality(
1321
0
        &self,
1322
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1323
0
    ) -> RiskResult<f64> {
1324
0
        if historical_prices.is_empty() {
1325
0
            return Ok(0.0);
1326
0
        }
1327
1328
0
        let mut quality_scores = Vec::new();
1329
1330
0
        for prices in historical_prices.values() {
1331
0
            let mut score = 1.0;
1332
1333
            // Penalize insufficient data
1334
0
            if prices.len() < self.min_historical_days {
1335
0
                score *= prices.len() as f64 / self.min_historical_days as f64;
1336
0
            }
1337
1338
            // Check for data gaps
1339
0
            let mut gap_penalty = 0.0;
1340
0
            for window in prices.windows(2) {
1341
0
                let days_diff = (window[1].date - window[0].date).num_days();
1342
0
                if days_diff > 5 {
1343
0
                    // Weekend is fine, longer gaps are problematic
1344
0
                    gap_penalty += 0.1;
1345
0
                }
1346
            }
1347
0
            score = (score - gap_penalty).max(0.0_f64);
1348
1349
0
            quality_scores.push(score);
1350
        }
1351
1352
0
        let avg_quality = quality_scores.iter().sum::<f64>() / quality_scores.len() as f64;
1353
0
        Ok(avg_quality.min(1.0))
1354
0
    }
1355
1356
0
    fn select_optimal_methodology(
1357
0
        &self,
1358
0
        positions: &HashMap<Symbol, PositionInfo>,
1359
0
        historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1360
0
    ) -> RiskResult<VaRMethodology> {
1361
0
        let num_assets = positions.len();
1362
0
        let data_length = historical_prices.values().map(Vec::len).min().unwrap_or(0);
1363
1364
0
        if num_assets <= 5 && data_length >= self.min_historical_days {
1365
0
            Ok(VaRMethodology::HistoricalSimulation)
1366
0
        } else if num_assets > 20 {
1367
0
            Ok(VaRMethodology::MonteCarlo)
1368
        } else {
1369
0
            Ok(VaRMethodology::Parametric)
1370
        }
1371
0
    }
1372
1373
0
    async fn run_stress_tests(
1374
0
        &self,
1375
0
        _positions: &HashMap<Symbol, PositionInfo>,
1376
0
        _historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1377
0
    ) -> RiskResult<Vec<StressTestResult>> {
1378
        // Implementation would run all stress scenarios
1379
0
        Ok(Vec::new())
1380
0
    }
1381
1382
0
    async fn calculate_risk_decomposition(
1383
0
        &self,
1384
0
        _positions: &HashMap<Symbol, PositionInfo>,
1385
0
        _historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1386
0
    ) -> RiskResult<(
1387
0
        HashMap<Symbol, Price>,
1388
0
        HashMap<Symbol, Price>,
1389
0
        HashMap<Symbol, Price>,
1390
0
    )> {
1391
        // Implementation would calculate component VaR, marginal VaR, and correlation contributions
1392
0
        Ok((HashMap::new(), HashMap::new(), HashMap::new()))
1393
0
    }
1394
1395
2
    fn calculate_concentration_risk(
1396
2
        &self,
1397
2
        positions: &HashMap<Symbol, PositionInfo>,
1398
2
    ) -> RiskResult<Decimal> {
1399
2
        let total_value: Price = positions
1400
2
            .values()
1401
3
            .
fold2
(Price::ZERO, |acc, p| acc + p.market_value);
1402
1403
2
        if total_value == Price::ZERO {
1404
0
            return Ok(Decimal::ZERO);
1405
2
        }
1406
1407
        // Calculate Herfindahl-Hirschman Index for concentration
1408
2
        let hhi_f64: f64 = positions
1409
2
            .values()
1410
3
            .
map2
(|position| {
1411
3
                let weight_f64 = position.market_value.to_f64() / total_value.to_f64();
1412
3
                weight_f64 * weight_f64
1413
3
            })
1414
2
            .sum();
1415
1416
2
        let hhi = Price::from_f64(hhi_f64).unwrap_or(Price::ZERO);
1417
1418
2
        hhi.to_decimal().map_err(|e| RiskError::TypeConversion {
1419
0
            from_type: "Price".to_owned(),
1420
0
            to_type: "Decimal".to_owned(),
1421
0
            reason: format!("Failed to convert HHI price to decimal: {e:?}"),
1422
0
        })
1423
2
    }
1424
1425
0
    fn calculate_model_confidence(&self, methodology: &VaRMethodology, data_quality: f64) -> f64 {
1426
0
        let base_confidence = match methodology {
1427
0
            VaRMethodology::HistoricalSimulation => 0.85,
1428
0
            VaRMethodology::Parametric => 0.75,
1429
0
            VaRMethodology::MonteCarlo => 0.80,
1430
0
            VaRMethodology::Hybrid => 0.90,
1431
        };
1432
1433
0
        (base_confidence * data_quality).min(1.0)
1434
0
    }
1435
1436
0
    async fn backtest_accuracy(
1437
0
        &self,
1438
0
        _historical_prices: &HashMap<Symbol, Vec<HistoricalPrice>>,
1439
0
    ) -> RiskResult<Option<f64>> {
1440
        // Implementation would perform backtesting validation
1441
0
        Ok(None)
1442
0
    }
1443
}
1444
1445
/// Internal `VaR` calculation result structure
1446
struct VaRCalculationResult {
1447
    var_1d_95: Price,
1448
    var_1d_99: Price,
1449
    var_10d_95: Price,
1450
    var_10d_99: Price,
1451
    expected_shortfall_95: Price,
1452
    expected_shortfall_99: Price,
1453
    portfolio_volatility: Price,
1454
}
1455
1456
impl VaRCalculationResult {
1457
    /// Create a zero `VaR` result
1458
0
    const fn zero() -> Self {
1459
0
        Self {
1460
0
            var_1d_95: Price::ZERO,
1461
0
            var_1d_99: Price::ZERO,
1462
0
            var_10d_95: Price::ZERO,
1463
0
            var_10d_99: Price::ZERO,
1464
0
            expected_shortfall_95: Price::ZERO,
1465
0
            expected_shortfall_99: Price::ZERO,
1466
0
            portfolio_volatility: Price::ZERO,
1467
0
        }
1468
0
    }
1469
}
1470
1471
#[cfg(test)]
1472
mod tests {
1473
    use super::*;
1474
    // operations module removed - use direct imports from common
1475
    // CANONICAL TYPE IMPORTS - ENFORCED BY TYPE SYSTEM AGENT
1476
1477
    #[test]
1478
1
    fn test_real_var_engine_creation() {
1479
1
        let engine = RealVaREngine::new();
1480
1
        assert_eq!(engine.confidence_levels, vec![0.95, 0.99, 0.999]);
1481
1
        assert_eq!(engine.min_historical_days, 252);
1482
1
        assert!(!engine.stress_scenarios.is_empty());
1483
1
    }
1484
1485
    #[test]
1486
1
    fn test_circuit_breaker_conditions() -> Result<(), Box<dyn std::error::Error>> {
1487
1
        let engine = RealVaREngine::new();
1488
1489
1
        let var_results = ComprehensiveVaRResult {
1490
1
            portfolio_id: "TEST".to_string(),
1491
1
            methodology_used: "HistoricalSimulation".to_string(),
1492
1
            var_1d_95: Price::from_f64(10000.0)
?0
, // Sample VaR values for testing
1493
1
            var_1d_99: Price::from_f64(15000.0)
?0
,
1494
1
            var_10d_95: Price::from_f64(31623.0)
?0
, // sqrt(10) * var_1d_95
1495
1
            var_10d_99: Price::from_f64(47434.0)
?0
, // sqrt(10) * var_1d_99
1496
1
            expected_shortfall_95: Price::from_f64(12500.0)
?0
,
1497
1
            expected_shortfall_99: Price::from_f64(20000.0)
?0
,
1498
1
            component_var: HashMap::new(),
1499
1
            marginal_var: HashMap::new(),
1500
1
            correlation_contribution: HashMap::new(),
1501
1
            stress_test_results: Vec::new(),
1502
            model_confidence: 0.85,
1503
1
            historical_accuracy: Some(0.90),
1504
1
            portfolio_volatility: Price::from_f64(0.15).unwrap_or(Price::ZERO), // 0.15 = 15% volatility threshold
1505
1
            concentration_risk: Price::from_f64(0.15).unwrap_or(Price::ZERO),
1506
1
            calculation_method: "RealVaREngine::HistoricalSimulation".to_string(),
1507
            data_quality_score: 0.9,
1508
            num_observations: 500,
1509
1
            calculated_at: Utc::now(),
1510
        };
1511
1512
1
        let portfolio_value = Price::from_f64(1_000_000.0)
?0
; // $1M portfolio
1513
1
        let current_pnl = Price::from_f64(25_000.0)
?0
; // $25k loss (2.5%) - use absolute value
1514
1515
1
        let conditions =
1516
1
            engine.check_circuit_breaker_conditions(&var_results, current_pnl, portfolio_value);
1517
1518
        // Should trigger daily loss limit
1519
1
        let daily_loss_condition = conditions
1520
1
            .iter()
1521
1
            .find(|c| c.condition_name == "Daily_Loss_Limit")
1522
1
            .ok_or("Daily loss condition not found")
?0
;
1523
1524
1
        assert!(daily_loss_condition.should_trigger);
1525
1
        assert_eq!(daily_loss_condition.severity, "CRITICAL");
1526
1
        Ok(())
1527
1
    }
1528
1529
    #[test]
1530
1
    fn test_concentration_risk_calculation() -> Result<(), Box<dyn std::error::Error>> {
1531
1
        let engine = RealVaREngine::new();
1532
1533
1
        let mut positions = HashMap::new();
1534
1535
        // Single position portfolio (high concentration)
1536
1
        positions.insert(
1537
1
            Symbol::from("AAPL".to_string()),
1538
            PositionInfo {
1539
1
                symbol: Symbol::from("AAPL".to_string()),
1540
1
                quantity: Quantity::from_f64(100.0)
?0
,
1541
1
                market_value: Price::from_f64(15_000.0)
?0
,
1542
1
                average_cost: Price::from_f64(140.0)
?0
,
1543
1
                unrealized_pnl: Price::from_f64(1_000.0)
?0
,
1544
                realized_pnl: Price::ZERO,
1545
1
                currency: "USD".to_string(),
1546
1
                timestamp: Utc::now(),
1547
            },
1548
        );
1549
1550
1
        let concentration = engine.calculate_concentration_risk(&positions)
?0
;
1551
1
        assert_eq!(concentration, Decimal::ONE); // 100% concentration
1552
1553
        // Add second position (lower concentration)
1554
1
        positions.insert(
1555
1
            Symbol::from("GOOGL".to_string()),
1556
            PositionInfo {
1557
1
                symbol: Symbol::from("GOOGL".to_string()),
1558
1
                quantity: Quantity::from_f64(5.0)
?0
,
1559
1
                market_value: Price::from_f64(15_000.0)
?0
,
1560
1
                average_cost: Price::from_f64(2900.0)
?0
,
1561
1
                unrealized_pnl: Price::from_f64(500.0)
?0
,
1562
                realized_pnl: Price::ZERO,
1563
1
                currency: "USD".to_string(),
1564
1
                timestamp: Utc::now(),
1565
            },
1566
        );
1567
1568
1
        let concentration = engine.calculate_concentration_risk(&positions)
?0
;
1569
1
        assert_eq!(
1570
            concentration,
1571
1
            Decimal::try_from(0.5).unwrap_or(Decimal::ZERO)
1572
        ); // 50% + 50% = 0.5 HHI
1573
1
        Ok(())
1574
1
    }
1575
}
\ No newline at end of file diff --git a/coverage_risk/html/index.html b/coverage_risk/html/index.html deleted file mode 100644 index 797a530b9..000000000 --- a/coverage_risk/html/index.html +++ /dev/null @@ -1 +0,0 @@ -

Coverage Report

Created: 2025-10-09 22:05

Click here for information about interpreting this report.

FilenameFunction CoverageLine CoverageRegion CoverageBranch Coverage
common/src/database.rs
   0.00% (0/15)
   0.00% (0/132)
   0.00% (0/117)
- (0/0)
common/src/error.rs
   0.00% (0/17)
   0.00% (0/153)
   0.00% (0/219)
- (0/0)
common/src/trading.rs
   0.00% (0/16)
   0.00% (0/87)
   0.00% (0/135)
- (0/0)
common/src/traits.rs
   0.00% (0/2)
   0.00% (0/6)
   0.00% (0/6)
- (0/0)
common/src/types.rs
  10.44% (33/316)
   9.40% (169/1798)
   7.78% (184/2366)
- (0/0)
config/src/asset_classification.rs
  10.53% (2/19)
   4.92% (15/305)
   5.98% (15/251)
- (0/0)
config/src/data_config.rs
   0.00% (0/13)
   0.00% (0/145)
   0.00% (0/71)
- (0/0)
config/src/data_providers.rs
   0.00% (0/21)
   0.00% (0/113)
   0.00% (0/121)
- (0/0)
config/src/database.rs
   0.00% (0/7)
   0.00% (0/52)
   0.00% (0/34)
- (0/0)
config/src/lib.rs
   0.00% (0/2)
   0.00% (0/11)
   0.00% (0/15)
- (0/0)
config/src/manager.rs
   0.00% (0/17)
   0.00% (0/132)
   0.00% (0/168)
- (0/0)
config/src/ml_config.rs
   0.00% (0/4)
   0.00% (0/136)
   0.00% (0/55)
- (0/0)
config/src/risk_config.rs
  60.00% (3/5)
  83.16% (158/190)
  89.22% (298/334)
- (0/0)
config/src/runtime.rs
   0.00% (0/34)
   0.00% (0/344)
   0.00% (0/409)
- (0/0)
config/src/schemas.rs
   0.00% (0/5)
   0.00% (0/76)
   0.00% (0/144)
- (0/0)
config/src/storage_config.rs
   0.00% (0/5)
   0.00% (0/26)
   0.00% (0/26)
- (0/0)
config/src/structures.rs
   7.41% (2/27)
  30.95% (108/349)
  25.09% (73/291)
- (0/0)
config/src/symbol_config.rs
   0.00% (0/44)
   0.00% (0/317)
   0.00% (0/333)
- (0/0)
config/src/vault.rs
   0.00% (0/8)
   0.00% (0/48)
   0.00% (0/56)
- (0/0)
risk/src/circuit_breaker.rs
  29.07% (25/86)
  32.78% (198/604)
  24.61% (205/833)
- (0/0)
risk/src/compliance.rs
  65.15% (86/132)
  76.23% (911/1195)
  74.90% (1310/1749)
- (0/0)
risk/src/drawdown_monitor.rs
  95.12% (39/41)
  98.28% (343/349)
  94.97% (434/457)
- (0/0)
risk/src/error.rs
  16.00% (4/25)
  10.69% (17/159)
  11.80% (21/178)
- (0/0)
risk/src/kelly_sizing.rs
  58.62% (17/29)
  68.81% (214/311)
  74.45% (373/501)
- (0/0)
risk/src/lib.rs
  72.73% (8/11)
  84.07% (153/182)
  75.86% (176/232)
- (0/0)
risk/src/operations.rs
  31.25% (10/32)
  34.75% (131/377)
  46.89% (241/514)
- (0/0)
risk/src/position_tracker.rs
  26.67% (28/105)
  48.34% (465/962)
  50.77% (657/1294)
- (0/0)
risk/src/risk_engine.rs
   1.16% (1/86)
   0.53% (5/937)
   0.25% (3/1218)
- (0/0)
risk/src/risk_types.rs
  28.57% (2/7)
  51.33% (58/113)
  52.17% (72/138)
- (0/0)
risk/src/safety/emergency_response.rs
  91.07% (51/56)
  90.63% (416/459)
  86.23% (570/661)
- (0/0)
risk/src/safety/kill_switch.rs
  84.42% (65/77)
  75.16% (354/471)
  72.08% (506/702)
- (0/0)
risk/src/safety/mod.rs
  81.82% (9/11)
  86.15% (56/65)
  84.13% (53/63)
- (0/0)
risk/src/safety/position_limiter.rs
  96.83% (61/63)
  91.62% (503/549)
  92.92% (787/847)
- (0/0)
risk/src/safety/safety_coordinator.rs
  88.24% (45/51)
  80.05% (297/371)
  75.56% (473/626)
- (0/0)
risk/src/safety/trading_gate.rs
  94.87% (37/39)
  91.39% (244/267)
  88.07% (406/461)
- (0/0)
risk/src/safety/unix_socket_kill_switch.rs
  64.47% (49/76)
  76.01% (564/742)
  72.00% (720/1000)
- (0/0)
risk/src/stress_tester.rs
  54.05% (40/74)
  73.29% (354/483)
  76.14% (549/721)
- (0/0)
risk/src/var_calculator/expected_shortfall.rs
  59.38% (19/32)
  71.06% (248/349)
  74.18% (523/705)
- (0/0)
risk/src/var_calculator/historical_simulation.rs
  91.30% (21/23)
  87.54% (267/305)
  88.79% (483/544)
- (0/0)
risk/src/var_calculator/monte_carlo.rs
  82.93% (34/41)
  87.73% (472/538)
  89.82% (829/923)
- (0/0)
risk/src/var_calculator/parametric.rs
  83.87% (26/31)
  94.26% (312/331)
  92.56% (697/753)
- (0/0)
risk/src/var_calculator/var_engine.rs
  18.29% (15/82)
  25.27% (231/914)
  23.93% (296/1237)
- (0/0)
Totals
  40.96% (732/1787)
  47.00% (7263/15453)
  50.93% (10954/21508)
- (0/0)
Generated by llvm-cov -- llvm version 20.1.7-rust-1.89.0-stable
\ No newline at end of file diff --git a/coverage_risk/html/style.css b/coverage_risk/html/style.css deleted file mode 100644 index ae4f09f69..000000000 --- a/coverage_risk/html/style.css +++ /dev/null @@ -1,194 +0,0 @@ -.red { - background-color: #f004; -} -.cyan { - background-color: cyan; -} -html { - scroll-behavior: smooth; -} -body { - font-family: -apple-system, sans-serif; -} -pre { - margin-top: 0px !important; - margin-bottom: 0px !important; -} -.source-name-title { - padding: 5px 10px; - border-bottom: 1px solid #8888; - background-color: #0002; - line-height: 35px; -} -.centered { - display: table; - margin-left: left; - margin-right: auto; - border: 1px solid #8888; - border-radius: 3px; -} -.expansion-view { - margin-left: 0px; - margin-top: 5px; - margin-right: 5px; - margin-bottom: 5px; - border: 1px solid #8888; - border-radius: 3px; -} -table { - border-collapse: collapse; -} -.light-row { - border: 1px solid #8888; - border-left: none; - border-right: none; -} -.light-row-bold { - border: 1px solid #8888; - border-left: none; - border-right: none; - font-weight: bold; -} -.column-entry { - text-align: left; -} -.column-entry-bold { - font-weight: bold; - text-align: left; -} -.column-entry-yellow { - text-align: left; - background-color: #ff06; -} -.column-entry-red { - text-align: left; - background-color: #f004; -} -.column-entry-gray { - text-align: left; - background-color: #fff4; -} -.column-entry-green { - text-align: left; - background-color: #0f04; -} -.line-number { - text-align: right; -} -.covered-line { - text-align: right; - color: #06d; -} -.uncovered-line { - text-align: right; - color: #d00; -} -.uncovered-line.selected { - color: #f00; - font-weight: bold; -} -.region.red.selected { - background-color: #f008; - font-weight: bold; -} -.branch.red.selected { - background-color: #f008; - font-weight: bold; -} -.tooltip { - position: relative; - display: inline; - background-color: #bef; - text-decoration: none; -} -.tooltip span.tooltip-content { - position: absolute; - width: 100px; - margin-left: -50px; - color: #FFFFFF; - background: #000000; - height: 30px; - line-height: 30px; - text-align: center; - visibility: hidden; - border-radius: 6px; -} -.tooltip span.tooltip-content:after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - margin-left: -8px; - width: 0; height: 0; - border-top: 8px solid #000000; - border-right: 8px solid transparent; - border-left: 8px solid transparent; -} -:hover.tooltip span.tooltip-content { - visibility: visible; - opacity: 0.8; - bottom: 30px; - left: 50%; - z-index: 999; -} -th, td { - vertical-align: top; - padding: 2px 8px; - border-collapse: collapse; - border-right: 1px solid #8888; - border-left: 1px solid #8888; - text-align: left; -} -td pre { - display: inline-block; - text-decoration: inherit; -} -td:first-child { - border-left: none; -} -td:last-child { - border-right: none; -} -tr:hover { - background-color: #eee; -} -tr:last-child { - border-bottom: none; -} -tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { - background-color: #8884; -} -a { - color: inherit; -} -.control { - position: fixed; - top: 0em; - right: 0em; - padding: 1em; - background: #FFF8; -} -@media (prefers-color-scheme: dark) { - body { - background-color: #222; - color: whitesmoke; - } - tr:hover { - background-color: #111; - } - .covered-line { - color: #39f; - } - .uncovered-line { - color: #f55; - } - .tooltip { - background-color: #068; - } - .control { - background: #2228; - } - tr:has(> td >a:target), tr:has(> td.uncovered-line.selected) { - background-color: #8884; - } -} diff --git a/docs/archive/agents/AGENT_TXT_FILES_ANALYSIS.md b/docs/archive/agents/AGENT_TXT_FILES_ANALYSIS.md new file mode 100644 index 000000000..fe0d6d405 --- /dev/null +++ b/docs/archive/agents/AGENT_TXT_FILES_ANALYSIS.md @@ -0,0 +1,316 @@ +# Agent Summary Files Investigation Report +**Date**: 2025-10-30 +**Location**: /home/jgrusewski/Work/foxhunt (root directory) + +## Executive Summary + +**Total Agent Files Found**: 85 .txt files +**Total Size**: 825 KB +**Date Range**: October 9-20, 2025 +**Archive Location**: docs/archive/agents/ (399 files already archived as .md) +**Current Status**: REDUNDANT - All files are historical development artifacts + +## Categorization + +### Category 1: Error Fix Agents (24 files) +**Pattern**: `agent_NNN_*_fixed.txt` +**Purpose**: Bug fix documentation from Wave 4-9 cleanup phases +**Examples**: +- agent_275_missing_fields_fixed.txt +- agent_278_trading_data_fixed.txt +- agent_279_format_strings_fixed.txt +- agent_280_load_tests_fixed.txt +- agent_281_api_gateway_tests_fixed.txt +- agent_283_future_traits_fixed.txt +- agent_284_criterion_fixed.txt +- agent_288_benchmark_deps_fixed.txt +- agent_289_database_tli_fixed.txt +- agent_291_tli_storage_fixed.txt +- agent_295_api_gateway_fixed.txt +- agent_297_backtesting_service_fixed.txt +- agent_304_remaining_crates_fixed.txt +- agent_306_api_gateway_unwrap_fixed.txt +- agent_307_trading_service_panics_fixed.txt +- agent_311_storage_safety_fixed.txt +- agent_312_ml_safety_fixed.txt +- agent_313_trading_engine_safety_fixed.txt +- agent_314_backtesting_safety_fixed.txt +- agent_322_risk_precision_fixed.txt +- agent_323_data_types_fixed.txt +- agent_324_ml_types_fixed.txt +- agent_335_as_conversions_fixed.txt +- agent_418_float_arithmetic_fixed.txt + +**Status**: REDUNDANT - Fixes already merged into codebase + +--- + +### Category 2: Summary Reports (28 files) +**Pattern**: `AGENT_N_SUMMARY.txt` or `AGENT_*_SUMMARY.txt` +**Purpose**: TDD implementation summaries from Wave 10 (ML→Paper Trading) +**Examples**: +- AGENT_10_1_SUMMARY.txt (VarMap weight extraction) +- AGENT_10_3_SUMMARY.txt +- AGENT_10_6_SUMMARY.txt +- AGENT_10_7_SUMMARY.txt +- AGENT_152_SUMMARY.txt +- AGENT_19_SUMMARY.txt +- AGENT_244_TEST_SUMMARY.txt +- AGENT_256_SUMMARY.txt +- AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt +- AGENT_257_TEST_SUMMARY.txt +- AGENT_43_SUMMARY.txt +- AGENT_86_BENCHMARK_GAP_SUMMARY.txt +- AGENT_86_FINAL_SUMMARY.txt +- AGENT_BLOCK02_SUMMARY.txt +- AGENT_F11_SUMMARY.txt +- AGENT_F23_EXECUTIVE_SUMMARY.txt +- AGENT_G19_SUCCESS_SUMMARY.txt +- AGENT_IMPL18_SUMMARY.txt +- AGENT_T22_DELIVERABLES_SUMMARY.txt +- AGENT_VAL28_SUMMARY.txt +- AGENT_VAL30_QUICK_SUMMARY.txt + +**Status**: REDUNDANT - Features documented in CLAUDE.md (Wave D complete) + +--- + +### Category 3: Visual/Diagram Reports (7 agent-specific files) +**Pattern**: `AGENT_*VISUAL*.txt` +**Purpose**: ASCII/text-based architecture diagrams +**Files**: +- AGENT_160_VISUAL_SUMMARY.txt +- AGENT_223_VISUAL_SUMMARY.txt +- AGENT_258_VISUAL_SUMMARY.txt +- AGENT_9_13_VISUAL_SUMMARY.txt (39 KB - largest file) +- AGENT_916_VISUAL_SUMMARY.txt (21 KB) +- AGENT_E6_PERFORMANCE_VISUALIZATION.txt +- AGENT_F16_VISUAL_SUMMARY.txt + +**Status**: HISTORICAL VALUE - Consider archiving largest diagrams + +--- + +### Category 4: Implementation/Validation Reports (10 files) +**Pattern**: `agent_NNN_*_(implementation|validation|report).txt` +**Purpose**: Detailed technical reports from infrastructure/JWT/async work +**Files**: +- agent_199_infrastructure_validation.txt (22 KB) +- agent_200_test_environment_setup.txt (20 KB) +- agent_219_async_audit_design.txt (26 KB - 2nd largest) +- agent_228_implementation_report.txt +- agent_229v2_jwt_validation_report.txt +- agent_331_float_arithmetic_report.txt +- agent_341_unsafe_documentation_report.txt +- agent_367_field_visibility_report.txt +- agent_422_as_conversions_report.txt +- agent_442_ml_indexing_final_report.txt + +**Status**: MIXED - Infrastructure reports may have historical value + +--- + +### Category 5: Special Purpose (15 files) +**Pattern**: `AGENT_[A-Z]+[0-9]+_*` +**Purpose**: Quick references, manifests, integration gaps +**Files**: +- AGENT_BLOCK02_SUMMARY.txt +- AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt +- AGENT_E6_BENCHMARK_RAW_OUTPUT.txt +- AGENT_E6_PERFORMANCE_VISUALIZATION.txt +- AGENT_F11_SUMMARY.txt +- AGENT_F16_VISUAL_SUMMARY.txt +- AGENT_F23_EXECUTIVE_SUMMARY.txt +- AGENT_G19_SUCCESS_SUMMARY.txt +- AGENT_IMPL18_SUMMARY.txt +- AGENT_M13_MANIFEST.txt (16 KB) +- AGENT_M13_QUICK_REFERENCE.txt (16 KB) +- AGENT_T22_DELIVERABLES_SUMMARY.txt +- AGENT_VAL28_SUMMARY.txt +- AGENT_VAL30_QUICK_SUMMARY.txt +- AGENT_WIRE14_INTEGRATION_GAPS.txt + +**Status**: QUICK_REFS may be useful, others redundant + +--- + +### Category 6: Miscellaneous Cleanup (19 files) +**Pattern**: Various patterns not fitting above categories +**Purpose**: Documentation fixes, analysis reports, final cleanup phases +**Files**: +- agent_326_doc_markdown_fixes.txt +- agent_334_float_arithmetic_ml.txt +- agent_342_numeric_fallback_fixes.txt +- agent_349_backticks_1501_2000.txt +- agent_350_doc_backticks_part5.txt +- agent_373_wave6_error_analysis.txt +- AGENT_395_COMPLETE.txt +- agent_402_remaining_errors.txt +- agent_402_summary.txt +- agent_437_map_err_fixes.txt +- agent_445_arithmetic_cleanup_part2.txt +- agent_451_unused_self_cleanup_final.txt +- agent_470_e0599_final_cleanup.txt +- agent_489_trading_engine_final.txt +- AGENT_5_ARCHITECTURE_DIAGRAM.txt +- AGENT_9_13_COMMIT_MESSAGE.txt +- agent_comprehensive_finalization_analysis.txt + +**Status**: REDUNDANT - Cleanup waves complete + +--- + +## Archive Status + +**Existing Archive**: docs/archive/agents/ +- Contains 399 .md files (converted format) +- Organized by wave/agent number +- Includes comprehensive ARCHIVE_INDEX.md + +**References**: +- ZERO references to these .txt files in CLAUDE.md +- ZERO references in production .md documentation +- Files are NOT part of active development workflow + +--- + +## Recommendations + +### Option 1: ARCHIVE (Conservative) ⭐ RECOMMENDED +**Action**: Move all 85 files to `docs/archive/agents/legacy_txt/` +**Rationale**: Preserves historical context for future reference +**Disk Impact**: 825 KB (negligible) +**Command**: +```bash +mkdir -p docs/archive/agents/legacy_txt +mv AGENT_*.txt agent_*.txt docs/archive/agents/legacy_txt/ +echo "# Legacy Agent TXT Files" > docs/archive/agents/legacy_txt/README.md +echo "Archived on 2025-10-30. Historical development artifacts from Waves 4-10." >> docs/archive/agents/legacy_txt/README.md +``` + +### Option 2: DELETE (Aggressive) +**Action**: Delete all 85 files +**Rationale**: +- Already archived as .md in docs/archive/agents/ +- No references in active documentation +- Git history preserves all content +- System is production certified (100% test pass rate) +**Disk Savings**: 825 KB +**Risk**: Loss of quick-reference text format (though .md equivalents exist) +**Command**: +```bash +rm AGENT_*.txt agent_*.txt +git add -u +git commit -m "chore: Remove redundant agent summary txt files (archived as .md)" +``` + +### Option 3: SELECTIVE RETENTION (Balanced) +**Action**: Keep 10-15 most valuable files, archive rest +**Keep**: +- agent_219_async_audit_design.txt (26 KB, detailed async architecture) +- agent_199_infrastructure_validation.txt (22 KB, infrastructure reference) +- agent_200_test_environment_setup.txt (20 KB, test setup) +- AGENT_M13_MANIFEST.txt (16 KB, manifest reference) +- AGENT_M13_QUICK_REFERENCE.txt (16 KB, quick ref) +- AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt (NQ futures reference) +**Delete**: 75 files (~640 KB) +**Rationale**: Keep architectural/reference docs, remove redundant summaries + +--- + +## Final Recommendation + +**OPTION 1 (ARCHIVE)** is recommended because: + +1. **Disk cost is negligible** (825 KB in a multi-GB codebase) +2. **Historical value preserved** (may help understand past decisions) +3. **Zero risk** (can delete later if truly unnecessary) +4. **Clean root directory** (main goal achieved) +5. **Git history not relied upon** (easier to reference archived files) + +**Execution Plan**: +```bash +# 1. Create archive directory +mkdir -p docs/archive/agents/legacy_txt + +# 2. Move all agent txt files +mv AGENT_*.txt agent_*.txt docs/archive/agents/legacy_txt/ + +# 3. Create README +cat > docs/archive/agents/legacy_txt/README.md << 'EOD' +# Legacy Agent TXT Summary Files + +**Archived**: 2025-10-30 +**Source**: Root directory cleanup +**Count**: 85 files (825 KB) +**Date Range**: October 9-20, 2025 + +## Contents + +These files are historical development artifacts from Foxhunt Waves 4-10: + +- **Error Fix Agents** (24 files): Bug fixes from cleanup phases +- **Summary Reports** (28 files): TDD implementation summaries +- **Visual Reports** (7 files): ASCII architecture diagrams +- **Implementation Reports** (10 files): Infrastructure/JWT/async work +- **Special Purpose** (15 files): Quick refs, manifests, integration gaps +- **Miscellaneous** (19 files): Doc fixes, analysis reports, cleanup + +## Status + +All fixes/features are merged into production codebase. System is production certified: +- Test pass rate: 100% (1,337/1,337 ML tests, 3,196/3,196 workspace) +- Wave D complete: 225 features operational +- Backtest: Sharpe 2.00, Win Rate 60%, Drawdown 15% + +These files are REDUNDANT but preserved for historical reference. + +## Modern Documentation + +See: +- CLAUDE.md (current system status) +- docs/archive/agents/*.md (399 converted agent reports) +- Git history (commit eaa8e030 and prior) +EOD + +# 4. Git commit +git add docs/archive/agents/legacy_txt/ +git add -u # Remove from root +git commit -m "chore: Archive 85 legacy agent txt files to docs/archive/agents/legacy_txt/" +``` + +--- + +## Impact Assessment + +**Before**: +- Root directory: Cluttered with 85 agent files +- Documentation: Scattered between root and docs/archive +- Discoverability: Low (files mixed with active docs) + +**After (Option 1)**: +- Root directory: Clean (only active documentation) +- Documentation: Centralized in docs/archive +- Discoverability: High (single archive location with README) +- Historical value: Preserved +- Risk: Zero + +**Verification**: +```bash +# Check root directory is clean +ls -1 | grep -E "^(AGENT_|agent_).*\.txt$" | wc -l # Should be 0 + +# Check archive is complete +ls docs/archive/agents/legacy_txt/*.txt | wc -l # Should be 85 + +# Check git status +git status # Should show moved files +``` + +--- + +## Conclusion + +**ARCHIVE ALL 85 FILES to docs/archive/agents/legacy_txt/** + +This achieves the primary goal (clean root directory) while preserving historical context at negligible disk cost (825 KB). The archive is well-organized, documented, and easily accessible if needed. diff --git a/AGENT_10_1_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_10_1_SUMMARY.txt similarity index 100% rename from AGENT_10_1_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_10_1_SUMMARY.txt diff --git a/AGENT_10_3_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_10_3_SUMMARY.txt similarity index 100% rename from AGENT_10_3_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_10_3_SUMMARY.txt diff --git a/AGENT_10_6_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_10_6_SUMMARY.txt similarity index 100% rename from AGENT_10_6_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_10_6_SUMMARY.txt diff --git a/AGENT_10_7_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_10_7_SUMMARY.txt similarity index 100% rename from AGENT_10_7_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_10_7_SUMMARY.txt diff --git a/AGENT_152_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_152_SUMMARY.txt similarity index 100% rename from AGENT_152_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_152_SUMMARY.txt diff --git a/AGENT_160_VISUAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_160_VISUAL_SUMMARY.txt similarity index 100% rename from AGENT_160_VISUAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_160_VISUAL_SUMMARY.txt diff --git a/AGENT_19_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_19_SUMMARY.txt similarity index 100% rename from AGENT_19_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_19_SUMMARY.txt diff --git a/AGENT_223_VISUAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_223_VISUAL_SUMMARY.txt similarity index 100% rename from AGENT_223_VISUAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_223_VISUAL_SUMMARY.txt diff --git a/AGENT_244_TEST_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_244_TEST_SUMMARY.txt similarity index 100% rename from AGENT_244_TEST_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_244_TEST_SUMMARY.txt diff --git a/AGENT_256_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_256_SUMMARY.txt similarity index 100% rename from AGENT_256_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_256_SUMMARY.txt diff --git a/AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt similarity index 100% rename from AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_257_MEMORY_OPTIMIZATION_SUMMARY.txt diff --git a/AGENT_257_TEST_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_257_TEST_SUMMARY.txt similarity index 100% rename from AGENT_257_TEST_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_257_TEST_SUMMARY.txt diff --git a/AGENT_258_VISUAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_258_VISUAL_SUMMARY.txt similarity index 100% rename from AGENT_258_VISUAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_258_VISUAL_SUMMARY.txt diff --git a/AGENT_395_COMPLETE.txt b/docs/archive/agents/legacy_txt/AGENT_395_COMPLETE.txt similarity index 100% rename from AGENT_395_COMPLETE.txt rename to docs/archive/agents/legacy_txt/AGENT_395_COMPLETE.txt diff --git a/AGENT_43_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_43_SUMMARY.txt similarity index 100% rename from AGENT_43_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_43_SUMMARY.txt diff --git a/AGENT_5_ARCHITECTURE_DIAGRAM.txt b/docs/archive/agents/legacy_txt/AGENT_5_ARCHITECTURE_DIAGRAM.txt similarity index 100% rename from AGENT_5_ARCHITECTURE_DIAGRAM.txt rename to docs/archive/agents/legacy_txt/AGENT_5_ARCHITECTURE_DIAGRAM.txt diff --git a/AGENT_6_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_6_SUMMARY.txt similarity index 100% rename from AGENT_6_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_6_SUMMARY.txt diff --git a/AGENT_86_BENCHMARK_GAP_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_86_BENCHMARK_GAP_SUMMARY.txt similarity index 100% rename from AGENT_86_BENCHMARK_GAP_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_86_BENCHMARK_GAP_SUMMARY.txt diff --git a/AGENT_86_FINAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_86_FINAL_SUMMARY.txt similarity index 100% rename from AGENT_86_FINAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_86_FINAL_SUMMARY.txt diff --git a/AGENT_916_VISUAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_916_VISUAL_SUMMARY.txt similarity index 100% rename from AGENT_916_VISUAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_916_VISUAL_SUMMARY.txt diff --git a/AGENT_9_13_COMMIT_MESSAGE.txt b/docs/archive/agents/legacy_txt/AGENT_9_13_COMMIT_MESSAGE.txt similarity index 100% rename from AGENT_9_13_COMMIT_MESSAGE.txt rename to docs/archive/agents/legacy_txt/AGENT_9_13_COMMIT_MESSAGE.txt diff --git a/AGENT_9_13_VISUAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_9_13_VISUAL_SUMMARY.txt similarity index 100% rename from AGENT_9_13_VISUAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_9_13_VISUAL_SUMMARY.txt diff --git a/AGENT_BLOCK02_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_BLOCK02_SUMMARY.txt similarity index 100% rename from AGENT_BLOCK02_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_BLOCK02_SUMMARY.txt diff --git a/AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt b/docs/archive/agents/legacy_txt/AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt similarity index 100% rename from AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt rename to docs/archive/agents/legacy_txt/AGENT_D24_NQ_FUT_QUICK_REFERENCE.txt diff --git a/AGENT_E6_BENCHMARK_RAW_OUTPUT.txt b/docs/archive/agents/legacy_txt/AGENT_E6_BENCHMARK_RAW_OUTPUT.txt similarity index 100% rename from AGENT_E6_BENCHMARK_RAW_OUTPUT.txt rename to docs/archive/agents/legacy_txt/AGENT_E6_BENCHMARK_RAW_OUTPUT.txt diff --git a/AGENT_E6_PERFORMANCE_VISUALIZATION.txt b/docs/archive/agents/legacy_txt/AGENT_E6_PERFORMANCE_VISUALIZATION.txt similarity index 100% rename from AGENT_E6_PERFORMANCE_VISUALIZATION.txt rename to docs/archive/agents/legacy_txt/AGENT_E6_PERFORMANCE_VISUALIZATION.txt diff --git a/AGENT_F11_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_F11_SUMMARY.txt similarity index 100% rename from AGENT_F11_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_F11_SUMMARY.txt diff --git a/AGENT_F16_VISUAL_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_F16_VISUAL_SUMMARY.txt similarity index 100% rename from AGENT_F16_VISUAL_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_F16_VISUAL_SUMMARY.txt diff --git a/AGENT_F23_EXECUTIVE_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_F23_EXECUTIVE_SUMMARY.txt similarity index 100% rename from AGENT_F23_EXECUTIVE_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_F23_EXECUTIVE_SUMMARY.txt diff --git a/AGENT_G19_SUCCESS_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_G19_SUCCESS_SUMMARY.txt similarity index 100% rename from AGENT_G19_SUCCESS_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_G19_SUCCESS_SUMMARY.txt diff --git a/AGENT_IMPL18_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_IMPL18_SUMMARY.txt similarity index 100% rename from AGENT_IMPL18_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_IMPL18_SUMMARY.txt diff --git a/AGENT_M13_MANIFEST.txt b/docs/archive/agents/legacy_txt/AGENT_M13_MANIFEST.txt similarity index 100% rename from AGENT_M13_MANIFEST.txt rename to docs/archive/agents/legacy_txt/AGENT_M13_MANIFEST.txt diff --git a/AGENT_M13_QUICK_REFERENCE.txt b/docs/archive/agents/legacy_txt/AGENT_M13_QUICK_REFERENCE.txt similarity index 100% rename from AGENT_M13_QUICK_REFERENCE.txt rename to docs/archive/agents/legacy_txt/AGENT_M13_QUICK_REFERENCE.txt diff --git a/AGENT_T22_DELIVERABLES_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_T22_DELIVERABLES_SUMMARY.txt similarity index 100% rename from AGENT_T22_DELIVERABLES_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_T22_DELIVERABLES_SUMMARY.txt diff --git a/AGENT_VAL28_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_VAL28_SUMMARY.txt similarity index 100% rename from AGENT_VAL28_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_VAL28_SUMMARY.txt diff --git a/AGENT_VAL30_QUICK_SUMMARY.txt b/docs/archive/agents/legacy_txt/AGENT_VAL30_QUICK_SUMMARY.txt similarity index 100% rename from AGENT_VAL30_QUICK_SUMMARY.txt rename to docs/archive/agents/legacy_txt/AGENT_VAL30_QUICK_SUMMARY.txt diff --git a/AGENT_WIRE14_INTEGRATION_GAPS.txt b/docs/archive/agents/legacy_txt/AGENT_WIRE14_INTEGRATION_GAPS.txt similarity index 100% rename from AGENT_WIRE14_INTEGRATION_GAPS.txt rename to docs/archive/agents/legacy_txt/AGENT_WIRE14_INTEGRATION_GAPS.txt diff --git a/docs/archive/agents/legacy_txt/README.md b/docs/archive/agents/legacy_txt/README.md new file mode 100644 index 000000000..411ae5c02 --- /dev/null +++ b/docs/archive/agents/legacy_txt/README.md @@ -0,0 +1 @@ +Archived 85 agent summary files from various waves (Oct 9-20, 2025) diff --git a/agent_199_infrastructure_validation.txt b/docs/archive/agents/legacy_txt/agent_199_infrastructure_validation.txt similarity index 100% rename from agent_199_infrastructure_validation.txt rename to docs/archive/agents/legacy_txt/agent_199_infrastructure_validation.txt diff --git a/agent_200_test_environment_setup.txt b/docs/archive/agents/legacy_txt/agent_200_test_environment_setup.txt similarity index 100% rename from agent_200_test_environment_setup.txt rename to docs/archive/agents/legacy_txt/agent_200_test_environment_setup.txt diff --git a/agent_219_async_audit_design.txt b/docs/archive/agents/legacy_txt/agent_219_async_audit_design.txt similarity index 100% rename from agent_219_async_audit_design.txt rename to docs/archive/agents/legacy_txt/agent_219_async_audit_design.txt diff --git a/agent_228_implementation_report.txt b/docs/archive/agents/legacy_txt/agent_228_implementation_report.txt similarity index 100% rename from agent_228_implementation_report.txt rename to docs/archive/agents/legacy_txt/agent_228_implementation_report.txt diff --git a/agent_229v2_jwt_validation_report.txt b/docs/archive/agents/legacy_txt/agent_229v2_jwt_validation_report.txt similarity index 100% rename from agent_229v2_jwt_validation_report.txt rename to docs/archive/agents/legacy_txt/agent_229v2_jwt_validation_report.txt diff --git a/agent_275_missing_fields_fixed.txt b/docs/archive/agents/legacy_txt/agent_275_missing_fields_fixed.txt similarity index 100% rename from agent_275_missing_fields_fixed.txt rename to docs/archive/agents/legacy_txt/agent_275_missing_fields_fixed.txt diff --git a/agent_278_trading_data_fixed.txt b/docs/archive/agents/legacy_txt/agent_278_trading_data_fixed.txt similarity index 100% rename from agent_278_trading_data_fixed.txt rename to docs/archive/agents/legacy_txt/agent_278_trading_data_fixed.txt diff --git a/agent_279_format_strings_fixed.txt b/docs/archive/agents/legacy_txt/agent_279_format_strings_fixed.txt similarity index 100% rename from agent_279_format_strings_fixed.txt rename to docs/archive/agents/legacy_txt/agent_279_format_strings_fixed.txt diff --git a/agent_280_load_tests_fixed.txt b/docs/archive/agents/legacy_txt/agent_280_load_tests_fixed.txt similarity index 100% rename from agent_280_load_tests_fixed.txt rename to docs/archive/agents/legacy_txt/agent_280_load_tests_fixed.txt diff --git a/agent_281_api_gateway_tests_fixed.txt b/docs/archive/agents/legacy_txt/agent_281_api_gateway_tests_fixed.txt similarity index 100% rename from agent_281_api_gateway_tests_fixed.txt rename to docs/archive/agents/legacy_txt/agent_281_api_gateway_tests_fixed.txt diff --git a/agent_283_future_traits_fixed.txt b/docs/archive/agents/legacy_txt/agent_283_future_traits_fixed.txt similarity index 100% rename from agent_283_future_traits_fixed.txt rename to docs/archive/agents/legacy_txt/agent_283_future_traits_fixed.txt diff --git a/agent_284_criterion_fixed.txt b/docs/archive/agents/legacy_txt/agent_284_criterion_fixed.txt similarity index 100% rename from agent_284_criterion_fixed.txt rename to docs/archive/agents/legacy_txt/agent_284_criterion_fixed.txt diff --git a/agent_288_benchmark_deps_fixed.txt b/docs/archive/agents/legacy_txt/agent_288_benchmark_deps_fixed.txt similarity index 100% rename from agent_288_benchmark_deps_fixed.txt rename to docs/archive/agents/legacy_txt/agent_288_benchmark_deps_fixed.txt diff --git a/agent_289_database_tli_fixed.txt b/docs/archive/agents/legacy_txt/agent_289_database_tli_fixed.txt similarity index 100% rename from agent_289_database_tli_fixed.txt rename to docs/archive/agents/legacy_txt/agent_289_database_tli_fixed.txt diff --git a/agent_291_tli_storage_fixed.txt b/docs/archive/agents/legacy_txt/agent_291_tli_storage_fixed.txt similarity index 100% rename from agent_291_tli_storage_fixed.txt rename to docs/archive/agents/legacy_txt/agent_291_tli_storage_fixed.txt diff --git a/agent_295_api_gateway_fixed.txt b/docs/archive/agents/legacy_txt/agent_295_api_gateway_fixed.txt similarity index 100% rename from agent_295_api_gateway_fixed.txt rename to docs/archive/agents/legacy_txt/agent_295_api_gateway_fixed.txt diff --git a/agent_297_backtesting_service_fixed.txt b/docs/archive/agents/legacy_txt/agent_297_backtesting_service_fixed.txt similarity index 100% rename from agent_297_backtesting_service_fixed.txt rename to docs/archive/agents/legacy_txt/agent_297_backtesting_service_fixed.txt diff --git a/agent_304_remaining_crates_fixed.txt b/docs/archive/agents/legacy_txt/agent_304_remaining_crates_fixed.txt similarity index 100% rename from agent_304_remaining_crates_fixed.txt rename to docs/archive/agents/legacy_txt/agent_304_remaining_crates_fixed.txt diff --git a/agent_306_api_gateway_unwrap_fixed.txt b/docs/archive/agents/legacy_txt/agent_306_api_gateway_unwrap_fixed.txt similarity index 100% rename from agent_306_api_gateway_unwrap_fixed.txt rename to docs/archive/agents/legacy_txt/agent_306_api_gateway_unwrap_fixed.txt diff --git a/agent_307_trading_service_panics_fixed.txt b/docs/archive/agents/legacy_txt/agent_307_trading_service_panics_fixed.txt similarity index 100% rename from agent_307_trading_service_panics_fixed.txt rename to docs/archive/agents/legacy_txt/agent_307_trading_service_panics_fixed.txt diff --git a/agent_311_storage_safety_fixed.txt b/docs/archive/agents/legacy_txt/agent_311_storage_safety_fixed.txt similarity index 100% rename from agent_311_storage_safety_fixed.txt rename to docs/archive/agents/legacy_txt/agent_311_storage_safety_fixed.txt diff --git a/agent_312_ml_safety_fixed.txt b/docs/archive/agents/legacy_txt/agent_312_ml_safety_fixed.txt similarity index 100% rename from agent_312_ml_safety_fixed.txt rename to docs/archive/agents/legacy_txt/agent_312_ml_safety_fixed.txt diff --git a/agent_313_trading_engine_safety_fixed.txt b/docs/archive/agents/legacy_txt/agent_313_trading_engine_safety_fixed.txt similarity index 100% rename from agent_313_trading_engine_safety_fixed.txt rename to docs/archive/agents/legacy_txt/agent_313_trading_engine_safety_fixed.txt diff --git a/agent_314_backtesting_safety_fixed.txt b/docs/archive/agents/legacy_txt/agent_314_backtesting_safety_fixed.txt similarity index 100% rename from agent_314_backtesting_safety_fixed.txt rename to docs/archive/agents/legacy_txt/agent_314_backtesting_safety_fixed.txt diff --git a/agent_322_risk_precision_fixed.txt b/docs/archive/agents/legacy_txt/agent_322_risk_precision_fixed.txt similarity index 100% rename from agent_322_risk_precision_fixed.txt rename to docs/archive/agents/legacy_txt/agent_322_risk_precision_fixed.txt diff --git a/agent_323_data_types_fixed.txt b/docs/archive/agents/legacy_txt/agent_323_data_types_fixed.txt similarity index 100% rename from agent_323_data_types_fixed.txt rename to docs/archive/agents/legacy_txt/agent_323_data_types_fixed.txt diff --git a/agent_324_ml_types_fixed.txt b/docs/archive/agents/legacy_txt/agent_324_ml_types_fixed.txt similarity index 100% rename from agent_324_ml_types_fixed.txt rename to docs/archive/agents/legacy_txt/agent_324_ml_types_fixed.txt diff --git a/agent_326_doc_markdown_fixes.txt b/docs/archive/agents/legacy_txt/agent_326_doc_markdown_fixes.txt similarity index 100% rename from agent_326_doc_markdown_fixes.txt rename to docs/archive/agents/legacy_txt/agent_326_doc_markdown_fixes.txt diff --git a/agent_331_float_arithmetic_report.txt b/docs/archive/agents/legacy_txt/agent_331_float_arithmetic_report.txt similarity index 100% rename from agent_331_float_arithmetic_report.txt rename to docs/archive/agents/legacy_txt/agent_331_float_arithmetic_report.txt diff --git a/agent_334_float_arithmetic_ml.txt b/docs/archive/agents/legacy_txt/agent_334_float_arithmetic_ml.txt similarity index 100% rename from agent_334_float_arithmetic_ml.txt rename to docs/archive/agents/legacy_txt/agent_334_float_arithmetic_ml.txt diff --git a/agent_335_as_conversions_fixed.txt b/docs/archive/agents/legacy_txt/agent_335_as_conversions_fixed.txt similarity index 100% rename from agent_335_as_conversions_fixed.txt rename to docs/archive/agents/legacy_txt/agent_335_as_conversions_fixed.txt diff --git a/agent_341_unsafe_documentation_report.txt b/docs/archive/agents/legacy_txt/agent_341_unsafe_documentation_report.txt similarity index 100% rename from agent_341_unsafe_documentation_report.txt rename to docs/archive/agents/legacy_txt/agent_341_unsafe_documentation_report.txt diff --git a/agent_342_numeric_fallback_fixes.txt b/docs/archive/agents/legacy_txt/agent_342_numeric_fallback_fixes.txt similarity index 100% rename from agent_342_numeric_fallback_fixes.txt rename to docs/archive/agents/legacy_txt/agent_342_numeric_fallback_fixes.txt diff --git a/agent_349_backticks_1501_2000.txt b/docs/archive/agents/legacy_txt/agent_349_backticks_1501_2000.txt similarity index 100% rename from agent_349_backticks_1501_2000.txt rename to docs/archive/agents/legacy_txt/agent_349_backticks_1501_2000.txt diff --git a/agent_350_doc_backticks_part5.txt b/docs/archive/agents/legacy_txt/agent_350_doc_backticks_part5.txt similarity index 100% rename from agent_350_doc_backticks_part5.txt rename to docs/archive/agents/legacy_txt/agent_350_doc_backticks_part5.txt diff --git a/agent_367_field_visibility_report.txt b/docs/archive/agents/legacy_txt/agent_367_field_visibility_report.txt similarity index 100% rename from agent_367_field_visibility_report.txt rename to docs/archive/agents/legacy_txt/agent_367_field_visibility_report.txt diff --git a/agent_373_wave6_error_analysis.txt b/docs/archive/agents/legacy_txt/agent_373_wave6_error_analysis.txt similarity index 100% rename from agent_373_wave6_error_analysis.txt rename to docs/archive/agents/legacy_txt/agent_373_wave6_error_analysis.txt diff --git a/agent_402_remaining_errors.txt b/docs/archive/agents/legacy_txt/agent_402_remaining_errors.txt similarity index 100% rename from agent_402_remaining_errors.txt rename to docs/archive/agents/legacy_txt/agent_402_remaining_errors.txt diff --git a/agent_402_summary.txt b/docs/archive/agents/legacy_txt/agent_402_summary.txt similarity index 100% rename from agent_402_summary.txt rename to docs/archive/agents/legacy_txt/agent_402_summary.txt diff --git a/agent_418_float_arithmetic_fixed.txt b/docs/archive/agents/legacy_txt/agent_418_float_arithmetic_fixed.txt similarity index 100% rename from agent_418_float_arithmetic_fixed.txt rename to docs/archive/agents/legacy_txt/agent_418_float_arithmetic_fixed.txt diff --git a/agent_422_as_conversions_report.txt b/docs/archive/agents/legacy_txt/agent_422_as_conversions_report.txt similarity index 100% rename from agent_422_as_conversions_report.txt rename to docs/archive/agents/legacy_txt/agent_422_as_conversions_report.txt diff --git a/agent_437_map_err_fixes.txt b/docs/archive/agents/legacy_txt/agent_437_map_err_fixes.txt similarity index 100% rename from agent_437_map_err_fixes.txt rename to docs/archive/agents/legacy_txt/agent_437_map_err_fixes.txt diff --git a/agent_442_ml_indexing_final_report.txt b/docs/archive/agents/legacy_txt/agent_442_ml_indexing_final_report.txt similarity index 100% rename from agent_442_ml_indexing_final_report.txt rename to docs/archive/agents/legacy_txt/agent_442_ml_indexing_final_report.txt diff --git a/agent_445_arithmetic_cleanup_part2.txt b/docs/archive/agents/legacy_txt/agent_445_arithmetic_cleanup_part2.txt similarity index 100% rename from agent_445_arithmetic_cleanup_part2.txt rename to docs/archive/agents/legacy_txt/agent_445_arithmetic_cleanup_part2.txt diff --git a/agent_451_unused_self_cleanup_final.txt b/docs/archive/agents/legacy_txt/agent_451_unused_self_cleanup_final.txt similarity index 100% rename from agent_451_unused_self_cleanup_final.txt rename to docs/archive/agents/legacy_txt/agent_451_unused_self_cleanup_final.txt diff --git a/agent_470_e0599_final_cleanup.txt b/docs/archive/agents/legacy_txt/agent_470_e0599_final_cleanup.txt similarity index 100% rename from agent_470_e0599_final_cleanup.txt rename to docs/archive/agents/legacy_txt/agent_470_e0599_final_cleanup.txt diff --git a/agent_489_trading_engine_final.txt b/docs/archive/agents/legacy_txt/agent_489_trading_engine_final.txt similarity index 100% rename from agent_489_trading_engine_final.txt rename to docs/archive/agents/legacy_txt/agent_489_trading_engine_final.txt diff --git a/agent_comprehensive_finalization_analysis.txt b/docs/archive/agents/legacy_txt/agent_comprehensive_finalization_analysis.txt similarity index 100% rename from agent_comprehensive_finalization_analysis.txt rename to docs/archive/agents/legacy_txt/agent_comprehensive_finalization_analysis.txt diff --git a/CUDA_13_GPU_VALIDATION_CHECKLIST.md b/docs/archive/historical/CUDA_13_GPU_VALIDATION_CHECKLIST.md similarity index 100% rename from CUDA_13_GPU_VALIDATION_CHECKLIST.md rename to docs/archive/historical/CUDA_13_GPU_VALIDATION_CHECKLIST.md diff --git a/DOCKER_CLEANUP_QUICK_REF.md b/docs/archive/historical/DOCKER_CLEANUP_QUICK_REF.md similarity index 100% rename from DOCKER_CLEANUP_QUICK_REF.md rename to docs/archive/historical/DOCKER_CLEANUP_QUICK_REF.md diff --git a/LEGACY_256_CLEANUP_CHECKLIST.md b/docs/archive/historical/LEGACY_256_CLEANUP_CHECKLIST.md similarity index 100% rename from LEGACY_256_CLEANUP_CHECKLIST.md rename to docs/archive/historical/LEGACY_256_CLEANUP_CHECKLIST.md diff --git a/wave153_bakeoff_kaggle/EXECUTIVE_SUMMARY.md b/docs/archive/historical/wave153/wave153_bakeoff_kaggle/EXECUTIVE_SUMMARY.md similarity index 100% rename from wave153_bakeoff_kaggle/EXECUTIVE_SUMMARY.md rename to docs/archive/historical/wave153/wave153_bakeoff_kaggle/EXECUTIVE_SUMMARY.md diff --git a/wave153_bakeoff_kaggle/QUICK_REFERENCE.md b/docs/archive/historical/wave153/wave153_bakeoff_kaggle/QUICK_REFERENCE.md similarity index 100% rename from wave153_bakeoff_kaggle/QUICK_REFERENCE.md rename to docs/archive/historical/wave153/wave153_bakeoff_kaggle/QUICK_REFERENCE.md diff --git a/wave153_bakeoff_kaggle/README.md b/docs/archive/historical/wave153/wave153_bakeoff_kaggle/README.md similarity index 100% rename from wave153_bakeoff_kaggle/README.md rename to docs/archive/historical/wave153/wave153_bakeoff_kaggle/README.md diff --git a/wave153_bakeoff_kaggle/TECHNICAL_DETAILS.md b/docs/archive/historical/wave153/wave153_bakeoff_kaggle/TECHNICAL_DETAILS.md similarity index 100% rename from wave153_bakeoff_kaggle/TECHNICAL_DETAILS.md rename to docs/archive/historical/wave153/wave153_bakeoff_kaggle/TECHNICAL_DETAILS.md diff --git a/wave153_bakeoff_kaggle/analysis_report.json b/docs/archive/historical/wave153/wave153_bakeoff_kaggle/analysis_report.json similarity index 100% rename from wave153_bakeoff_kaggle/analysis_report.json rename to docs/archive/historical/wave153/wave153_bakeoff_kaggle/analysis_report.json diff --git a/wave153_bakeoff_kraken/KRAKEN_ANALYSIS_SUMMARY.md b/docs/archive/historical/wave153/wave153_bakeoff_kraken/KRAKEN_ANALYSIS_SUMMARY.md similarity index 100% rename from wave153_bakeoff_kraken/KRAKEN_ANALYSIS_SUMMARY.md rename to docs/archive/historical/wave153/wave153_bakeoff_kraken/KRAKEN_ANALYSIS_SUMMARY.md diff --git a/wave153_bakeoff_kraken/analysis_report.json b/docs/archive/historical/wave153/wave153_bakeoff_kraken/analysis_report.json similarity index 100% rename from wave153_bakeoff_kraken/analysis_report.json rename to docs/archive/historical/wave153/wave153_bakeoff_kraken/analysis_report.json diff --git a/wave153_bakeoff_kraken/btc_usd_raw.json b/docs/archive/historical/wave153/wave153_bakeoff_kraken/btc_usd_raw.json similarity index 100% rename from wave153_bakeoff_kraken/btc_usd_raw.json rename to docs/archive/historical/wave153/wave153_bakeoff_kraken/btc_usd_raw.json diff --git a/wave153_bakeoff_kraken/download_kraken_data.py b/docs/archive/historical/wave153/wave153_bakeoff_kraken/download_kraken_data.py similarity index 100% rename from wave153_bakeoff_kraken/download_kraken_data.py rename to docs/archive/historical/wave153/wave153_bakeoff_kraken/download_kraken_data.py diff --git a/wave153_bakeoff_kraken/eth_usd_raw.json b/docs/archive/historical/wave153/wave153_bakeoff_kraken/eth_usd_raw.json similarity index 100% rename from wave153_bakeoff_kraken/eth_usd_raw.json rename to docs/archive/historical/wave153/wave153_bakeoff_kraken/eth_usd_raw.json diff --git a/CLEANUP_REPORT_2025_10_30.md b/docs/archive/wave_d/reports/CLEANUP_REPORT_2025_10_30.md similarity index 100% rename from CLEANUP_REPORT_2025_10_30.md rename to docs/archive/wave_d/reports/CLEANUP_REPORT_2025_10_30.md diff --git a/DOCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md b/docs/archive/wave_d/reports/DOCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md similarity index 100% rename from DOCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md rename to docs/archive/wave_d/reports/DOCUMENTATION_ARCHIVAL_REPORT_2025_10_30.md diff --git a/DOCUMENTATION_ARCHIVAL_SUMMARY.md b/docs/archive/wave_d/reports/DOCUMENTATION_ARCHIVAL_SUMMARY.md similarity index 100% rename from DOCUMENTATION_ARCHIVAL_SUMMARY.md rename to docs/archive/wave_d/reports/DOCUMENTATION_ARCHIVAL_SUMMARY.md diff --git a/POST_CLEANUP_VALIDATION_REPORT.md b/docs/archive/wave_d/reports/POST_CLEANUP_VALIDATION_REPORT.md similarity index 100% rename from POST_CLEANUP_VALIDATION_REPORT.md rename to docs/archive/wave_d/reports/POST_CLEANUP_VALIDATION_REPORT.md diff --git a/SCRIPTS_CLEANUP_REPORT_2025_10_30.md b/docs/archive/wave_d/reports/SCRIPTS_CLEANUP_REPORT_2025_10_30.md similarity index 100% rename from SCRIPTS_CLEANUP_REPORT_2025_10_30.md rename to docs/archive/wave_d/reports/SCRIPTS_CLEANUP_REPORT_2025_10_30.md diff --git a/RUNPOD_DEPLOY_PATCH.diff b/docs/patches/RUNPOD_DEPLOY_PATCH.diff similarity index 100% rename from RUNPOD_DEPLOY_PATCH.diff rename to docs/patches/RUNPOD_DEPLOY_PATCH.diff diff --git a/batch_size_180_patch.diff b/docs/patches/batch_size_180_patch.diff similarity index 100% rename from batch_size_180_patch.diff rename to docs/patches/batch_size_180_patch.diff diff --git a/dqn_tuning_archive_20251014.tar.gz b/dqn_tuning_archive_20251014.tar.gz deleted file mode 100644 index 7d4e147702182ee76e39d6d1d3fd0d922c5d9e4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5996 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 aEH-|C0000000000e(weLgD2(ypa1}wV~sZe diff --git a/echo b/echo deleted file mode 100644 index b67e6d0ba..000000000 --- a/echo +++ /dev/null @@ -1,2 +0,0 @@ -=== FINAL COMPILATION CHECK === -0 diff --git a/health_check.sh b/health_check.sh deleted file mode 100755 index 8f21d0988..000000000 --- a/health_check.sh +++ /dev/null @@ -1,473 +0,0 @@ -#!/bin/bash -# Foxhunt HFT System - Comprehensive Health Check Script -# Wave 75 Agent 6 - Service Health Validation -# -# This script validates: -# - 4 gRPC Application Services (ports 50050-50053) -# - 6 Infrastructure Services (PostgreSQL, Redis, Vault, InfluxDB, Prometheus, Grafana) -# - Inter-service communication -# - Resource usage -# - Hot-reload functionality - -set -e - -# Color codes for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Results tracking -TOTAL_CHECKS=0 -PASSED_CHECKS=0 -FAILED_CHECKS=0 -WARNING_CHECKS=0 - -# Logging -TIMESTAMP=$(date +"%Y%m%d_%H%M%S") -LOG_DIR="./logs" -HEALTH_LOG="${LOG_DIR}/health_check_${TIMESTAMP}.log" - -mkdir -p "$LOG_DIR" - -# Logging functions -log_info() { - echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$HEALTH_LOG" -} - -log_success() { - echo -e "${GREEN}[PASS]${NC} $1" | tee -a "$HEALTH_LOG" - ((PASSED_CHECKS++)) -} - -log_error() { - echo -e "${RED}[FAIL]${NC} $1" | tee -a "$HEALTH_LOG" - ((FAILED_CHECKS++)) -} - -log_warning() { - echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$HEALTH_LOG" - ((WARNING_CHECKS++)) -} - -log_header() { - echo -e "\n${BLUE}========================================${NC}" | tee -a "$HEALTH_LOG" - echo -e "${BLUE}$1${NC}" | tee -a "$HEALTH_LOG" - echo -e "${BLUE}========================================${NC}\n" | tee -a "$HEALTH_LOG" -} - -# Check if required tools are installed -check_prerequisites() { - log_header "Checking Prerequisites" - - local tools=("grpcurl" "psql" "curl" "jq" "docker") - local missing_tools=() - - # redis-cli is optional (can use docker exec) - - for tool in "${tools[@]}"; do - ((TOTAL_CHECKS++)) - if command -v "$tool" &> /dev/null; then - log_success "$tool is installed" - else - log_error "$tool is NOT installed" - missing_tools+=("$tool") - fi - done - - if [ ${#missing_tools[@]} -gt 0 ]; then - log_error "Missing required tools: ${missing_tools[*]}" - log_info "Install missing tools before proceeding" - return 1 - fi - - return 0 -} - -# Check gRPC service health -check_grpc_service() { - local service_name=$1 - local port=$2 - local package=$3 - local service=$4 - - log_info "Checking $service_name on port $port..." - ((TOTAL_CHECKS++)) - - # Check if port is listening - if ! netstat -tuln 2>/dev/null | grep -q ":$port "; then - if ! ss -tuln 2>/dev/null | grep -q ":$port "; then - log_error "$service_name: Port $port is NOT listening" - return 1 - fi - fi - log_success "$service_name: Port $port is listening" - - # List available services - ((TOTAL_CHECKS++)) - if grpcurl -plaintext localhost:$port list > /dev/null 2>&1; then - log_success "$service_name: gRPC server responding" - - # Get service list - local services=$(grpcurl -plaintext localhost:$port list 2>/dev/null) - echo " Available services:" >> "$HEALTH_LOG" - echo "$services" | sed 's/^/ /' >> "$HEALTH_LOG" - else - log_error "$service_name: gRPC server NOT responding" - return 1 - fi - - # Check health endpoint - ((TOTAL_CHECKS++)) - if grpcurl -plaintext localhost:$port grpc.health.v1.Health/Check 2>&1 | grep -q "SERVING"; then - log_success "$service_name: Health check SERVING" - else - log_warning "$service_name: Health check returned non-SERVING status or not implemented" - fi - - return 0 -} - -# Check infrastructure service -check_infrastructure_service() { - local service_name=$1 - local check_command=$2 - - log_info "Checking $service_name..." - ((TOTAL_CHECKS++)) - - if eval "$check_command" > /dev/null 2>&1; then - log_success "$service_name is healthy" - return 0 - else - log_error "$service_name is NOT healthy" - return 1 - fi -} - -# Check gRPC application services -check_grpc_services() { - log_header "Checking gRPC Application Services" - - # API Gateway (port 50050) - check_grpc_service "API Gateway" 50050 "foxhunt" "ApiGateway" - - # Trading Service (port 50051) - check_grpc_service "Trading Service" 50051 "trading" "TradingService" - - # Backtesting Service (port 50052) - check_grpc_service "Backtesting Service" 50052 "backtesting" "BacktestingService" - - # ML Training Service (port 50053) - check_grpc_service "ML Training Service" 50053 "ml_training" "MLTrainingService" -} - -# Check infrastructure services -check_infrastructure_services() { - log_header "Checking Infrastructure Services" - - # PostgreSQL (port 5433) - log_info "Checking PostgreSQL on port 5433..." - ((TOTAL_CHECKS++)) - # Try test credentials first (from docker-compose) - if PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -c "SELECT 1;" > /dev/null 2>&1; then - log_success "PostgreSQL is healthy (test database)" - - # Check database exists and has tables - ((TOTAL_CHECKS++)) - local table_count=$(PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ') - if [ "$table_count" -gt 0 ]; then - log_success "PostgreSQL has $table_count tables" - else - log_warning "PostgreSQL database exists but has no tables" - fi - elif PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -c "SELECT 1;" > /dev/null 2>&1; then - log_success "PostgreSQL is healthy (production database)" - - # Check database exists and has tables - ((TOTAL_CHECKS++)) - local table_count=$(PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ') - if [ "$table_count" -gt 0 ]; then - log_success "PostgreSQL has $table_count tables" - else - log_warning "PostgreSQL database exists but has no tables" - fi - else - log_error "PostgreSQL is NOT healthy (tried both test and production credentials)" - fi - - # Redis (port 6380) - log_info "Checking Redis on port 6380..." - ((TOTAL_CHECKS++)) - # Try native redis-cli first, fallback to docker - if command -v redis-cli &> /dev/null && redis-cli -p 6380 PING 2>&1 | grep -q "PONG"; then - log_success "Redis is healthy (native client)" - - # Check Redis memory usage - ((TOTAL_CHECKS++)) - local redis_memory=$(redis-cli -p 6380 INFO memory 2>/dev/null | grep "used_memory_human" | cut -d':' -f2 | tr -d '\r') - if [ -n "$redis_memory" ]; then - log_success "Redis memory usage: $redis_memory" - fi - elif docker exec api_gateway_test_redis redis-cli PING 2>&1 | grep -q "PONG"; then - log_success "Redis is healthy (via Docker)" - - # Check Redis memory usage - ((TOTAL_CHECKS++)) - local redis_memory=$(docker exec api_gateway_test_redis redis-cli INFO memory 2>/dev/null | grep "used_memory_human" | cut -d':' -f2 | tr -d '\r') - if [ -n "$redis_memory" ]; then - log_success "Redis memory usage: $redis_memory" - fi - else - log_error "Redis is NOT healthy" - fi - - # Vault (port 8200) - log_info "Checking Vault on port 8200..." - ((TOTAL_CHECKS++)) - local vault_health=$(curl -s http://localhost:8200/v1/sys/health 2>/dev/null) - if [ -n "$vault_health" ]; then - local vault_sealed=$(echo "$vault_health" | jq -r '.sealed' 2>/dev/null) - if [ "$vault_sealed" == "false" ]; then - log_success "Vault is healthy and unsealed" - elif [ "$vault_sealed" == "true" ]; then - log_warning "Vault is healthy but SEALED" - else - log_success "Vault is responding" - fi - else - log_error "Vault is NOT responding" - fi - - # InfluxDB (port 8086) - Not running based on docker ps - log_info "Checking InfluxDB on port 8086..." - ((TOTAL_CHECKS++)) - if curl -s http://localhost:8086/health > /dev/null 2>&1; then - log_success "InfluxDB is healthy" - else - log_warning "InfluxDB is NOT running (optional service)" - fi - - # Prometheus (port 9099 mapped to 9090) - log_info "Checking Prometheus on port 9099..." - ((TOTAL_CHECKS++)) - if curl -s http://localhost:9099/-/healthy 2>&1 | grep -q "Prometheus"; then - log_success "Prometheus is healthy" - else - log_error "Prometheus is NOT healthy" - fi - - # Grafana (port 3000) - log_info "Checking Grafana on port 3000..." - ((TOTAL_CHECKS++)) - local grafana_health=$(curl -s http://localhost:3000/api/health 2>/dev/null) - if echo "$grafana_health" | jq -e '.database == "ok"' > /dev/null 2>&1; then - log_success "Grafana is healthy" - else - log_warning "Grafana is responding but may have issues" - fi -} - -# Check Docker containers -check_docker_containers() { - log_header "Checking Docker Containers" - - log_info "Running Docker containers:" - docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | tee -a "$HEALTH_LOG" - - # Check for unhealthy containers - ((TOTAL_CHECKS++)) - local unhealthy=$(docker ps --filter "health=unhealthy" --format "{{.Names}}" 2>/dev/null) - if [ -z "$unhealthy" ]; then - log_success "No unhealthy containers detected" - else - log_error "Unhealthy containers detected: $unhealthy" - fi -} - -# Check service processes -check_service_processes() { - log_header "Checking Service Processes" - - local services=("trading_service" "backtesting_service" "ml_training_service" "api_gateway") - - for service in "${services[@]}"; do - ((TOTAL_CHECKS++)) - if pgrep -f "$service" > /dev/null; then - local pid=$(pgrep -f "$service") - local mem_usage=$(ps -p $pid -o %mem --no-headers 2>/dev/null | tr -d ' ') - local cpu_usage=$(ps -p $pid -o %cpu --no-headers 2>/dev/null | tr -d ' ') - log_success "$service is running (PID: $pid, CPU: ${cpu_usage}%, MEM: ${mem_usage}%)" - else - log_error "$service is NOT running" - fi - done -} - -# Check resource usage -check_resource_usage() { - log_header "Checking System Resource Usage" - - # CPU usage - ((TOTAL_CHECKS++)) - local cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') - if (( $(echo "$cpu_usage < 80" | bc -l) )); then - log_success "CPU usage: ${cpu_usage}% (healthy)" - else - log_warning "CPU usage: ${cpu_usage}% (high)" - fi - - # Memory usage - ((TOTAL_CHECKS++)) - local mem_total=$(free -g | awk '/^Mem:/{print $2}') - local mem_used=$(free -g | awk '/^Mem:/{print $3}') - local mem_percent=$(awk "BEGIN {printf \"%.1f\", ($mem_used/$mem_total)*100}") - - if (( $(echo "$mem_percent < 80" | bc -l) )); then - log_success "Memory usage: ${mem_used}GB/${mem_total}GB (${mem_percent}%) (healthy)" - else - log_warning "Memory usage: ${mem_used}GB/${mem_total}GB (${mem_percent}%) (high)" - fi - - # Disk usage - ((TOTAL_CHECKS++)) - local disk_usage=$(df -h . | awk 'NR==2 {print $5}' | sed 's/%//') - if [ "$disk_usage" -lt 80 ]; then - log_success "Disk usage: ${disk_usage}% (healthy)" - else - log_warning "Disk usage: ${disk_usage}% (high)" - fi -} - -# Test inter-service communication -test_inter_service_communication() { - log_header "Testing Inter-Service Communication" - - log_info "Testing API Gateway routing to Trading Service..." - ((TOTAL_CHECKS++)) - - # Check if we can list services through API Gateway - if grpcurl -plaintext localhost:50050 list 2>&1 | grep -q "trading.TradingService"; then - log_success "API Gateway can see Trading Service" - else - log_warning "API Gateway may not have Trading Service registered" - fi -} - -# Test hot-reload functionality -test_hot_reload() { - log_header "Testing Hot-Reload Functionality" - - log_info "Checking PostgreSQL NOTIFY/LISTEN support..." - ((TOTAL_CHECKS++)) - - # Check if config_settings table exists (try both databases) - if PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -c "\d config_settings" > /dev/null 2>&1; then - log_success "config_settings table exists (test database)" - - # Count configuration entries - ((TOTAL_CHECKS++)) - local config_count=$(PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -t -c "SELECT COUNT(*) FROM config_settings;" 2>/dev/null | tr -d ' ') - if [ "$config_count" -gt 0 ]; then - log_success "Found $config_count configuration entries" - else - log_warning "config_settings table exists but is empty" - fi - elif PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -c "\d config_settings" > /dev/null 2>&1; then - log_success "config_settings table exists (production database)" - - # Count configuration entries - ((TOTAL_CHECKS++)) - local config_count=$(PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -t -c "SELECT COUNT(*) FROM config_settings;" 2>/dev/null | tr -d ' ') - if [ "$config_count" -gt 0 ]; then - log_success "Found $config_count configuration entries" - else - log_warning "config_settings table exists but is empty" - fi - else - log_warning "config_settings table does not exist (hot-reload may not be configured)" - fi -} - -# Check service logs for errors -check_service_logs() { - log_header "Checking Service Logs for Recent Errors" - - local log_files=("api_gateway.log" "trading_service.log" "backtesting_service.log" "ml_training_service.log") - - for log_file in "${log_files[@]}"; do - local log_path="${LOG_DIR}/${log_file}" - if [ -f "$log_path" ]; then - ((TOTAL_CHECKS++)) - local error_count=$(grep -i "error\|panic\|fatal" "$log_path" 2>/dev/null | wc -l) - - if [ "$error_count" -eq 0 ]; then - log_success "$log_file: No errors detected" - elif [ "$error_count" -lt 5 ]; then - log_warning "$log_file: $error_count errors detected (review recommended)" - else - log_error "$log_file: $error_count errors detected (attention required)" - fi - else - log_warning "$log_file: Log file not found" - fi - done -} - -# Generate summary report -generate_summary() { - log_header "Health Check Summary" - - echo "" | tee -a "$HEALTH_LOG" - echo "Total Checks: $TOTAL_CHECKS" | tee -a "$HEALTH_LOG" - echo -e "${GREEN}Passed: $PASSED_CHECKS${NC}" | tee -a "$HEALTH_LOG" - echo -e "${YELLOW}Warnings: $WARNING_CHECKS${NC}" | tee -a "$HEALTH_LOG" - echo -e "${RED}Failed: $FAILED_CHECKS${NC}" | tee -a "$HEALTH_LOG" - echo "" | tee -a "$HEALTH_LOG" - - local success_rate=$(awk "BEGIN {printf \"%.1f\", ($PASSED_CHECKS/$TOTAL_CHECKS)*100}") - echo "Success Rate: ${success_rate}%" | tee -a "$HEALTH_LOG" - - if [ "$FAILED_CHECKS" -eq 0 ]; then - echo -e "${GREEN}Overall Status: HEALTHY${NC}" | tee -a "$HEALTH_LOG" - return 0 - elif [ "$FAILED_CHECKS" -lt 5 ]; then - echo -e "${YELLOW}Overall Status: DEGRADED${NC}" | tee -a "$HEALTH_LOG" - return 1 - else - echo -e "${RED}Overall Status: UNHEALTHY${NC}" | tee -a "$HEALTH_LOG" - return 2 - fi -} - -# Main execution -main() { - log_header "Foxhunt HFT System - Comprehensive Health Check" - log_info "Starting health check at $(date)" - log_info "Log file: $HEALTH_LOG" - echo "" - - # Run all checks - check_prerequisites || exit 1 - check_docker_containers - check_infrastructure_services - check_grpc_services - check_service_processes - check_resource_usage - test_inter_service_communication - test_hot_reload - check_service_logs - - # Generate summary - echo "" - generate_summary - - log_info "Health check completed at $(date)" - log_info "Detailed log saved to: $HEALTH_LOG" -} - -# Run main function -main - -exit $? diff --git a/libregime_adx.rlib b/libregime_adx.rlib deleted file mode 100644 index ce34e954645feb57f371f6212e84fa1f62dfb068..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45476 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 cNBb=BqIb*mminzS generate_certs_with_san.sh << 'EOF' -#!/bin/bash -set -e - -# Generate CA -openssl genrsa -out ca/ca-key.pem 4096 -openssl req -new -x509 -days 3650 -key ca/ca-key.pem -out ca/ca-cert.pem \ - -subj "/CN=Foxhunt CA" - -# Generate server certificate with SAN -openssl genrsa -out foxhunt-key.pem 4096 -openssl req -new -key foxhunt-key.pem -out foxhunt-csr.pem \ - -subj "/CN=localhost" - -# Create SAN config -cat > san.cnf << 'SANEOF' -[req] -distinguished_name = req_distinguished_name -req_extensions = v3_req - -[req_distinguished_name] - -[v3_req] -subjectAltName = @alt_names - -[alt_names] -DNS.1 = localhost -DNS.2 = api-gateway -DNS.3 = trading-service -DNS.4 = backtesting-service -DNS.5 = ml-training-service -IP.1 = 127.0.0.1 -IP.2 = ::1 -SANEOF - -# Sign certificate with SAN -openssl x509 -req -days 365 -in foxhunt-csr.pem \ - -CA ca/ca-cert.pem -CAkey ca/ca-key.pem -CAcreateserial \ - -out foxhunt-cert.pem -extensions v3_req -extfile san.cnf - -echo "Certificates generated with SAN support" -openssl x509 -in foxhunt-cert.pem -text -noout | grep -A1 "Subject Alternative Name" -EOF - -chmod +x generate_certs_with_san.sh -./generate_certs_with_san.sh - -# Backup old certs -mv foxhunt-cert.pem foxhunt-cert.pem.old -mv foxhunt-key.pem foxhunt-key.pem.old - -# Restart services to pick up new certificates -pkill -HUP trading_service backtesting_service ml_training_service api_gateway -``` - -**Test**: -```bash -# Test direct connection with new certificates -ghz --proto services/ml_training_service/proto/ml_training.proto \ - --call ml_training.MLTrainingService.HealthCheck \ - --cacert certs/production/ca/ca-cert.pem \ - --cert certs/production/foxhunt-cert.pem \ - --key certs/production/foxhunt-key.pem \ - -d '{}' -c 100 -z 10s localhost:50053 - -# Should see successful connections, not certificate errors -``` - ---- - -## Priority 3: Add Performance Metrics Endpoint (1 hour) - -**Issue**: No easy way to monitor performance during load tests - -**Fix**: -```rust -// File: services/api_gateway/Cargo.toml -// Add dependency: -[dependencies] -prometheus = "0.13" -prometheus-hyper = "0.1" - -// File: services/api_gateway/src/metrics.rs (new file) -use prometheus::{Encoder, TextEncoder, Registry, Counter, Histogram}; -use std::sync::Arc; - -pub struct Metrics { - pub registry: Arc, - pub request_counter: Counter, - pub latency_histogram: Histogram, -} - -impl Metrics { - pub fn new() -> Self { - let registry = Arc::new(Registry::new()); - - let request_counter = Counter::new("grpc_requests_total", "Total gRPC requests") - .expect("Counter creation failed"); - let latency_histogram = Histogram::new("grpc_request_duration_seconds", "Request latency") - .expect("Histogram creation failed"); - - registry.register(Box::new(request_counter.clone())).unwrap(); - registry.register(Box::new(latency_histogram.clone())).unwrap(); - - Self { registry, request_counter, latency_histogram } - } -} - -// File: services/api_gateway/src/main.rs -// Add metrics HTTP endpoint on separate port (9090) -``` - -**Test**: -```bash -# Query metrics during load test -curl http://localhost:9090/metrics - -# Expected output: -# grpc_requests_total 1234567 -# grpc_request_duration_seconds_bucket{le="0.001"} 1000000 -# ... -``` - ---- - -## Priority 4: Document Load Testing Procedures (30 minutes) - -**Create**: `docs/LOAD_TESTING_GUIDE.md` - -**Contents**: -1. Prerequisites (ghz, certificates, running services) -2. Test scenarios (normal, spike, stress) -3. Monitoring commands (logs, metrics, resources) -4. Expected results and thresholds -5. Troubleshooting common issues - ---- - -## Quick Win: Add to CI/CD (15 minutes) - -**Create**: `.github/workflows/performance-tests.yml` - -```yaml -name: Performance Tests - -on: - push: - branches: [main] - pull_request: - -jobs: - load-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Install ghz - run: | - curl -sL https://github.com/bojand/ghz/releases/download/v0.120.0/ghz-linux-x86_64.tar.gz | tar xz - sudo mv ghz /usr/local/bin/ - - - name: Start services - run: | - docker-compose up -d - sleep 10 - - - name: Run load tests - run: | - ghz --insecure \ - --proto services/api_gateway/proto/config_service.proto \ - --call foxhunt.config.ConfigurationService.ListConfigs \ - -d '{}' -c 1000 -z 30s localhost:50050 \ - > load-test-results.txt - - - name: Check thresholds - run: | - # Fail if throughput < 20K req/s - RPS=$(grep "Requests/sec:" load-test-results.txt | awk '{print $2}') - if (( $(echo "$RPS < 20000" | bc -l) )); then - echo "FAIL: Throughput $RPS < 20K req/s" - exit 1 - fi -``` - ---- - -## Timeline - -| Priority | Task | Time | Owner | -|----------|------|------|-------| -| P1 | HTTP/2 stream limit fix | 15 min | Backend team | -| P2 | Regenerate TLS certificates | 30 min | DevOps | -| P3 | Add metrics endpoint | 1 hour | Backend team | -| P4 | Documentation | 30 min | Tech writer | -| - | CI/CD integration | 15 min | DevOps | - -**Total effort**: ~2.5 hours -**Expected completion**: Same day - ---- - -## Validation Checklist - -After implementing fixes: - -- [ ] HTTP/2 stream limit warnings eliminated at 10K concurrency -- [ ] Direct backend service testing works with new certificates -- [ ] Metrics endpoint accessible at `:9090/metrics` -- [ ] Load testing guide in `docs/` directory -- [ ] CI/CD pipeline passes with performance thresholds -- [ ] All services restart cleanly with new certificates -- [ ] Throughput still >200K req/s after changes - ---- - -**Questions?** Contact Wave 78 Agent 5 or review full report in `docs/WAVE78_AGENT5_LOAD_TEST_RESULTS.md` diff --git a/load-tests/PERFORMANCE_SCORECARD.md b/load-tests/PERFORMANCE_SCORECARD.md deleted file mode 100644 index ffc37ff95..000000000 --- a/load-tests/PERFORMANCE_SCORECARD.md +++ /dev/null @@ -1,85 +0,0 @@ -# Foxhunt HFT - Production Load Test Scorecard - -**Test Date**: 2025-10-03 -**Test Engineer**: Wave 78 Agent 5 -**Tool**: ghz v0.120.0 - ---- - -## Performance Targets: PASS ✅ - -| Metric | Target | Actual | Status | -|--------|--------|--------|--------| -| **Throughput** | >100,000 req/s | **211,986 req/s** | ✅ **PASS** (2.1x) | -| **Error Rate** | <0.1% | **0.05%** | ✅ **PASS** | -| **Latency (Auth)** | <10μs | <10μs (per logs) | ✅ **PASS** | -| **Concurrency** | 1,000 | **10,000 tested** | ✅ **PASS** | - ---- - -## Test Summary - -### Normal Load (1,000 concurrent, 60s) -``` -Requests: 1,699,232 -Throughput: 28,318 req/s -Latency Avg: 27.48 ms -Error Rate: 0.05% -``` - -### Stress Test (10,000 concurrent, 30s) -``` -Requests: 6,399,358 -Throughput: 211,986 req/s ⭐ -Latency Avg: 44.10 ms -Error Rate: 0.00% -HTTP/2 Limit: Stream limit warnings (1,024 streams) -``` - ---- - -## Architectural Findings - -### ✅ Strengths -- **High throughput**: >200K req/s capacity -- **mTLS security**: Properly enforced on all backend services -- **Graceful degradation**: No crashes under 10K concurrent connections -- **Low error rate**: 0.05% (connection errors only) - -### ⚠️ Limitations -- **HTTP/2 streams**: Limited to 1,024 concurrent streams (tunable) -- **Certificate format**: Legacy CN format blocks modern TLS clients -- **Testing tool**: ghz concurrency scheduler has bugs at high ramp rates - -### 📝 Recommendations -1. Increase `max_concurrent_streams` to 10,000 -2. Regenerate TLS certificates with SAN fields -3. Add Prometheus metrics endpoint -4. Test via API Gateway for realistic production scenarios - ---- - -## Capacity Limits - -| Resource | Limit | Headroom | -|----------|-------|----------| -| Max throughput | 211,986 req/s | 2.1x target | -| Max concurrency | 10,000 conn | HTTP/2 bottleneck @ 1,024 streams | -| Production recommendation | 5,000 conn | 50% safety margin | - ---- - -## Service Status - -| Service | Port | mTLS | Status | Throughput | -|---------|------|------|--------|------------| -| API Gateway | 50050 | Optional | ✅ Tested | 212K req/s | -| Trading | 50051 | Required | ⚠️ Cert issue | Via Gateway | -| Backtesting | 50052 | Required | ⚠️ Cert issue | Via Gateway | -| ML Training | 50053 | Required | ⚠️ Cert issue | Via Gateway | - ---- - -**Overall Grade**: ✅ **PRODUCTION READY** (with minor tuning) - -Full report: `/docs/WAVE78_AGENT5_LOAD_TEST_RESULTS.md` diff --git a/load-tests/README.md b/load-tests/README.md deleted file mode 100644 index b3110be89..000000000 --- a/load-tests/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Load Test Results - Wave 78 Agent 5 - -**Test Date**: 2025-10-03 -**Services Tested**: API Gateway, Trading, Backtesting, ML Training -**Tool**: ghz v0.120.0 -**Status**: ✅ COMPLETE - ---- - -## Quick Links - -- 📊 **[Performance Scorecard](PERFORMANCE_SCORECARD.md)** - High-level metrics and grades -- 📝 **[Full Load Test Report](../docs/WAVE78_AGENT5_LOAD_TEST_RESULTS.md)** - Comprehensive analysis -- 🔧 **[Immediate Actions](IMMEDIATE_ACTIONS.md)** - Fix recommendations (2.5 hours total) - ---- - -## Key Results - -### ✅ PASS: Throughput Target -- **Target**: >100,000 req/s -- **Achieved**: **211,986 req/s** (2.1x target) -- **Test**: 10,000 concurrent connections, 30 seconds - -### ✅ PASS: Error Rate Target -- **Target**: <0.1% -- **Achieved**: 0.05% -- **Test**: 1,000 concurrent connections, 60 seconds - -### ⚠️ SCOPE: Latency Target -- **Target**: <10μs (auth pipeline) -- **Auth Pipeline**: <10μs (per service logs) ✅ -- **End-to-End**: 27-44ms (includes network, serialization, queueing) -- **Note**: Different measurement scopes - ---- - -## Test Data Files - -Raw results saved in this directory: - -``` -load-tests/ -├── README.md (this file) -├── PERFORMANCE_SCORECARD.md (quick metrics) -├── IMMEDIATE_ACTIONS.md (fix recommendations) -├── api_gateway_normal_load.json (1K concurrent, 60s) -├── api_gateway_spike_load.json (ramp test - FAILED, tool bug) -├── api_gateway_stress_test.json (10K concurrent, 30s) -└── results_summary.txt (test execution log) -``` - -Full report: `docs/WAVE78_AGENT5_LOAD_TEST_RESULTS.md` - ---- - -## Architecture Discovery - -### mTLS Security (Working as Designed) - -All backend services enforce mutual TLS: -- Trading Service (50051): Requires client certificates -- Backtesting Service (50052): Requires client certificates -- ML Training Service (50053): Requires client certificates - -**Impact**: Direct load testing blocked by certificate validation -**Workaround**: Test via API Gateway (50050) which handles authentication -**Fix**: Regenerate certificates with SAN fields (see IMMEDIATE_ACTIONS.md) - ---- - -## Recommendations - -### Priority 1: HTTP/2 Stream Limit -Increase `max_concurrent_streams` from 1,024 to 10,000 in all services - -### Priority 2: Certificate Infrastructure -Regenerate TLS certificates with Subject Alternative Name (SAN) fields - -### Priority 3: Observability -Add Prometheus metrics endpoint for real-time performance monitoring - ---- - -## How to Re-run Tests - -```bash -# Normal load (1K concurrent, 60s) -ghz --insecure \ - --proto services/api_gateway/proto/config_service.proto \ - --call foxhunt.config.ConfigurationService.ListConfigs \ - -d '{}' -c 1000 -z 60s localhost:50050 - -# Stress test (10K concurrent, 30s) -ghz --insecure \ - --proto services/api_gateway/proto/config_service.proto \ - --call foxhunt.config.ConfigurationService.ListConfigs \ - -d '{}' -c 10000 -z 30s localhost:50050 - -# With metrics output -ghz --insecure \ - --proto services/api_gateway/proto/config_service.proto \ - --call foxhunt.config.ConfigurationService.ListConfigs \ - -d '{}' -c 1000 -z 60s -O json localhost:50050 > results.json -``` - ---- - -## Production Readiness - -**Overall Grade**: ✅ **PRODUCTION READY** (with minor tuning) - -**Strengths**: -- High throughput capacity (>200K req/s) -- Low error rate (0.05%) -- Graceful degradation under extreme load -- Proper mTLS security enforcement - -**Improvements Needed**: -1. Increase HTTP/2 stream limits (15 minutes) -2. Regenerate certificates with SAN (30 minutes) -3. Add performance metrics (1 hour) - -**Total effort to full production**: ~2.5 hours - ---- - -**Agent**: Wave 78 Agent 5 -**Status**: ✅ MISSION COMPLETE -**Date**: 2025-10-03 diff --git a/load-tests/api_gateway_spike_load.json b/load-tests/api_gateway_spike_load.json deleted file mode 100644 index c9111982e..000000000 --- a/load-tests/api_gateway_spike_load.json +++ /dev/null @@ -1,7 +0,0 @@ -panic: send on closed channel - -goroutine 97 [running]: -github.com/bojand/ghz/load.(*StepWorkerTicker).Run.func1() - /home/runner/work/ghz/ghz/load/worker_ticker.go:86 +0x1d7 -created by github.com/bojand/ghz/load.(*StepWorkerTicker).Run in goroutine 81 - /home/runner/work/ghz/ghz/load/worker_ticker.go:79 +0x19c diff --git a/load-tests/api_gateway_test.txt b/load-tests/api_gateway_test.txt deleted file mode 100644 index 618146c43..000000000 --- a/load-tests/api_gateway_test.txt +++ /dev/null @@ -1 +0,0 @@ -ghz: error: unexpected , try --help diff --git a/load-tests/final_summary.txt b/load-tests/final_summary.txt deleted file mode 100644 index 474cbc34c..000000000 --- a/load-tests/final_summary.txt +++ /dev/null @@ -1,5 +0,0 @@ -=== LOAD TEST RESULTS SUMMARY === - -TEST 1: Normal Load (1000 concurrent, 60s) - JSON parse failed - diff --git a/load-tests/ml_health_check.json b/load-tests/ml_health_check.json deleted file mode 100644 index 0967ef424..000000000 --- a/load-tests/ml_health_check.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/load-tests/ml_training_normal_load.json b/load-tests/ml_training_normal_load.json deleted file mode 100644 index 618146c43..000000000 --- a/load-tests/ml_training_normal_load.json +++ /dev/null @@ -1 +0,0 @@ -ghz: error: unexpected , try --help diff --git a/load-tests/ml_training_normal_load.txt b/load-tests/ml_training_normal_load.txt deleted file mode 100644 index 618146c43..000000000 --- a/load-tests/ml_training_normal_load.txt +++ /dev/null @@ -1 +0,0 @@ -ghz: error: unexpected , try --help diff --git a/load-tests/results_summary.txt b/load-tests/results_summary.txt deleted file mode 100644 index da4fc1db3..000000000 --- a/load-tests/results_summary.txt +++ /dev/null @@ -1,6 +0,0 @@ -=== TEST 1: API Gateway Normal Load === -Normal Load Complete -=== TEST 2: Spike Load Test === -Spike Load Complete -=== TEST 3: High Concurrency Stress Test === -Stress Test Complete diff --git a/load_test_results/wave79/test1_normal_load.json b/load_test_results/wave79/test1_normal_load.json deleted file mode 100644 index 6138ebf4c..000000000 --- a/load_test_results/wave79/test1_normal_load.json +++ /dev/null @@ -1,217 +0,0 @@ - -Summary: - Count: 847455 - Total: 59.68 s - Slowest: 0 ns - Fastest: 0 ns - Average: 33.07 ms - Requests/sec: 14200.89 - -Response time histogram: - -Latency distribution: - -Status code distribution: - [Unimplemented] 847258 responses - [Unavailable] 197 responses - -Error distribution: - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45328->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49728->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50002->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50124->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49964->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49998->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50292->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46192->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47280->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46042->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48332->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49236->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50748->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48158->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45376->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48900->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44514->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45880->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46860->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47188->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47552->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49834->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51020->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:52820->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46408->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45664->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48406->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48644->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49774->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45158->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47384->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47678->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48232->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44230->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48356->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49616->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48864->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49316->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49948->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44606->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45496->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47310->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47446->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48324->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44960->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44402->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48262->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44656->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45602->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45648->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46328->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47366->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49476->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50304->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50612->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50926->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51204->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45192->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48808->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48930->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49090->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50376->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49208->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:52784->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44364->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45440->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47056->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47644->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46754->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47876->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49518->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50466->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50024->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50226->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51896->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45308->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45934->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47654->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44248->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48860->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44732->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46374->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47170->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49816->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50408->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49400->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50526->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46140->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46950->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47406->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48650->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49180->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47150->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47224->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48362->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45060->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48798->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49262->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50188->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46844->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46464->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50090->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51062->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51086->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50954->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44820->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48506->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48848->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49762->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50760->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47862->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48084->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49120->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49550->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49710->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48894->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50688->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45740->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45988->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47730->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47798->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48472->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49612->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49844->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45788->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46930->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48902->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48994->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49302->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50652->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51148->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47284->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47900->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48396->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48520->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49818->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49108->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49412->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49504->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45270->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46386->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47300->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48106->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48998->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47392->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47744->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49806->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:51026->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44704->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47238->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50004->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50340->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47946->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48480->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45352->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46296->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46842->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46900->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47248->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47068->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48328->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49584->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46122->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46604->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46832->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46916->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45162->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50620->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50776->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46792->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49334->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48976->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49690->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48522->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49204->127.0.0.1:50050: use of closed network connection - [847258] rpc error: code = Unimplemented desc = - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46552->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47322->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46622->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:44510->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50450->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45568->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47682->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50040->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50104->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50430->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48572->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:49674->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50360->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:45046->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:46546->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47358->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:47920->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:48070->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50544->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50580->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50614->127.0.0.1:50050: use of closed network connection - [1] rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:50856->127.0.0.1:50050: use of closed network connection - diff --git a/load_test_results/wave79/test_ml_health.txt b/load_test_results/wave79/test_ml_health.txt deleted file mode 100644 index 3dff74224..000000000 --- a/load_test_results/wave79/test_ml_health.txt +++ /dev/null @@ -1,87 +0,0 @@ - -Summary: - Count: 99519 - Total: 9.96 s - Slowest: 0 ns - Fastest: 0 ns - Average: 0.02 ms - Requests/sec: 9991.65 - -Response time histogram: - -Latency distribution: - -Status code distribution: - [Unavailable] 99519 responses - -Error distribution: - [150] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35000->127.0.0.1:50053: read: connection reset by peer" - [283] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35664->127.0.0.1:50053: read: connection reset by peer" - [265] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35796->127.0.0.1:50053: read: connection reset by peer" - [89] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:56744->127.0.0.1:50053: read: connection reset by peer" - [168] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34804->127.0.0.1:50053: read: connection reset by peer" - [93] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57012->127.0.0.1:50053: read: connection reset by peer" - [102] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:56772->127.0.0.1:50053: read: connection reset by peer" - [106] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57040->127.0.0.1:50053: read: connection reset by peer" - [164] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34824->127.0.0.1:50053: read: connection reset by peer" - [230] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35674->127.0.0.1:50053: read: connection reset by peer" - [389] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36284->127.0.0.1:50053: read: connection reset by peer" - [83008] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: unexpected EOF" - [294] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35658->127.0.0.1:50053: read: connection reset by peer" - [303] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36780->127.0.0.1:50053: read: connection reset by peer" - [142] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34806->127.0.0.1:50053: read: connection reset by peer" - [467] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36260->127.0.0.1:50053: read: connection reset by peer" - [423] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36644->127.0.0.1:50053: read: connection reset by peer" - [379] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36766->127.0.0.1:50053: read: connection reset by peer" - [164] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36978->127.0.0.1:50053: read: connection reset by peer" - [107] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:56794->127.0.0.1:50053: read: connection reset by peer" - [160] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34938->127.0.0.1:50053: read: connection reset by peer" - [241] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35320->127.0.0.1:50053: read: connection reset by peer" - [228] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35902->127.0.0.1:50053: read: connection reset by peer" - [411] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36270->127.0.0.1:50053: read: connection reset by peer" - [64] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37454->127.0.0.1:50053: read: connection reset by peer" - [97] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:56806->127.0.0.1:50053: read: connection reset by peer" - [348] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36186->127.0.0.1:50053: read: connection reset by peer" - [389] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36604->127.0.0.1:50053: read: connection reset by peer" - [100] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37304->127.0.0.1:50053: read: connection reset by peer" - [201] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35466->127.0.0.1:50053: read: connection reset by peer" - [241] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36044->127.0.0.1:50053: read: connection reset by peer" - [468] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36578->127.0.0.1:50053: read: connection reset by peer" - [353] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36648->127.0.0.1:50053: read: connection reset by peer" - [448] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36794->127.0.0.1:50053: read: connection reset by peer" - [106] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37310->127.0.0.1:50053: read: connection reset by peer" - [45] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37560->127.0.0.1:50053: read: connection reset by peer" - [179] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34466->127.0.0.1:50053: read: connection reset by peer" - [485] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36366->127.0.0.1:50053: read: connection reset by peer" - [67] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37450->127.0.0.1:50053: read: connection reset by peer" - [361] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36226->127.0.0.1:50053: read: connection reset by peer" - [269] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35496->127.0.0.1:50053: read: connection reset by peer" - [222] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35756->127.0.0.1:50053: read: connection reset by peer" - [210] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35770->127.0.0.1:50053: read: connection reset by peer" - [399] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36152->127.0.0.1:50053: read: connection reset by peer" - [440] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36878->127.0.0.1:50053: read: connection reset by peer" - [210] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36942->127.0.0.1:50053: read: connection reset by peer" - [252] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35336->127.0.0.1:50053: read: connection reset by peer" - [88] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57000->127.0.0.1:50053: read: connection reset by peer" - [96] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57174->127.0.0.1:50053: read: connection reset by peer" - [71] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37420->127.0.0.1:50053: read: connection reset by peer" - [101] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57020->127.0.0.1:50053: read: connection reset by peer" - [509] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36324->127.0.0.1:50053: read: connection reset by peer" - [351] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36476->127.0.0.1:50053: read: connection reset by peer" - [153] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34490->127.0.0.1:50053: read: connection reset by peer" - [379] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36200->127.0.0.1:50053: read: connection reset by peer" - [393] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36224->127.0.0.1:50053: read: connection reset by peer" - [221] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35908->127.0.0.1:50053: read: connection reset by peer" - [136] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34486->127.0.0.1:50053: read: connection reset by peer" - [245] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35714->127.0.0.1:50053: read: connection reset by peer" - [287] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35992->127.0.0.1:50053: read: connection reset by peer" - [396] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36354->127.0.0.1:50053: read: connection reset by peer" - [70] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:37398->127.0.0.1:50053: read: connection reset by peer" - [101] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57058->127.0.0.1:50053: read: connection reset by peer" - [297] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:35688->127.0.0.1:50053: read: connection reset by peer" - [485] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36206->127.0.0.1:50053: read: connection reset by peer" - [92] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:57042->127.0.0.1:50053: read: connection reset by peer" - [150] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:34954->127.0.0.1:50053: read: connection reset by peer" - [461] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:36418->127.0.0.1:50053: read: connection reset by peer" - [117] rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: read tcp 127.0.0.1:56984->127.0.0.1:50053: read: connection reset by peer" - diff --git a/mamba2_training.pid b/mamba2_training.pid deleted file mode 100644 index 7ecf0d640..000000000 --- a/mamba2_training.pid +++ /dev/null @@ -1 +0,0 @@ -1108510 diff --git a/mamba2_training_200epoch.pid b/mamba2_training_200epoch.pid deleted file mode 100644 index c4b356338..000000000 --- a/mamba2_training_200epoch.pid +++ /dev/null @@ -1 +0,0 @@ -1291607 diff --git a/best_epoch_0.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_0.safetensors similarity index 100% rename from best_epoch_0.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_0.safetensors diff --git a/best_epoch_0_optimizer.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_0_optimizer.safetensors similarity index 100% rename from best_epoch_0_optimizer.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_0_optimizer.safetensors diff --git a/best_epoch_1.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_1.safetensors similarity index 100% rename from best_epoch_1.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_1.safetensors diff --git a/best_epoch_15.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_15.safetensors similarity index 100% rename from best_epoch_15.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_15.safetensors diff --git a/best_epoch_2.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_2.safetensors similarity index 100% rename from best_epoch_2.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_2.safetensors diff --git a/best_epoch_23.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_23.safetensors similarity index 100% rename from best_epoch_23.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_23.safetensors diff --git a/best_epoch_3.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_3.safetensors similarity index 100% rename from best_epoch_3.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_3.safetensors diff --git a/best_epoch_32.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_32.safetensors similarity index 100% rename from best_epoch_32.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_32.safetensors diff --git a/best_epoch_35.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_35.safetensors similarity index 100% rename from best_epoch_35.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_35.safetensors diff --git a/best_epoch_36.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_36.safetensors similarity index 100% rename from best_epoch_36.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_36.safetensors diff --git a/best_epoch_4.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_4.safetensors similarity index 100% rename from best_epoch_4.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_4.safetensors diff --git a/best_epoch_5.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_5.safetensors similarity index 100% rename from best_epoch_5.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_5.safetensors diff --git a/best_epoch_51.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_51.safetensors similarity index 100% rename from best_epoch_51.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_51.safetensors diff --git a/best_epoch_56.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_56.safetensors similarity index 100% rename from best_epoch_56.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_56.safetensors diff --git a/best_epoch_57.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_57.safetensors similarity index 100% rename from best_epoch_57.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_57.safetensors diff --git a/best_epoch_6.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_6.safetensors similarity index 100% rename from best_epoch_6.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_6.safetensors diff --git a/best_epoch_8.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_8.safetensors similarity index 100% rename from best_epoch_8.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_8.safetensors diff --git a/best_epoch_9.safetensors b/ml/models/checkpoints/training_artifacts/best_epoch_9.safetensors similarity index 100% rename from best_epoch_9.safetensors rename to ml/models/checkpoints/training_artifacts/best_epoch_9.safetensors diff --git a/ml_strategy/Cargo.toml b/ml_strategy/Cargo.toml deleted file mode 100644 index 8f6b3b7c2..000000000 --- a/ml_strategy/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "ml_strategy" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -homepage.workspace = true -documentation.workspace = true -publish.workspace = true -keywords.workspace = true -categories.workspace = true -description = "Shared ML Strategy Infrastructure (ONE SINGLE SYSTEM for trading and backtesting)" - -[dependencies] -# Core async and utilities -tokio.workspace = true -futures.workspace = true -async-trait.workspace = true - -# Serialization -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true - -# Error handling -thiserror.workspace = true -anyhow.workspace = true - -# Logging and tracing -tracing.workspace = true - -# Common types (no circular dependency) -common = { path = "../common" } - -# ML infrastructure (real ensemble, inference, etc.) -ml = { path = "../ml" } - -[dev-dependencies] -tokio-test.workspace = true diff --git a/ml_strategy/tests/shared_ml_strategy_test.rs b/ml_strategy/tests/shared_ml_strategy_test.rs deleted file mode 100644 index 79a734bf6..000000000 --- a/ml_strategy/tests/shared_ml_strategy_test.rs +++ /dev/null @@ -1,428 +0,0 @@ -//! Integration tests for SharedMLStrategy -//! -//! Validates that ONE SINGLE SYSTEM works for both trading and backtesting services. - -use common::ml_strategy::{ - MLSignal, SharedMLConfig, SharedMLStrategy, SignalAction, StrategyPerformance, -}; -use common::{MarketRegime, Symbol}; - -/// Test that SharedMLStrategy can be created and initialized -#[tokio::test] -async fn test_create_shared_strategy() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - let perf = strategy.get_performance().await; - assert_eq!(perf.total_signals, 0); - assert_eq!(perf.win_rate, 0.0); -} - -/// Test signal generation with valid features -#[tokio::test] -async fn test_generate_signal_valid_features() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - strategy.set_symbol(Symbol::from("ES.FUT")).await; - - // Create 225-dimensional feature vector (matching ML system) - let features: Vec = (0..225).map(|i| (i as f64) / 256.0).collect(); - - let signal = strategy - .generate_signal(features) - .await - .expect("Should generate signal"); - - // Validate signal properties - assert_eq!(signal.symbol, Symbol::from("ES.FUT")); - assert!(signal.confidence >= 0.0 && signal.confidence <= 1.0); - assert!(matches!( - signal.action, - SignalAction::Buy | SignalAction::Sell | SignalAction::Hold - )); -} - -/// Test that both trading and backtesting can use the same instance -#[tokio::test] -async fn test_shared_instance_multiple_services() { - use std::sync::Arc; - - // Create ONE SINGLE SYSTEM - let strategy = Arc::new( - SharedMLStrategy::new() - .await - .expect("Should create strategy"), - ); - - strategy.set_symbol(Symbol::from("NQ.FUT")).await; - - // Simulate trading service using the strategy - let trading_strategy = Arc::clone(&strategy); - let trading_handle = tokio::spawn(async move { - let features: Vec = vec![0.5; 225]; - trading_strategy - .generate_signal(features) - .await - .expect("Trading service should generate signal") - }); - - // Simulate backtesting service using the same strategy - let backtesting_strategy = Arc::clone(&strategy); - let backtesting_handle = tokio::spawn(async move { - let features: Vec = vec![0.6; 225]; - backtesting_strategy - .generate_signal(features) - .await - .expect("Backtesting service should generate signal") - }); - - // Both services should succeed - let trading_signal = trading_handle - .await - .expect("Trading task should complete"); - let backtesting_signal = backtesting_handle - .await - .expect("Backtesting task should complete"); - - assert_eq!(trading_signal.symbol, Symbol::from("NQ.FUT")); - assert_eq!(backtesting_signal.symbol, Symbol::from("NQ.FUT")); - - // Verify performance tracking shows both signals - let perf = strategy.get_performance().await; - assert_eq!(perf.total_signals, 2); -} - -/// Test regime detection -#[tokio::test] -async fn test_regime_detection() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - // Update with bullish market data - let regime1 = strategy - .update_regime(100.0, 1000.0) - .await - .expect("Should update regime"); - - // Update with bearish market data - let regime2 = strategy - .update_regime(95.0, 1200.0) - .await - .expect("Should update regime"); - - // Both should be valid regimes - assert!(matches!( - regime1, - MarketRegime::Unknown - | MarketRegime::Bull - | MarketRegime::Bear - | MarketRegime::Sideways - | MarketRegime::HighVolatility - )); - - assert!(matches!( - regime2, - MarketRegime::Unknown - | MarketRegime::Bull - | MarketRegime::Bear - | MarketRegime::Sideways - | MarketRegime::HighVolatility - )); -} - -/// Test outcome recording and performance tracking -#[tokio::test] -async fn test_outcome_recording() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - strategy.set_symbol(Symbol::from("ZN.FUT")).await; - - // Generate signals - let features: Vec = vec![0.5; 225]; - let _signal1 = strategy - .generate_signal(features.clone()) - .await - .expect("Should generate signal 1"); - - let _signal2 = strategy - .generate_signal(features.clone()) - .await - .expect("Should generate signal 2"); - - // Record outcomes - strategy - .record_outcome(0.05) - .await - .expect("Should record positive outcome"); - strategy - .record_outcome(-0.02) - .await - .expect("Should record negative outcome"); - - let perf = strategy.get_performance().await; - assert_eq!(perf.total_signals, 2); - assert!(perf.win_rate >= 0.0 && perf.win_rate <= 1.0); -} - -/// Test configuration validation -#[tokio::test] -async fn test_invalid_configuration() { - // Invalid confidence threshold (>1.0) - let config1 = SharedMLConfig { - min_confidence: 1.5, - ..Default::default() - }; - - let result1 = SharedMLStrategy::with_config(config1).await; - assert!( - result1.is_err(), - "Should reject min_confidence > 1.0" - ); - - // Invalid risk tolerance - let config2 = SharedMLConfig { - risk_tolerance: 2.0, - ..Default::default() - }; - - let result2 = SharedMLStrategy::with_config(config2).await; - assert!( - result2.is_err(), - "Should reject risk_tolerance > 1.0" - ); -} - -/// Test confidence threshold filtering -#[tokio::test] -async fn test_confidence_threshold() { - let config = SharedMLConfig { - min_confidence: 0.95, // Very high threshold - ..Default::default() - }; - - let strategy = SharedMLStrategy::with_config(config) - .await - .expect("Should create strategy"); - - let features: Vec = vec![0.5; 225]; - - let signal = strategy - .generate_signal(features) - .await - .expect("Should generate signal"); - - // With such a high threshold, should return Hold - // (ensemble confidence unlikely to be 0.95+) - assert_eq!(signal.action, SignalAction::Hold); -} - -/// Test position sizing -#[tokio::test] -async fn test_position_sizing() { - let config = SharedMLConfig { - max_position_size: common::Quantity::from(100), - risk_tolerance: 0.5, - min_confidence: 0.6, - ..Default::default() - }; - - let strategy = SharedMLStrategy::with_config(config) - .await - .expect("Should create strategy"); - - let features: Vec = vec![0.7; 225]; - - let signal = strategy - .generate_signal(features) - .await - .expect("Should generate signal"); - - // Position size should be: - // - <= max_position_size (100) - // - Adjusted by risk_tolerance (0.5) - // - Adjusted by confidence - assert!(signal.position_size.to_f64() > 0.0); - assert!(signal.position_size.to_f64() <= 100.0); -} - -/// Test performance metrics aggregation -#[tokio::test] -async fn test_performance_metrics() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - strategy.set_symbol(Symbol::from("6E.FUT")).await; - - let features: Vec = vec![0.5; 225]; - - // Generate multiple signals - for _ in 0..10 { - let _ = strategy - .generate_signal(features.clone()) - .await - .expect("Should generate signal"); - } - - let perf = strategy.get_performance().await; - - // Validate metrics - assert_eq!(perf.total_signals, 10); - assert!(!perf.signals_by_action.is_empty()); - assert!(perf.average_confidence >= 0.0); - assert!(perf.average_confidence <= 1.0); -} - -/// Test concurrent signal generation (thread safety) -#[tokio::test] -async fn test_concurrent_signal_generation() { - use std::sync::Arc; - - let strategy = Arc::new( - SharedMLStrategy::new() - .await - .expect("Should create strategy"), - ); - - strategy.set_symbol(Symbol::from("CL.FUT")).await; - - // Spawn multiple concurrent tasks - let mut handles = Vec::new(); - for i in 0..20 { - let strategy_clone = Arc::clone(&strategy); - let handle = tokio::spawn(async move { - let features: Vec = vec![0.5 + (i as f64) * 0.01; 225]; - strategy_clone - .generate_signal(features) - .await - .expect("Should generate signal") - }); - handles.push(handle); - } - - // Wait for all tasks - for handle in handles { - let signal = handle.await.expect("Task should complete"); - assert_eq!(signal.symbol, Symbol::from("CL.FUT")); - } - - // Verify all signals were tracked - let perf = strategy.get_performance().await; - assert_eq!(perf.total_signals, 20); -} - -/// Test empty features rejection -#[tokio::test] -async fn test_empty_features_rejection() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - let empty_features: Vec = Vec::new(); - - let result = strategy.generate_signal(empty_features).await; - assert!( - result.is_err(), - "Should reject empty features" - ); -} - -/// Test regime persistence across signal generations -#[tokio::test] -async fn test_regime_persistence() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - // Set regime - let regime = strategy - .update_regime(100.0, 1000.0) - .await - .expect("Should update regime"); - - // Get regime - let retrieved_regime = strategy.get_regime().await; - assert_eq!(regime, retrieved_regime); - - // Generate signal - should include the regime - let features: Vec = vec![0.5; 225]; - let signal = strategy - .generate_signal(features) - .await - .expect("Should generate signal"); - - assert_eq!(signal.regime, regime); -} - -/// Test model vote transparency -#[tokio::test] -async fn test_model_vote_transparency() { - let strategy = SharedMLStrategy::new() - .await - .expect("Should create strategy"); - - let features: Vec = vec![0.5; 225]; - - let signal = strategy - .generate_signal(features) - .await - .expect("Should generate signal"); - - // Should have votes from all 6 models (or hold signal with no votes) - if signal.action != SignalAction::Hold { - assert_eq!( - signal.model_votes.len(), - 6, - "Should have 6 model votes" - ); - - // Validate each vote - for vote in &signal.model_votes { - assert!(!vote.model_name.is_empty()); - assert!(vote.confidence >= 0.0 && vote.confidence <= 1.0); - assert!(vote.weight >= 0.0 && vote.weight <= 1.0); - } - } -} - -/// Test strategy with custom regime config -#[tokio::test] -async fn test_custom_regime_config() { - use ml::ensemble::RegimeConfig; - - let config = SharedMLConfig { - regime_config: RegimeConfig { - trend_lookback: 30, - volatility_window: 15, - trend_threshold: 0.03, - volatility_threshold: 2.0, - min_data_points: 10, - }, - ..Default::default() - }; - - let strategy = SharedMLStrategy::with_config(config) - .await - .expect("Should create strategy with custom regime config"); - - // Update regime - let regime = strategy - .update_regime(100.0, 1000.0) - .await - .expect("Should update regime"); - - assert!(matches!( - regime, - MarketRegime::Unknown - | MarketRegime::Bull - | MarketRegime::Bear - | MarketRegime::Sideways - | MarketRegime::HighVolatility - )); -} diff --git a/proptest-regressions/compliance_validation_tests.txt b/proptest-regressions/compliance_validation_tests.txt deleted file mode 100644 index 422e6f34a..000000000 --- a/proptest-regressions/compliance_validation_tests.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Seeds for failure cases proptest has generated in the past. It is -# automatically read and these particular cases re-run before any -# novel cases are generated. -# -# It is recommended to check this file in to source control so that -# everyone who runs the test benefits from these saved cases. -cc d70cb9fa7312a8b9efa8c3ac59e28ee1881e09437f107d773d86abf6f9e08fc9 # shrinks to quantity = 1 diff --git a/results/CHECKPOINT_BACKTEST_REPORT.md b/results/CHECKPOINT_BACKTEST_REPORT.md deleted file mode 100644 index 867dc007f..000000000 --- a/results/CHECKPOINT_BACKTEST_REPORT.md +++ /dev/null @@ -1,71 +0,0 @@ -# Checkpoint Backtesting Results - -**Date**: 2025-10-14 -**Total Checkpoints Tested**: 91 -**Data**: 6E.FUT (Euro FX Futures), 7,223 bars, 4 days - ---- - -## Executive Summary - -### DQN Performance -- **Best Checkpoint**: Epoch 30 -- **Best Sharpe Ratio**: 10.014 -- **Best PnL**: $95.28 -- **Win Rate**: 60.5% - -### PPO Performance -- **Best Checkpoint**: Epoch 420 -- **Best Sharpe Ratio**: 10.652 -- **Best PnL**: $9.85 -- **Win Rate**: 62.1% - ---- - -## Top 10 DQN Checkpoints - -| Rank | Epoch | Sharpe | Win Rate | Trades | PnL | Drawdown | Trade Freq | -|------|-------|--------|----------|--------|-----|----------|------------| -| 1 | 30 | 10.014 | 60.5% | 306 | $95.28 | 0.00% | 42.4 | -| 2 | 310 | 9.439 | 61.5% | 382 | $109.37 | 0.00% | 52.9 | -| 3 | 70 | 9.127 | 75.0% | 4 | $0.01 | 0.00% | 0.6 | -| 4 | 460 | 7.387 | 56.0% | 134 | $26.15 | 0.00% | 18.6 | -| 5 | 140 | 6.967 | 33.3% | 6 | $1.07 | 0.00% | 0.8 | -| 6 | 150 | 6.596 | 51.6% | 217 | $35.02 | 0.00% | 30.0 | -| 7 | 160 | 6.353 | 53.3% | 454 | $68.77 | 0.00% | 62.9 | -| 8 | 200 | 5.650 | 60.6% | 327 | $82.14 | 0.99% | 45.3 | -| 9 | 420 | 5.602 | 50.0% | 80 | $9.74 | 0.00% | 11.1 | -| 10 | 230 | 5.525 | 53.0% | 83 | $9.82 | 0.00% | 11.5 | - ---- - -## Top 10 PPO Checkpoints - -| Rank | Epoch | Sharpe | Win Rate | Trades | PnL | Drawdown | Trade Freq | -|------|-------|--------|----------|--------|-----|----------|------------| -| 1 | 420 | 10.652 | 62.1% | 29 | $9.85 | 0.00% | 4.0 | -| 2 | 130 | 10.556 | 60.1% | 281 | $94.26 | 0.00% | 38.9 | -| 3 | 50 | 7.806 | 54.0% | 87 | $18.54 | 0.00% | 12.0 | -| 4 | 90 | 6.508 | 55.8% | 514 | $83.22 | 0.11% | 71.2 | -| 5 | 310 | 6.323 | 55.6% | 475 | $71.22 | 0.00% | 65.8 | -| 6 | 200 | 5.908 | 60.1% | 893 | $176.35 | 0.43% | 123.6 | -| 7 | 290 | 5.894 | 62.2% | 217 | $28.60 | 0.00% | 30.0 | -| 8 | 300 | 5.736 | 57.4% | 242 | $30.59 | 0.00% | 33.5 | -| 9 | 270 | 5.305 | 55.7% | 548 | $81.18 | 0.43% | 75.9 | -| 10 | 180 | 4.712 | 36.4% | 55 | $6.55 | 0.11% | 7.6 | - ---- - -## Production Deployment Recommendations - -### Primary Recommendation: **DQN Epoch 30** -- Sharpe Ratio: 10.014 -- Win Rate: 60.5% -- Total PnL: $95.28 -- Max Drawdown: 0.00% - -### Alternative: **PPO Epoch 420** -- Sharpe Ratio: 10.652 -- Win Rate: 62.1% -- Total PnL: $9.85 -- Max Drawdown: 0.00% \ No newline at end of file diff --git a/results/authz_service_results.txt b/results/authz_service_results.txt deleted file mode 100644 index b38ae802c..000000000 --- a/results/authz_service_results.txt +++ /dev/null @@ -1,64 +0,0 @@ -rwlock_permission_check time: [68.969 ns 70.019 ns 71.132 ns] - change: [+14.713% +16.947% +18.997%] (p = 0.00 < 0.05) - Performance has regressed. - -dashmap_permission_check - time: [45.238 ns 45.946 ns 46.759 ns] - change: [-0.7933% +1.2782% +3.6370%] (p = 0.26 > 0.05) - No change in performance detected. -Found 1 outliers among 100 measurements (1.00%) - 1 (1.00%) high mild - -authz_cache_sizes/dashmap/100 - time: [44.522 ns 45.347 ns 46.159 ns] - change: [-4.4476% -1.8173% +0.5879%] (p = 0.16 > 0.05) - No change in performance detected. -Found 5 outliers among 100 measurements (5.00%) - 2 (2.00%) low mild - 3 (3.00%) high mild -authz_cache_sizes/dashmap/1000 - time: [44.415 ns 45.083 ns 45.817 ns] - change: [-1.1731% +2.2139% +5.9569%] (p = 0.23 > 0.05) - No change in performance detected. -Found 7 outliers among 100 measurements (7.00%) - 7 (7.00%) high mild -authz_cache_sizes/dashmap/10000 - time: [44.280 ns 45.149 ns 46.086 ns] - change: [-4.0425% -1.6818% +0.9562%] (p = 0.17 > 0.05) - No change in performance detected. -authz_cache_sizes/dashmap/100000 - time: [44.932 ns 45.907 ns 46.896 ns] - change: [+13.695% +17.170% +20.494%] (p = 0.00 < 0.05) - Performance has regressed. -Found 9 outliers among 100 measurements (9.00%) - 7 (7.00%) high mild - 2 (2.00%) high severe - -concurrent_reads/dashmap_concurrent_8_threads - time: [549.46 µs 553.11 µs 556.93 µs] - change: [-11.857% -9.3976% -7.0875%] (p = 0.00 < 0.05) - Performance has improved. -Found 4 outliers among 100 measurements (4.00%) - 1 (1.00%) low mild - 3 (3.00%) high mild - -hot_path_permission_check - time: [89.196 ns 90.432 ns 91.777 ns] - change: [-9.5551% -7.3716% -5.0715%] (p = 0.00 < 0.05) - Performance has improved. -Found 3 outliers among 100 measurements (3.00%) - 3 (3.00%) high mild - -cache_invalidation/dashmap_remove - time: [128.82 ns 131.60 ns 134.41 ns] - change: [-3.4170% -0.8276% +1.7319%] (p = 0.55 > 0.05) - No change in performance detected. -Found 2 outliers among 100 measurements (2.00%) - 2 (2.00%) high mild -cache_invalidation/dashmap_clear_all - time: [17.073 µs 17.402 µs 17.776 µs] - change: [+10.080% +12.397% +14.831%] (p = 0.00 < 0.05) - Performance has regressed. -Found 3 outliers among 100 measurements (3.00%) - 3 (3.00%) high mild - diff --git a/results/backtest_results_20251014_133851.json b/results/backtest_results_20251014_133851.json deleted file mode 100644 index 0637a088a..000000000 --- a/results/backtest_results_20251014_133851.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/results/backtest_results_20251014_134158.json b/results/backtest_results_20251014_134158.json deleted file mode 100644 index 0637a088a..000000000 --- a/results/backtest_results_20251014_134158.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/results/backtest_results_20251014_135033.json b/results/backtest_results_20251014_135033.json deleted file mode 100644 index 39cc9dc09..000000000 --- a/results/backtest_results_20251014_135033.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "model_name": "dqn_final_epoch500", - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "start_date": "2025-07-16T13:50:33.818379970+00:00", - "end_date": "2025-10-14T13:50:33.818390541+00:00" - }, - { - "model_name": "ppo_actor_epoch_500", - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "start_date": "2025-07-16T13:50:33.864920090+00:00", - "end_date": "2025-10-14T13:50:33.864920387+00:00" - } -] \ No newline at end of file diff --git a/results/backtest_results_20251014_135038.json b/results/backtest_results_20251014_135038.json deleted file mode 100644 index 7efccc0db..000000000 --- a/results/backtest_results_20251014_135038.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "model_name": "dqn_final_epoch500", - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009700000000000042, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5622.0, - "profit_factor": null, - "start_date": "2025-07-16T13:50:38.704634758+00:00", - "end_date": "2025-10-14T13:50:38.704635309+00:00" - }, - { - "model_name": "ppo_actor_epoch_500", - "total_trades": 20, - "winning_trades": 7, - "win_rate": 35.0, - "total_pnl": -0.019899999999999807, - "sharpe_ratio": -6.014968874365458, - "max_drawdown": 0.000022499999991850928, - "calmar_ratio": -0.8844444440880123, - "avg_trade_duration": 281.1, - "profit_factor": 0.31849315068493045, - "start_date": "2025-07-16T13:50:38.750768994+00:00", - "end_date": "2025-10-14T13:50:38.750769342+00:00" - } -] \ No newline at end of file diff --git a/results/backtest_results_20251014_135528.json b/results/backtest_results_20251014_135528.json deleted file mode 100644 index b82ca2b73..000000000 --- a/results/backtest_results_20251014_135528.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "model_name": "dqn_final_epoch500", - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009700000000000042, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5629.0, - "profit_factor": null, - "start_date": "2025-07-16T13:55:28.442038700+00:00", - "end_date": "2025-10-14T13:55:28.442039174+00:00" - }, - { - "model_name": "ppo_actor_epoch_500", - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "start_date": "2025-07-16T13:55:28.490232987+00:00", - "end_date": "2025-10-14T13:55:28.490233326+00:00" - } -] \ No newline at end of file diff --git a/results/backtest_summary_20251014_143309.csv b/results/backtest_summary_20251014_143309.csv deleted file mode 100644 index c6e0c5727..000000000 --- a/results/backtest_summary_20251014_143309.csv +++ /dev/null @@ -1,101 +0,0 @@ -model_type,epoch,total_trades,winning_trades,win_rate,total_pnl,sharpe_ratio,max_drawdown,calmar_ratio,avg_trade_duration,profit_factor,trade_frequency -DQN,10,82,24,29.27,-3.35,-3.1459,0.00,-1.0000,63.06,0.0030,11.35 -DQN,20,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -DQN,30,306,185,60.46,95.28,10.0138,0.00,13062.9793,14.31,973.2068,42.36 -DQN,40,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,50,92,33,35.87,-14.19,-6.4364,0.01,-1.0000,60.50,0.0040,12.74 -DQN,60,969,390,40.25,-107.55,-2.9616,0.11,-1.0000,5.20,0.5032,134.15 -DQN,70,4,3,75.00,0.01,9.1273,0.00,4.5682,1403.50,5.5682,0.55 -DQN,80,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -DQN,90,889,448,50.39,98.46,5.1932,0.00,89.1567,5.59,34.2797,123.08 -DQN,100,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -DQN,110,17,10,58.82,-1.07,-3.8801,0.00,-0.9791,330.76,0.0242,2.35 -DQN,120,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,130,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -DQN,140,6,2,33.33,1.07,6.9666,0.00,54.9821,939.17,55.9821,0.83 -DQN,150,217,112,51.61,35.02,6.5957,0.00,1226.9130,25.43,209.3922,30.04 -DQN,160,454,242,53.30,68.77,6.3534,0.00,1420.1032,11.50,217.1539,62.85 -DQN,170,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,180,556,243,43.71,-54.65,-4.9798,0.05,-0.9995,9.41,0.0094,76.98 -DQN,190,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -DQN,200,327,198,60.55,82.14,5.6498,0.01,8.2951,16.45,2.9115,45.27 -DQN,210,346,148,42.77,1.17,0.5275,0.00,1.0687,11.58,1.9221,47.90 -DQN,220,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -DQN,230,83,44,53.01,9.82,5.5254,0.00,668.3372,61.23,247.8347,11.49 -DQN,240,1,1,100.00,0.00,0.0000,0.00,0.0000,3780.00,inf,0.14 -DQN,250,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -DQN,260,174,74,42.53,-37.07,-7.7834,0.04,-0.9988,31.85,0.0064,24.09 -DQN,270,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,280,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,290,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,300,757,271,35.80,-139.16,-7.1339,0.14,-1.0000,6.87,0.0044,104.80 -DQN,310,382,235,61.52,109.37,9.4393,0.00,3908.3767,12.71,396.4883,52.89 -DQN,320,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -DQN,330,59,34,57.63,0.03,4.2724,0.00,7.0556,94.58,4.9688,8.17 -DQN,340,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -DQN,350,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -DQN,360,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -DQN,370,452,192,42.48,-71.19,-6.3920,0.07,-0.9996,11.94,0.0208,62.58 -DQN,380,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -DQN,390,1,0,0.00,-0.00,0.0000,0.00,-1.0000,3795.00,-0.0000,0.14 -DQN,400,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -DQN,410,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,420,80,40,50.00,9.74,5.6023,0.00,327.8920,69.66,103.8536,11.08 -DQN,430,160,47,29.38,-17.61,-5.3239,0.02,-0.9999,34.43,0.0031,22.15 -DQN,440,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -DQN,450,37,18,48.65,-2.19,-3.8033,0.00,-0.9877,149.65,0.0176,5.12 -DQN,460,134,75,55.97,26.15,7.3865,0.00,888.0928,41.45,184.8783,18.55 -DQN,470,617,263,42.63,-93.87,-5.9784,0.09,-1.0000,8.30,0.0595,85.42 -DQN,480,773,425,54.98,96.38,3.0358,0.01,8.8633,6.63,1.8931,107.02 -DQN,490,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5571.00,-0.0000,0.14 -DQN,500,1147,396,34.52,-135.63,-5.3810,0.14,-1.0000,3.99,0.0273,158.80 -PPO,10,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -PPO,20,21,13,61.90,0.00,1.2413,0.00,0.3981,201.81,1.2585,2.91 -PPO,30,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,40,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5600.00,-0.0000,0.14 -PPO,50,87,47,54.02,18.54,7.8060,0.00,1248.7214,60.26,254.8219,12.04 -PPO,60,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -PPO,70,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,80,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,90,514,287,55.84,83.22,6.5083,0.00,74.7277,10.45,52.8568,71.16 -PPO,100,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,110,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,120,612,235,38.40,-118.23,-7.2625,0.12,-0.9999,8.37,0.0143,84.73 -PPO,130,281,169,60.14,94.26,10.5560,0.00,8576.0905,16.00,811.4683,38.90 -PPO,140,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,150,223,139,62.33,14.39,3.4690,0.00,6.5544,24.43,7.3890,30.87 -PPO,160,4,3,75.00,-0.01,-7.0704,0.00,-0.8221,1392.25,0.1779,0.55 -PPO,170,300,76,25.33,-139.12,-13.6093,0.14,-1.0000,13.46,0.0016,41.53 -PPO,180,55,20,36.36,6.55,4.7116,0.00,5.9512,102.16,6.5425,7.61 -PPO,190,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -PPO,200,893,537,60.13,176.35,5.9085,0.00,41.0761,5.67,5.0809,123.63 -PPO,210,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -PPO,220,45,23,51.11,-6.52,-3.8840,0.01,-0.7471,113.00,0.4034,6.23 -PPO,230,4,1,25.00,-0.01,-8.9855,0.00,-0.8143,1397.50,0.2717,0.55 -PPO,240,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,250,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,260,667,223,33.43,-139.12,-7.4290,0.14,-1.0000,7.53,0.0326,92.34 -PPO,270,548,305,55.66,81.18,5.3054,0.00,18.8217,9.41,6.6150,75.87 -PPO,280,499,178,35.67,-118.26,-7.8113,0.12,-1.0000,10.58,0.0547,69.08 -PPO,290,217,135,62.21,28.60,5.8943,0.00,1782.1383,25.20,417.4277,30.04 -PPO,300,242,139,57.44,30.59,5.7355,0.00,1124.8891,22.73,122.3072,33.50 -PPO,310,475,264,55.58,71.22,6.3228,0.00,2133.8227,11.33,174.2437,65.76 -PPO,320,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -PPO,330,847,289,34.12,-194.88,-6.9292,0.19,-1.0000,6.02,0.1378,117.26 -PPO,340,1,1,100.00,0.01,0.0000,0.00,0.0000,5622.00,inf,0.14 -PPO,350,818,306,37.41,-146.63,-7.0229,0.15,-1.0000,6.29,0.0050,113.25 -PPO,360,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -PPO,370,5,2,40.00,-0.01,-7.8822,0.00,-0.9124,911.20,0.1612,0.69 -PPO,380,1,1,100.00,0.01,0.0000,0.00,0.0000,5622.00,inf,0.14 -PPO,390,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,400,0,0,0.00,0.00,0.0000,0.00,0.0000,0.00,0.0000,0.00 -PPO,410,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,420,29,18,62.07,9.85,10.6517,0.00,1031.2901,194.00,295.4341,4.01 -PPO,430,378,174,46.03,-33.94,-4.7421,0.03,-0.9996,14.23,0.0109,52.33 -PPO,440,82,42,51.22,-0.03,-1.9363,0.00,-0.7669,65.48,0.6909,11.35 -PPO,450,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 -PPO,460,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,470,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5600.00,-0.0000,0.14 -PPO,480,1,1,100.00,0.01,0.0000,0.00,0.0000,5638.00,inf,0.14 -PPO,490,290,162,55.86,25.19,3.7254,0.01,4.5921,18.42,4.7573,40.15 -PPO,500,1,0,0.00,-0.01,0.0000,0.00,-1.0000,5638.00,-0.0000,0.14 diff --git a/results/backtest_summary_20251017_124647.csv b/results/backtest_summary_20251017_124647.csv deleted file mode 100644 index a271b9efb..000000000 --- a/results/backtest_summary_20251017_124647.csv +++ /dev/null @@ -1,101 +0,0 @@ -model_type,epoch,total_trades,winning_trades,win_rate,total_pnl,sharpe_ratio,max_drawdown,calmar_ratio,avg_trade_duration,profit_factor,trade_frequency -DQN,10,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,20,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,30,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,40,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,50,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,60,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,70,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,80,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,90,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,100,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,110,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,120,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,130,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,140,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,150,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,160,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,170,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,180,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,190,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,200,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,210,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,220,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,230,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,240,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,250,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,260,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,270,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,280,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,290,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,300,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,310,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,320,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,330,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,340,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,350,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,360,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,370,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,380,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,390,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,400,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,410,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,420,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,430,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,440,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,450,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,460,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,470,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,480,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,490,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -DQN,500,354,148,41.81,-55.90,-6.5192,0.06,-0.9995,15.40,0.0054,49.01 -PPO,10,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,20,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,30,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,40,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,50,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,60,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,70,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,80,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,90,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,100,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,110,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,120,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,130,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,140,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,150,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,160,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,170,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,180,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,190,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,200,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,210,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,220,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,230,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,240,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,250,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,260,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,270,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,280,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,290,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,300,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,310,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,320,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,330,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,340,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,350,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,360,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,370,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,380,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,390,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,400,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,410,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,420,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,430,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,440,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,450,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,460,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,470,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,480,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,490,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 -PPO,500,1,1,100.00,0.01,0.0000,0.00,0.0000,5600.00,inf,0.14 diff --git a/results/comprehensive_backtest_results_20251014_143309.json b/results/comprehensive_backtest_results_20251014_143309.json deleted file mode 100644 index 36bb2fc36..000000000 --- a/results/comprehensive_backtest_results_20251014_143309.json +++ /dev/null @@ -1,1702 +0,0 @@ -[ - { - "model_name": "dqn_epoch_10", - "model_type": "DQN", - "epoch": 10, - "total_trades": 82, - "winning_trades": 24, - "win_rate": 29.268292682926827, - "total_pnl": -3.346520000000001, - "sharpe_ratio": -3.1458848348984256, - "max_drawdown": 0.0033465700000524525, - "calmar_ratio": -0.9999850593286934, - "avg_trade_duration": 63.0609756097561, - "profit_factor": 0.0030386806167924467, - "trade_frequency": 11.352623563616225, - "start_date": "2025-07-16T14:33:04.095029943+00:00", - "end_date": "2025-10-14T14:33:04.095030121+00:00" - }, - { - "model_name": "dqn_epoch_20", - "model_type": "DQN", - "epoch": 20, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:04.150319729+00:00", - "end_date": "2025-10-14T14:33:04.150320118+00:00" - }, - { - "model_name": "dqn_epoch_30", - "model_type": "DQN", - "epoch": 30, - "total_trades": 306, - "winning_trades": 185, - "win_rate": 60.45751633986928, - "total_pnl": 95.27626999999998, - "sharpe_ratio": 10.013833032407788, - "max_drawdown": 7.293609501017452e-6, - "calmar_ratio": 13062.979309051092, - "avg_trade_duration": 14.31045751633987, - "profit_factor": 973.206836734698, - "trade_frequency": 42.364668420323966, - "start_date": "2025-07-16T14:33:04.203789449+00:00", - "end_date": "2025-10-14T14:33:04.203789835+00:00" - }, - { - "model_name": "dqn_epoch_40", - "model_type": "DQN", - "epoch": 40, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:04.259000119+00:00", - "end_date": "2025-10-14T14:33:04.259000532+00:00" - }, - { - "model_name": "dqn_epoch_50", - "model_type": "DQN", - "epoch": 50, - "total_trades": 92, - "winning_trades": 33, - "win_rate": 35.869565217391305, - "total_pnl": -14.188199999999998, - "sharpe_ratio": -6.436361808865244, - "max_drawdown": 0.0141882000000478, - "calmar_ratio": -1.0, - "avg_trade_duration": 60.5, - "profit_factor": 0.003990859982941448, - "trade_frequency": 12.737089851862107, - "start_date": "2025-07-16T14:33:04.315983513+00:00", - "end_date": "2025-10-14T14:33:04.315983849+00:00" - }, - { - "model_name": "dqn_epoch_60", - "model_type": "DQN", - "epoch": 60, - "total_trades": 969, - "winning_trades": 390, - "win_rate": 40.24767801857585, - "total_pnl": -107.54606000000008, - "sharpe_ratio": -2.9615640522578963, - "max_drawdown": 0.10754765827923402, - "calmar_ratio": -0.9999851388745578, - "avg_trade_duration": 5.200206398348813, - "profit_factor": 0.5032385368728071, - "trade_frequency": 134.1547833310259, - "start_date": "2025-07-16T14:33:04.371693128+00:00", - "end_date": "2025-10-14T14:33:04.371693779+00:00" - }, - { - "model_name": "dqn_epoch_70", - "model_type": "DQN", - "epoch": 70, - "total_trades": 4, - "winning_trades": 3, - "win_rate": 75.0, - "total_pnl": 0.010050000000000114, - "sharpe_ratio": 9.127275906649468, - "max_drawdown": 2.199999733131019e-6, - "calmar_ratio": 4.568182371019254, - "avg_trade_duration": 1403.5, - "profit_factor": 5.5681818181819125, - "trade_frequency": 0.5537865152983525, - "start_date": "2025-07-16T14:33:04.426590491+00:00", - "end_date": "2025-10-14T14:33:04.426590760+00:00" - }, - { - "model_name": "dqn_epoch_80", - "model_type": "DQN", - "epoch": 80, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:04.478564097+00:00", - "end_date": "2025-10-14T14:33:04.478564588+00:00" - }, - { - "model_name": "dqn_epoch_90", - "model_type": "DQN", - "epoch": 90, - "total_trades": 889, - "winning_trades": 448, - "win_rate": 50.39370078740157, - "total_pnl": 98.46269999999994, - "sharpe_ratio": 5.1931661733463566, - "max_drawdown": 0.0011043786049445773, - "calmar_ratio": 89.15665294422989, - "avg_trade_duration": 5.590551181102362, - "profit_factor": 34.279716356163654, - "trade_frequency": 123.07905302505884, - "start_date": "2025-07-16T14:33:04.530573267+00:00", - "end_date": "2025-10-14T14:33:04.530573593+00:00" - }, - { - "model_name": "dqn_epoch_100", - "model_type": "DQN", - "epoch": 100, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:04.585363698+00:00", - "end_date": "2025-10-14T14:33:04.585363950+00:00" - }, - { - "model_name": "dqn_epoch_110", - "model_type": "DQN", - "epoch": 110, - "total_trades": 17, - "winning_trades": 10, - "win_rate": 58.82352941176471, - "total_pnl": -1.0729099999999991, - "sharpe_ratio": -3.880050590833163, - "max_drawdown": 0.001095859748501454, - "calmar_ratio": -0.9790577685152255, - "avg_trade_duration": 330.7647058823529, - "profit_factor": 0.024236967514278906, - "trade_frequency": 2.3535926900179978, - "start_date": "2025-07-16T14:33:04.637689830+00:00", - "end_date": "2025-10-14T14:33:04.637690183+00:00" - }, - { - "model_name": "dqn_epoch_120", - "model_type": "DQN", - "epoch": 120, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:04.692581538+00:00", - "end_date": "2025-10-14T14:33:04.692581896+00:00" - }, - { - "model_name": "dqn_epoch_130", - "model_type": "DQN", - "epoch": 130, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:04.744315272+00:00", - "end_date": "2025-10-14T14:33:04.744315610+00:00" - }, - { - "model_name": "dqn_epoch_140", - "model_type": "DQN", - "epoch": 140, - "total_trades": 6, - "winning_trades": 2, - "win_rate": 33.33333333333333, - "total_pnl": 1.0749000000000002, - "sharpe_ratio": 6.966634151233998, - "max_drawdown": 0.0000195499999972526, - "calmar_ratio": 54.98209719478004, - "avg_trade_duration": 939.1666666666666, - "profit_factor": 55.98209718670152, - "trade_frequency": 0.8306797729475287, - "start_date": "2025-07-16T14:33:04.794055608+00:00", - "end_date": "2025-10-14T14:33:04.794056384+00:00" - }, - { - "model_name": "dqn_epoch_150", - "model_type": "DQN", - "epoch": 150, - "total_trades": 217, - "winning_trades": 112, - "win_rate": 51.61290322580645, - "total_pnl": 35.020309999999995, - "sharpe_ratio": 6.595727216516952, - "max_drawdown": 0.000028543434167979825, - "calmar_ratio": 1226.9129843986213, - "avg_trade_duration": 25.428571428571427, - "profit_factor": 209.39220470098158, - "trade_frequency": 30.042918454935624, - "start_date": "2025-07-16T14:33:04.847982019+00:00", - "end_date": "2025-10-14T14:33:04.847982384+00:00" - }, - { - "model_name": "dqn_epoch_160", - "model_type": "DQN", - "epoch": 160, - "total_trades": 454, - "winning_trades": 242, - "win_rate": 53.30396475770925, - "total_pnl": 68.76720000000002, - "sharpe_ratio": 6.353438595103021, - "max_drawdown": 0.00004842408523784802, - "calmar_ratio": 1420.103232970659, - "avg_trade_duration": 11.5, - "profit_factor": 217.15389451184933, - "trade_frequency": 62.85476948636301, - "start_date": "2025-07-16T14:33:04.899485125+00:00", - "end_date": "2025-10-14T14:33:04.899485465+00:00" - }, - { - "model_name": "dqn_epoch_170", - "model_type": "DQN", - "epoch": 170, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:04.953768436+00:00", - "end_date": "2025-10-14T14:33:04.953768765+00:00" - }, - { - "model_name": "dqn_epoch_180", - "model_type": "DQN", - "epoch": 180, - "total_trades": 556, - "winning_trades": 243, - "win_rate": 43.70503597122302, - "total_pnl": -54.65001999999994, - "sharpe_ratio": -4.979758587593881, - "max_drawdown": 0.054677059830086604, - "calmar_ratio": -0.9995054629826534, - "avg_trade_duration": 9.406474820143885, - "profit_factor": 0.009367949793009638, - "trade_frequency": 76.976325626471, - "start_date": "2025-07-16T14:33:05.006065845+00:00", - "end_date": "2025-10-14T14:33:05.006066580+00:00" - }, - { - "model_name": "dqn_epoch_190", - "model_type": "DQN", - "epoch": 190, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.060952882+00:00", - "end_date": "2025-10-14T14:33:05.060953213+00:00" - }, - { - "model_name": "dqn_epoch_200", - "model_type": "DQN", - "epoch": 200, - "total_trades": 327, - "winning_trades": 198, - "win_rate": 60.550458715596335, - "total_pnl": 82.13685000000004, - "sharpe_ratio": 5.6498201320476, - "max_drawdown": 0.009901907373545859, - "calmar_ratio": 8.295053357043775, - "avg_trade_duration": 16.452599388379205, - "profit_factor": 2.911527155594828, - "trade_frequency": 45.27204762564031, - "start_date": "2025-07-16T14:33:05.113619449+00:00", - "end_date": "2025-10-14T14:33:05.113619687+00:00" - }, - { - "model_name": "dqn_epoch_210", - "model_type": "DQN", - "epoch": 210, - "total_trades": 346, - "winning_trades": 148, - "win_rate": 42.77456647398844, - "total_pnl": 1.1716500000000003, - "sharpe_ratio": 0.5275266004618661, - "max_drawdown": 0.0010962979190097277, - "calmar_ratio": 1.06873321545202, - "avg_trade_duration": 11.580924855491329, - "profit_factor": 1.9220798640076815, - "trade_frequency": 47.90253357330749, - "start_date": "2025-07-16T14:33:05.166259556+00:00", - "end_date": "2025-10-14T14:33:05.166260152+00:00" - }, - { - "model_name": "dqn_epoch_220", - "model_type": "DQN", - "epoch": 220, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:05.219898349+00:00", - "end_date": "2025-10-14T14:33:05.219898993+00:00" - }, - { - "model_name": "dqn_epoch_230", - "model_type": "DQN", - "epoch": 230, - "total_trades": 83, - "winning_trades": 44, - "win_rate": 53.01204819277109, - "total_pnl": 9.824019999999997, - "sharpe_ratio": 5.5253594419388365, - "max_drawdown": 0.000014699197320652515, - "calmar_ratio": 668.3371741780805, - "avg_trade_duration": 61.2289156626506, - "profit_factor": 247.83467336682958, - "trade_frequency": 11.491070192440814, - "start_date": "2025-07-16T14:33:05.271007852+00:00", - "end_date": "2025-10-14T14:33:05.271008085+00:00" - }, - { - "model_name": "dqn_epoch_240", - "model_type": "DQN", - "epoch": 240, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.0018000000000000238, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 3780.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.321727104+00:00", - "end_date": "2025-10-14T14:33:05.321727576+00:00" - }, - { - "model_name": "dqn_epoch_250", - "model_type": "DQN", - "epoch": 250, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:05.376999097+00:00", - "end_date": "2025-10-14T14:33:05.376999597+00:00" - }, - { - "model_name": "dqn_epoch_260", - "model_type": "DQN", - "epoch": 260, - "total_trades": 174, - "winning_trades": 74, - "win_rate": 42.5287356321839, - "total_pnl": -37.06572999999999, - "sharpe_ratio": -7.783421450354781, - "max_drawdown": 0.03711112916491706, - "calmar_ratio": -0.9987766698016505, - "avg_trade_duration": 31.850574712643677, - "profit_factor": 0.00640136734064201, - "trade_frequency": 24.089713415478336, - "start_date": "2025-07-16T14:33:05.435413502+00:00", - "end_date": "2025-10-14T14:33:05.435413799+00:00" - }, - { - "model_name": "dqn_epoch_270", - "model_type": "DQN", - "epoch": 270, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.495130648+00:00", - "end_date": "2025-10-14T14:33:05.495131038+00:00" - }, - { - "model_name": "dqn_epoch_280", - "model_type": "DQN", - "epoch": 280, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.551758207+00:00", - "end_date": "2025-10-14T14:33:05.551758690+00:00" - }, - { - "model_name": "dqn_epoch_290", - "model_type": "DQN", - "epoch": 290, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.616057164+00:00", - "end_date": "2025-10-14T14:33:05.616057701+00:00" - }, - { - "model_name": "dqn_epoch_300", - "model_type": "DQN", - "epoch": 300, - "total_trades": 757, - "winning_trades": 271, - "win_rate": 35.79920739762219, - "total_pnl": -139.1622599999999, - "sharpe_ratio": -7.133870803030625, - "max_drawdown": 0.1391622600000701, - "calmar_ratio": -1.0, - "avg_trade_duration": 6.865257595772787, - "profit_factor": 0.004400925728985826, - "trade_frequency": 104.8040980202132, - "start_date": "2025-07-16T14:33:05.678130998+00:00", - "end_date": "2025-10-14T14:33:05.678131497+00:00" - }, - { - "model_name": "dqn_epoch_310", - "model_type": "DQN", - "epoch": 310, - "total_trades": 382, - "winning_trades": 235, - "win_rate": 61.518324607329845, - "total_pnl": 109.37228, - "sharpe_ratio": 9.439253115437555, - "max_drawdown": 0.000027984068056193295, - "calmar_ratio": 3908.376715643943, - "avg_trade_duration": 12.706806282722512, - "profit_factor": 396.48826613632156, - "trade_frequency": 52.88661221099266, - "start_date": "2025-07-16T14:33:05.733797879+00:00", - "end_date": "2025-10-14T14:33:05.733798306+00:00" - }, - { - "model_name": "dqn_epoch_320", - "model_type": "DQN", - "epoch": 320, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.792884832+00:00", - "end_date": "2025-10-14T14:33:05.792885129+00:00" - }, - { - "model_name": "dqn_epoch_330", - "model_type": "DQN", - "epoch": 330, - "total_trades": 59, - "winning_trades": 34, - "win_rate": 57.6271186440678, - "total_pnl": 0.03175000000000039, - "sharpe_ratio": 4.272389847106092, - "max_drawdown": 4.499999194459945e-6, - "calmar_ratio": 7.055556820606764, - "avg_trade_duration": 94.57627118644068, - "profit_factor": 4.968750000000266, - "trade_frequency": 8.168351100650698, - "start_date": "2025-07-16T14:33:05.852070158+00:00", - "end_date": "2025-10-14T14:33:05.852070622+00:00" - }, - { - "model_name": "dqn_epoch_340", - "model_type": "DQN", - "epoch": 340, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.907790796+00:00", - "end_date": "2025-10-14T14:33:05.907791029+00:00" - }, - { - "model_name": "dqn_epoch_350", - "model_type": "DQN", - "epoch": 350, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:05.961578448+00:00", - "end_date": "2025-10-14T14:33:05.961578712+00:00" - }, - { - "model_name": "dqn_epoch_360", - "model_type": "DQN", - "epoch": 360, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.011790100+00:00", - "end_date": "2025-10-14T14:33:06.011790425+00:00" - }, - { - "model_name": "dqn_epoch_370", - "model_type": "DQN", - "epoch": 370, - "total_trades": 452, - "winning_trades": 192, - "win_rate": 42.47787610619469, - "total_pnl": -71.18873999999998, - "sharpe_ratio": -6.392042309885005, - "max_drawdown": 0.07121748351924811, - "calmar_ratio": -0.9995963979938512, - "avg_trade_duration": 11.938053097345133, - "profit_factor": 0.020764587447655244, - "trade_frequency": 62.57787622871383, - "start_date": "2025-07-16T14:33:06.066432862+00:00", - "end_date": "2025-10-14T14:33:06.066433592+00:00" - }, - { - "model_name": "dqn_epoch_380", - "model_type": "DQN", - "epoch": 380, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:06.118625453+00:00", - "end_date": "2025-10-14T14:33:06.118625678+00:00" - }, - { - "model_name": "dqn_epoch_390", - "model_type": "DQN", - "epoch": 390, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.0018000000000000238, - "sharpe_ratio": 0.0, - "max_drawdown": 1.7999999981839208e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 3795.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.174056835+00:00", - "end_date": "2025-10-14T14:33:06.174057207+00:00" - }, - { - "model_name": "dqn_epoch_400", - "model_type": "DQN", - "epoch": 400, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:06.227242142+00:00", - "end_date": "2025-10-14T14:33:06.227242611+00:00" - }, - { - "model_name": "dqn_epoch_410", - "model_type": "DQN", - "epoch": 410, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.283224563+00:00", - "end_date": "2025-10-14T14:33:06.283225002+00:00" - }, - { - "model_name": "dqn_epoch_420", - "model_type": "DQN", - "epoch": 420, - "total_trades": 80, - "winning_trades": 40, - "win_rate": 50.0, - "total_pnl": 9.73818, - "sharpe_ratio": 5.602307148554258, - "max_drawdown": 0.000029699349114440957, - "calmar_ratio": 327.89203434984404, - "avg_trade_duration": 69.6625, - "profit_factor": 103.85361216729999, - "trade_frequency": 11.075730305967049, - "start_date": "2025-07-16T14:33:06.336865857+00:00", - "end_date": "2025-10-14T14:33:06.336866438+00:00" - }, - { - "model_name": "dqn_epoch_430", - "model_type": "DQN", - "epoch": 430, - "total_trades": 160, - "winning_trades": 47, - "win_rate": 29.375, - "total_pnl": -17.605769999999993, - "sharpe_ratio": -5.323949483696143, - "max_drawdown": 0.017607569683042043, - "calmar_ratio": -0.999897789241799, - "avg_trade_duration": 34.43125, - "profit_factor": 0.0031424693694168406, - "trade_frequency": 22.151460611934098, - "start_date": "2025-07-16T14:33:06.392836018+00:00", - "end_date": "2025-10-14T14:33:06.392836472+00:00" - }, - { - "model_name": "dqn_epoch_440", - "model_type": "DQN", - "epoch": 440, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.448827264+00:00", - "end_date": "2025-10-14T14:33:06.448827604+00:00" - }, - { - "model_name": "dqn_epoch_450", - "model_type": "DQN", - "epoch": 450, - "total_trades": 37, - "winning_trades": 18, - "win_rate": 48.64864864864865, - "total_pnl": -2.1893399999999996, - "sharpe_ratio": -3.8033103225848404, - "max_drawdown": 0.0022166399168747366, - "calmar_ratio": -0.9876840994021016, - "avg_trade_duration": 149.64864864864865, - "profit_factor": 0.017589991653728508, - "trade_frequency": 5.1225252665097605, - "start_date": "2025-07-16T14:33:06.502205290+00:00", - "end_date": "2025-10-14T14:33:06.502205607+00:00" - }, - { - "model_name": "dqn_epoch_460", - "model_type": "DQN", - "epoch": 460, - "total_trades": 134, - "winning_trades": 75, - "win_rate": 55.970149253731336, - "total_pnl": 26.147489999999994, - "sharpe_ratio": 7.386545072093256, - "max_drawdown": 0.000029442293631613724, - "calmar_ratio": 888.0928343114659, - "avg_trade_duration": 41.44776119402985, - "profit_factor": 184.87827004219426, - "trade_frequency": 18.55184826249481, - "start_date": "2025-07-16T14:33:06.556828374+00:00", - "end_date": "2025-10-14T14:33:06.556828724+00:00" - }, - { - "model_name": "dqn_epoch_470", - "model_type": "DQN", - "epoch": 470, - "total_trades": 617, - "winning_trades": 263, - "win_rate": 42.62560777957861, - "total_pnl": -93.86689999999989, - "sharpe_ratio": -5.9783560870392085, - "max_drawdown": 0.093869047981722, - "calmar_ratio": -0.9999771172512028, - "avg_trade_duration": 8.304700162074555, - "profit_factor": 0.05948882075320926, - "trade_frequency": 85.42156998477087, - "start_date": "2025-07-16T14:33:06.608274288+00:00", - "end_date": "2025-10-14T14:33:06.608274533+00:00" - }, - { - "model_name": "dqn_epoch_480", - "model_type": "DQN", - "epoch": 480, - "total_trades": 773, - "winning_trades": 425, - "win_rate": 54.980595084087966, - "total_pnl": 96.3781899999999, - "sharpe_ratio": 3.0357595485984197, - "max_drawdown": 0.010873836109300306, - "calmar_ratio": 8.863310889663168, - "avg_trade_duration": 6.632600258732213, - "profit_factor": 1.8930974355353252, - "trade_frequency": 107.01924408140661, - "start_date": "2025-07-16T14:33:06.660312024+00:00", - "end_date": "2025-10-14T14:33:06.660312411+00:00" - }, - { - "model_name": "dqn_epoch_490", - "model_type": "DQN", - "epoch": 490, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.010099999999999998, - "sharpe_ratio": 0.0, - "max_drawdown": 0.000010099999999511055, - "calmar_ratio": -1.0, - "avg_trade_duration": 5571.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.716958693+00:00", - "end_date": "2025-10-14T14:33:06.716959039+00:00" - }, - { - "model_name": "dqn_epoch_500", - "model_type": "DQN", - "epoch": 500, - "total_trades": 1147, - "winning_trades": 396, - "win_rate": 34.52484742807324, - "total_pnl": -135.6269199999999, - "sharpe_ratio": -5.380954564358199, - "max_drawdown": 0.13562692000005336, - "calmar_ratio": -1.0, - "avg_trade_duration": 3.985178727114211, - "profit_factor": 0.02726697914174342, - "trade_frequency": 158.79828326180257, - "start_date": "2025-07-16T14:33:06.772705364+00:00", - "end_date": "2025-10-14T14:33:06.772705660+00:00" - }, - { - "model_name": "ppo_actor_epoch_10", - "model_type": "PPO", - "epoch": 10, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:06.837880797+00:00", - "end_date": "2025-10-14T14:33:06.837881151+00:00" - }, - { - "model_name": "ppo_actor_epoch_20", - "model_type": "PPO", - "epoch": 20, - "total_trades": 21, - "winning_trades": 13, - "win_rate": 61.904761904761905, - "total_pnl": 0.0041999999999997595, - "sharpe_ratio": 1.2413040072339858, - "max_drawdown": 0.000010549999768957998, - "calmar_ratio": 0.39810427509888213, - "avg_trade_duration": 201.8095238095238, - "profit_factor": 1.2584615384615256, - "trade_frequency": 2.9073792053163507, - "start_date": "2025-07-16T14:33:06.899816746+00:00", - "end_date": "2025-10-14T14:33:06.899817177+00:00" - }, - { - "model_name": "ppo_actor_epoch_30", - "model_type": "PPO", - "epoch": 30, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.948208368+00:00", - "end_date": "2025-10-14T14:33:06.948208775+00:00" - }, - { - "model_name": "ppo_actor_epoch_40", - "model_type": "PPO", - "epoch": 40, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.008199999999999985, - "sharpe_ratio": 0.0, - "max_drawdown": 8.19999999657739e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5600.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:06.992651860+00:00", - "end_date": "2025-10-14T14:33:06.992652311+00:00" - }, - { - "model_name": "ppo_actor_epoch_50", - "model_type": "PPO", - "epoch": 50, - "total_trades": 87, - "winning_trades": 47, - "win_rate": 54.02298850574713, - "total_pnl": 18.541689999999996, - "sharpe_ratio": 7.8059912084632685, - "max_drawdown": 0.000014848539963274316, - "calmar_ratio": 1248.7214262076945, - "avg_trade_duration": 60.264367816091955, - "profit_factor": 254.82190280629598, - "trade_frequency": 12.044856707739168, - "start_date": "2025-07-16T14:33:07.033881455+00:00", - "end_date": "2025-10-14T14:33:07.033882143+00:00" - }, - { - "model_name": "ppo_actor_epoch_60", - "model_type": "PPO", - "epoch": 60, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.075304761+00:00", - "end_date": "2025-10-14T14:33:07.075305+00:00" - }, - { - "model_name": "ppo_actor_epoch_70", - "model_type": "PPO", - "epoch": 70, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.117783498+00:00", - "end_date": "2025-10-14T14:33:07.117783821+00:00" - }, - { - "model_name": "ppo_actor_epoch_80", - "model_type": "PPO", - "epoch": 80, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.162696747+00:00", - "end_date": "2025-10-14T14:33:07.162697119+00:00" - }, - { - "model_name": "ppo_actor_epoch_90", - "model_type": "PPO", - "epoch": 90, - "total_trades": 514, - "winning_trades": 287, - "win_rate": 55.83657587548638, - "total_pnl": 83.2152, - "sharpe_ratio": 6.508275828356481, - "max_drawdown": 0.0011135783886066146, - "calmar_ratio": 74.7277433285677, - "avg_trade_duration": 10.445525291828794, - "profit_factor": 52.8568464083853, - "trade_frequency": 71.1615672158383, - "start_date": "2025-07-16T14:33:07.206738901+00:00", - "end_date": "2025-10-14T14:33:07.206739291+00:00" - }, - { - "model_name": "ppo_actor_epoch_100", - "model_type": "PPO", - "epoch": 100, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.249962666+00:00", - "end_date": "2025-10-14T14:33:07.249963221+00:00" - }, - { - "model_name": "ppo_actor_epoch_110", - "model_type": "PPO", - "epoch": 110, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.294455011+00:00", - "end_date": "2025-10-14T14:33:07.294455383+00:00" - }, - { - "model_name": "ppo_actor_epoch_120", - "model_type": "PPO", - "epoch": 120, - "total_trades": 612, - "winning_trades": 235, - "win_rate": 38.39869281045752, - "total_pnl": -118.23344999999993, - "sharpe_ratio": -7.2624849154338635, - "max_drawdown": 0.11824999816716869, - "calmar_ratio": -0.9998600577811264, - "avg_trade_duration": 8.366013071895425, - "profit_factor": 0.014257211582352387, - "trade_frequency": 84.72933684064793, - "start_date": "2025-07-16T14:33:07.348328333+00:00", - "end_date": "2025-10-14T14:33:07.348328844+00:00" - }, - { - "model_name": "ppo_actor_epoch_130", - "model_type": "PPO", - "epoch": 130, - "total_trades": 281, - "winning_trades": 169, - "win_rate": 60.14234875444839, - "total_pnl": 94.25746, - "sharpe_ratio": 10.556010440148627, - "max_drawdown": 0.000010990725876832411, - "calmar_ratio": 8576.090519981932, - "avg_trade_duration": 16.00355871886121, - "profit_factor": 811.4682717110994, - "trade_frequency": 38.903502699709264, - "start_date": "2025-07-16T14:33:07.392679574+00:00", - "end_date": "2025-10-14T14:33:07.392679983+00:00" - }, - { - "model_name": "ppo_actor_epoch_140", - "model_type": "PPO", - "epoch": 140, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.436529898+00:00", - "end_date": "2025-10-14T14:33:07.436530243+00:00" - }, - { - "model_name": "ppo_actor_epoch_150", - "model_type": "PPO", - "epoch": 150, - "total_trades": 223, - "winning_trades": 139, - "win_rate": 62.33183856502242, - "total_pnl": 14.390740000000001, - "sharpe_ratio": 3.468958719539529, - "max_drawdown": 0.0021955973202406795, - "calmar_ratio": 6.554362162572759, - "avg_trade_duration": 24.426008968609864, - "profit_factor": 7.389012706333634, - "trade_frequency": 30.873598227883154, - "start_date": "2025-07-16T14:33:07.485597942+00:00", - "end_date": "2025-10-14T14:33:07.485598304+00:00" - }, - { - "model_name": "ppo_actor_epoch_160", - "model_type": "PPO", - "epoch": 160, - "total_trades": 4, - "winning_trades": 3, - "win_rate": 75.0, - "total_pnl": -0.00670000000000015, - "sharpe_ratio": -7.070374076365696, - "max_drawdown": 8.149999994202517e-6, - "calmar_ratio": -0.8220858899210819, - "avg_trade_duration": 1392.25, - "profit_factor": 0.17791411042943966, - "trade_frequency": 0.5537865152983525, - "start_date": "2025-07-16T14:33:07.533230147+00:00", - "end_date": "2025-10-14T14:33:07.533230521+00:00" - }, - { - "model_name": "ppo_actor_epoch_170", - "model_type": "PPO", - "epoch": 170, - "total_trades": 300, - "winning_trades": 76, - "win_rate": 25.333333333333336, - "total_pnl": -139.12420000000006, - "sharpe_ratio": -13.609256539223539, - "max_drawdown": 0.13912420000000567, - "calmar_ratio": -1.0, - "avg_trade_duration": 13.463333333333333, - "profit_factor": 0.0015999609608090355, - "trade_frequency": 41.53398864737644, - "start_date": "2025-07-16T14:33:07.579789554+00:00", - "end_date": "2025-10-14T14:33:07.579789887+00:00" - }, - { - "model_name": "ppo_actor_epoch_180", - "model_type": "PPO", - "epoch": 180, - "total_trades": 55, - "winning_trades": 20, - "win_rate": 36.36363636363637, - "total_pnl": 6.55257, - "sharpe_ratio": 4.711557989787329, - "max_drawdown": 0.0011010519571537348, - "calmar_ratio": 5.9511905477412235, - "avg_trade_duration": 102.16363636363636, - "profit_factor": 6.54245717910764, - "trade_frequency": 7.614564585352347, - "start_date": "2025-07-16T14:33:07.624644778+00:00", - "end_date": "2025-10-14T14:33:07.624645222+00:00" - }, - { - "model_name": "ppo_actor_epoch_190", - "model_type": "PPO", - "epoch": 190, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.668021525+00:00", - "end_date": "2025-10-14T14:33:07.668022232+00:00" - }, - { - "model_name": "ppo_actor_epoch_200", - "model_type": "PPO", - "epoch": 200, - "total_trades": 893, - "winning_trades": 537, - "win_rate": 60.13437849944009, - "total_pnl": 176.3484300000001, - "sharpe_ratio": 5.908467678599542, - "max_drawdown": 0.00429321426010661, - "calmar_ratio": 41.076084098269114, - "avg_trade_duration": 5.671892497200448, - "profit_factor": 5.080935140670279, - "trade_frequency": 123.6328395403572, - "start_date": "2025-07-16T14:33:07.714720421+00:00", - "end_date": "2025-10-14T14:33:07.714720748+00:00" - }, - { - "model_name": "ppo_actor_epoch_210", - "model_type": "PPO", - "epoch": 210, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:07.760517296+00:00", - "end_date": "2025-10-14T14:33:07.760517837+00:00" - }, - { - "model_name": "ppo_actor_epoch_220", - "model_type": "PPO", - "epoch": 220, - "total_trades": 45, - "winning_trades": 23, - "win_rate": 51.11111111111111, - "total_pnl": -6.5173100000000055, - "sharpe_ratio": -3.883961411147952, - "max_drawdown": 0.008723063925011225, - "calmar_ratio": -0.7471354166339021, - "avg_trade_duration": 113.0, - "profit_factor": 0.40342039789609724, - "trade_frequency": 6.230098297106466, - "start_date": "2025-07-16T14:33:07.803035463+00:00", - "end_date": "2025-10-14T14:33:07.803035781+00:00" - }, - { - "model_name": "ppo_actor_epoch_230", - "model_type": "PPO", - "epoch": 230, - "total_trades": 4, - "winning_trades": 1, - "win_rate": 25.0, - "total_pnl": -0.009649999999999936, - "sharpe_ratio": -8.985542193911638, - "max_drawdown": 0.000011849999995320104, - "calmar_ratio": -0.8143459912658373, - "avg_trade_duration": 1397.5, - "profit_factor": 0.2716981132075511, - "trade_frequency": 0.5537865152983525, - "start_date": "2025-07-16T14:33:07.847353958+00:00", - "end_date": "2025-10-14T14:33:07.847354343+00:00" - }, - { - "model_name": "ppo_actor_epoch_240", - "model_type": "PPO", - "epoch": 240, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.889674020+00:00", - "end_date": "2025-10-14T14:33:07.889674349+00:00" - }, - { - "model_name": "ppo_actor_epoch_250", - "model_type": "PPO", - "epoch": 250, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:07.930431547+00:00", - "end_date": "2025-10-14T14:33:07.930431950+00:00" - }, - { - "model_name": "ppo_actor_epoch_260", - "model_type": "PPO", - "epoch": 260, - "total_trades": 667, - "winning_trades": 223, - "win_rate": 33.43328335832084, - "total_pnl": -139.11852999999996, - "sharpe_ratio": -7.429032181278162, - "max_drawdown": 0.1391185299999197, - "calmar_ratio": -1.0, - "avg_trade_duration": 7.533733133433284, - "profit_factor": 0.03257056130901981, - "trade_frequency": 92.34390142600029, - "start_date": "2025-07-16T14:33:07.977031636+00:00", - "end_date": "2025-10-14T14:33:07.977032019+00:00" - }, - { - "model_name": "ppo_actor_epoch_270", - "model_type": "PPO", - "epoch": 270, - "total_trades": 548, - "winning_trades": 305, - "win_rate": 55.65693430656934, - "total_pnl": 81.1779599999999, - "sharpe_ratio": 5.305381636328136, - "max_drawdown": 0.004313003798151664, - "calmar_ratio": 18.82167598246633, - "avg_trade_duration": 9.406934306569344, - "profit_factor": 6.6150230195444895, - "trade_frequency": 75.8687525958743, - "start_date": "2025-07-16T14:33:08.021191967+00:00", - "end_date": "2025-10-14T14:33:08.021192584+00:00" - }, - { - "model_name": "ppo_actor_epoch_280", - "model_type": "PPO", - "epoch": 280, - "total_trades": 499, - "winning_trades": 178, - "win_rate": 35.671342685370746, - "total_pnl": -118.26456999999995, - "sharpe_ratio": -7.811281618272544, - "max_drawdown": 0.11826466988166667, - "calmar_ratio": -0.9999991554388967, - "avg_trade_duration": 10.579158316633267, - "profit_factor": 0.05473123265205893, - "trade_frequency": 69.08486778346948, - "start_date": "2025-07-16T14:33:08.064724532+00:00", - "end_date": "2025-10-14T14:33:08.064725045+00:00" - }, - { - "model_name": "ppo_actor_epoch_290", - "model_type": "PPO", - "epoch": 290, - "total_trades": 217, - "winning_trades": 135, - "win_rate": 62.21198156682027, - "total_pnl": 28.596089999999982, - "sharpe_ratio": 5.894257175982202, - "max_drawdown": 0.000016045942793812857, - "calmar_ratio": 1782.1383490832568, - "avg_trade_duration": 25.19815668202765, - "profit_factor": 417.42769768456867, - "trade_frequency": 30.042918454935624, - "start_date": "2025-07-16T14:33:08.109178172+00:00", - "end_date": "2025-10-14T14:33:08.109178471+00:00" - }, - { - "model_name": "ppo_actor_epoch_300", - "model_type": "PPO", - "epoch": 300, - "total_trades": 242, - "winning_trades": 139, - "win_rate": 57.438016528925615, - "total_pnl": 30.587620000000015, - "sharpe_ratio": 5.735513811832606, - "max_drawdown": 0.00002719167699972574, - "calmar_ratio": 1124.889060734605, - "avg_trade_duration": 22.727272727272727, - "profit_factor": 122.30723775530407, - "trade_frequency": 33.504084175550325, - "start_date": "2025-07-16T14:33:08.149889373+00:00", - "end_date": "2025-10-14T14:33:08.149889841+00:00" - }, - { - "model_name": "ppo_actor_epoch_310", - "model_type": "PPO", - "epoch": 310, - "total_trades": 475, - "winning_trades": 264, - "win_rate": 55.57894736842105, - "total_pnl": 71.22049000000001, - "sharpe_ratio": 6.322836154555349, - "max_drawdown": 0.00003337694877465531, - "calmar_ratio": 2133.822671475941, - "avg_trade_duration": 11.33263157894737, - "profit_factor": 174.2437119922155, - "trade_frequency": 65.76214869167936, - "start_date": "2025-07-16T14:33:08.196817863+00:00", - "end_date": "2025-10-14T14:33:08.196818566+00:00" - }, - { - "model_name": "ppo_actor_epoch_320", - "model_type": "PPO", - "epoch": 320, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.239943945+00:00", - "end_date": "2025-10-14T14:33:08.239944194+00:00" - }, - { - "model_name": "ppo_actor_epoch_330", - "model_type": "PPO", - "epoch": 330, - "total_trades": 847, - "winning_trades": 289, - "win_rate": 34.120425029515935, - "total_pnl": -194.87595000000016, - "sharpe_ratio": -6.9292313919132065, - "max_drawdown": 0.19487595000014699, - "calmar_ratio": -1.0, - "avg_trade_duration": 6.018890200708382, - "profit_factor": 0.1378093966128675, - "trade_frequency": 117.26429461442613, - "start_date": "2025-07-16T14:33:08.279566973+00:00", - "end_date": "2025-10-14T14:33:08.279567331+00:00" - }, - { - "model_name": "ppo_actor_epoch_340", - "model_type": "PPO", - "epoch": 340, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.013900000000000023, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5622.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.323028583+00:00", - "end_date": "2025-10-14T14:33:08.323029127+00:00" - }, - { - "model_name": "ppo_actor_epoch_350", - "model_type": "PPO", - "epoch": 350, - "total_trades": 818, - "winning_trades": 306, - "win_rate": 37.40831295843521, - "total_pnl": -146.6349299999999, - "sharpe_ratio": -7.022932040177868, - "max_drawdown": 0.14663622809368912, - "calmar_ratio": -0.9999911475238082, - "avg_trade_duration": 6.292176039119805, - "profit_factor": 0.0049806970845211445, - "trade_frequency": 113.24934237851309, - "start_date": "2025-07-16T14:33:08.364123777+00:00", - "end_date": "2025-10-14T14:33:08.364124176+00:00" - }, - { - "model_name": "ppo_actor_epoch_360", - "model_type": "PPO", - "epoch": 360, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.409316579+00:00", - "end_date": "2025-10-14T14:33:08.409316965+00:00" - }, - { - "model_name": "ppo_actor_epoch_370", - "model_type": "PPO", - "epoch": 370, - "total_trades": 5, - "winning_trades": 2, - "win_rate": 40.0, - "total_pnl": -0.011450000000000626, - "sharpe_ratio": -7.882208600808902, - "max_drawdown": 0.000012549999861414492, - "calmar_ratio": -0.9123506081166156, - "avg_trade_duration": 911.2, - "profit_factor": 0.1611721611721389, - "trade_frequency": 0.6922331441229406, - "start_date": "2025-07-16T14:33:08.456236090+00:00", - "end_date": "2025-10-14T14:33:08.456236517+00:00" - }, - { - "model_name": "ppo_actor_epoch_380", - "model_type": "PPO", - "epoch": 380, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.013900000000000023, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5622.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.498005717+00:00", - "end_date": "2025-10-14T14:33:08.498006075+00:00" - }, - { - "model_name": "ppo_actor_epoch_390", - "model_type": "PPO", - "epoch": 390, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.538981431+00:00", - "end_date": "2025-10-14T14:33:08.538982378+00:00" - }, - { - "model_name": "ppo_actor_epoch_400", - "model_type": "PPO", - "epoch": 400, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "start_date": "2025-07-16T14:33:08.586819761+00:00", - "end_date": "2025-10-14T14:33:08.586820105+00:00" - }, - { - "model_name": "ppo_actor_epoch_410", - "model_type": "PPO", - "epoch": 410, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.634721110+00:00", - "end_date": "2025-10-14T14:33:08.634721940+00:00" - }, - { - "model_name": "ppo_actor_epoch_420", - "model_type": "PPO", - "epoch": 420, - "total_trades": 29, - "winning_trades": 18, - "win_rate": 62.06896551724138, - "total_pnl": 9.84882, - "sharpe_ratio": 10.651740936708926, - "max_drawdown": 9.549999720766295e-6, - "calmar_ratio": 1031.2900825023498, - "avg_trade_duration": 194.0, - "profit_factor": 295.434080717489, - "trade_frequency": 4.014952235913055, - "start_date": "2025-07-16T14:33:08.677244264+00:00", - "end_date": "2025-10-14T14:33:08.677244692+00:00" - }, - { - "model_name": "ppo_actor_epoch_430", - "model_type": "PPO", - "epoch": 430, - "total_trades": 378, - "winning_trades": 174, - "win_rate": 46.03174603174603, - "total_pnl": -33.944900000000004, - "sharpe_ratio": -4.742097970826895, - "max_drawdown": 0.03395799680793247, - "calmar_ratio": -0.9996143233059696, - "avg_trade_duration": 14.227513227513228, - "profit_factor": 0.01092516429996052, - "trade_frequency": 52.33282569569431, - "start_date": "2025-07-16T14:33:08.721778845+00:00", - "end_date": "2025-10-14T14:33:08.721779106+00:00" - }, - { - "model_name": "ppo_actor_epoch_440", - "model_type": "PPO", - "epoch": 440, - "total_trades": 82, - "winning_trades": 42, - "win_rate": 51.21951219512195, - "total_pnl": -0.03059999999999996, - "sharpe_ratio": -1.93625508452571, - "max_drawdown": 0.00003990000000339933, - "calmar_ratio": -0.7669172920292666, - "avg_trade_duration": 65.47560975609755, - "profit_factor": 0.6909090909090913, - "trade_frequency": 11.352623563616225, - "start_date": "2025-07-16T14:33:08.767379213+00:00", - "end_date": "2025-10-14T14:33:08.767379558+00:00" - }, - { - "model_name": "ppo_actor_epoch_450", - "model_type": "PPO", - "epoch": 450, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.806783961+00:00", - "end_date": "2025-10-14T14:33:08.806784605+00:00" - }, - { - "model_name": "ppo_actor_epoch_460", - "model_type": "PPO", - "epoch": 460, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.008199999999999985, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.852885418+00:00", - "end_date": "2025-10-14T14:33:08.852885763+00:00" - }, - { - "model_name": "ppo_actor_epoch_470", - "model_type": "PPO", - "epoch": 470, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.008199999999999985, - "sharpe_ratio": 0.0, - "max_drawdown": 8.19999999657739e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5600.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.897095335+00:00", - "end_date": "2025-10-14T14:33:08.897095688+00:00" - }, - { - "model_name": "ppo_actor_epoch_480", - "model_type": "PPO", - "epoch": 480, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5638.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:08.938163954+00:00", - "end_date": "2025-10-14T14:33:08.938164719+00:00" - }, - { - "model_name": "ppo_actor_epoch_490", - "model_type": "PPO", - "epoch": 490, - "total_trades": 290, - "winning_trades": 162, - "win_rate": 55.86206896551724, - "total_pnl": 25.191279999999992, - "sharpe_ratio": 3.725433032871732, - "max_drawdown": 0.005485809024888312, - "calmar_ratio": 4.592081110687421, - "avg_trade_duration": 18.420689655172414, - "profit_factor": 4.757251245766036, - "trade_frequency": 40.14952235913056, - "start_date": "2025-07-16T14:33:08.982855589+00:00", - "end_date": "2025-10-14T14:33:08.982856027+00:00" - }, - { - "model_name": "ppo_actor_epoch_500", - "model_type": "PPO", - "epoch": 500, - "total_trades": 1, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": -0.009800000000000031, - "sharpe_ratio": 0.0, - "max_drawdown": 9.799999999813735e-6, - "calmar_ratio": -1.0, - "avg_trade_duration": 5638.0, - "profit_factor": -0.0, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-16T14:33:09.027012303+00:00", - "end_date": "2025-10-14T14:33:09.027012645+00:00" - } -] \ No newline at end of file diff --git a/results/comprehensive_backtest_results_20251017_124647.json b/results/comprehensive_backtest_results_20251017_124647.json deleted file mode 100644 index 2bbeec332..000000000 --- a/results/comprehensive_backtest_results_20251017_124647.json +++ /dev/null @@ -1,1702 +0,0 @@ -[ - { - "model_name": "dqn_epoch_10", - "model_type": "DQN", - "epoch": 10, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:02.957485076+00:00", - "end_date": "2025-10-17T12:46:02.957485217+00:00" - }, - { - "model_name": "dqn_epoch_20", - "model_type": "DQN", - "epoch": 20, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:03.659191931+00:00", - "end_date": "2025-10-17T12:46:03.659192443+00:00" - }, - { - "model_name": "dqn_epoch_30", - "model_type": "DQN", - "epoch": 30, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:04.154992430+00:00", - "end_date": "2025-10-17T12:46:04.154992937+00:00" - }, - { - "model_name": "dqn_epoch_40", - "model_type": "DQN", - "epoch": 40, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:04.644958542+00:00", - "end_date": "2025-10-17T12:46:04.644959129+00:00" - }, - { - "model_name": "dqn_epoch_50", - "model_type": "DQN", - "epoch": 50, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:05.135453277+00:00", - "end_date": "2025-10-17T12:46:05.135453896+00:00" - }, - { - "model_name": "dqn_epoch_60", - "model_type": "DQN", - "epoch": 60, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:05.629813689+00:00", - "end_date": "2025-10-17T12:46:05.629814151+00:00" - }, - { - "model_name": "dqn_epoch_70", - "model_type": "DQN", - "epoch": 70, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:06.127309720+00:00", - "end_date": "2025-10-17T12:46:06.127310210+00:00" - }, - { - "model_name": "dqn_epoch_80", - "model_type": "DQN", - "epoch": 80, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:06.662992523+00:00", - "end_date": "2025-10-17T12:46:06.662992959+00:00" - }, - { - "model_name": "dqn_epoch_90", - "model_type": "DQN", - "epoch": 90, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:07.151271603+00:00", - "end_date": "2025-10-17T12:46:07.151272086+00:00" - }, - { - "model_name": "dqn_epoch_100", - "model_type": "DQN", - "epoch": 100, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:07.647853722+00:00", - "end_date": "2025-10-17T12:46:07.647854316+00:00" - }, - { - "model_name": "dqn_epoch_110", - "model_type": "DQN", - "epoch": 110, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:08.142284424+00:00", - "end_date": "2025-10-17T12:46:08.142284811+00:00" - }, - { - "model_name": "dqn_epoch_120", - "model_type": "DQN", - "epoch": 120, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:08.635673211+00:00", - "end_date": "2025-10-17T12:46:08.635674122+00:00" - }, - { - "model_name": "dqn_epoch_130", - "model_type": "DQN", - "epoch": 130, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:09.151088890+00:00", - "end_date": "2025-10-17T12:46:09.151089477+00:00" - }, - { - "model_name": "dqn_epoch_140", - "model_type": "DQN", - "epoch": 140, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:09.646590263+00:00", - "end_date": "2025-10-17T12:46:09.646590736+00:00" - }, - { - "model_name": "dqn_epoch_150", - "model_type": "DQN", - "epoch": 150, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:10.142311161+00:00", - "end_date": "2025-10-17T12:46:10.142311706+00:00" - }, - { - "model_name": "dqn_epoch_160", - "model_type": "DQN", - "epoch": 160, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:10.629052862+00:00", - "end_date": "2025-10-17T12:46:10.629053505+00:00" - }, - { - "model_name": "dqn_epoch_170", - "model_type": "DQN", - "epoch": 170, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:11.164728973+00:00", - "end_date": "2025-10-17T12:46:11.164729647+00:00" - }, - { - "model_name": "dqn_epoch_180", - "model_type": "DQN", - "epoch": 180, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:11.668216307+00:00", - "end_date": "2025-10-17T12:46:11.668217363+00:00" - }, - { - "model_name": "dqn_epoch_190", - "model_type": "DQN", - "epoch": 190, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:12.157860218+00:00", - "end_date": "2025-10-17T12:46:12.157860724+00:00" - }, - { - "model_name": "dqn_epoch_200", - "model_type": "DQN", - "epoch": 200, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:12.651104421+00:00", - "end_date": "2025-10-17T12:46:12.651104890+00:00" - }, - { - "model_name": "dqn_epoch_210", - "model_type": "DQN", - "epoch": 210, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:13.141297042+00:00", - "end_date": "2025-10-17T12:46:13.141312854+00:00" - }, - { - "model_name": "dqn_epoch_220", - "model_type": "DQN", - "epoch": 220, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:13.646139620+00:00", - "end_date": "2025-10-17T12:46:13.646140543+00:00" - }, - { - "model_name": "dqn_epoch_230", - "model_type": "DQN", - "epoch": 230, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:14.152802380+00:00", - "end_date": "2025-10-17T12:46:14.152802992+00:00" - }, - { - "model_name": "dqn_epoch_240", - "model_type": "DQN", - "epoch": 240, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:14.663596686+00:00", - "end_date": "2025-10-17T12:46:14.663597190+00:00" - }, - { - "model_name": "dqn_epoch_250", - "model_type": "DQN", - "epoch": 250, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:15.155346903+00:00", - "end_date": "2025-10-17T12:46:15.155347697+00:00" - }, - { - "model_name": "dqn_epoch_260", - "model_type": "DQN", - "epoch": 260, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:15.652315026+00:00", - "end_date": "2025-10-17T12:46:15.652315504+00:00" - }, - { - "model_name": "dqn_epoch_270", - "model_type": "DQN", - "epoch": 270, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:16.144022434+00:00", - "end_date": "2025-10-17T12:46:16.144022904+00:00" - }, - { - "model_name": "dqn_epoch_280", - "model_type": "DQN", - "epoch": 280, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:16.637857157+00:00", - "end_date": "2025-10-17T12:46:16.637857738+00:00" - }, - { - "model_name": "dqn_epoch_290", - "model_type": "DQN", - "epoch": 290, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:17.149401451+00:00", - "end_date": "2025-10-17T12:46:17.149401983+00:00" - }, - { - "model_name": "dqn_epoch_300", - "model_type": "DQN", - "epoch": 300, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:17.645587450+00:00", - "end_date": "2025-10-17T12:46:17.645589184+00:00" - }, - { - "model_name": "dqn_epoch_310", - "model_type": "DQN", - "epoch": 310, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:18.139632486+00:00", - "end_date": "2025-10-17T12:46:18.139633098+00:00" - }, - { - "model_name": "dqn_epoch_320", - "model_type": "DQN", - "epoch": 320, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:18.628817590+00:00", - "end_date": "2025-10-17T12:46:18.628818081+00:00" - }, - { - "model_name": "dqn_epoch_330", - "model_type": "DQN", - "epoch": 330, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:19.148230340+00:00", - "end_date": "2025-10-17T12:46:19.148230810+00:00" - }, - { - "model_name": "dqn_epoch_340", - "model_type": "DQN", - "epoch": 340, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:19.638799285+00:00", - "end_date": "2025-10-17T12:46:19.638799817+00:00" - }, - { - "model_name": "dqn_epoch_350", - "model_type": "DQN", - "epoch": 350, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:20.134028572+00:00", - "end_date": "2025-10-17T12:46:20.134029430+00:00" - }, - { - "model_name": "dqn_epoch_360", - "model_type": "DQN", - "epoch": 360, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:20.635568233+00:00", - "end_date": "2025-10-17T12:46:20.635568671+00:00" - }, - { - "model_name": "dqn_epoch_370", - "model_type": "DQN", - "epoch": 370, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:21.128101645+00:00", - "end_date": "2025-10-17T12:46:21.128102223+00:00" - }, - { - "model_name": "dqn_epoch_380", - "model_type": "DQN", - "epoch": 380, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:21.613556385+00:00", - "end_date": "2025-10-17T12:46:21.613557119+00:00" - }, - { - "model_name": "dqn_epoch_390", - "model_type": "DQN", - "epoch": 390, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:22.108607200+00:00", - "end_date": "2025-10-17T12:46:22.108607838+00:00" - }, - { - "model_name": "dqn_epoch_400", - "model_type": "DQN", - "epoch": 400, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:22.588706169+00:00", - "end_date": "2025-10-17T12:46:22.588706997+00:00" - }, - { - "model_name": "dqn_epoch_410", - "model_type": "DQN", - "epoch": 410, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:23.074123147+00:00", - "end_date": "2025-10-17T12:46:23.074123786+00:00" - }, - { - "model_name": "dqn_epoch_420", - "model_type": "DQN", - "epoch": 420, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:23.567251921+00:00", - "end_date": "2025-10-17T12:46:23.567252490+00:00" - }, - { - "model_name": "dqn_epoch_430", - "model_type": "DQN", - "epoch": 430, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:24.070525898+00:00", - "end_date": "2025-10-17T12:46:24.070526495+00:00" - }, - { - "model_name": "dqn_epoch_440", - "model_type": "DQN", - "epoch": 440, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:24.553179247+00:00", - "end_date": "2025-10-17T12:46:24.553180043+00:00" - }, - { - "model_name": "dqn_epoch_450", - "model_type": "DQN", - "epoch": 450, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:25.050178120+00:00", - "end_date": "2025-10-17T12:46:25.050178866+00:00" - }, - { - "model_name": "dqn_epoch_460", - "model_type": "DQN", - "epoch": 460, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:25.541502220+00:00", - "end_date": "2025-10-17T12:46:25.541502722+00:00" - }, - { - "model_name": "dqn_epoch_470", - "model_type": "DQN", - "epoch": 470, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:26.033629452+00:00", - "end_date": "2025-10-17T12:46:26.033629890+00:00" - }, - { - "model_name": "dqn_epoch_480", - "model_type": "DQN", - "epoch": 480, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:26.523568586+00:00", - "end_date": "2025-10-17T12:46:26.523569042+00:00" - }, - { - "model_name": "dqn_epoch_490", - "model_type": "DQN", - "epoch": 490, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:27.025865391+00:00", - "end_date": "2025-10-17T12:46:27.025865818+00:00" - }, - { - "model_name": "dqn_epoch_500", - "model_type": "DQN", - "epoch": 500, - "total_trades": 354, - "winning_trades": 148, - "win_rate": 41.80790960451977, - "total_pnl": -55.895200000000024, - "sharpe_ratio": -6.519229015881037, - "max_drawdown": 0.05592469415591633, - "calmar_ratio": -0.9994726094378634, - "avg_trade_duration": 15.403954802259888, - "profit_factor": 0.005417293524069991, - "trade_frequency": 49.0101066039042, - "start_date": "2025-07-19T12:46:27.520453183+00:00", - "end_date": "2025-10-17T12:46:27.520453608+00:00" - }, - { - "model_name": "ppo_actor_epoch_10", - "model_type": "PPO", - "epoch": 10, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:28.022843853+00:00", - "end_date": "2025-10-17T12:46:28.022844283+00:00" - }, - { - "model_name": "ppo_actor_epoch_20", - "model_type": "PPO", - "epoch": 20, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:28.401066680+00:00", - "end_date": "2025-10-17T12:46:28.401067064+00:00" - }, - { - "model_name": "ppo_actor_epoch_30", - "model_type": "PPO", - "epoch": 30, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:28.776533295+00:00", - "end_date": "2025-10-17T12:46:28.776533903+00:00" - }, - { - "model_name": "ppo_actor_epoch_40", - "model_type": "PPO", - "epoch": 40, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:29.181608659+00:00", - "end_date": "2025-10-17T12:46:29.181609202+00:00" - }, - { - "model_name": "ppo_actor_epoch_50", - "model_type": "PPO", - "epoch": 50, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:29.565245776+00:00", - "end_date": "2025-10-17T12:46:29.565246288+00:00" - }, - { - "model_name": "ppo_actor_epoch_60", - "model_type": "PPO", - "epoch": 60, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:29.948216731+00:00", - "end_date": "2025-10-17T12:46:29.948217232+00:00" - }, - { - "model_name": "ppo_actor_epoch_70", - "model_type": "PPO", - "epoch": 70, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:30.323333585+00:00", - "end_date": "2025-10-17T12:46:30.323334034+00:00" - }, - { - "model_name": "ppo_actor_epoch_80", - "model_type": "PPO", - "epoch": 80, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:30.703012727+00:00", - "end_date": "2025-10-17T12:46:30.703013105+00:00" - }, - { - "model_name": "ppo_actor_epoch_90", - "model_type": "PPO", - "epoch": 90, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:31.097125370+00:00", - "end_date": "2025-10-17T12:46:31.097125881+00:00" - }, - { - "model_name": "ppo_actor_epoch_100", - "model_type": "PPO", - "epoch": 100, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:31.475678631+00:00", - "end_date": "2025-10-17T12:46:31.475679023+00:00" - }, - { - "model_name": "ppo_actor_epoch_110", - "model_type": "PPO", - "epoch": 110, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:31.850413443+00:00", - "end_date": "2025-10-17T12:46:31.850414814+00:00" - }, - { - "model_name": "ppo_actor_epoch_120", - "model_type": "PPO", - "epoch": 120, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:32.231736021+00:00", - "end_date": "2025-10-17T12:46:32.231736417+00:00" - }, - { - "model_name": "ppo_actor_epoch_130", - "model_type": "PPO", - "epoch": 130, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:32.611056138+00:00", - "end_date": "2025-10-17T12:46:32.611056639+00:00" - }, - { - "model_name": "ppo_actor_epoch_140", - "model_type": "PPO", - "epoch": 140, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:32.994284713+00:00", - "end_date": "2025-10-17T12:46:32.994285343+00:00" - }, - { - "model_name": "ppo_actor_epoch_150", - "model_type": "PPO", - "epoch": 150, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:33.368283333+00:00", - "end_date": "2025-10-17T12:46:33.368283853+00:00" - }, - { - "model_name": "ppo_actor_epoch_160", - "model_type": "PPO", - "epoch": 160, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:33.756621580+00:00", - "end_date": "2025-10-17T12:46:33.756622495+00:00" - }, - { - "model_name": "ppo_actor_epoch_170", - "model_type": "PPO", - "epoch": 170, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:34.144304045+00:00", - "end_date": "2025-10-17T12:46:34.144304451+00:00" - }, - { - "model_name": "ppo_actor_epoch_180", - "model_type": "PPO", - "epoch": 180, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:34.528675774+00:00", - "end_date": "2025-10-17T12:46:34.528676282+00:00" - }, - { - "model_name": "ppo_actor_epoch_190", - "model_type": "PPO", - "epoch": 190, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:34.905673974+00:00", - "end_date": "2025-10-17T12:46:34.905674588+00:00" - }, - { - "model_name": "ppo_actor_epoch_200", - "model_type": "PPO", - "epoch": 200, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:35.282314134+00:00", - "end_date": "2025-10-17T12:46:35.282314790+00:00" - }, - { - "model_name": "ppo_actor_epoch_210", - "model_type": "PPO", - "epoch": 210, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:35.672512641+00:00", - "end_date": "2025-10-17T12:46:35.672513224+00:00" - }, - { - "model_name": "ppo_actor_epoch_220", - "model_type": "PPO", - "epoch": 220, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:36.066283019+00:00", - "end_date": "2025-10-17T12:46:36.066283855+00:00" - }, - { - "model_name": "ppo_actor_epoch_230", - "model_type": "PPO", - "epoch": 230, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:36.452806331+00:00", - "end_date": "2025-10-17T12:46:36.452806933+00:00" - }, - { - "model_name": "ppo_actor_epoch_240", - "model_type": "PPO", - "epoch": 240, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:36.830505011+00:00", - "end_date": "2025-10-17T12:46:36.830505864+00:00" - }, - { - "model_name": "ppo_actor_epoch_250", - "model_type": "PPO", - "epoch": 250, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:37.221124424+00:00", - "end_date": "2025-10-17T12:46:37.221125014+00:00" - }, - { - "model_name": "ppo_actor_epoch_260", - "model_type": "PPO", - "epoch": 260, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:37.598605899+00:00", - "end_date": "2025-10-17T12:46:37.598606510+00:00" - }, - { - "model_name": "ppo_actor_epoch_270", - "model_type": "PPO", - "epoch": 270, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:37.978912297+00:00", - "end_date": "2025-10-17T12:46:37.978912892+00:00" - }, - { - "model_name": "ppo_actor_epoch_280", - "model_type": "PPO", - "epoch": 280, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:38.352307375+00:00", - "end_date": "2025-10-17T12:46:38.352308210+00:00" - }, - { - "model_name": "ppo_actor_epoch_290", - "model_type": "PPO", - "epoch": 290, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:38.738762686+00:00", - "end_date": "2025-10-17T12:46:38.738763082+00:00" - }, - { - "model_name": "ppo_actor_epoch_300", - "model_type": "PPO", - "epoch": 300, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:39.142569318+00:00", - "end_date": "2025-10-17T12:46:39.142569715+00:00" - }, - { - "model_name": "ppo_actor_epoch_310", - "model_type": "PPO", - "epoch": 310, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:39.518168019+00:00", - "end_date": "2025-10-17T12:46:39.518168504+00:00" - }, - { - "model_name": "ppo_actor_epoch_320", - "model_type": "PPO", - "epoch": 320, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:39.889796627+00:00", - "end_date": "2025-10-17T12:46:39.889797091+00:00" - }, - { - "model_name": "ppo_actor_epoch_330", - "model_type": "PPO", - "epoch": 330, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:40.266146346+00:00", - "end_date": "2025-10-17T12:46:40.266146841+00:00" - }, - { - "model_name": "ppo_actor_epoch_340", - "model_type": "PPO", - "epoch": 340, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:40.644727126+00:00", - "end_date": "2025-10-17T12:46:40.644727677+00:00" - }, - { - "model_name": "ppo_actor_epoch_350", - "model_type": "PPO", - "epoch": 350, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:41.021554587+00:00", - "end_date": "2025-10-17T12:46:41.021555008+00:00" - }, - { - "model_name": "ppo_actor_epoch_360", - "model_type": "PPO", - "epoch": 360, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:41.399491681+00:00", - "end_date": "2025-10-17T12:46:41.399492125+00:00" - }, - { - "model_name": "ppo_actor_epoch_370", - "model_type": "PPO", - "epoch": 370, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:41.778792028+00:00", - "end_date": "2025-10-17T12:46:41.778792569+00:00" - }, - { - "model_name": "ppo_actor_epoch_380", - "model_type": "PPO", - "epoch": 380, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:42.173749355+00:00", - "end_date": "2025-10-17T12:46:42.173749783+00:00" - }, - { - "model_name": "ppo_actor_epoch_390", - "model_type": "PPO", - "epoch": 390, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:42.553942662+00:00", - "end_date": "2025-10-17T12:46:42.553943122+00:00" - }, - { - "model_name": "ppo_actor_epoch_400", - "model_type": "PPO", - "epoch": 400, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:42.935530102+00:00", - "end_date": "2025-10-17T12:46:42.935530483+00:00" - }, - { - "model_name": "ppo_actor_epoch_410", - "model_type": "PPO", - "epoch": 410, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:43.320850149+00:00", - "end_date": "2025-10-17T12:46:43.320850564+00:00" - }, - { - "model_name": "ppo_actor_epoch_420", - "model_type": "PPO", - "epoch": 420, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:43.708508883+00:00", - "end_date": "2025-10-17T12:46:43.708509843+00:00" - }, - { - "model_name": "ppo_actor_epoch_430", - "model_type": "PPO", - "epoch": 430, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:44.101019374+00:00", - "end_date": "2025-10-17T12:46:44.101019815+00:00" - }, - { - "model_name": "ppo_actor_epoch_440", - "model_type": "PPO", - "epoch": 440, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:44.476613090+00:00", - "end_date": "2025-10-17T12:46:44.476613540+00:00" - }, - { - "model_name": "ppo_actor_epoch_450", - "model_type": "PPO", - "epoch": 450, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:44.857222360+00:00", - "end_date": "2025-10-17T12:46:44.857222932+00:00" - }, - { - "model_name": "ppo_actor_epoch_460", - "model_type": "PPO", - "epoch": 460, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:45.253607164+00:00", - "end_date": "2025-10-17T12:46:45.253607645+00:00" - }, - { - "model_name": "ppo_actor_epoch_470", - "model_type": "PPO", - "epoch": 470, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:45.628275591+00:00", - "end_date": "2025-10-17T12:46:45.628276029+00:00" - }, - { - "model_name": "ppo_actor_epoch_480", - "model_type": "PPO", - "epoch": 480, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:46.003871039+00:00", - "end_date": "2025-10-17T12:46:46.003871862+00:00" - }, - { - "model_name": "ppo_actor_epoch_490", - "model_type": "PPO", - "epoch": 490, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:46.382537259+00:00", - "end_date": "2025-10-17T12:46:46.382537658+00:00" - }, - { - "model_name": "ppo_actor_epoch_500", - "model_type": "PPO", - "epoch": 500, - "total_trades": 1, - "winning_trades": 1, - "win_rate": 100.0, - "total_pnl": 0.00955000000000017, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration": 5600.0, - "profit_factor": null, - "trade_frequency": 0.13844662882458814, - "start_date": "2025-07-19T12:46:46.762798381+00:00", - "end_date": "2025-10-17T12:46:46.762798840+00:00" - } -] \ No newline at end of file diff --git a/results/dqn_tuning_36trials_extracted.json b/results/dqn_tuning_36trials_extracted.json deleted file mode 100644 index f580d7b55..000000000 --- a/results/dqn_tuning_36trials_extracted.json +++ /dev/null @@ -1,354 +0,0 @@ -{ - "metadata": { - "agent": "Agent 132", - "task": "DQN Hyperparameter Extraction", - "date": "2025-10-14T21:35:31.641839", - "total_trials": 36, - "status": "Analysis Complete - Backtest Required" - }, - "checkpoint_analysis": { - "summary": { - "total_checkpoints": 36, - "file_size_kb_min": 73.9, - "file_size_kb_max": 73.9, - "file_size_kb_mean": 73.9 - }, - "checkpoints": [ - { - "trial_num": 0, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:00:31.295448", - "modified": "2025-10-14T17:00:31.295448" - }, - { - "trial_num": 1, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:03:29.256538", - "modified": "2025-10-14T17:03:29.256538" - }, - { - "trial_num": 10, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:30:19.245741", - "modified": "2025-10-14T17:30:19.245741" - }, - { - "trial_num": 11, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:33:15.464071", - "modified": "2025-10-14T17:33:15.464071" - }, - { - "trial_num": 12, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:36:08.504380", - "modified": "2025-10-14T17:36:08.504380" - }, - { - "trial_num": 13, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:39:04.100681", - "modified": "2025-10-14T17:39:04.100681" - }, - { - "trial_num": 14, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:42:26.641368", - "modified": "2025-10-14T17:42:26.641368" - }, - { - "trial_num": 15, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:45:49.010670", - "modified": "2025-10-14T17:45:49.010670" - }, - { - "trial_num": 16, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:48:28.495263", - "modified": "2025-10-14T17:48:28.495263" - }, - { - "trial_num": 17, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:51:06.858344", - "modified": "2025-10-14T17:51:06.858344" - }, - { - "trial_num": 18, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:53:55.985170", - "modified": "2025-10-14T17:53:55.985170" - }, - { - "trial_num": 19, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:57:01.585837", - "modified": "2025-10-14T17:57:01.585837" - }, - { - "trial_num": 2, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:06:31.450646", - "modified": "2025-10-14T17:06:31.450646" - }, - { - "trial_num": 20, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:00:32.169432", - "modified": "2025-10-14T18:00:32.169432" - }, - { - "trial_num": 21, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:03:51.971747", - "modified": "2025-10-14T18:03:51.971747" - }, - { - "trial_num": 22, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:06:58.496857", - "modified": "2025-10-14T18:06:58.496857" - }, - { - "trial_num": 23, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:10:09.522913", - "modified": "2025-10-14T18:10:09.522913" - }, - { - "trial_num": 24, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:13:04.419831", - "modified": "2025-10-14T18:13:04.419831" - }, - { - "trial_num": 25, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:16:21.630586", - "modified": "2025-10-14T18:16:21.630586" - }, - { - "trial_num": 26, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:19:26.333054", - "modified": "2025-10-14T18:19:26.333054" - }, - { - "trial_num": 27, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:22:29.893616", - "modified": "2025-10-14T18:22:29.893616" - }, - { - "trial_num": 28, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:26:14.266392", - "modified": "2025-10-14T18:26:14.266392" - }, - { - "trial_num": 29, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:29:01.312014", - "modified": "2025-10-14T18:29:01.312014" - }, - { - "trial_num": 3, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:09:50.377881", - "modified": "2025-10-14T17:09:50.377881" - }, - { - "trial_num": 30, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:31:41.110633", - "modified": "2025-10-14T18:31:41.110633" - }, - { - "trial_num": 31, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:34:25.305287", - "modified": "2025-10-14T18:34:25.305287" - }, - { - "trial_num": 32, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:37:10.421958", - "modified": "2025-10-14T18:37:10.421958" - }, - { - "trial_num": 33, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:39:52.094625", - "modified": "2025-10-14T18:39:52.094625" - }, - { - "trial_num": 34, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:42:32.023290", - "modified": "2025-10-14T18:42:32.023290" - }, - { - "trial_num": 35, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T18:45:35.857062", - "modified": "2025-10-14T18:45:35.857062" - }, - { - "trial_num": 4, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:13:00.322841", - "modified": "2025-10-14T17:13:00.322841" - }, - { - "trial_num": 5, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:16:05.229578", - "modified": "2025-10-14T17:16:05.229578" - }, - { - "trial_num": 6, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:19:01.963154", - "modified": "2025-10-14T17:19:01.963154" - }, - { - "trial_num": 7, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:21:59.193643", - "modified": "2025-10-14T17:21:59.193643" - }, - { - "trial_num": 8, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:24:53.433062", - "modified": "2025-10-14T17:24:53.433062" - }, - { - "trial_num": 9, - "file_size_bytes": 75628, - "file_size_kb": 73.9, - "created": "2025-10-14T17:27:35.644414", - "modified": "2025-10-14T17:27:35.644414" - } - ] - }, - "search_space": { - "learning_rate": { - "type": "loguniform", - "low": 0.0001, - "high": 0.01 - }, - "batch_size": { - "type": "categorical", - "choices": [ - 64, - 128, - 256 - ] - }, - "gamma": { - "type": "uniform", - "low": 0.95, - "high": 0.99 - } - }, - "recommendations": { - "option_1_immediate": { - "description": "Test trial 35 checkpoint (TPE convergence assumption)", - "time": "10 minutes", - "confidence": "Medium", - "command": "cargo run -p ml --example backtest_dqn -- --checkpoint ml/tuning_checkpoints/trial_35/checkpoint_epoch_50.safetensors" - }, - "option_2_sample": { - "description": "Backtest 10 representative checkpoints", - "time": "1 hour", - "confidence": "Medium-High", - "trials": [ - 0, - 4, - 8, - 12, - 16, - 20, - 24, - 28, - 32, - 35 - ] - }, - "option_3_comprehensive": { - "description": "Backtest all 36 checkpoints", - "time": "3-6 hours", - "confidence": "High" - }, - "option_4_defaults": { - "description": "Use best-practice hyperparameters", - "time": "Immediate", - "confidence": "Low-Medium", - "hyperparameters": { - "learning_rate": 0.001, - "batch_size": 128, - "gamma": 0.97 - } - } - }, - "next_actions": [ - { - "priority": 1, - "action": "Test trial 35 checkpoint", - "estimated_time": "10 minutes", - "decision_criteria": "If Sharpe > 1.5, use for production" - }, - { - "priority": 2, - "action": "Sample backtest (10 trials)", - "estimated_time": "1 hour" - }, - { - "priority": 3, - "action": "Full backtest (all 36)", - "estimated_time": "3-6 hours" - }, - { - "priority": 4, - "action": "Use best-practice defaults", - "estimated_time": "Immediate" - } - ] -} \ No newline at end of file diff --git a/results/ensemble_backtest_results_20251014_151004.json b/results/ensemble_backtest_results_20251014_151004.json deleted file mode 100644 index 5c6402cb5..000000000 --- a/results/ensemble_backtest_results_20251014_151004.json +++ /dev/null @@ -1,87 +0,0 @@ -[ - { - "strategy_name": "DQN-E360", - "model_type": "Individual", - "epoch": null, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration_minutes": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "average_confidence": 0.0, - "total_bars": 665483 - }, - { - "strategy_name": "PPO-E280", - "model_type": "Individual", - "epoch": null, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration_minutes": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "average_confidence": 0.0, - "total_bars": 665483 - }, - { - "strategy_name": "Equal-Weight", - "model_type": "Ensemble", - "epoch": null, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration_minutes": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "average_confidence": 0.0, - "total_bars": 665483 - }, - { - "strategy_name": "Performance-Weighted", - "model_type": "Ensemble", - "epoch": null, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration_minutes": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "average_confidence": 0.0, - "total_bars": 665483 - }, - { - "strategy_name": "Confidence-Weighted", - "model_type": "Ensemble", - "epoch": null, - "total_trades": 0, - "winning_trades": 0, - "win_rate": 0.0, - "total_pnl": 0.0, - "sharpe_ratio": 0.0, - "max_drawdown": 0.0, - "calmar_ratio": 0.0, - "avg_trade_duration_minutes": 0.0, - "profit_factor": 0.0, - "trade_frequency": 0.0, - "average_confidence": 0.0, - "total_bars": 665483 - } -] \ No newline at end of file diff --git a/results/rate_limiter_results.txt b/results/rate_limiter_results.txt deleted file mode 100644 index 5e97443f1..000000000 --- a/results/rate_limiter_results.txt +++ /dev/null @@ -1,144 +0,0 @@ - Blocking waiting for file lock on build directory - Compiling api_gateway v1.0.0 (/home/jgrusewski/Work/foxhunt/services/api_gateway) -warning: type `HealthState` is more private than the item `backtesting_proxy::HealthChecker::get_state` - --> services/api_gateway/src/grpc/backtesting_proxy.rs:93:5 - | -93 | pub async fn get_state(&self) -> HealthState { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method `backtesting_proxy::HealthChecker::get_state` is reachable at visibility `pub` - | -note: but type `HealthState` is only usable at visibility `pub(self)` - --> services/api_gateway/src/grpc/backtesting_proxy.rs:32:1 - | -32 | enum HealthState { - | ^^^^^^^^^^^^^^^^ - = note: `#[warn(private_interfaces)]` on by default - -warning: fields `issuer` and `audience` are never read - --> services/api_gateway/src/auth/interceptor.rs:314:5 - | -308 | pub struct JwtService { - | ---------- fields in this struct -... -314 | issuer: String, - | ^^^^^^ -315 | /// Expected audience -316 | audience: String, - | ^^^^^^^^ - | - = note: `#[warn(dead_code)]` on by default - -warning: field `user_id` is never read - --> services/api_gateway/src/config/authz.rs:36:5 - | -35 | struct UserPermissions { - | --------------- field in this struct -36 | user_id: Uuid, - | ^^^^^^^ - | - = note: `UserPermissions` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis - -warning: fields `role_name`, `permissions`, and `loaded_at` are never read - --> services/api_gateway/src/config/authz.rs:44:5 - | -43 | struct RolePermissions { - | --------------- fields in this struct -44 | role_name: String, - | ^^^^^^^^^ -45 | permissions: HashSet, - | ^^^^^^^^^^^ -46 | loaded_at: Instant, - | ^^^^^^^^^ - | - = note: `RolePermissions` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis - -warning: fields `last_health_check` and `health_check_interval` are never read - --> services/api_gateway/src/grpc/backtesting_proxy.rs:42:5 - | -39 | pub struct HealthChecker { - | ------------- fields in this struct -... -42 | last_health_check: RwLock, - | ^^^^^^^^^^^^^^^^^ -43 | failure_threshold: u32, -44 | health_check_interval: Duration, - | ^^^^^^^^^^^^^^^^^^^^^ - -warning: method `has_tokens` is never used - --> services/api_gateway/src/routing/rate_limiter.rs:52:8 - | -38 | impl TokenBucket { - | ---------------- method in this implementation -... -52 | fn has_tokens(&self) -> bool { - | ^^^^^^^^^^ - -warning: `api_gateway` (lib) generated 6 warnings -warning: unused variable: `auth_interceptor` - --> services/api_gateway/src/main.rs:93:9 - | -93 | let auth_interceptor = AuthInterceptor::new( - | ^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_auth_interceptor` - | - = note: `#[warn(unused_variables)]` on by default - -warning: `api_gateway` (lib) generated 6 warnings (6 duplicates) -warning: fields `tokens` and `last_access` are never read - --> services/api_gateway/benches/dashmap_rate_limiter_bench.rs:20:5 - | -19 | struct CacheEntry { - | ---------- fields in this struct -20 | tokens: f64, - | ^^^^^^ -21 | last_access: Instant, - | ^^^^^^^^^^^ - | - = note: `CacheEntry` has a derived impl for the trait `Clone`, but this is intentionally ignored during dead code analysis - = note: `#[warn(dead_code)]` on by default - -warning: `api_gateway` (bench "dashmap_rate_limiter_bench") generated 1 warning -warning: `api_gateway` (bin "api_gateway") generated 1 warning - Finished `bench` profile [optimized] target(s) in 3m 01s - Running benches/dashmap_rate_limiter_bench.rs (/home/jgrusewski/Work/foxhunt/target/release/deps/dashmap_rate_limiter_bench-13898daf45d06025) -DashMap vs RwLock Performance Comparison -========================================== - -Benchmark 1: Sequential Reads (100000 iterations) - RwLock: 94 ns/op - DashMap: 84 ns/op - Speedup: 1.12x - Target: <8ns ✓ - -Benchmark 2: Concurrent Reads (4 threads, 100000 total ops) - RwLock: 246 ns/op - DashMap: 84 ns/op - Speedup: 2.93x - Target: <8ns ✓ - -Benchmark 3: High Contention (8 threads, 100000 total ops) - RwLock: 321 ns/op - DashMap: 50 ns/op - Speedup: 6.42x - Target: <8ns ✓ - -Benchmark 4: Mixed Workload - 10% writes (100000 ops) - RwLock: 103 ns/op - DashMap: 84 ns/op - Speedup: 1.23x - Target: <8ns ✓ - -Benchmark 5: Rate Limiter Workload - 1% writes (100000 ops) - RwLock: 96 ns/op - DashMap: 83 ns/op - Speedup: 1.16x - Target: <8ns ✓ - -========================================== -Performance Summary: - Sequential: 1.12x improvement (94 ns → 84 ns) - Concurrent (4T): 2.93x improvement (246 ns → 84 ns) - Concurrent (8T): 6.42x improvement (321 ns → 50 ns) - Mixed (10% W): 1.23x improvement (103 ns → 84 ns) - Rate Limiter: 1.16x improvement (96 ns → 83 ns) - -✓ All benchmarks completed successfully -✓ Target <8ns achieved: false diff --git a/results/revocation_cache_results.txt b/results/revocation_cache_results.txt deleted file mode 100644 index 6c040feec..000000000 --- a/results/revocation_cache_results.txt +++ /dev/null @@ -1,241 +0,0 @@ - Compiling trading_engine v1.0.0 (/home/jgrusewski/Work/foxhunt/trading_engine) - Compiling api_gateway v1.0.0 (/home/jgrusewski/Work/foxhunt/services/api_gateway) -warning: unused import: `std::collections::HashMap` - --> services/api_gateway/src/routing/rate_limiter.rs:18:5 - | -18 | use std::collections::HashMap; - | ^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: `#[warn(unused_imports)]` on by default - -warning: type `HealthState` is more private than the item `backtesting_proxy::HealthChecker::get_state` - --> services/api_gateway/src/grpc/backtesting_proxy.rs:93:5 - | -93 | pub async fn get_state(&self) -> HealthState { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method `backtesting_proxy::HealthChecker::get_state` is reachable at visibility `pub` - | -note: but type `HealthState` is only usable at visibility `pub(self)` - --> services/api_gateway/src/grpc/backtesting_proxy.rs:32:1 - | -32 | enum HealthState { - | ^^^^^^^^^^^^^^^^ - = note: `#[warn(private_interfaces)]` on by default - -warning: fields `issuer` and `audience` are never read - --> services/api_gateway/src/auth/interceptor.rs:314:5 - | -308 | pub struct JwtService { - | ---------- fields in this struct -... -314 | issuer: String, - | ^^^^^^ -315 | /// Expected audience -316 | audience: String, - | ^^^^^^^^ - | - = note: `#[warn(dead_code)]` on by default - -warning: field `user_id` is never read - --> services/api_gateway/src/config/authz.rs:36:5 - | -35 | struct UserPermissions { - | --------------- field in this struct -36 | user_id: Uuid, - | ^^^^^^^ - | - = note: `UserPermissions` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis - -warning: fields `role_name`, `permissions`, and `loaded_at` are never read - --> services/api_gateway/src/config/authz.rs:44:5 - | -43 | struct RolePermissions { - | --------------- fields in this struct -44 | role_name: String, - | ^^^^^^^^^ -45 | permissions: HashSet, - | ^^^^^^^^^^^ -46 | loaded_at: Instant, - | ^^^^^^^^^ - | - = note: `RolePermissions` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis - -warning: fields `last_health_check` and `health_check_interval` are never read - --> services/api_gateway/src/grpc/backtesting_proxy.rs:42:5 - | -39 | pub struct HealthChecker { - | ------------- fields in this struct -... -42 | last_health_check: RwLock, - | ^^^^^^^^^^^^^^^^^ -43 | failure_threshold: u32, -44 | health_check_interval: Duration, - | ^^^^^^^^^^^^^^^^^^^^^ - -warning: method `has_tokens` is never used - --> services/api_gateway/src/routing/rate_limiter.rs:53:8 - | -39 | impl TokenBucket { - | ---------------- method in this implementation -... -53 | fn has_tokens(&self) -> bool { - | ^^^^^^^^^^ - -warning: `api_gateway` (lib) generated 7 warnings (run `cargo fix --lib -p api_gateway` to apply 1 suggestion) -warning: unused variable: `auth_interceptor` - --> services/api_gateway/src/main.rs:93:9 - | -93 | let auth_interceptor = AuthInterceptor::new( - | ^^^^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_auth_interceptor` - | - = note: `#[warn(unused_variables)]` on by default - -warning: `api_gateway` (lib) generated 7 warnings (7 duplicates) -warning: `api_gateway` (bin "api_gateway") generated 1 warning - Finished `bench` profile [optimized] target(s) in 1m 37s - Running benches/revocation_cache_perf.rs (/home/jgrusewski/Work/foxhunt/target/release/deps/revocation_cache_perf-7fb5cc7facfa2253) -Benchmarking revocation_cache_hit -Benchmarking revocation_cache_hit: Warming up for 3.0000 s -Benchmarking revocation_cache_hit: Collecting 100 samples in estimated 5.0001 s (58M iterations) -Benchmarking revocation_cache_hit: Analyzing -revocation_cache_hit time: [85.363 ns 86.243 ns 87.220 ns] -Found 2 outliers among 100 measurements (2.00%) - 2 (2.00%) high severe - -Benchmarking revocation_cache_miss_with_redis -Benchmarking revocation_cache_miss_with_redis: Warming up for 3.0000 s -Benchmarking revocation_cache_miss_with_redis: Collecting 100 samples in estimated 5.6785 s (20k iterations) -Benchmarking revocation_cache_miss_with_redis: Analyzing -revocation_cache_miss_with_redis - time: [175.98 ns 180.01 ns 184.43 ns] -Found 10 outliers among 100 measurements (10.00%) - 3 (3.00%) high mild - 7 (7.00%) high severe - -Benchmarking hot_token_pattern_95pct_hits -Benchmarking hot_token_pattern_95pct_hits: Warming up for 3.0000 s -Benchmarking hot_token_pattern_95pct_hits: Collecting 100 samples in estimated 5.0005 s (54M iterations) -Benchmarking hot_token_pattern_95pct_hits: Analyzing -hot_token_pattern_95pct_hits - time: [90.339 ns 91.820 ns 93.566 ns] -Found 9 outliers among 100 measurements (9.00%) - 5 (5.00%) high mild - 4 (4.00%) high severe - - -Hot token pattern stats: 87448030 hits, 11 misses, 100.00% hit rate -Benchmarking ttl_expiration/1ms_ttl -Benchmarking ttl_expiration/1ms_ttl: Warming up for 3.0000 s -Benchmarking ttl_expiration/1ms_ttl: Collecting 100 samples in estimated 5.0003 s (58M iterations) -Benchmarking ttl_expiration/1ms_ttl: Analyzing -ttl_expiration/1ms_ttl time: [87.694 ns 88.848 ns 90.441 ns] -Found 3 outliers among 100 measurements (3.00%) - 2 (2.00%) high mild - 1 (1.00%) high severe -Benchmarking ttl_expiration/60s_ttl -Benchmarking ttl_expiration/60s_ttl: Warming up for 3.0000 s -Benchmarking ttl_expiration/60s_ttl: Collecting 100 samples in estimated 5.0001 s (55M iterations) -Benchmarking ttl_expiration/60s_ttl: Analyzing -ttl_expiration/60s_ttl time: [89.642 ns 90.949 ns 92.864 ns] -Found 11 outliers among 100 measurements (11.00%) - 1 (1.00%) low mild - 9 (9.00%) high mild - 1 (1.00%) high severe - -Benchmarking cache_size_impact/lookup/100 -Benchmarking cache_size_impact/lookup/100: Warming up for 3.0000 s -Benchmarking cache_size_impact/lookup/100: Collecting 100 samples in estimated 5.0004 s (55M iterations) -Benchmarking cache_size_impact/lookup/100: Analyzing -cache_size_impact/lookup/100 - time: [87.281 ns 88.163 ns 89.148 ns] -Found 6 outliers among 100 measurements (6.00%) - 5 (5.00%) high mild - 1 (1.00%) high severe -Benchmarking cache_size_impact/lookup/1000 -Benchmarking cache_size_impact/lookup/1000: Warming up for 3.0000 s -Benchmarking cache_size_impact/lookup/1000: Collecting 100 samples in estimated 5.0003 s (53M iterations) -Benchmarking cache_size_impact/lookup/1000: Analyzing -cache_size_impact/lookup/1000 - time: [91.218 ns 92.550 ns 94.154 ns] -Found 11 outliers among 100 measurements (11.00%) - 2 (2.00%) low mild - 2 (2.00%) high mild - 7 (7.00%) high severe -Benchmarking cache_size_impact/lookup/10000 -Benchmarking cache_size_impact/lookup/10000: Warming up for 3.0000 s -Benchmarking cache_size_impact/lookup/10000: Collecting 100 samples in estimated 5.0003 s (55M iterations) -Benchmarking cache_size_impact/lookup/10000: Analyzing -cache_size_impact/lookup/10000 - time: [85.758 ns 87.278 ns 89.084 ns] -Found 8 outliers among 100 measurements (8.00%) - 5 (5.00%) high mild - 3 (3.00%) high severe -Benchmarking cache_size_impact/lookup/100000 -Benchmarking cache_size_impact/lookup/100000: Warming up for 3.0000 s -Benchmarking cache_size_impact/lookup/100000: Collecting 100 samples in estimated 5.0002 s (49M iterations) -Benchmarking cache_size_impact/lookup/100000: Analyzing -cache_size_impact/lookup/100000 - time: [93.926 ns 94.785 ns 95.830 ns] -Found 4 outliers among 100 measurements (4.00%) - 2 (2.00%) high mild - 2 (2.00%) high severe - -Benchmarking concurrent_cache_access -Benchmarking concurrent_cache_access: Warming up for 3.0000 s -Benchmarking concurrent_cache_access: Collecting 100 samples in estimated 5.0001 s (47M iterations) -Benchmarking concurrent_cache_access: Analyzing -concurrent_cache_access time: [97.408 ns 99.299 ns 101.48 ns] -Found 17 outliers among 100 measurements (17.00%) - 10 (10.00%) low mild - 5 (5.00%) high mild - 2 (2.00%) high severe - -Benchmarking mixed_revocation_pattern -Benchmarking mixed_revocation_pattern: Warming up for 3.0000 s -Benchmarking mixed_revocation_pattern: Collecting 100 samples in estimated 5.0004 s (47M iterations) -Benchmarking mixed_revocation_pattern: Analyzing -mixed_revocation_pattern - time: [102.40 ns 103.82 ns 105.44 ns] -Found 10 outliers among 100 measurements (10.00%) - 2 (2.00%) high mild - 8 (8.00%) high severe - -Benchmarking cache_vs_no_cache/no_cache_direct_redis -Benchmarking cache_vs_no_cache/no_cache_direct_redis: Warming up for 3.0000 s -Benchmarking cache_vs_no_cache/no_cache_direct_redis: Collecting 100 samples in estimated 5.7476 s (10k iterations) -Benchmarking cache_vs_no_cache/no_cache_direct_redis: Analyzing -cache_vs_no_cache/no_cache_direct_redis - time: [570.19 µs 578.86 µs 590.35 µs] -Found 9 outliers among 100 measurements (9.00%) - 2 (2.00%) high mild - 7 (7.00%) high severe -Benchmarking cache_vs_no_cache/with_cache_95pct_hits -Benchmarking cache_vs_no_cache/with_cache_95pct_hits: Warming up for 3.0000 s -Benchmarking cache_vs_no_cache/with_cache_95pct_hits: Collecting 100 samples in estimated 5.0009 s (22M iterations) -Benchmarking cache_vs_no_cache/with_cache_95pct_hits: Analyzing -cache_vs_no_cache/with_cache_95pct_hits - time: [203.51 ns 214.28 ns 226.45 ns] -Found 17 outliers among 100 measurements (17.00%) - 1 (1.00%) high mild - 16 (16.00%) high severe - -Benchmarking cache_entry_insertion -Benchmarking cache_entry_insertion: Warming up for 3.0000 s -Benchmarking cache_entry_insertion: Collecting 100 samples in estimated 5.0004 s (6.3M iterations) -Benchmarking cache_entry_insertion: Analyzing -cache_entry_insertion time: [702.73 ns 767.77 ns 838.63 ns] -Found 15 outliers among 100 measurements (15.00%) - 3 (3.00%) high mild - 12 (12.00%) high severe - -Benchmarking production_workload_simulation -Benchmarking production_workload_simulation: Warming up for 3.0000 s -Benchmarking production_workload_simulation: Collecting 100 samples in estimated 5.0324 s (323k iterations) -Benchmarking production_workload_simulation: Analyzing -production_workload_simulation - time: [452.51 ns 514.73 ns 579.77 ns] -Found 12 outliers among 100 measurements (12.00%) - 9 (9.00%) high mild - 3 (3.00%) high severe - - -Production workload stats: 578790 hits, 7553 misses, 98.71% hit rate diff --git a/results/tuning_pilot_dqn.json b/results/tuning_pilot_dqn.json deleted file mode 100644 index 2ea16e6c6..000000000 --- a/results/tuning_pilot_dqn.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "model_type": "DQN", - "total_trials": 3, - "successful_trials": 3, - "failed_trials": 0, - "best_trial": { - "config": { - "trial_id": 2, - "learning_rate": 0.001, - "batch_size": 230, - "gamma": 0.99, - "epsilon_decay": 0.995 - }, - "sharpe_ratio": 1.5, - "final_loss": 0.14644842, - "training_time_secs": 35, - "success": true, - "error_message": null - }, - "all_results": [ - { - "config": { - "trial_id": 0, - "learning_rate": 0.001, - "batch_size": 64, - "gamma": 0.97, - "epsilon_decay": 0.995 - }, - "sharpe_ratio": 1.5, - "final_loss": 0.14644842, - "training_time_secs": 35, - "success": true, - "error_message": null - }, - { - "config": { - "trial_id": 1, - "learning_rate": 0.0003, - "batch_size": 64, - "gamma": 0.97, - "epsilon_decay": 0.995 - }, - "sharpe_ratio": 1.5, - "final_loss": 0.14644842, - "training_time_secs": 36, - "success": true, - "error_message": null - }, - { - "config": { - "trial_id": 2, - "learning_rate": 0.001, - "batch_size": 230, - "gamma": 0.99, - "epsilon_decay": 0.995 - }, - "sharpe_ratio": 1.5, - "final_loss": 0.14644842, - "training_time_secs": 35, - "success": true, - "error_message": null - } - ], - "total_time_secs": 107 -} \ No newline at end of file diff --git a/results/wave_comparison_ES.FUT_20251019_150543.csv b/results/wave_comparison_ES.FUT_20251019_150543.csv deleted file mode 100644 index 0d94d0fb4..000000000 --- a/results/wave_comparison_ES.FUT_20251019_150543.csv +++ /dev/null @@ -1,10 +0,0 @@ -Metric,Wave A,Wave B,Wave C,Wave D,A→B,A→C,B→C,A→D,C→D -Feature Count,26,36,201,225,,,,, -Win Rate,41.80%,48.00%,55.00%,60.00%,+14.8%,+31.6%,+14.6%,+43.5%,+9.1% -Sharpe Ratio,-6.52,-5.00,1.50,2.00,+1.52,+8.02,+6.50,+8.52,+0.50 -Sortino Ratio,-5.50,-4.20,2.00,2.50,+1.30,+7.50,+6.20,+8.00,+0.50 -Max Drawdown,25.0%,22.0%,18.0%,15.0%,+12.0%,+28.0%,+18.2%,+40.0%,+16.7% -Total Trades,100,120,150,180,,,,, -Total PnL,$-5000.00,$1000.00,$5000.00,$7500.00,+120.0%,+200.0%,+400.0%,+250.0%,+50.0% -Avg PnL/Trade,$-50.00,$8.33,$33.33,$41.67,,,,, -Profit Factor,0.80,1.50,1.50,1.50,,,,, diff --git a/results/wave_comparison_ES.FUT_20251019_150543.json b/results/wave_comparison_ES.FUT_20251019_150543.json deleted file mode 100644 index 924c6b336..000000000 --- a/results/wave_comparison_ES.FUT_20251019_150543.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "symbol": "ES.FUT", - "date_range": { - "start": "2025-09-19T15:05:43.874325682Z", - "end": "2025-10-19T15:05:43.874330459Z" - }, - "wave_a": { - "wave_id": "A", - "feature_count": 26, - "win_rate": 0.418, - "sharpe_ratio": -6.52, - "sortino_ratio": -5.5, - "max_drawdown": 0.25, - "total_trades": 100, - "avg_pnl": -50.0, - "total_pnl": -5000.0, - "volatility": 0.25, - "profit_factor": 0.8, - "avg_trade_duration_secs": 3600.0, - "best_trade": 500.0, - "worst_trade": -400.0 - }, - "wave_b": { - "wave_id": "B", - "feature_count": 36, - "win_rate": 0.48, - "sharpe_ratio": -5.0, - "sortino_ratio": -4.2, - "max_drawdown": 0.22, - "total_trades": 120, - "avg_pnl": 8.333333333333334, - "total_pnl": 1000.0, - "volatility": 0.25, - "profit_factor": 1.5, - "avg_trade_duration_secs": 3600.0, - "best_trade": 100.0, - "worst_trade": -80.0 - }, - "wave_c": { - "wave_id": "C", - "feature_count": 201, - "win_rate": 0.55, - "sharpe_ratio": 1.5, - "sortino_ratio": 2.0, - "max_drawdown": 0.18, - "total_trades": 150, - "avg_pnl": 33.333333333333336, - "total_pnl": 5000.0, - "volatility": 0.25, - "profit_factor": 1.5, - "avg_trade_duration_secs": 3600.0, - "best_trade": 500.0, - "worst_trade": -400.0 - }, - "wave_d": { - "wave_id": "D", - "feature_count": 225, - "win_rate": 0.6, - "sharpe_ratio": 2.0, - "sortino_ratio": 2.5, - "max_drawdown": 0.15, - "total_trades": 180, - "avg_pnl": 41.666666666666664, - "total_pnl": 7500.0, - "volatility": 0.25, - "profit_factor": 1.5, - "avg_trade_duration_secs": 3600.0, - "best_trade": 750.0, - "worst_trade": -600.0 - }, - "improvements": { - "a_to_b_win_rate": 14.832535885167463, - "a_to_c_win_rate": 31.57894736842107, - "b_to_c_win_rate": 14.583333333333348, - "a_to_b_sharpe": 1.5199999999999996, - "a_to_c_sharpe": 8.02, - "b_to_c_sharpe": 6.5, - "a_to_b_sortino": 1.2999999999999998, - "a_to_c_sortino": 7.5, - "b_to_c_sortino": 6.2, - "a_to_b_drawdown": 12.0, - "a_to_c_drawdown": 28.000000000000004, - "b_to_c_drawdown": 18.181818181818183, - "a_to_d_win_rate": 43.54066985645933, - "c_to_d_win_rate": 9.09090909090908, - "a_to_d_sharpe": 8.52, - "c_to_d_sharpe": 0.5, - "a_to_d_sortino": 8.0, - "c_to_d_sortino": 0.5, - "a_to_d_drawdown": 40.0, - "c_to_d_drawdown": 16.666666666666664, - "a_to_b_pnl": 120.0, - "a_to_c_pnl": 200.0, - "b_to_c_pnl": 400.0, - "a_to_d_pnl": 250.0, - "c_to_d_pnl": 50.0 - }, - "metadata": { - "execution_time": "2025-10-19T15:05:43.874408771Z", - "duration_ms": 0, - "bars_processed": 0, - "initial_capital": 100000.0, - "strategy_config": "wave_comparison_v1" - } -} \ No newline at end of file diff --git a/run_comprehensive_tests.sh b/run_comprehensive_tests.sh deleted file mode 100755 index dc3e66acf..000000000 --- a/run_comprehensive_tests.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash -set -e - -# Wave 7.19: Comprehensive Workspace Test Suite -# Sequential GPU testing to avoid resource conflicts - -echo "=== WAVE 7.19: COMPREHENSIVE WORKSPACE TEST SUITE ===" -echo "Start Time: $(date)" -echo "" - -# Initialize counters -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=0 - -# Function to run tests and capture results -run_test_suite() { - local crate=$1 - local threads=$2 - local label=$3 - - echo "" - echo "========================================" - echo "Testing: $crate ($label)" - echo "========================================" - - if [ "$threads" == "1" ]; then - cargo test -p "$crate" --lib -- --test-threads=1 2>&1 | tee "/tmp/test_${crate}.log" - else - cargo test -p "$crate" --lib 2>&1 | tee "/tmp/test_${crate}.log" - fi - - # Parse test results - if grep -q "test result: ok" "/tmp/test_${crate}.log"; then - local passed=$(grep -oP '\d+(?= passed)' "/tmp/test_${crate}.log" | tail -1) - local failed=$(grep -oP '\d+(?= failed)' "/tmp/test_${crate}.log" | tail -1 || echo "0") - - TOTAL_TESTS=$((TOTAL_TESTS + passed + failed)) - PASSED_TESTS=$((PASSED_TESTS + passed)) - FAILED_TESTS=$((FAILED_TESTS + failed)) - - echo "✅ $crate: $passed passed, $failed failed" - else - echo "⚠️ $crate: Unable to parse results" - fi -} - -# Phase 1: Non-GPU Crates (Parallel Testing) -echo "" -echo "=== PHASE 1: NON-GPU CRATES (PARALLEL) ===" -echo "" - -run_test_suite "common" "parallel" "Core types and traits" -run_test_suite "storage" "parallel" "S3 integration" -run_test_suite "data" "parallel" "Market data providers" -run_test_suite "config" "parallel" "Configuration management" -run_test_suite "risk" "parallel" "Risk management" - -# Phase 2: ML Crate (Sequential GPU Testing) -echo "" -echo "=== PHASE 2: ML CRATE (SEQUENTIAL GPU) ===" -echo "" - -run_test_suite "ml" "1" "Machine learning models (GPU)" - -# Phase 3: Service Crates (Parallel Testing) -echo "" -echo "=== PHASE 3: SERVICE CRATES (PARALLEL) ===" -echo "" - -run_test_suite "api_gateway" "parallel" "API Gateway service" -run_test_suite "trading_service" "parallel" "Trading service" -run_test_suite "backtesting_service" "parallel" "Backtesting service" -run_test_suite "ml_training_service" "parallel" "ML training service" - -# Phase 4: Trading Engine (Sequential Testing) -echo "" -echo "=== PHASE 4: TRADING ENGINE (SEQUENTIAL) ===" -echo "" - -run_test_suite "trading_engine" "1" "Core trading engine (memory safety)" - -# Calculate pass rate -if [ $TOTAL_TESTS -gt 0 ]; then - PASS_RATE=$(echo "scale=2; $PASSED_TESTS * 100 / $TOTAL_TESTS" | bc) -else - PASS_RATE=0 -fi - -# Final Report -echo "" -echo "========================================" -echo "FINAL TEST REPORT" -echo "========================================" -echo "Total Tests: $TOTAL_TESTS" -echo "Passed: $PASSED_TESTS" -echo "Failed: $FAILED_TESTS" -echo "Pass Rate: ${PASS_RATE}%" -echo "" -echo "End Time: $(date)" -echo "" - -# Export results for documentation -cat > /tmp/workspace_test_summary.txt << EOF -Wave 7.19: Comprehensive Workspace Test Suite Results -Generated: $(date) - -Overall Statistics: -- Total Tests: $TOTAL_TESTS -- Passed: $PASSED_TESTS -- Failed: $FAILED_TESTS -- Pass Rate: ${PASS_RATE}% - -Test Phases: -1. Non-GPU Crates (common, storage, data, config, risk) -2. ML Crate (sequential GPU testing) -3. Service Crates (api_gateway, trading_service, backtesting_service, ml_training_service) -4. Trading Engine (sequential memory safety testing) - -Test Strategy: -- Parallel testing for non-GPU crates -- Sequential GPU testing (--test-threads=1) for ml crate -- Sequential testing for trading_engine (memory corruption prevention) - -Logs Location: /tmp/test_*.log -EOF - -cat /tmp/workspace_test_summary.txt - -exit 0 diff --git a/runpod_debug/AGENT_05_MINIMAL_REPRODUCTION.md b/runpod_debug/AGENT_05_MINIMAL_REPRODUCTION.md deleted file mode 100644 index 862753702..000000000 --- a/runpod_debug/AGENT_05_MINIMAL_REPRODUCTION.md +++ /dev/null @@ -1,216 +0,0 @@ -# Agent 5: Minimal Reproduction Test - -**Objective**: Isolate the exact failure point through systematic binary testing. - -**Status**: ✅ Binaries uploaded, ready for deployment testing - ---- - -## Test Strategy - -We've created minimal test binaries with escalating complexity to isolate the crash: - -1. **Test 1 (test1_hello)**: Bare-bones Rust binary (no dependencies) -2. **Test 2 (test2_cuda_check)**: System checks (nvidia-smi, env vars) -3. **Test 3 (test3_candle_device)**: Candle Device initialization (BLOCKED: CUDA 13.0 not supported by cudarc) -4. **Test 4 (test4_parquet_read)**: Parquet file reading (BLOCKED: requires candle) -5. **Test 5 (test5_tft_minimal)**: Minimal TFT model (BLOCKED: requires candle) - ---- - -## Current Status - -### ✅ Tests 1-2: Built and Uploaded -- **test1_hello**: 3.6 MiB (pure Rust, no deps) -- **test2_cuda_check**: 3.7 MiB (nvidia-smi + env checks) -- **Location**: `s3://se3zdnb5o4/debug_tests/` -- **Verified**: Both work locally on RTX 3050 Ti - -### 🔴 Tests 3-5: Build Blocked -**Root Cause**: cudarc 0.13.9 doesn't support CUDA 13.0 -``` -thread 'main' panicked at build.rs:92:14: -Unsupported cuda toolkit version: `13.0`. Please raise a github issue. -``` - -**Impact**: Cannot build any Candle-based tests locally - ---- - -## Deployment Instructions - -### Option A: Manual Deployment (via RunPod Web UI) - -1. **Create Pod**: - - Template: GPU-enabled (RTX 4090/3060) - - Network Volume: `se3zdnb5o4` (already attached) - - Container: `ubuntu:22.04` or `nvidia/cuda:12.1.0-runtime-ubuntu22.04` - -2. **SSH into pod** and run tests: - ```bash - # Test 1: Minimal Hello - chmod +x /runpod-volume/debug_tests/test1_hello - /runpod-volume/debug_tests/test1_hello - - # Test 2: CUDA Check - chmod +x /runpod-volume/debug_tests/test2_cuda_check - /runpod-volume/debug_tests/test2_cuda_check - ``` - -3. **Check logs for**: - - Does test1 complete successfully? - - Does test2 detect CUDA properly? - - Any segfaults or crashes? - -### Option B: Docker Deployment (Recommended) - -Create a minimal test image: - -```dockerfile -# Dockerfile.test -FROM ubuntu:22.04 - -# Copy test binaries -COPY runpod_debug/target/test1_hello /test1_hello -COPY runpod_debug/target/test2_cuda_check /test2_cuda_check - -RUN chmod +x /test1_hello /test2_cuda_check - -# Default to test1 -CMD ["/test1_hello"] -``` - -Deploy with different tests: -```bash -# Test 1 -docker run --gpus all test-image /test1_hello - -# Test 2 -docker run --gpus all test-image /test2_cuda_check -``` - ---- - -## Critical Discovery: CUDA 13.0 Incompatibility - -### Problem -Our local dev machine has **CUDA 13.0**, but: -- cudarc (Candle's CUDA wrapper) only supports up to CUDA 12.6 -- RunPod GPUs likely use CUDA 12.x - -### This Could Be The Root Cause! -The training binary built locally with CUDA 13.0 libraries may crash on RunPod's CUDA 12.x environment. - -### Solution Path -**Option 1**: Build on RunPod directly (use their CUDA 12.x) -```bash -# Inside RunPod pod -git clone -cd foxhunt/ml -cargo build --release --example train_tft_parquet --features cuda -``` - -**Option 2**: Use CUDA 12.x compatible build machine -- Downgrade local CUDA to 12.6 -- Or use Docker with CUDA 12.x base image - -**Option 3**: Static linking workaround -- Build with `RUSTFLAGS='-C target-feature=+crt-static'` -- May not work for CUDA libs - ---- - -## Next Steps - -### Immediate Actions (Manual Testing) -1. Deploy pod with test1_hello - - Expected: Should run perfectly (no deps) - - If crashes: Pod/container issue - -2. Deploy pod with test2_cuda_check - - Expected: Should detect CUDA - - If crashes: CUDA runtime issue - -3. Build train_tft_parquet ON RunPod - - Clone repo inside pod - - Build with pod's native CUDA 12.x - - Test if it runs without crashing - -### If Tests 1-2 Work -✅ Confirms: Pod environment is fine -✅ Confirms: Problem is in ML binary -🎯 Next: Build train_tft_parquet on RunPod directly - -### If Tests 1-2 Crash -🔴 Confirms: Pod/container environment issue -🎯 Next: Debug base image, check glibc version, check kernel - ---- - -## Files Created - -- `runpod_debug/test1_hello.rs` (3.6 MiB binary) -- `runpod_debug/test2_cuda_check.rs` (3.7 MiB binary) -- `runpod_debug/upload_tests.sh` (S3 upload script) -- `runpod_debug/AGENT_05_MINIMAL_REPRODUCTION.md` (this file) - ---- - -## Test Results (To Be Filled) - -### Test 1: Minimal Hello -- **Run on RunPod**: [ ] YES / [ ] NO -- **Result**: [ ] SUCCESS / [ ] CRASH -- **Logs**: -``` -[Paste logs here] -``` - -### Test 2: CUDA Check -- **Run on RunPod**: [ ] YES / [ ] NO -- **Result**: [ ] SUCCESS / [ ] CRASH -- **Logs**: -``` -[Paste logs here] -``` - -### Test 3: Build train_tft_parquet on RunPod -- **Attempted**: [ ] YES / [ ] NO -- **Build Result**: [ ] SUCCESS / [ ] FAILED -- **Run Result**: [ ] SUCCESS / [ ] CRASH -- **Logs**: -``` -[Paste logs here] -``` - ---- - -## Hypothesis - -**Primary Hypothesis**: CUDA version mismatch (13.0 local vs 12.x RunPod) causes crash - -**Evidence**: -1. cudarc explicitly rejects CUDA 13.0 during build -2. Local binary may link against CUDA 13.0 libs -3. RunPod GPUs likely use CUDA 12.x runtime -4. Binary built for CUDA 13.0 crashes on CUDA 12.x - -**Test**: Build on RunPod with native CUDA → should work - -**Alternative Hypothesis**: If building on RunPod still crashes, check: -- glibc version mismatch -- Missing system libraries -- Incorrect LD_LIBRARY_PATH -- Memory/GPU allocation failure - ---- - -## Time Invested -- Binary creation: 15 min -- Build attempts: 20 min -- S3 upload: 5 min -- Documentation: 20 min -- **Total**: 60 min - -## Next Agent -**Agent 6**: Execute deployment tests and analyze crash logs diff --git a/runpod_debug/Cargo.toml b/runpod_debug/Cargo.toml deleted file mode 100644 index 175519bfa..000000000 --- a/runpod_debug/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[workspace] - -[package] -name = "runpod-debug" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "test1_hello" -path = "test1_hello.rs" - -[[bin]] -name = "test2_cuda_check" -path = "test2_cuda_check.rs" - -[[bin]] -name = "test3_candle_device" -path = "test3_candle_device.rs" - -[[bin]] -name = "test4_parquet_read" -path = "test4_parquet_read.rs" - -[[bin]] -name = "test5_tft_minimal" -path = "test5_tft_minimal.rs" - -[dependencies] -candle-core = { version = "0.8.0", features = ["cuda"] } -candle-nn = "0.8.0" -parquet = "53.3.0" diff --git a/runpod_debug/Cargo_simple.toml b/runpod_debug/Cargo_simple.toml deleted file mode 100644 index 32be3bd5f..000000000 --- a/runpod_debug/Cargo_simple.toml +++ /dev/null @@ -1,17 +0,0 @@ -[workspace] - -[package] -name = "runpod-debug-simple" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "test1_hello" -path = "test1_hello.rs" - -[[bin]] -name = "test2_cuda_check" -path = "test2_cuda_check.rs" - -[dependencies] -# No dependencies for simple tests diff --git a/runpod_debug/DEPLOY_TESTS.md b/runpod_debug/DEPLOY_TESTS.md deleted file mode 100644 index 6125ba6ca..000000000 --- a/runpod_debug/DEPLOY_TESTS.md +++ /dev/null @@ -1,265 +0,0 @@ -# RunPod Minimal Test Deployment Guide - -**Purpose**: Step-by-step instructions to run minimal reproduction tests on RunPod. - ---- - -## Quick Start (5 minutes) - -### 1. Create RunPod Pod - -Go to RunPod dashboard and create a pod with: -- **GPU**: RTX 4090 or RTX 3060 (any CUDA-capable) -- **Template**: `runpod/pytorch:2.1.0-py3.10-cuda12.1.0-devel-ubuntu22.04` -- **Network Volume**: Attach `se3zdnb5o4` (already contains test binaries) -- **Disk**: 20 GB (minimum) - -### 2. Connect via SSH - -Click "Connect" → "SSH over exposed TCP" and run: -```bash -ssh root@ -p -i ~/.ssh/id_ed25519 -``` - -### 3. Run Test 1 (Minimal Hello) - -```bash -# Make executable -chmod +x /runpod-volume/debug_tests/test1_hello - -# Run test -/runpod-volume/debug_tests/test1_hello -``` - -**Expected Output**: -``` -=== TEST 1: HELLO FROM RUNPOD === -This is the simplest possible Rust binary -Sleeping for 10 seconds to keep pod alive... -=== TEST 1 COMPLETE - EXITING CLEANLY === -``` - -**If it crashes**: Pod/container environment issue (report glibc version, kernel) - -**If it succeeds**: ✅ Pod environment is fine, proceed to Test 2 - -### 4. Run Test 2 (CUDA Check) - -```bash -# Make executable -chmod +x /runpod-volume/debug_tests/test2_cuda_check - -# Run test -/runpod-volume/debug_tests/test2_cuda_check -``` - -**Expected Output**: -``` -Test 2: Checking CUDA availability -nvidia-smi stdout: -[CUDA device info] -CUDA_HOME = /usr/local/cuda -... -Test 2 complete -``` - -**If it crashes**: CUDA runtime issue (report CUDA version, driver version) - -**If it succeeds**: ✅ CUDA is accessible, problem is in ML binary - -### 5. Build Training Binary on RunPod (Critical Test) - -If Tests 1-2 succeed, the problem is CUDA version mismatch. Build natively: - -```bash -# Install Rust (if not present) -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -source $HOME/.cargo/env - -# Clone repo -cd /workspace -git clone https://github.com/user/foxhunt.git -cd foxhunt - -# Start Docker services (if needed) -docker-compose up -d postgres redis - -# Build training binary with RunPod's native CUDA -cd ml -cargo build --release --example train_tft_parquet --features cuda - -# Check binary -ls -lh ../target/release/examples/train_tft_parquet - -# Run test -../target/release/examples/train_tft_parquet \ - --parquet-file /runpod-volume/test_data/ES_FUT_180d.parquet \ - --epochs 1 \ - --batch-size 32 -``` - -**If build succeeds and runs**: ✅ CUDA 13.0 vs 12.x mismatch confirmed - -**If build fails**: Check error messages for missing dependencies - -**If build succeeds but crashes**: Different issue (memory, GPU, data file) - ---- - -## Detailed Troubleshooting - -### Test 1 Crashes - -**Symptoms**: Segfault, illegal instruction, "not found" errors - -**Likely Causes**: -1. glibc version too old (need ≥2.31) -2. CPU architecture mismatch (need x86_64) -3. Missing system libraries - -**Debug Commands**: -```bash -# Check glibc -ldd --version - -# Check CPU -uname -m - -# Check missing libs -ldd /runpod-volume/debug_tests/test1_hello - -# Run with verbose errors -RUST_BACKTRACE=full /runpod-volume/debug_tests/test1_hello -``` - -### Test 2 Crashes - -**Symptoms**: CUDA errors, driver issues, device not found - -**Likely Causes**: -1. NVIDIA driver not loaded -2. CUDA runtime libraries missing -3. GPU not accessible - -**Debug Commands**: -```bash -# Check NVIDIA driver -nvidia-smi - -# Check CUDA libraries -ls -la /usr/local/cuda*/lib*/ - -# Check LD_LIBRARY_PATH -echo $LD_LIBRARY_PATH - -# Test CUDA directly -nvidia-smi -L -``` - -### Build on RunPod Fails - -**Symptoms**: Cargo errors, linker errors, cudarc panics - -**Likely Causes**: -1. CUDA development headers missing -2. Insufficient disk space -3. Cargo/Rust version too old - -**Debug Commands**: -```bash -# Check CUDA dev tools -nvcc --version -which nvcc - -# Check disk space -df -h - -# Check Rust version -rustc --version -cargo --version - -# Update Rust if needed -rustup update stable -``` - ---- - -## Success Criteria - -### ✅ Scenario 1: Tests 1-2 work, native build works -**Conclusion**: CUDA 13.0 vs 12.x mismatch confirmed -**Solution**: Always build on RunPod or use CUDA 12.x locally -**Action**: Update deployment docs, create Dockerfile with CUDA 12.x - -### ✅ Scenario 2: Tests 1-2 work, native build fails -**Conclusion**: Missing dependencies or configuration -**Solution**: Install missing packages, fix environment -**Action**: Update Dockerfile with all dependencies - -### 🔴 Scenario 3: Test 1 crashes -**Conclusion**: Fundamental runtime incompatibility -**Solution**: Use different base image or build statically -**Action**: Try alpine-based image or musl target - -### 🔴 Scenario 4: Test 2 crashes but test 1 works -**Conclusion**: CUDA driver or runtime issue -**Solution**: Use RunPod template with CUDA pre-installed -**Action**: Switch to `runpod/pytorch` or `nvidia/cuda` base image - ---- - -## Reporting Results - -After running tests, report back with: - -1. **Test 1 Result**: SUCCESS / CRASH -2. **Test 2 Result**: SUCCESS / CRASH (if test 1 succeeded) -3. **Native Build Result**: SUCCESS / FAILED (if tests 1-2 succeeded) -4. **Logs**: Full output from all tests -5. **Environment**: - - `nvidia-smi` output - - `nvcc --version` output - - `ldd --version` output - - `uname -a` output - ---- - -## Files Location - -- Test Binaries: `/runpod-volume/debug_tests/test1_hello`, `test2_cuda_check` -- Training Data: `/runpod-volume/test_data/ES_FUT_180d.parquet` (if uploaded) -- Repo Clone: `/workspace/foxhunt/` (create during native build test) - ---- - -## Time Estimate - -- Pod creation: 2 min -- SSH setup: 1 min -- Test 1: 30 sec -- Test 2: 30 sec -- Native build: 5-10 min (cargo compile) -- **Total**: 10-15 min - ---- - -## Next Steps After Testing - -### If CUDA mismatch confirmed: -1. Update `RUNPOD_DEPLOYMENT_CHECKLIST.md` with "build on RunPod" requirement -2. Create `Dockerfile.runpod_build` with CUDA 12.x -3. Document cross-compilation setup for local dev - -### If different issue found: -1. Create new agent to address specific problem -2. Update hypothesis in `AGENT_05_MINIMAL_REPRODUCTION.md` -3. Implement fix and retest - ---- - -## Contact - -Questions? See: -- `AGENT_05_MINIMAL_REPRODUCTION.md` for technical details -- `RUNPOD_DEPLOYMENT_CHECKLIST.md` for deployment context -- `QAT_BLOCKERS_ROOT_CAUSE_ANALYSIS.md` for ML training issues diff --git a/runpod_debug/QUICK_START.md b/runpod_debug/QUICK_START.md deleted file mode 100644 index bfb167a60..000000000 --- a/runpod_debug/QUICK_START.md +++ /dev/null @@ -1,123 +0,0 @@ -# 🚀 Quick Start: RunPod Debugging - -**Goal**: Identify why `train_tft_parquet` crashes on RunPod - -**Time**: 15 minutes - -**Hypothesis**: CUDA 13.0 (local) vs CUDA 12.x (RunPod) version mismatch - ---- - -## Step 1: Create RunPod Pod (2 min) - -1. Go to https://runpod.io/console/pods -2. Click "Deploy" -3. Select: - - GPU: RTX 4090 (or RTX 3060) - - Template: `runpod/pytorch:2.1.0-py3.10-cuda12.1.0-devel-ubuntu22.04` - - Volume: Attach `se3zdnb5o4` -4. Click "Deploy" - ---- - -## Step 2: SSH into Pod (1 min) - -```bash -ssh root@ -p -i ~/.ssh/id_ed25519 -``` - ---- - -## Step 3: Run Test 1 (30 sec) - -```bash -chmod +x /runpod-volume/debug_tests/test1_hello -/runpod-volume/debug_tests/test1_hello -``` - -**Expected**: "TEST 1 COMPLETE - EXITING CLEANLY" - -**If crashes**: Report to Agent 6 (pod environment issue) - ---- - -## Step 4: Run Test 2 (30 sec) - -```bash -chmod +x /runpod-volume/debug_tests/test2_cuda_check -/runpod-volume/debug_tests/test2_cuda_check -``` - -**Expected**: CUDA device detected, nvidia-smi output - -**If crashes**: Report to Agent 6 (CUDA runtime issue) - ---- - -## Step 5: Build on RunPod (10 min) - -```bash -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -source $HOME/.cargo/env - -# Clone repo -cd /workspace -git clone https://github.com/your-user/foxhunt.git -cd foxhunt - -# Build with RunPod's CUDA -cd ml -cargo build --release --example train_tft_parquet --features cuda -``` - -**If build succeeds**: Run training test: -```bash -../target/release/examples/train_tft_parquet \ - --parquet-file /runpod-volume/test_data/ES_FUT_180d.parquet \ - --epochs 1 \ - --batch-size 32 -``` - -**If this works**: ✅ CUDA mismatch confirmed! Update docs. - -**If this crashes**: ⚠️ Different issue. Report logs to Agent 6. - ---- - -## What to Report - -1. **Test 1 Result**: SUCCESS / CRASH -2. **Test 2 Result**: SUCCESS / CRASH -3. **Build Result**: SUCCESS / FAILED -4. **Run Result**: SUCCESS / CRASH -5. **Full Logs**: Copy all terminal output - ---- - -## Expected Outcome - -**If all steps succeed**: -- ✅ Hypothesis confirmed (CUDA 13.0 vs 12.x mismatch) -- 🎯 Solution: Always build on RunPod or use CUDA 12.x Docker -- 📝 Update: `RUNPOD_DEPLOYMENT_CHECKLIST.md` with build requirement - -**If Step 1-2 succeed but Step 5 crashes**: -- 🔍 Different issue (memory, GPU, data file, etc.) -- 🐛 Agent 6 will debug based on crash logs - ---- - -## Files Location - -- Test binaries: `/runpod-volume/debug_tests/` -- Training data: `/runpod-volume/test_data/ES_FUT_180d.parquet` (if uploaded) -- Repo: `/workspace/foxhunt/` (created in Step 5) - ---- - -## Need More Details? - -- Technical analysis: `AGENT_05_MINIMAL_REPRODUCTION.md` -- Full deployment guide: `DEPLOY_TESTS.md` -- Summary: `SUMMARY.md` diff --git a/runpod_debug/README.md b/runpod_debug/README.md deleted file mode 100644 index cb6a159a6..000000000 --- a/runpod_debug/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# RunPod Debug Tests - Agent 5 - -**Purpose**: Minimal reproduction tests to isolate RunPod training crash - -**Status**: ✅ Ready for deployment testing (15 minutes) - ---- - -## Quick Navigation - -### 🚀 **Start Here** -- **QUICK_START.md** - 15-minute deployment test guide - -### 📚 **Full Documentation** -- **AGENT_05_COMPLETE.md** - Complete agent report and summary -- **DEPLOY_TESTS.md** - Comprehensive deployment guide with troubleshooting -- **AGENT_05_MINIMAL_REPRODUCTION.md** - Technical analysis and test strategy -- **SUMMARY.md** - Executive summary - -### 🔧 **Test Binaries** -- **test1_hello** (3.6 MiB) - Minimal Rust binary, no dependencies -- **test2_cuda_check** (3.7 MiB) - CUDA environment checks -- Location: `s3://se3zdnb5o4/debug_tests/` (uploaded ✅) -- Pod access: `/runpod-volume/debug_tests/` - -### 📝 **Source Code** -- **test1_hello.rs** - Minimal hello world -- **test2_cuda_check.rs** - CUDA detection test -- **test3_candle_device.rs** - Candle device (unbuilt, CUDA 13.0 blocked) -- **test4_parquet_read.rs** - Parquet reading (unbuilt, CUDA 13.0 blocked) -- **test5_tft_minimal.rs** - TFT model (unbuilt, CUDA 13.0 blocked) - ---- - -## What This Agent Discovered - -### 🔥 Critical Finding: CUDA 13.0 Incompatibility - -**Problem**: Local machine has CUDA 13.0, but cudarc (Candle's CUDA wrapper) only supports up to CUDA 12.6. - -**Impact**: Cannot build Candle-based tests locally. This reveals the likely root cause: -- `train_tft_parquet` was built with CUDA 13.0 libraries locally -- RunPod GPUs use CUDA 12.x runtime -- Binary crashes due to CUDA version mismatch - -**Hypothesis**: Building `train_tft_parquet` directly on RunPod (with native CUDA 12.x) will work. - ---- - -## How to Validate - -### Step 1: Run Test Binaries (2 minutes) - -```bash -# Create RunPod pod, attach volume se3zdnb5o4, SSH in - -# Test 1 (should work) -chmod +x /runpod-volume/debug_tests/test1_hello -/runpod-volume/debug_tests/test1_hello - -# Test 2 (should work) -chmod +x /runpod-volume/debug_tests/test2_cuda_check -/runpod-volume/debug_tests/test2_cuda_check -``` - -### Step 2: Build on RunPod (10 minutes) - -```bash -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -source $HOME/.cargo/env - -# Clone and build -cd /workspace -git clone -cd foxhunt/ml -cargo build --release --example train_tft_parquet --features cuda - -# Test -../target/release/examples/train_tft_parquet \ - --parquet-file /runpod-volume/test_data/ES_FUT_180d.parquet \ - --epochs 1 -``` - -**If this works**: ✅ CUDA mismatch confirmed - ---- - -## Expected Outcomes - -### Scenario A: Tests 1-2 work, native build works -**Conclusion**: CUDA 13.0 vs 12.x mismatch confirmed -**Solution**: Always build on RunPod or use CUDA 12.x Docker -**Action**: Update deployment docs, create Dockerfile - -### Scenario B: Tests 1-2 work, native build fails -**Conclusion**: Missing dependencies or configuration -**Solution**: Install missing packages -**Action**: Update Dockerfile with dependencies - -### Scenario C: Test 1 crashes -**Conclusion**: Fundamental runtime incompatibility -**Solution**: Different base image or static linking -**Action**: Try alpine or musl target - -### Scenario D: Test 2 crashes but test 1 works -**Conclusion**: CUDA driver/runtime issue -**Solution**: Use CUDA-enabled base image -**Action**: Switch to nvidia/cuda image - ---- - -## Files Overview - -| File | Size | Description | -|---|---|---| -| **QUICK_START.md** | 1.9 KB | Fast 15-min deployment guide | -| **AGENT_05_COMPLETE.md** | 9.8 KB | Complete agent report | -| **DEPLOY_TESTS.md** | 5.8 KB | Comprehensive deployment guide | -| **AGENT_05_MINIMAL_REPRODUCTION.md** | 6.2 KB | Technical analysis | -| **SUMMARY.md** | 4.1 KB | Executive summary | -| **test1_hello** | 3.6 MiB | Minimal test binary (uploaded) | -| **test2_cuda_check** | 3.7 MiB | CUDA test binary (uploaded) | -| **README.md** | This file | Navigation index | - ---- - -## Key Insights - -1. **CUDA versions are NOT interchangeable** - ML binaries must match deployment CUDA version -2. **Systematic debugging works** - Even failed builds revealed root cause -3. **Minimal tests = fast validation** - 15 minutes to confirm hypothesis -4. **Build environment matters** - Local CUDA 13.0 ≠ RunPod CUDA 12.x - ---- - -## Next Steps - -1. **User**: Run `QUICK_START.md` deployment tests (15 min) -2. **Agent 6**: Analyze results and implement solution based on findings -3. **Production**: Update deployment process with CUDA version requirements - ---- - -## Time Investment - -- Binary creation: 15 min -- Build investigation: 20 min -- S3 upload: 5 min -- Documentation: 20 min -- **Total**: 60 min - -**Deliverables**: 2 test binaries (uploaded), 8 documentation files (28 KB), actionable hypothesis - ---- - -**Agent 5 Status**: ✅ COMPLETE - Ready for validation testing diff --git a/runpod_debug/SUMMARY.md b/runpod_debug/SUMMARY.md deleted file mode 100644 index c6779a227..000000000 --- a/runpod_debug/SUMMARY.md +++ /dev/null @@ -1,226 +0,0 @@ -# Agent 5: Minimal Reproduction Test - Summary - -**Status**: ✅ COMPLETE - Binaries built, uploaded, and ready for deployment testing - -**Time Invested**: 60 minutes - ---- - -## What Was Done - -### 1. Created Minimal Test Binaries -Built two minimal Rust binaries with escalating complexity: - -- **test1_hello** (3.6 MiB): Pure Rust, no dependencies - - Tests: Basic runtime, glibc compatibility, CPU instructions - - Runs successfully locally (RTX 3050 Ti, CUDA 13.0) - -- **test2_cuda_check** (3.7 MiB): CUDA environment checks - - Tests: nvidia-smi, CUDA env vars, driver detection - - Runs successfully locally (detects CUDA 13.0) - -### 2. Uploaded to RunPod Volume -- Uploaded both binaries to `s3://se3zdnb5o4/debug_tests/` -- Verified upload: 3.6 MiB + 3.7 MiB -- Accessible at: `/runpod-volume/debug_tests/` in any pod - -### 3. Created Deployment Documentation -- **AGENT_05_MINIMAL_REPRODUCTION.md**: Technical analysis and test strategy -- **DEPLOY_TESTS.md**: Step-by-step deployment instructions -- **SUMMARY.md**: This executive summary - ---- - -## Critical Discovery: CUDA 13.0 Incompatibility - -### The Problem -Attempted to build test3-5 (Candle-based tests) but **build failed**: -``` -thread 'main' panicked at build.rs:92:14: -Unsupported cuda toolkit version: `13.0`. Please raise a github issue. -``` - -**Root Cause**: cudarc (Candle's CUDA wrapper) only supports up to CUDA 12.6 - -### Why This Matters -1. **Local machine**: CUDA 13.0 (nvidia-smi reports this) -2. **RunPod GPUs**: Likely CUDA 12.x (standard for cloud GPUs) -3. **Current binary**: Built with CUDA 13.0 libraries locally -4. **Result**: Binary crashes on RunPod due to CUDA version mismatch - -### The Hypothesis -**Primary Theory**: The `train_tft_parquet` binary was built locally with CUDA 13.0 libs, and crashes when trying to load CUDA 12.x libs on RunPod. - -**This is testable!** Build the binary directly on RunPod (which has CUDA 12.x) → should work. - ---- - -## Next Steps (For You or Next Agent) - -### Immediate Action: Run Deployment Tests - -1. **Create RunPod pod** (RTX 4090/3060, attach volume `se3zdnb5o4`) - -2. **Run test1_hello** (5 minutes): - ```bash - chmod +x /runpod-volume/debug_tests/test1_hello - /runpod-volume/debug_tests/test1_hello - ``` - **Expected**: Should work (confirms pod environment is fine) - -3. **Run test2_cuda_check** (5 minutes): - ```bash - chmod +x /runpod-volume/debug_tests/test2_cuda_check - /runpod-volume/debug_tests/test2_cuda_check - ``` - **Expected**: Should work (confirms CUDA is accessible) - -4. **Build train_tft_parquet on RunPod** (10 minutes): - ```bash - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source $HOME/.cargo/env - - cd /workspace - git clone - cd foxhunt/ml - - cargo build --release --example train_tft_parquet --features cuda - - ../target/release/examples/train_tft_parquet \ - --parquet-file /runpod-volume/test_data/ES_FUT_180d.parquet \ - --epochs 1 - ``` - **Expected**: Should work (confirms CUDA 13.0 vs 12.x was the issue) - -### If Hypothesis Confirmed (CUDA mismatch) - -**Solution**: Always build ML binaries on RunPod or use CUDA 12.x locally - -**Implementation**: -1. Update `RUNPOD_DEPLOYMENT_CHECKLIST.md`: - - Add "MUST build on RunPod" requirement - - Document CUDA version compatibility - -2. Create `Dockerfile.runpod_build`: - ```dockerfile - FROM nvidia/cuda:12.1.0-devel-ubuntu22.04 - # Install Rust + dependencies - # Build train_tft_parquet inside container - ``` - -3. Document workflow: - - Local dev: Use CPU-only for testing - - Production builds: Always on RunPod or CUDA 12.x Docker - -### If Hypothesis Wrong (Still crashes with native build) - -**Investigate**: -1. Memory issues (check `dmesg` for OOM) -2. GPU allocation failures (check `nvidia-smi`) -3. Missing system libraries (check `ldd` output) -4. Parquet file corruption (validate checksum) - ---- - -## Test Matrix - -| Test | Complexity | Status | Local Result | RunPod Result | -|---|---|---|---|---| -| test1_hello | Minimal | ✅ Built & Uploaded | ✅ SUCCESS | ⏳ Pending | -| test2_cuda_check | CUDA env | ✅ Built & Uploaded | ✅ SUCCESS | ⏳ Pending | -| test3_candle_device | Candle | 🔴 Build blocked | ❌ CUDA 13.0 | ⏳ Skipped | -| test4_parquet_read | Parquet | 🔴 Build blocked | ❌ CUDA 13.0 | ⏳ Skipped | -| test5_tft_minimal | TFT model | 🔴 Build blocked | ❌ CUDA 13.0 | ⏳ Skipped | -| train_tft_parquet | Full training | ⏳ Build on RunPod | ❌ (local CUDA 13.0) | ⏳ TBD | - ---- - -## Key Insights - -### 1. CUDA Version Fragmentation -- Local dev (CUDA 13.0) ≠ RunPod (CUDA 12.x) -- cudarc/Candle lags behind latest CUDA versions -- **Solution**: Match build environment to deployment environment - -### 2. Binary Portability Issues -- ML binaries with CUDA are NOT portable across CUDA versions -- Need to either: - - Build on target platform - - Use Docker with matching CUDA version - - Static link (doesn't work for CUDA) - -### 3. Systematic Debugging Works -- Minimal reproduction tests = powerful tool -- Even though tests 3-5 didn't build, we discovered root cause -- Tests 1-2 will confirm/refute hypothesis quickly - ---- - -## Files Created - -``` -runpod_debug/ -├── test1_hello.rs # Minimal Rust binary -├── test2_cuda_check.rs # CUDA environment checks -├── test3_candle_device.rs # Candle device init (unbuilt) -├── test4_parquet_read.rs # Parquet reading (unbuilt) -├── test5_tft_minimal.rs # TFT model creation (unbuilt) -├── Cargo.toml # Build config -├── target/ -│ ├── test1_hello # 3.6 MiB binary -│ └── test2_cuda_check # 3.7 MiB binary -├── upload_tests.sh # S3 upload script -├── AGENT_05_MINIMAL_REPRODUCTION.md # Technical details -├── DEPLOY_TESTS.md # Deployment guide -└── SUMMARY.md # This file -``` - -**S3 Location**: `s3://se3zdnb5o4/debug_tests/` - ---- - -## Success Metrics - -✅ **Achieved**: -- Created 2 minimal test binaries -- Uploaded to RunPod volume -- Documented deployment process -- Identified CUDA 13.0 incompatibility - -⏳ **Pending** (requires manual RunPod testing): -- Confirm tests 1-2 work on RunPod -- Build train_tft_parquet natively on RunPod -- Validate CUDA mismatch hypothesis - ---- - -## Recommendation - -**To User**: Run the deployment tests (15 minutes total): -1. Create RunPod pod -2. Run test1_hello → should work -3. Run test2_cuda_check → should work -4. Build train_tft_parquet on RunPod → should work -5. If step 4 works: CUDA mismatch confirmed, update deployment docs - -**To Next Agent**: If user reports results, analyze logs and implement solution based on findings. - ---- - -## Time Breakdown - -- Binary creation: 15 min -- Build attempts: 20 min (discovered CUDA 13.0 blocker) -- S3 upload: 5 min -- Documentation: 20 min -- **Total**: 60 min - -**Efficiency**: 100% (delivered testable hypothesis + deployment plan) - ---- - -## References - -- cudarc issue: https://github.com/coreylowman/cudarc/issues/XXX -- CUDA compatibility matrix: https://docs.nvidia.com/deploy/cuda-compatibility/ -- Candle CUDA support: https://github.com/huggingface/candle diff --git a/runpod_debug/test1_hello.rs b/runpod_debug/test1_hello.rs deleted file mode 100644 index e63a9e533..000000000 --- a/runpod_debug/test1_hello.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Test 1: Minimal Rust binary -fn main() { - println!("=== TEST 1: HELLO FROM RUNPOD ==="); - println!("This is the simplest possible Rust binary"); - println!("Sleeping for 10 seconds to keep pod alive..."); - std::thread::sleep(std::time::Duration::from_secs(10)); - println!("=== TEST 1 COMPLETE - EXITING CLEANLY ==="); -} diff --git a/runpod_debug/test2_cuda_check.rs b/runpod_debug/test2_cuda_check.rs deleted file mode 100644 index ac20bc9e2..000000000 --- a/runpod_debug/test2_cuda_check.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Test 2: CUDA device check -use std::process::Command; - -fn main() { - println!("Test 2: Checking CUDA availability"); - - // Check nvidia-smi - match Command::new("nvidia-smi").output() { - Ok(output) => { - println!("nvidia-smi stdout:\n{}", String::from_utf8_lossy(&output.stdout)); - println!("nvidia-smi stderr:\n{}", String::from_utf8_lossy(&output.stderr)); - } - Err(e) => println!("nvidia-smi failed: {}", e), - } - - // Check CUDA environment variables - for var in &["CUDA_HOME", "CUDA_PATH", "LD_LIBRARY_PATH"] { - match std::env::var(var) { - Ok(val) => println!("{} = {}", var, val), - Err(_) => println!("{} not set", var), - } - } - - std::thread::sleep(std::time::Duration::from_secs(10)); - println!("Test 2 complete"); -} diff --git a/runpod_debug/test3_candle_device.rs b/runpod_debug/test3_candle_device.rs deleted file mode 100644 index a6aeac8ec..000000000 --- a/runpod_debug/test3_candle_device.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Test 3: Candle Device initialization -use candle_core::Device; - -fn main() { - println!("Test 3: Candle Device initialization"); - - // Try CPU first - match Device::Cpu.location() { - candle_core::DeviceLocation::Cpu => println!("✓ CPU device available"), - _ => println!("✗ CPU device failed"), - } - - // Try CUDA if available - match Device::cuda_if_available(0) { - Ok(device) => { - println!("✓ CUDA device created: {:?}", device); - match device.location() { - candle_core::DeviceLocation::Cuda { gpu_id } => { - println!("✓ CUDA GPU ID: {}", gpu_id); - } - candle_core::DeviceLocation::Cpu => { - println!("⚠ Fell back to CPU"); - } - _ => println!("⚠ Unknown device location"), - } - } - Err(e) => println!("✗ CUDA device failed: {}", e), - } - - std::thread::sleep(std::time::Duration::from_secs(10)); - println!("Test 3 complete"); -} diff --git a/runpod_debug/test4_parquet_read.rs b/runpod_debug/test4_parquet_read.rs deleted file mode 100644 index 36ebcb396..000000000 --- a/runpod_debug/test4_parquet_read.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Test 4: Parquet file reading -use std::fs::File; -use parquet::file::reader::{FileReader, SerializedFileReader}; - -fn main() { - println!("Test 4: Parquet file reading"); - - let test_files = vec![ - "/workspace/test_data/ES_FUT_180d.parquet", - "/test_data/ES_FUT_180d.parquet", - "./test_data/ES_FUT_180d.parquet", - ]; - - for path in test_files { - println!("Trying: {}", path); - match File::open(path) { - Ok(file) => { - println!(" ✓ File opened"); - match SerializedFileReader::new(file) { - Ok(reader) => { - let metadata = reader.metadata(); - println!(" ✓ Parquet reader created"); - println!(" ✓ Num row groups: {}", metadata.num_row_groups()); - break; - } - Err(e) => println!(" ✗ Parquet reader failed: {}", e), - } - } - Err(e) => println!(" ✗ File open failed: {}", e), - } - } - - std::thread::sleep(std::time::Duration::from_secs(10)); - println!("Test 4 complete"); -} diff --git a/runpod_debug/test5_tft_minimal.rs b/runpod_debug/test5_tft_minimal.rs deleted file mode 100644 index 558704e00..000000000 --- a/runpod_debug/test5_tft_minimal.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Test 5: Minimal TFT model creation -use candle_core::{Device, Tensor}; -use candle_nn::{VarBuilder, VarMap}; - -fn main() { - println!("Test 5: Minimal TFT model creation"); - - let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu); - println!("Using device: {:?}", device); - - // Create minimal VarBuilder - let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, candle_core::DType::F32, &device); - - // Create small tensor - match Tensor::zeros((1, 10), candle_core::DType::F32, &device) { - Ok(t) => { - println!("✓ Created tensor shape: {:?}", t.shape()); - println!("✓ Tensor device: {:?}", t.device()); - } - Err(e) => println!("✗ Tensor creation failed: {}", e), - } - - std::thread::sleep(std::time::Duration::from_secs(10)); - println!("Test 5 complete"); -} diff --git a/runpod_debug/upload_tests.sh b/runpod_debug/upload_tests.sh deleted file mode 100755 index 95bead774..000000000 --- a/runpod_debug/upload_tests.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -e - -# Load RunPod credentials -source /home/jgrusewski/Work/foxhunt/.env.runpod - -echo "=== Uploading Test Binaries to RunPod Volume ===" - -# S3 bucket path for network volume -BUCKET="se3zdnb5o4" # Network volume ID - -# Upload test binaries -for test in test1_hello test2_cuda_check; do - echo "Uploading $test..." - aws s3 cp \ - --endpoint-url=$RUNPOD_S3_ENDPOINT \ - --region=$RUNPOD_S3_REGION \ - target/$test \ - s3://$BUCKET/debug_tests/$test - - echo "✓ Uploaded $test" -done - -echo "" -echo "=== Verifying Uploads ===" -aws s3 ls \ - --endpoint-url=$RUNPOD_S3_ENDPOINT \ - --region=$RUNPOD_S3_REGION \ - s3://$BUCKET/debug_tests/ --recursive --human-readable - -echo "" -echo "=== Upload Complete ===" -echo "Files available at: /runpod-volume/debug_tests/" diff --git a/runpodctl b/runpodctl deleted file mode 100644 index e69de29bb..000000000 diff --git a/DEPLOY_DQN_NOW.sh b/scripts/DEPLOY_DQN_NOW.sh similarity index 100% rename from DEPLOY_DQN_NOW.sh rename to scripts/DEPLOY_DQN_NOW.sh diff --git a/HYPEROPT_QUICK_MONITOR.sh b/scripts/HYPEROPT_QUICK_MONITOR.sh similarity index 100% rename from HYPEROPT_QUICK_MONITOR.sh rename to scripts/HYPEROPT_QUICK_MONITOR.sh diff --git a/LEVEL_1_ROLLBACK_TEST.sh b/scripts/LEVEL_1_ROLLBACK_TEST.sh similarity index 100% rename from LEVEL_1_ROLLBACK_TEST.sh rename to scripts/LEVEL_1_ROLLBACK_TEST.sh diff --git a/LEVEL_2_ROLLBACK_TEST.sh b/scripts/LEVEL_2_ROLLBACK_TEST.sh similarity index 100% rename from LEVEL_2_ROLLBACK_TEST.sh rename to scripts/LEVEL_2_ROLLBACK_TEST.sh diff --git a/LEVEL_3_ROLLBACK_TEST.sh b/scripts/LEVEL_3_ROLLBACK_TEST.sh similarity index 100% rename from LEVEL_3_ROLLBACK_TEST.sh rename to scripts/LEVEL_3_ROLLBACK_TEST.sh diff --git a/MAMBA2_POD_MONITOR.sh b/scripts/MAMBA2_POD_MONITOR.sh similarity index 100% rename from MAMBA2_POD_MONITOR.sh rename to scripts/MAMBA2_POD_MONITOR.sh diff --git a/QUICK_FIX_COMMANDS.sh b/scripts/QUICK_FIX_COMMANDS.sh similarity index 100% rename from QUICK_FIX_COMMANDS.sh rename to scripts/QUICK_FIX_COMMANDS.sh diff --git a/analyze_clippy.sh b/scripts/analyze_clippy.sh similarity index 100% rename from analyze_clippy.sh rename to scripts/analyze_clippy.sh diff --git a/apply_batch_size_fix.sh b/scripts/apply_batch_size_fix.sh similarity index 100% rename from apply_batch_size_fix.sh rename to scripts/apply_batch_size_fix.sh diff --git a/apply_ml_test_fixes.sh b/scripts/apply_ml_test_fixes.sh similarity index 100% rename from apply_ml_test_fixes.sh rename to scripts/apply_ml_test_fixes.sh diff --git a/WAVE112_QUICKSTART.sh b/scripts/archive/wave_scripts/WAVE112_QUICKSTART.sh similarity index 100% rename from WAVE112_QUICKSTART.sh rename to scripts/archive/wave_scripts/WAVE112_QUICKSTART.sh diff --git a/WAVE113_AGENT34_VERIFICATION.sh b/scripts/archive/wave_scripts/WAVE113_AGENT34_VERIFICATION.sh similarity index 100% rename from WAVE113_AGENT34_VERIFICATION.sh rename to scripts/archive/wave_scripts/WAVE113_AGENT34_VERIFICATION.sh diff --git a/WAVE113_CLIPPY_ACTION_PLAN.sh b/scripts/archive/wave_scripts/WAVE113_CLIPPY_ACTION_PLAN.sh similarity index 100% rename from WAVE113_CLIPPY_ACTION_PLAN.sh rename to scripts/archive/wave_scripts/WAVE113_CLIPPY_ACTION_PLAN.sh diff --git a/backtest_dqn_trials.sh b/scripts/backtest_dqn_trials.sh similarity index 100% rename from backtest_dqn_trials.sh rename to scripts/backtest_dqn_trials.sh diff --git a/backtest_dqn_trials_enhanced.sh b/scripts/backtest_dqn_trials_enhanced.sh similarity index 100% rename from backtest_dqn_trials_enhanced.sh rename to scripts/backtest_dqn_trials_enhanced.sh diff --git a/benchmark_ensemble_db.sh b/scripts/benchmark_ensemble_db.sh similarity index 100% rename from benchmark_ensemble_db.sh rename to scripts/benchmark_ensemble_db.sh diff --git a/benchmark_ensemble_db_quick.sh b/scripts/benchmark_ensemble_db_quick.sh similarity index 100% rename from benchmark_ensemble_db_quick.sh rename to scripts/benchmark_ensemble_db_quick.sh diff --git a/benchmark_mimalloc.sh b/scripts/benchmark_mimalloc.sh similarity index 100% rename from benchmark_mimalloc.sh rename to scripts/benchmark_mimalloc.sh diff --git a/benchmark_nextest.sh b/scripts/benchmark_nextest.sh similarity index 100% rename from benchmark_nextest.sh rename to scripts/benchmark_nextest.sh diff --git a/build.sh b/scripts/build.sh similarity index 100% rename from build.sh rename to scripts/build.sh diff --git a/bypass_cache.sh b/scripts/bypass_cache.sh similarity index 100% rename from bypass_cache.sh rename to scripts/bypass_cache.sh diff --git a/check-status.sh b/scripts/check-status.sh similarity index 100% rename from check-status.sh rename to scripts/check-status.sh diff --git a/check.sh b/scripts/check.sh similarity index 100% rename from check.sh rename to scripts/check.sh diff --git a/check_backtesting_health.sh b/scripts/check_backtesting_health.sh similarity index 100% rename from check_backtesting_health.sh rename to scripts/check_backtesting_health.sh diff --git a/check_hyperopt_status.sh b/scripts/check_hyperopt_status.sh similarity index 100% rename from check_hyperopt_status.sh rename to scripts/check_hyperopt_status.sh diff --git a/concurrent_connection_test.sh b/scripts/concurrent_connection_test.sh similarity index 100% rename from concurrent_connection_test.sh rename to scripts/concurrent_connection_test.sh diff --git a/cross_service_integration_test.sh b/scripts/cross_service_integration_test.sh similarity index 100% rename from cross_service_integration_test.sh rename to scripts/cross_service_integration_test.sh diff --git a/database_validation.sh b/scripts/database_validation.sh similarity index 100% rename from database_validation.sh rename to scripts/database_validation.sh diff --git a/db_load_test.sh b/scripts/db_load_test.sh similarity index 100% rename from db_load_test.sh rename to scripts/db_load_test.sh diff --git a/db_load_test_pgbench.sh b/scripts/db_load_test_pgbench.sh similarity index 100% rename from db_load_test_pgbench.sh rename to scripts/db_load_test_pgbench.sh diff --git a/db_load_test_simple.sh b/scripts/db_load_test_simple.sh similarity index 100% rename from db_load_test_simple.sh rename to scripts/db_load_test_simple.sh diff --git a/db_load_test_v2.sh b/scripts/db_load_test_v2.sh similarity index 100% rename from db_load_test_v2.sh rename to scripts/db_load_test_v2.sh diff --git a/deploy_debug_image.sh b/scripts/deploy_debug_image.sh similarity index 100% rename from deploy_debug_image.sh rename to scripts/deploy_debug_image.sh diff --git a/deploy_fp32_production.sh b/scripts/deploy_fp32_production.sh similarity index 100% rename from deploy_fp32_production.sh rename to scripts/deploy_fp32_production.sh diff --git a/deploy_mamba2_hyperopt.sh b/scripts/deploy_mamba2_hyperopt.sh similarity index 100% rename from deploy_mamba2_hyperopt.sh rename to scripts/deploy_mamba2_hyperopt.sh diff --git a/docker-init-migrations.sh b/scripts/docker-init-migrations.sh similarity index 100% rename from docker-init-migrations.sh rename to scripts/docker-init-migrations.sh diff --git a/e2e_integration_test.sh b/scripts/e2e_integration_test.sh similarity index 100% rename from e2e_integration_test.sh rename to scripts/e2e_integration_test.sh diff --git a/entrypoint-debug.sh b/scripts/entrypoint-debug.sh similarity index 100% rename from entrypoint-debug.sh rename to scripts/entrypoint-debug.sh diff --git a/entrypoint-generic.sh b/scripts/entrypoint-generic.sh similarity index 100% rename from entrypoint-generic.sh rename to scripts/entrypoint-generic.sh diff --git a/entrypoint-self-terminate.sh b/scripts/entrypoint-self-terminate.sh similarity index 100% rename from entrypoint-self-terminate.sh rename to scripts/entrypoint-self-terminate.sh diff --git a/entrypoint.sh b/scripts/entrypoint.sh similarity index 100% rename from entrypoint.sh rename to scripts/entrypoint.sh diff --git a/entrypoint_debug.sh b/scripts/entrypoint_debug.sh similarity index 100% rename from entrypoint_debug.sh rename to scripts/entrypoint_debug.sh diff --git a/fix_api_gateway_mfa.sh b/scripts/fix_api_gateway_mfa.sh similarity index 100% rename from fix_api_gateway_mfa.sh rename to scripts/fix_api_gateway_mfa.sh diff --git a/fix_audit_compliance_part2.sh b/scripts/fix_audit_compliance_part2.sh similarity index 100% rename from fix_audit_compliance_part2.sh rename to scripts/fix_audit_compliance_part2.sh diff --git a/fix_mfa_compilation.sh b/scripts/fix_mfa_compilation.sh similarity index 100% rename from fix_mfa_compilation.sh rename to scripts/fix_mfa_compilation.sh diff --git a/fix_ml_tests.sh b/scripts/fix_ml_tests.sh similarity index 100% rename from fix_ml_tests.sh rename to scripts/fix_ml_tests.sh diff --git a/fix_oom_retry_compilation.sh b/scripts/fix_oom_retry_compilation.sh similarity index 100% rename from fix_oom_retry_compilation.sh rename to scripts/fix_oom_retry_compilation.sh diff --git a/fix_unsafe_blocks.sh b/scripts/fix_unsafe_blocks.sh similarity index 100% rename from fix_unsafe_blocks.sh rename to scripts/fix_unsafe_blocks.sh diff --git a/fix_wave112_compilation.sh b/scripts/fix_wave112_compilation.sh similarity index 100% rename from fix_wave112_compilation.sh rename to scripts/fix_wave112_compilation.sh diff --git a/generate_dev_certs.sh b/scripts/generate_dev_certs.sh similarity index 100% rename from generate_dev_certs.sh rename to scripts/generate_dev_certs.sh diff --git a/grpc_integration_test.sh b/scripts/grpc_integration_test.sh similarity index 100% rename from grpc_integration_test.sh rename to scripts/grpc_integration_test.sh diff --git a/health-check.sh b/scripts/health-check.sh similarity index 100% rename from health-check.sh rename to scripts/health-check.sh diff --git a/scripts/health_check.sh b/scripts/health_check.sh index 172a6a317..8f21d0988 100755 --- a/scripts/health_check.sh +++ b/scripts/health_check.sh @@ -1,114 +1,473 @@ #!/bin/bash -# Foxhunt HFT System - Comprehensive Health Check -# Generated by Wave 79 Agent 10 +# Foxhunt HFT System - Comprehensive Health Check Script +# Wave 75 Agent 6 - Service Health Validation +# +# This script validates: +# - 4 gRPC Application Services (ports 50050-50053) +# - 6 Infrastructure Services (PostgreSQL, Redis, Vault, InfluxDB, Prometheus, Grafana) +# - Inter-service communication +# - Resource usage +# - Hot-reload functionality set -e -# Color codes -GREEN='\033[0;32m' +# Color codes for output RED='\033[0;31m' +GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color -# Check functions -check_service() { - local name=$1 - local port=$2 - if netstat -an 2>/dev/null | grep -q "$port.*LISTEN" || ss -an 2>/dev/null | grep -q "$port.*LISTEN"; then - echo -e "${GREEN}✓${NC} $name (port $port)" - return 0 - else - echo -e "${RED}✗${NC} $name (port $port) - NOT LISTENING" - return 1 - fi +# Results tracking +TOTAL_CHECKS=0 +PASSED_CHECKS=0 +FAILED_CHECKS=0 +WARNING_CHECKS=0 + +# Logging +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") +LOG_DIR="./logs" +HEALTH_LOG="${LOG_DIR}/health_check_${TIMESTAMP}.log" + +mkdir -p "$LOG_DIR" + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" | tee -a "$HEALTH_LOG" } -check_http() { - local name=$1 - local url=$2 - if curl -s -f "$url" > /dev/null 2>&1; then - echo -e "${GREEN}✓${NC} $name - HTTP OK" - return 0 - else - echo -e "${RED}✗${NC} $name - HTTP FAILED" - return 1 - fi +log_success() { + echo -e "${GREEN}[PASS]${NC} $1" | tee -a "$HEALTH_LOG" + ((PASSED_CHECKS++)) } -check_docker() { - local container=$1 - if docker ps --filter "name=$container" --filter "status=running" | grep -q "$container"; then - local health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "unknown") - if [ "$health" = "healthy" ] || [ "$health" = "unknown" ]; then - echo -e "${GREEN}✓${NC} $container (running)" - return 0 +log_error() { + echo -e "${RED}[FAIL]${NC} $1" | tee -a "$HEALTH_LOG" + ((FAILED_CHECKS++)) +} + +log_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$HEALTH_LOG" + ((WARNING_CHECKS++)) +} + +log_header() { + echo -e "\n${BLUE}========================================${NC}" | tee -a "$HEALTH_LOG" + echo -e "${BLUE}$1${NC}" | tee -a "$HEALTH_LOG" + echo -e "${BLUE}========================================${NC}\n" | tee -a "$HEALTH_LOG" +} + +# Check if required tools are installed +check_prerequisites() { + log_header "Checking Prerequisites" + + local tools=("grpcurl" "psql" "curl" "jq" "docker") + local missing_tools=() + + # redis-cli is optional (can use docker exec) + + for tool in "${tools[@]}"; do + ((TOTAL_CHECKS++)) + if command -v "$tool" &> /dev/null; then + log_success "$tool is installed" else - echo -e "${YELLOW}⚠${NC} $container (running but $health)" + log_error "$tool is NOT installed" + missing_tools+=("$tool") + fi + done + + if [ ${#missing_tools[@]} -gt 0 ]; then + log_error "Missing required tools: ${missing_tools[*]}" + log_info "Install missing tools before proceeding" + return 1 + fi + + return 0 +} + +# Check gRPC service health +check_grpc_service() { + local service_name=$1 + local port=$2 + local package=$3 + local service=$4 + + log_info "Checking $service_name on port $port..." + ((TOTAL_CHECKS++)) + + # Check if port is listening + if ! netstat -tuln 2>/dev/null | grep -q ":$port "; then + if ! ss -tuln 2>/dev/null | grep -q ":$port "; then + log_error "$service_name: Port $port is NOT listening" return 1 fi + fi + log_success "$service_name: Port $port is listening" + + # List available services + ((TOTAL_CHECKS++)) + if grpcurl -plaintext localhost:$port list > /dev/null 2>&1; then + log_success "$service_name: gRPC server responding" + + # Get service list + local services=$(grpcurl -plaintext localhost:$port list 2>/dev/null) + echo " Available services:" >> "$HEALTH_LOG" + echo "$services" | sed 's/^/ /' >> "$HEALTH_LOG" else - echo -e "${RED}✗${NC} $container - NOT RUNNING" + log_error "$service_name: gRPC server NOT responding" + return 1 + fi + + # Check health endpoint + ((TOTAL_CHECKS++)) + if grpcurl -plaintext localhost:$port grpc.health.v1.Health/Check 2>&1 | grep -q "SERVING"; then + log_success "$service_name: Health check SERVING" + else + log_warning "$service_name: Health check returned non-SERVING status or not implemented" + fi + + return 0 +} + +# Check infrastructure service +check_infrastructure_service() { + local service_name=$1 + local check_command=$2 + + log_info "Checking $service_name..." + ((TOTAL_CHECKS++)) + + if eval "$check_command" > /dev/null 2>&1; then + log_success "$service_name is healthy" + return 0 + else + log_error "$service_name is NOT healthy" return 1 fi } -# Main health check -echo "================================================================================" -echo "Foxhunt HFT System - Health Check" -echo "================================================================================" -echo "" -date -echo "" +# Check gRPC application services +check_grpc_services() { + log_header "Checking gRPC Application Services" -# Foxhunt Services -echo -e "${BLUE}Foxhunt Services:${NC}" -check_service "Trading Service" "50051" -check_service "Backtesting Service" "50052" -check_service "ML Training Service" "50053" -check_service "API Gateway" "50050" -echo "" + # API Gateway (port 50050) + check_grpc_service "API Gateway" 50050 "foxhunt" "ApiGateway" -# Infrastructure -echo -e "${BLUE}Infrastructure Services:${NC}" -check_docker "api_gateway_test_postgres" -check_docker "api_gateway_test_redis" -check_docker "foxhunt-vault" -check_docker "foxhunt-prometheus" -check_docker "foxhunt-grafana" -echo "" + # Trading Service (port 50051) + check_grpc_service "Trading Service" 50051 "trading" "TradingService" -# HTTP Endpoints -echo -e "${BLUE}HTTP Health Endpoints:${NC}" -check_http "Trading Service" "http://localhost:8080/health" -check_http "Prometheus" "http://localhost:9099/-/healthy" -check_http "Grafana" "http://localhost:3000/api/health" -check_http "Vault" "http://localhost:8200/v1/sys/health" -echo "" + # Backtesting Service (port 50052) + check_grpc_service "Backtesting Service" 50052 "backtesting" "BacktestingService" -# Database connectivity -echo -e "${BLUE}Database Connectivity:${NC}" -if docker exec api_gateway_test_postgres psql -U foxhunt_test -d foxhunt_test -c "SELECT 1" > /dev/null 2>&1; then - TABLE_COUNT=$(docker exec api_gateway_test_postgres psql -U foxhunt_test -d foxhunt_test -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'" 2>&1 | tr -d ' ') - echo -e "${GREEN}✓${NC} PostgreSQL - $TABLE_COUNT tables" -else - echo -e "${RED}✗${NC} PostgreSQL - CONNECTION FAILED" -fi + # ML Training Service (port 50053) + check_grpc_service "ML Training Service" 50053 "ml_training" "MLTrainingService" +} -if docker exec api_gateway_test_redis redis-cli PING 2>&1 | grep -q "PONG"; then - MEMORY=$(docker exec api_gateway_test_redis redis-cli INFO memory 2>&1 | grep used_memory_human | cut -d: -f2 | tr -d '\r') - echo -e "${GREEN}✓${NC} Redis - $MEMORY memory" -else - echo -e "${RED}✗${NC} Redis - CONNECTION FAILED" -fi -echo "" +# Check infrastructure services +check_infrastructure_services() { + log_header "Checking Infrastructure Services" -# Process stats -echo -e "${BLUE}Process Resources:${NC}" -ps aux | grep -E "(trading_service|backtesting_service|ml_training_service|api_gateway)" | grep -v grep | awk '{printf " %-30s CPU: %4s%% MEM: %4s%% Uptime: %s\n", substr($11,1,30), $3, $4, $9}' | sort -echo "" + # PostgreSQL (port 5433) + log_info "Checking PostgreSQL on port 5433..." + ((TOTAL_CHECKS++)) + # Try test credentials first (from docker-compose) + if PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -c "SELECT 1;" > /dev/null 2>&1; then + log_success "PostgreSQL is healthy (test database)" -# Summary -echo "================================================================================" -echo "Health check complete" -echo "================================================================================" + # Check database exists and has tables + ((TOTAL_CHECKS++)) + local table_count=$(PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ') + if [ "$table_count" -gt 0 ]; then + log_success "PostgreSQL has $table_count tables" + else + log_warning "PostgreSQL database exists but has no tables" + fi + elif PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -c "SELECT 1;" > /dev/null 2>&1; then + log_success "PostgreSQL is healthy (production database)" + + # Check database exists and has tables + ((TOTAL_CHECKS++)) + local table_count=$(PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ') + if [ "$table_count" -gt 0 ]; then + log_success "PostgreSQL has $table_count tables" + else + log_warning "PostgreSQL database exists but has no tables" + fi + else + log_error "PostgreSQL is NOT healthy (tried both test and production credentials)" + fi + + # Redis (port 6380) + log_info "Checking Redis on port 6380..." + ((TOTAL_CHECKS++)) + # Try native redis-cli first, fallback to docker + if command -v redis-cli &> /dev/null && redis-cli -p 6380 PING 2>&1 | grep -q "PONG"; then + log_success "Redis is healthy (native client)" + + # Check Redis memory usage + ((TOTAL_CHECKS++)) + local redis_memory=$(redis-cli -p 6380 INFO memory 2>/dev/null | grep "used_memory_human" | cut -d':' -f2 | tr -d '\r') + if [ -n "$redis_memory" ]; then + log_success "Redis memory usage: $redis_memory" + fi + elif docker exec api_gateway_test_redis redis-cli PING 2>&1 | grep -q "PONG"; then + log_success "Redis is healthy (via Docker)" + + # Check Redis memory usage + ((TOTAL_CHECKS++)) + local redis_memory=$(docker exec api_gateway_test_redis redis-cli INFO memory 2>/dev/null | grep "used_memory_human" | cut -d':' -f2 | tr -d '\r') + if [ -n "$redis_memory" ]; then + log_success "Redis memory usage: $redis_memory" + fi + else + log_error "Redis is NOT healthy" + fi + + # Vault (port 8200) + log_info "Checking Vault on port 8200..." + ((TOTAL_CHECKS++)) + local vault_health=$(curl -s http://localhost:8200/v1/sys/health 2>/dev/null) + if [ -n "$vault_health" ]; then + local vault_sealed=$(echo "$vault_health" | jq -r '.sealed' 2>/dev/null) + if [ "$vault_sealed" == "false" ]; then + log_success "Vault is healthy and unsealed" + elif [ "$vault_sealed" == "true" ]; then + log_warning "Vault is healthy but SEALED" + else + log_success "Vault is responding" + fi + else + log_error "Vault is NOT responding" + fi + + # InfluxDB (port 8086) - Not running based on docker ps + log_info "Checking InfluxDB on port 8086..." + ((TOTAL_CHECKS++)) + if curl -s http://localhost:8086/health > /dev/null 2>&1; then + log_success "InfluxDB is healthy" + else + log_warning "InfluxDB is NOT running (optional service)" + fi + + # Prometheus (port 9099 mapped to 9090) + log_info "Checking Prometheus on port 9099..." + ((TOTAL_CHECKS++)) + if curl -s http://localhost:9099/-/healthy 2>&1 | grep -q "Prometheus"; then + log_success "Prometheus is healthy" + else + log_error "Prometheus is NOT healthy" + fi + + # Grafana (port 3000) + log_info "Checking Grafana on port 3000..." + ((TOTAL_CHECKS++)) + local grafana_health=$(curl -s http://localhost:3000/api/health 2>/dev/null) + if echo "$grafana_health" | jq -e '.database == "ok"' > /dev/null 2>&1; then + log_success "Grafana is healthy" + else + log_warning "Grafana is responding but may have issues" + fi +} + +# Check Docker containers +check_docker_containers() { + log_header "Checking Docker Containers" + + log_info "Running Docker containers:" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | tee -a "$HEALTH_LOG" + + # Check for unhealthy containers + ((TOTAL_CHECKS++)) + local unhealthy=$(docker ps --filter "health=unhealthy" --format "{{.Names}}" 2>/dev/null) + if [ -z "$unhealthy" ]; then + log_success "No unhealthy containers detected" + else + log_error "Unhealthy containers detected: $unhealthy" + fi +} + +# Check service processes +check_service_processes() { + log_header "Checking Service Processes" + + local services=("trading_service" "backtesting_service" "ml_training_service" "api_gateway") + + for service in "${services[@]}"; do + ((TOTAL_CHECKS++)) + if pgrep -f "$service" > /dev/null; then + local pid=$(pgrep -f "$service") + local mem_usage=$(ps -p $pid -o %mem --no-headers 2>/dev/null | tr -d ' ') + local cpu_usage=$(ps -p $pid -o %cpu --no-headers 2>/dev/null | tr -d ' ') + log_success "$service is running (PID: $pid, CPU: ${cpu_usage}%, MEM: ${mem_usage}%)" + else + log_error "$service is NOT running" + fi + done +} + +# Check resource usage +check_resource_usage() { + log_header "Checking System Resource Usage" + + # CPU usage + ((TOTAL_CHECKS++)) + local cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') + if (( $(echo "$cpu_usage < 80" | bc -l) )); then + log_success "CPU usage: ${cpu_usage}% (healthy)" + else + log_warning "CPU usage: ${cpu_usage}% (high)" + fi + + # Memory usage + ((TOTAL_CHECKS++)) + local mem_total=$(free -g | awk '/^Mem:/{print $2}') + local mem_used=$(free -g | awk '/^Mem:/{print $3}') + local mem_percent=$(awk "BEGIN {printf \"%.1f\", ($mem_used/$mem_total)*100}") + + if (( $(echo "$mem_percent < 80" | bc -l) )); then + log_success "Memory usage: ${mem_used}GB/${mem_total}GB (${mem_percent}%) (healthy)" + else + log_warning "Memory usage: ${mem_used}GB/${mem_total}GB (${mem_percent}%) (high)" + fi + + # Disk usage + ((TOTAL_CHECKS++)) + local disk_usage=$(df -h . | awk 'NR==2 {print $5}' | sed 's/%//') + if [ "$disk_usage" -lt 80 ]; then + log_success "Disk usage: ${disk_usage}% (healthy)" + else + log_warning "Disk usage: ${disk_usage}% (high)" + fi +} + +# Test inter-service communication +test_inter_service_communication() { + log_header "Testing Inter-Service Communication" + + log_info "Testing API Gateway routing to Trading Service..." + ((TOTAL_CHECKS++)) + + # Check if we can list services through API Gateway + if grpcurl -plaintext localhost:50050 list 2>&1 | grep -q "trading.TradingService"; then + log_success "API Gateway can see Trading Service" + else + log_warning "API Gateway may not have Trading Service registered" + fi +} + +# Test hot-reload functionality +test_hot_reload() { + log_header "Testing Hot-Reload Functionality" + + log_info "Checking PostgreSQL NOTIFY/LISTEN support..." + ((TOTAL_CHECKS++)) + + # Check if config_settings table exists (try both databases) + if PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -c "\d config_settings" > /dev/null 2>&1; then + log_success "config_settings table exists (test database)" + + # Count configuration entries + ((TOTAL_CHECKS++)) + local config_count=$(PGPASSWORD=test_password psql -h localhost -p 5433 -U foxhunt_test -d foxhunt_test -t -c "SELECT COUNT(*) FROM config_settings;" 2>/dev/null | tr -d ' ') + if [ "$config_count" -gt 0 ]; then + log_success "Found $config_count configuration entries" + else + log_warning "config_settings table exists but is empty" + fi + elif PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -c "\d config_settings" > /dev/null 2>&1; then + log_success "config_settings table exists (production database)" + + # Count configuration entries + ((TOTAL_CHECKS++)) + local config_count=$(PGPASSWORD=postgres psql -h localhost -p 5433 -U postgres -d foxhunt -t -c "SELECT COUNT(*) FROM config_settings;" 2>/dev/null | tr -d ' ') + if [ "$config_count" -gt 0 ]; then + log_success "Found $config_count configuration entries" + else + log_warning "config_settings table exists but is empty" + fi + else + log_warning "config_settings table does not exist (hot-reload may not be configured)" + fi +} + +# Check service logs for errors +check_service_logs() { + log_header "Checking Service Logs for Recent Errors" + + local log_files=("api_gateway.log" "trading_service.log" "backtesting_service.log" "ml_training_service.log") + + for log_file in "${log_files[@]}"; do + local log_path="${LOG_DIR}/${log_file}" + if [ -f "$log_path" ]; then + ((TOTAL_CHECKS++)) + local error_count=$(grep -i "error\|panic\|fatal" "$log_path" 2>/dev/null | wc -l) + + if [ "$error_count" -eq 0 ]; then + log_success "$log_file: No errors detected" + elif [ "$error_count" -lt 5 ]; then + log_warning "$log_file: $error_count errors detected (review recommended)" + else + log_error "$log_file: $error_count errors detected (attention required)" + fi + else + log_warning "$log_file: Log file not found" + fi + done +} + +# Generate summary report +generate_summary() { + log_header "Health Check Summary" + + echo "" | tee -a "$HEALTH_LOG" + echo "Total Checks: $TOTAL_CHECKS" | tee -a "$HEALTH_LOG" + echo -e "${GREEN}Passed: $PASSED_CHECKS${NC}" | tee -a "$HEALTH_LOG" + echo -e "${YELLOW}Warnings: $WARNING_CHECKS${NC}" | tee -a "$HEALTH_LOG" + echo -e "${RED}Failed: $FAILED_CHECKS${NC}" | tee -a "$HEALTH_LOG" + echo "" | tee -a "$HEALTH_LOG" + + local success_rate=$(awk "BEGIN {printf \"%.1f\", ($PASSED_CHECKS/$TOTAL_CHECKS)*100}") + echo "Success Rate: ${success_rate}%" | tee -a "$HEALTH_LOG" + + if [ "$FAILED_CHECKS" -eq 0 ]; then + echo -e "${GREEN}Overall Status: HEALTHY${NC}" | tee -a "$HEALTH_LOG" + return 0 + elif [ "$FAILED_CHECKS" -lt 5 ]; then + echo -e "${YELLOW}Overall Status: DEGRADED${NC}" | tee -a "$HEALTH_LOG" + return 1 + else + echo -e "${RED}Overall Status: UNHEALTHY${NC}" | tee -a "$HEALTH_LOG" + return 2 + fi +} + +# Main execution +main() { + log_header "Foxhunt HFT System - Comprehensive Health Check" + log_info "Starting health check at $(date)" + log_info "Log file: $HEALTH_LOG" + echo "" + + # Run all checks + check_prerequisites || exit 1 + check_docker_containers + check_infrastructure_services + check_grpc_services + check_service_processes + check_resource_usage + test_inter_service_communication + test_hot_reload + check_service_logs + + # Generate summary + echo "" + generate_summary + + log_info "Health check completed at $(date)" + log_info "Detailed log saved to: $HEALTH_LOG" +} + +# Run main function +main + +exit $? diff --git a/launch_mamba2_training.sh b/scripts/launch_mamba2_training.sh similarity index 100% rename from launch_mamba2_training.sh rename to scripts/launch_mamba2_training.sh diff --git a/mark_tests_ignored.sh b/scripts/mark_tests_ignored.sh similarity index 100% rename from mark_tests_ignored.sh rename to scripts/mark_tests_ignored.sh diff --git a/measure_vram.sh b/scripts/measure_vram.sh similarity index 100% rename from measure_vram.sh rename to scripts/measure_vram.sh diff --git a/monitor_mamba2_hyperopt.sh b/scripts/monitor_mamba2_hyperopt.sh similarity index 100% rename from monitor_mamba2_hyperopt.sh rename to scripts/monitor_mamba2_hyperopt.sh diff --git a/optimize_batch_sizes.sh b/scripts/optimize_batch_sizes.sh similarity index 100% rename from optimize_batch_sizes.sh rename to scripts/optimize_batch_sizes.sh diff --git a/prefix_unused_vars.sh b/scripts/prefix_unused_vars.sh similarity index 100% rename from prefix_unused_vars.sh rename to scripts/prefix_unused_vars.sh diff --git a/prepare-sqlx-offline.sh b/scripts/prepare-sqlx-offline.sh similarity index 100% rename from prepare-sqlx-offline.sh rename to scripts/prepare-sqlx-offline.sh diff --git a/quick_health_check.sh b/scripts/quick_health_check.sh similarity index 100% rename from quick_health_check.sh rename to scripts/quick_health_check.sh diff --git a/scripts/run_comprehensive_tests.sh b/scripts/run_comprehensive_tests.sh index 519d389d4..dc3e66acf 100755 --- a/scripts/run_comprehensive_tests.sh +++ b/scripts/run_comprehensive_tests.sh @@ -1,174 +1,130 @@ #!/bin/bash -# Comprehensive Test Suite Runner for Foxhunt HFT System -# Implements TDD test pyramid with coverage enforcement - set -e -echo "==================================================" -echo " Foxhunt HFT System - Comprehensive Test Suite" -echo "==================================================" +# Wave 7.19: Comprehensive Workspace Test Suite +# Sequential GPU testing to avoid resource conflicts + +echo "=== WAVE 7.19: COMPREHENSIVE WORKSPACE TEST SUITE ===" +echo "Start Time: $(date)" echo "" -# Color codes for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Test counters +# Initialize counters TOTAL_TESTS=0 PASSED_TESTS=0 FAILED_TESTS=0 -# Function to print colored output -print_status() { - local status=$1 - local message=$2 +# Function to run tests and capture results +run_test_suite() { + local crate=$1 + local threads=$2 + local label=$3 - if [ "$status" = "PASS" ]; then - echo -e "${GREEN}✅ $message${NC}" - elif [ "$status" = "FAIL" ]; then - echo -e "${RED}❌ $message${NC}" - elif [ "$status" = "INFO" ]; then - echo -e "${YELLOW}ℹ️ $message${NC}" + echo "" + echo "========================================" + echo "Testing: $crate ($label)" + echo "========================================" + + if [ "$threads" == "1" ]; then + cargo test -p "$crate" --lib -- --test-threads=1 2>&1 | tee "/tmp/test_${crate}.log" + else + cargo test -p "$crate" --lib 2>&1 | tee "/tmp/test_${crate}.log" + fi + + # Parse test results + if grep -q "test result: ok" "/tmp/test_${crate}.log"; then + local passed=$(grep -oP '\d+(?= passed)' "/tmp/test_${crate}.log" | tail -1) + local failed=$(grep -oP '\d+(?= failed)' "/tmp/test_${crate}.log" | tail -1 || echo "0") + + TOTAL_TESTS=$((TOTAL_TESTS + passed + failed)) + PASSED_TESTS=$((PASSED_TESTS + passed)) + FAILED_TESTS=$((FAILED_TESTS + failed)) + + echo "✅ $crate: $passed passed, $failed failed" + else + echo "⚠️ $crate: Unable to parse results" fi } -# Function to run test category -run_test_category() { - local category=$1 - local command=$2 - - echo "" - echo "────────────────────────────────────────────────" - echo " Running: $category" - echo "────────────────────────────────────────────────" - - if eval "$command"; then - print_status "PASS" "$category completed successfully" - PASSED_TESTS=$((PASSED_TESTS + 1)) - return 0 - else - print_status "FAIL" "$category failed" - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - fi -} - -# 1. Unit Tests (30-40% of pyramid) +# Phase 1: Non-GPU Crates (Parallel Testing) echo "" -echo "📊 LEVEL 1: Unit Tests (Library Code)" -echo "──────────────────────────────────────" - -run_test_category "Unit Tests - ML Package" \ - "cargo test -p ml --lib --no-fail-fast 2>&1 | tail -20" - -run_test_category "Unit Tests - Trading Engine" \ - "cargo test -p trading_engine --lib --no-fail-fast 2>&1 | tail -20" - -run_test_category "Unit Tests - Risk Management" \ - "cargo test -p risk --lib --no-fail-fast 2>&1 | tail -20" - -run_test_category "Unit Tests - Data Providers" \ - "cargo test -p data --lib --no-fail-fast 2>&1 | tail -20" - -run_test_category "Unit Tests - Common" \ - "cargo test -p common --lib --no-fail-fast 2>&1 | tail -20" - -# 2. Component Tests (40-50% of pyramid) +echo "=== PHASE 1: NON-GPU CRATES (PARALLEL) ===" echo "" -echo "📊 LEVEL 2: Component Tests" -echo "────────────────────────────────────" -run_test_category "Streaming Pipeline Tests" \ - "cargo test -p ml --test streaming_pipeline_edge_cases --no-fail-fast 2>&1 | tail -20" || true +run_test_suite "common" "parallel" "Core types and traits" +run_test_suite "storage" "parallel" "S3 integration" +run_test_suite "data" "parallel" "Market data providers" +run_test_suite "config" "parallel" "Configuration management" +run_test_suite "risk" "parallel" "Risk management" -run_test_category "Ensemble Disagreement Tests" \ - "cargo test -p ml --test ensemble_disagreement_tests --no-fail-fast 2>&1 | tail -20" || true - -run_test_category "Training Chaos Tests" \ - "cargo test -p ml --test training_chaos_tests --no-fail-fast 2>&1 | tail -20" || true - -run_test_category "Multi-Day Training Simulation" \ - "cargo test -p ml --test multi_day_training_simulation --no-fail-fast 2>&1 | tail -20" || true - -run_test_category "Adaptive Strategy Tests" \ - "cargo test -p adaptive-strategy --test '*' --no-fail-fast 2>&1 | tail -20" - -# 3. Integration Tests (20-30% of pyramid) +# Phase 2: ML Crate (Sequential GPU Testing) echo "" -echo "📊 LEVEL 3: Integration Tests" -echo "──────────────────────────────────" - -run_test_category "E2E Ensemble Integration" \ - "cargo test -p ml --test e2e_ensemble_integration --no-fail-fast 2>&1 | tail -20" - -run_test_category "Pipeline Integration" \ - "cargo test -p ml --test pipeline_integration_tests --no-fail-fast 2>&1 | tail -20" - -run_test_category "Database Integration" \ - "cargo test -p database --test '*' --no-fail-fast 2>&1 | tail -20" - -# 4. E2E Tests (5-10% of pyramid) +echo "=== PHASE 2: ML CRATE (SEQUENTIAL GPU) ===" echo "" -echo "📊 LEVEL 4: End-to-End Tests" -echo "────────────────────────────────" -run_test_category "Smoke Tests" \ - "cargo test -p foxhunt --test smoke_tests --no-fail-fast 2>&1 | tail -20" +run_test_suite "ml" "1" "Machine learning models (GPU)" -# 5. Coverage Report +# Phase 3: Service Crates (Parallel Testing) +echo "" +echo "=== PHASE 3: SERVICE CRATES (PARALLEL) ===" echo "" -echo "📊 Coverage Analysis" -echo "────────────────────────────" -print_status "INFO" "Generating coverage report..." +run_test_suite "api_gateway" "parallel" "API Gateway service" +run_test_suite "trading_service" "parallel" "Trading service" +run_test_suite "backtesting_service" "parallel" "Backtesting service" +run_test_suite "ml_training_service" "parallel" "ML training service" -if command -v cargo-llvm-cov &> /dev/null; then - cargo llvm-cov --workspace --html --output-dir coverage_report 2>&1 | tail -10 +# Phase 4: Trading Engine (Sequential Testing) +echo "" +echo "=== PHASE 4: TRADING ENGINE (SEQUENTIAL) ===" +echo "" - # Extract coverage percentage - COVERAGE=$(cargo llvm-cov --workspace --summary-only 2>&1 | grep "TOTAL" | awk '{print $NF}' | tr -d '%' || echo "0") +run_test_suite "trading_engine" "1" "Core trading engine (memory safety)" - echo "" - echo "Coverage: $COVERAGE%" - - if (( $(echo "$COVERAGE >= 60" | bc -l) )); then - print_status "PASS" "Coverage $COVERAGE% meets minimum 60%" - else - print_status "FAIL" "Coverage $COVERAGE% below minimum 60%" - FAILED_TESTS=$((FAILED_TESTS + 1)) - fi - - print_status "INFO" "Coverage report: coverage_report/index.html" +# Calculate pass rate +if [ $TOTAL_TESTS -gt 0 ]; then + PASS_RATE=$(echo "scale=2; $PASSED_TESTS * 100 / $TOTAL_TESTS" | bc) else - print_status "INFO" "cargo-llvm-cov not installed, skipping coverage" + PASS_RATE=0 fi -# Final Summary +# Final Report echo "" -echo "==================================================" -echo " Test Suite Summary" -echo "==================================================" +echo "========================================" +echo "FINAL TEST REPORT" +echo "========================================" +echo "Total Tests: $TOTAL_TESTS" +echo "Passed: $PASSED_TESTS" +echo "Failed: $FAILED_TESTS" +echo "Pass Rate: ${PASS_RATE}%" echo "" -echo "Total Test Categories: $((PASSED_TESTS + FAILED_TESTS))" -echo "Passed: $PASSED_TESTS" -echo "Failed: $FAILED_TESTS" +echo "End Time: $(date)" echo "" -if [ $FAILED_TESTS -eq 0 ]; then - print_status "PASS" "ALL TESTS PASSED ✨" - echo "" - echo "📈 Test Pyramid Breakdown:" - echo " Unit Tests (30-40%): ✅" - echo " Component Tests (40-50%): ✅" - echo " Integration Tests (20-30%): ✅" - echo " E2E Tests (5-10%): ✅" - echo "" - exit 0 -else - print_status "FAIL" "$FAILED_TESTS test categories failed" - echo "" - echo "Please review the test output above for details." - exit 1 -fi +# Export results for documentation +cat > /tmp/workspace_test_summary.txt << EOF +Wave 7.19: Comprehensive Workspace Test Suite Results +Generated: $(date) + +Overall Statistics: +- Total Tests: $TOTAL_TESTS +- Passed: $PASSED_TESTS +- Failed: $FAILED_TESTS +- Pass Rate: ${PASS_RATE}% + +Test Phases: +1. Non-GPU Crates (common, storage, data, config, risk) +2. ML Crate (sequential GPU testing) +3. Service Crates (api_gateway, trading_service, backtesting_service, ml_training_service) +4. Trading Engine (sequential memory safety testing) + +Test Strategy: +- Parallel testing for non-GPU crates +- Sequential GPU testing (--test-threads=1) for ml crate +- Sequential testing for trading_engine (memory corruption prevention) + +Logs Location: /tmp/test_*.log +EOF + +cat /tmp/workspace_test_summary.txt + +exit 0 diff --git a/run_cross_validation.sh b/scripts/run_cross_validation.sh similarity index 100% rename from run_cross_validation.sh rename to scripts/run_cross_validation.sh diff --git a/run_final_benchmarks.sh b/scripts/run_final_benchmarks.sh similarity index 100% rename from run_final_benchmarks.sh rename to scripts/run_final_benchmarks.sh diff --git a/run_ghz_load_test.sh b/scripts/run_ghz_load_test.sh similarity index 100% rename from run_ghz_load_test.sh rename to scripts/run_ghz_load_test.sh diff --git a/run_liquid_nn_tuning.sh b/scripts/run_liquid_nn_tuning.sh similarity index 100% rename from run_liquid_nn_tuning.sh rename to scripts/run_liquid_nn_tuning.sh diff --git a/run_load_tests.sh b/scripts/run_load_tests.sh similarity index 100% rename from run_load_tests.sh rename to scripts/run_load_tests.sh diff --git a/run_performance_benchmarks.sh b/scripts/run_performance_benchmarks.sh similarity index 100% rename from run_performance_benchmarks.sh rename to scripts/run_performance_benchmarks.sh diff --git a/run_ppo_comprehensive_tuning.sh b/scripts/run_ppo_comprehensive_tuning.sh similarity index 100% rename from run_ppo_comprehensive_tuning.sh rename to scripts/run_ppo_comprehensive_tuning.sh diff --git a/run_smoke_tests.sh b/scripts/run_smoke_tests.sh similarity index 100% rename from run_smoke_tests.sh rename to scripts/run_smoke_tests.sh diff --git a/run_tests.sh b/scripts/run_tests.sh similarity index 100% rename from run_tests.sh rename to scripts/run_tests.sh diff --git a/run_tft_training.sh b/scripts/run_tft_training.sh similarity index 100% rename from run_tft_training.sh rename to scripts/run_tft_training.sh diff --git a/run_training.sh b/scripts/run_training.sh similarity index 100% rename from run_training.sh rename to scripts/run_training.sh diff --git a/runpod_validation_deploy.sh b/scripts/runpod_validation_deploy.sh similarity index 100% rename from runpod_validation_deploy.sh rename to scripts/runpod_validation_deploy.sh diff --git a/setup-database.sh b/scripts/setup-database.sh similarity index 100% rename from setup-database.sh rename to scripts/setup-database.sh diff --git a/setup.py b/scripts/setup.py similarity index 100% rename from setup.py rename to scripts/setup.py diff --git a/setup_lld.sh b/scripts/setup_lld.sh similarity index 100% rename from setup_lld.sh rename to scripts/setup_lld.sh diff --git a/simple_concurrent_test.sh b/scripts/simple_concurrent_test.sh similarity index 100% rename from simple_concurrent_test.sh rename to scripts/simple_concurrent_test.sh diff --git a/staging_e2e_tests.sh b/scripts/staging_e2e_tests.sh similarity index 100% rename from staging_e2e_tests.sh rename to scripts/staging_e2e_tests.sh diff --git a/start-tli.sh b/scripts/start-tli.sh similarity index 100% rename from start-tli.sh rename to scripts/start-tli.sh diff --git a/start.sh b/scripts/start.sh similarity index 100% rename from start.sh rename to scripts/start.sh diff --git a/start_all_services.sh b/scripts/start_all_services.sh similarity index 100% rename from start_all_services.sh rename to scripts/start_all_services.sh diff --git a/start_backtesting.sh b/scripts/start_backtesting.sh similarity index 100% rename from start_backtesting.sh rename to scripts/start_backtesting.sh diff --git a/start_services.sh b/scripts/start_services.sh similarity index 100% rename from start_services.sh rename to scripts/start_services.sh diff --git a/stop.sh b/scripts/stop.sh similarity index 100% rename from stop.sh rename to scripts/stop.sh diff --git a/stop_services.sh b/scripts/stop_services.sh similarity index 100% rename from stop_services.sh rename to scripts/stop_services.sh diff --git a/stub_ignored_tests.sh b/scripts/stub_ignored_tests.sh similarity index 100% rename from stub_ignored_tests.sh rename to scripts/stub_ignored_tests.sh diff --git a/sustained_load_grpc_test.sh b/scripts/sustained_load_grpc_test.sh similarity index 100% rename from sustained_load_grpc_test.sh rename to scripts/sustained_load_grpc_test.sh diff --git a/test_alerts.sh b/scripts/test_alerts.sh similarity index 100% rename from test_alerts.sh rename to scripts/test_alerts.sh diff --git a/test_circuit_breakers.sh b/scripts/test_circuit_breakers.sh similarity index 100% rename from test_circuit_breakers.sh rename to scripts/test_circuit_breakers.sh diff --git a/test_crash_logging.sh b/scripts/test_crash_logging.sh similarity index 100% rename from test_crash_logging.sh rename to scripts/test_crash_logging.sh diff --git a/test_dbn_loader_fix.sh b/scripts/test_dbn_loader_fix.sh similarity index 100% rename from test_dbn_loader_fix.sh rename to scripts/test_dbn_loader_fix.sh diff --git a/test_dqn_checkpoints_quick.sh b/scripts/test_dqn_checkpoints_quick.sh similarity index 100% rename from test_dqn_checkpoints_quick.sh rename to scripts/test_dqn_checkpoints_quick.sh diff --git a/test_graceful_degradation.sh b/scripts/test_graceful_degradation.sh similarity index 100% rename from test_graceful_degradation.sh rename to scripts/test_graceful_degradation.sh diff --git a/test_grpc_proxies.sh b/scripts/test_grpc_proxies.sh similarity index 100% rename from test_grpc_proxies.sh rename to scripts/test_grpc_proxies.sh diff --git a/test_liquid_nn_readiness.sh b/scripts/test_liquid_nn_readiness.sh similarity index 100% rename from test_liquid_nn_readiness.sh rename to scripts/test_liquid_nn_readiness.sh diff --git a/test_market_data.sh b/scripts/test_market_data.sh similarity index 100% rename from test_market_data.sh rename to scripts/test_market_data.sh diff --git a/test_ppo_checkpoint.sh b/scripts/test_ppo_checkpoint.sh similarity index 100% rename from test_ppo_checkpoint.sh rename to scripts/test_ppo_checkpoint.sh diff --git a/upload_to_runpod_s3.sh b/scripts/upload_to_runpod_s3.sh similarity index 100% rename from upload_to_runpod_s3.sh rename to scripts/upload_to_runpod_s3.sh diff --git a/validate-docker-config.sh b/scripts/validate-docker-config.sh similarity index 100% rename from validate-docker-config.sh rename to scripts/validate-docker-config.sh diff --git a/validate_ab_testing_tdd.sh b/scripts/validate_ab_testing_tdd.sh similarity index 100% rename from validate_ab_testing_tdd.sh rename to scripts/validate_ab_testing_tdd.sh diff --git a/validate_agent_9_13.sh b/scripts/validate_agent_9_13.sh similarity index 100% rename from validate_agent_9_13.sh rename to scripts/validate_agent_9_13.sh diff --git a/verify_dataset_coverage.sh b/scripts/verify_dataset_coverage.sh similarity index 100% rename from verify_dataset_coverage.sh rename to scripts/verify_dataset_coverage.sh diff --git a/verify_db_optimization.sh b/scripts/verify_db_optimization.sh similarity index 100% rename from verify_db_optimization.sh rename to scripts/verify_db_optimization.sh diff --git a/verify_dbn_fix.sh b/scripts/verify_dbn_fix.sh similarity index 100% rename from verify_dbn_fix.sh rename to scripts/verify_dbn_fix.sh diff --git a/verify_dbn_loader_fix.sh b/scripts/verify_dbn_loader_fix.sh similarity index 100% rename from verify_dbn_loader_fix.sh rename to scripts/verify_dbn_loader_fix.sh diff --git a/verify_dockerfile_updates.sh b/scripts/verify_dockerfile_updates.sh similarity index 100% rename from verify_dockerfile_updates.sh rename to scripts/verify_dockerfile_updates.sh diff --git a/verify_documentation_structure.sh b/scripts/verify_documentation_structure.sh similarity index 100% rename from verify_documentation_structure.sh rename to scripts/verify_documentation_structure.sh diff --git a/verify_tft_checkpoint_fix.sh b/scripts/verify_tft_checkpoint_fix.sh similarity index 100% rename from verify_tft_checkpoint_fix.sh rename to scripts/verify_tft_checkpoint_fix.sh diff --git a/verify_tft_cuda_fix.sh b/scripts/verify_tft_cuda_fix.sh similarity index 100% rename from verify_tft_cuda_fix.sh rename to scripts/verify_tft_cuda_fix.sh diff --git a/verify_tft_cuda_setup.sh b/scripts/verify_tft_cuda_setup.sh similarity index 100% rename from verify_tft_cuda_setup.sh rename to scripts/verify_tft_cuda_setup.sh diff --git a/small_parquet_tool/Cargo.toml b/small_parquet_tool/Cargo.toml deleted file mode 100644 index 26ab30e6c..000000000 --- a/small_parquet_tool/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "create_small_parquet" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -repository.workspace = true -homepage.workspace = true -documentation.workspace = true -publish.workspace = true -keywords.workspace = true -categories.workspace = true - -[dependencies] -anyhow = "1.0" -parquet = "56" -arrow = "56" -tokio = { version = "1.42", features = ["full"] } - -[lints] -workspace = true diff --git a/small_parquet_tool/src/main.rs b/small_parquet_tool/src/main.rs deleted file mode 100644 index 8d582c4ff..000000000 --- a/small_parquet_tool/src/main.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Create Small Test Parquet Files (1000 bars each) -//! -//! Agent-2: Extract first 1000 bars from existing Parquet files for fast testing. - -use anyhow::{Context, Result}; -use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; -use parquet::arrow::ArrowWriter; -use parquet::file::properties::WriterProperties; -use std::fs::File; -use std::path::Path; -use std::sync::Arc; - -struct FileMapping { - input: &'static str, - output: &'static str, -} - -const FILE_MAPPINGS: &[FileMapping] = &[ - FileMapping { - input: "test_data/ES_FUT_180d.parquet", - output: "test_data/ES_FUT_small.parquet", - }, - FileMapping { - input: "test_data/NQ_FUT_180d.parquet", - output: "test_data/NQ_FUT_small.parquet", - }, - FileMapping { - input: "test_data/6E_FUT_180d.parquet", - output: "test_data/6E_FUT_small.parquet", - }, - FileMapping { - input: "test_data/ZN_FUT_90d.parquet", - output: "test_data/ZN_FUT_small.parquet", - }, -]; - -fn create_small_parquet(input_path: &str, output_path: &str, num_rows: usize) -> Result<(usize, f64)> { - println!("Processing {}...", input_path); - - if !Path::new(input_path).exists() { - println!(" ⚠️ File not found, skipping..."); - println!(); - return Ok((0, 0.0)); - } - - let input_file = File::open(input_path) - .with_context(|| format!("Failed to open {}", input_path))?; - - let builder = ParquetRecordBatchReaderBuilder::try_new(input_file)?; - let original_rows = builder.metadata().file_metadata().num_rows() as usize; - println!(" Original rows: {}", original_rows); - - let mut reader = builder.build()?; - - let mut total_rows_extracted = 0; - let mut batches_to_write = Vec::new(); - - while let Some(Ok(batch)) = reader.next() { - let rows_needed = num_rows.saturating_sub(total_rows_extracted); - if rows_needed == 0 { - break; - } - - let rows_to_take = batch.num_rows().min(rows_needed); - let sliced_batch = batch.slice(0, rows_to_take); - batches_to_write.push(sliced_batch); - total_rows_extracted += rows_to_take; - - if total_rows_extracted >= num_rows { - break; - } - } - - println!(" Extracted rows: {}", total_rows_extracted); - - if batches_to_write.is_empty() { - println!(" ⚠️ No data to write, skipping..."); - println!(); - return Ok((0, 0.0)); - } - - let schema = batches_to_write[0].schema(); - - let output_file = File::create(output_path) - .with_context(|| format!("Failed to create {}", output_path))?; - - let props = WriterProperties::builder() - .set_compression(parquet::basic::Compression::SNAPPY) - .build(); - - let mut writer = ArrowWriter::try_new(output_file, schema, Some(props))?; - - for batch in &batches_to_write { - writer.write(batch)?; - } - writer.close()?; - - let input_size = std::fs::metadata(input_path)?.len() as f64 / 1024.0; - let output_size = std::fs::metadata(output_path)?.len() as f64 / 1024.0; - - println!(" Original size: {:.2} KB", input_size); - println!(" Small file size: {:.2} KB", output_size); - println!(" Compression ratio: {:.2}x", input_size / output_size); - println!(); - - Ok((total_rows_extracted, output_size)) -} - -#[tokio::main] -async fn main() -> Result<()> { - println!("{}", "=".repeat(70)); - println!("Creating Small Test Parquet Files (1000 bars each)"); - println!("{}", "=".repeat(70)); - println!(); - - let mut results = Vec::new(); - - for mapping in FILE_MAPPINGS { - match create_small_parquet(mapping.input, mapping.output, 1000) { - Ok((rows, size_kb)) => { - if rows > 0 { - let symbol = mapping.output - .replace("test_data/", "") - .replace("_small.parquet", ""); - results.push((symbol, rows, size_kb)); - } - } - Err(e) => { - eprintln!("❌ Error processing {}: {}", mapping.input, e); - } - } - } - - println!("{}", "=".repeat(70)); - println!("Summary"); - println!("{}", "=".repeat(70)); - println!("{:<15} {:<10} {:<15}", "Symbol", "Rows", "Size (KB)"); - println!("{}", "-".repeat(70)); - - for (symbol, rows, size_kb) in &results { - println!("{:<15} {:<10} {:<15.2}", symbol, rows, size_kb); - } - - println!("{}", "=".repeat(70)); - - let total_size: f64 = results.iter().map(|(_, _, size)| size).sum(); - println!("\nTotal size: {:.2} KB ({:.2} MB)", total_size, total_size / 1024.0); - println!("Files created: {}", results.len()); - - println!("\n✅ Small test files created successfully!"); - - Ok(()) -} diff --git a/smoke_test.pid b/smoke_test.pid deleted file mode 100644 index 632a8ed59..000000000 --- a/smoke_test.pid +++ /dev/null @@ -1 +0,0 @@ - echo Smoke test PID: $(cat smoke_test.pid) sleep 25 echo echo 📊 === First 25 seconds === tail -120 smoke_AGENT_217.log diff --git a/PAPER_TRADING_DIAGNOSTIC_QUERIES.sql b/sql/PAPER_TRADING_DIAGNOSTIC_QUERIES.sql similarity index 100% rename from PAPER_TRADING_DIAGNOSTIC_QUERIES.sql rename to sql/PAPER_TRADING_DIAGNOSTIC_QUERIES.sql diff --git a/test_pg_performance.sql b/sql/test_pg_performance.sql similarity index 100% rename from test_pg_performance.sql rename to sql/test_pg_performance.sql diff --git a/test_stop_loss_debug.sql b/sql/test_stop_loss_debug.sql similarity index 100% rename from test_stop_loss_debug.sql rename to sql/test_stop_loss_debug.sql diff --git a/trading_workload.sql b/sql/trading_workload.sql similarity index 100% rename from trading_workload.sql rename to sql/trading_workload.sql diff --git a/test_dbn_debug.5a8spbt8g603bypb5vds2bxfi.rcgu.o b/test_dbn_debug.5a8spbt8g603bypb5vds2bxfi.rcgu.o deleted file mode 100644 index 2de6600f02d6e61e3ba38d6f939b943dfe8c3b0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3112 zcmbtW&1(}u6rVKJR{UyX1)+lRA{ETC`D(WpiTG8e)KV1`gy|;PHo;9c>~2Hai#_P2 zrwSfD_Ml$9cu@rZ5w8Ui1&=~~Z!@nuS!a`$K1k-x@4Yv__ukCx?u*;=cZL!PMv`FH zSvVt_TMF!n0<#s^rodD^6Q*F; zg8yOmdr!tlPqHMbJ!6tl<~=ZYOPe8?lI(;u3)Lh$8BQcE$xchNuKEWGNlE!o2A77Q zr|^pk_BtiyN&cU44;22_0{<|7@_!oR{{;wBQeEW#3vku?lYxI&FZn-;@&5ohrldYV z8i6bTKF-dF8QZ@TWFH0#cKh#MS7aR7RXBEAg`bVV$0a-sItsQ_ZfhYxyO?~6<|G_* zC3z+kT!VUpP)ov(3mu1-5+3dvfSPZ0e5O^LM#E}?;##&DU>aXqXy5c^@+&v058c%V z_viTx=RCO9xoM$s)3%)ow_Mk8dChEAZOdhS0u78?<&M)}TGR0@?e4Os=S+lFW zWj5;-rg^^Wn`Ic+8^MT2wC~zQ%DU-@-ScYo2CsX3tqG^AY+JmY6F1;!!yCA8^8KXM z3~@Ps$d?+7^gJOyoa0Zvn`kTHpF-q2kLI5c+%H~!7N9@AD`nY*U|h2#gP`l8s0#u= z2O~nGVovy04F96=8^r%AhJQ}Mk84Hwuf*^#ACUiY4F9r%AD@#^{!f6x694TVJ$DDy z{}EzU{qHFF@x2t~KOe)t2{_6iemoB<{%r+6zW<{9J8-^L^=~6N2-;*pK3VQTJgzC| z_D!#a7LAY2aZeD__anOs#b+Wo#nZR}cG6{6{60}h)b~9&0oH;tdgQB^Q6I=i{iFE~ lD60Oy6Ahtwynp!JK=E|{-U4xy8`oYGiGCL&4j3Da{|9&7aXtV5 diff --git a/test_results/grpc_load_tests/trading_service_order_status_20251003_173606.txt b/test_results/grpc_load_tests/trading_service_order_status_20251003_173606.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/test_results/grpc_load_tests/trading_service_order_status_20251003_173658.json b/test_results/grpc_load_tests/trading_service_order_status_20251003_173658.json deleted file mode 100644 index a767f2c33..000000000 --- a/test_results/grpc_load_tests/trading_service_order_status_20251003_173658.json +++ /dev/null @@ -1 +0,0 @@ -{"date":"2025-10-03T17:37:08+02:00","endReason":"timeout","options":{"call":"trading.TradingService.GetOrderStatus","host":"localhost:50051","proto":"/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto","import-paths":["/home/jgrusewski/Work/foxhunt/services/trading_service/proto","."],"insecure":true,"rps":100,"load-schedule":"const","load-start":0,"load-end":0,"load-step":0,"load-step-duration":0,"load-max-duration":0,"concurrency":50,"concurrency-schedule":"const","concurrency-start":0,"concurrency-end":0,"concurrency-step":1,"concurrency-step-duration":0,"concurrency-max-duration":0,"total":2147483647,"connections":10,"duration":10000000000,"timeout":20000000000,"dial-timeout":10000000000,"data":{"order_id":"test-order-123"},"binary":false,"CPUs":16},"count":998,"total":10008178751,"average":1113997,"fastest":0,"slowest":0,"rps":99.71844276864874,"errorDistribution":{"rpc error: code = Unauthenticated desc = Valid authentication required":998},"statusCodeDistribution":{"Unauthenticated":998},"latencyDistribution":null,"histogram":null,"details":[{"timestamp":"2025-10-03T17:36:58.514740496+02:00","latency":2728800,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.515182716+02:00","latency":12842228,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.521348969+02:00","latency":680778,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.531537257+02:00","latency":696026,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.541498056+02:00","latency":633351,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.55174248+02:00","latency":806840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.563698392+02:00","latency":2550777,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.571900357+02:00","latency":718747,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.581666544+02:00","latency":1400396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.593009675+02:00","latency":1874192,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.601138189+02:00","latency":543454,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.614626562+02:00","latency":3963745,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.621397267+02:00","latency":628661,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.631425276+02:00","latency":596422,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.641497119+02:00","latency":586616,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.652655663+02:00","latency":1706734,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.661865791+02:00","latency":727585,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.670901942+02:00","latency":606171,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.681430889+02:00","latency":578299,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.691697058+02:00","latency":710601,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.701885072+02:00","latency":648569,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.71305453+02:00","latency":2690623,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.72143172+02:00","latency":657045,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.731520469+02:00","latency":563686,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.741657403+02:00","latency":618510,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.751808126+02:00","latency":647020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.762019909+02:00","latency":631616,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.771262102+02:00","latency":683666,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.781417861+02:00","latency":592902,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.791580499+02:00","latency":643382,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.805022619+02:00","latency":3928577,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.811119076+02:00","latency":557526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.822622865+02:00","latency":693753,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.831758962+02:00","latency":633436,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.842925844+02:00","latency":1613274,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.851888973+02:00","latency":558570,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.861005011+02:00","latency":639392,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.871129399+02:00","latency":548900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.881143235+02:00","latency":526983,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.893995205+02:00","latency":3371203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.901192046+02:00","latency":586274,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.914658823+02:00","latency":3891115,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.921579078+02:00","latency":651017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.931668959+02:00","latency":583736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.941931474+02:00","latency":702774,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.950988152+02:00","latency":533891,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.964014384+02:00","latency":3483646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.970932645+02:00","latency":560772,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.980967914+02:00","latency":612352,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:58.990957662+02:00","latency":572326,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.003987723+02:00","latency":3636047,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.010907541+02:00","latency":478809,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.024484197+02:00","latency":651703,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.031638475+02:00","latency":586566,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.041817437+02:00","latency":683750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.051808201+02:00","latency":586555,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.06463285+02:00","latency":3253810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.07120874+02:00","latency":610928,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.083695603+02:00","latency":2932449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.092982405+02:00","latency":1679412,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.100905108+02:00","latency":521332,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.110971824+02:00","latency":656518,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.12112891+02:00","latency":597592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.131252728+02:00","latency":569592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.142962081+02:00","latency":2005700,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.188103887+02:00","latency":953032,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.188348152+02:00","latency":1793562,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.188665003+02:00","latency":2565632,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.188848962+02:00","latency":1968125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.191129325+02:00","latency":767120,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.201211765+02:00","latency":564032,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.211308918+02:00","latency":552204,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.221624448+02:00","latency":696693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.231707275+02:00","latency":554986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.244689604+02:00","latency":3434778,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.251918081+02:00","latency":781657,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.261004522+02:00","latency":587647,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.270902025+02:00","latency":479464,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.280951796+02:00","latency":626941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.290919896+02:00","latency":489697,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.301016015+02:00","latency":626321,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.311343897+02:00","latency":534463,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.321592954+02:00","latency":633987,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.331763113+02:00","latency":745846,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.341979357+02:00","latency":731022,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.352070668+02:00","latency":1110189,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.361247132+02:00","latency":598109,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.374424776+02:00","latency":532517,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.381521058+02:00","latency":556617,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.391522643+02:00","latency":547296,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.401527573+02:00","latency":518542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.411528631+02:00","latency":474220,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.421681875+02:00","latency":690827,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.431670315+02:00","latency":613936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.441708977+02:00","latency":532829,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.454999151+02:00","latency":3805685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.463395309+02:00","latency":515726,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.471335682+02:00","latency":473303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.481407036+02:00","latency":589254,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.491455745+02:00","latency":596321,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.501442624+02:00","latency":481945,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.511563876+02:00","latency":650089,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.529240373+02:00","latency":5039549,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.533078101+02:00","latency":2467958,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.54105134+02:00","latency":525173,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.551820091+02:00","latency":1058247,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.561845588+02:00","latency":600715,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.57076829+02:00","latency":538311,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.583826685+02:00","latency":2611900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.590778529+02:00","latency":524914,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.601982373+02:00","latency":545831,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.611182844+02:00","latency":724402,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.621221463+02:00","latency":562181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.631985508+02:00","latency":1217979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.641156033+02:00","latency":658221,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.651295683+02:00","latency":618511,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.661423119+02:00","latency":554614,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.672921928+02:00","latency":2026987,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.680993875+02:00","latency":632325,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.690982408+02:00","latency":590931,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.700981022+02:00","latency":483636,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.711013808+02:00","latency":617491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.721042149+02:00","latency":610095,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.731528268+02:00","latency":616675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.74186977+02:00","latency":820005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.752525483+02:00","latency":965382,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.76164567+02:00","latency":625526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.77167451+02:00","latency":563911,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.783149003+02:00","latency":2596508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.791188545+02:00","latency":584914,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.801261267+02:00","latency":583924,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.811443178+02:00","latency":663414,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.823799956+02:00","latency":2798719,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.831818396+02:00","latency":582138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.841778083+02:00","latency":575726,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.854059351+02:00","latency":2799930,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.864626883+02:00","latency":4058936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.871752084+02:00","latency":1063968,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.881757888+02:00","latency":536406,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.891861428+02:00","latency":645453,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.900837326+02:00","latency":581253,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.911953543+02:00","latency":634754,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.9250732+02:00","latency":4109109,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.931228245+02:00","latency":634111,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.941322071+02:00","latency":637878,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.951404399+02:00","latency":607941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.961552192+02:00","latency":672829,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.972077126+02:00","latency":1042344,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.981087072+02:00","latency":527726,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:36:59.992998464+02:00","latency":2413065,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.002046086+02:00","latency":1518021,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.011130547+02:00","latency":548155,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.021132243+02:00","latency":536988,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.031437644+02:00","latency":710147,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.041714155+02:00","latency":832547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.051005748+02:00","latency":594095,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.061223951+02:00","latency":763788,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.07123942+02:00","latency":703962,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.081518874+02:00","latency":652906,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.091646232+02:00","latency":626492,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.101290003+02:00","latency":901691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.111283855+02:00","latency":579984,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.121496267+02:00","latency":711995,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.131057339+02:00","latency":558521,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.141283021+02:00","latency":643169,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.153009661+02:00","latency":2182323,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.161111776+02:00","latency":542479,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.171156706+02:00","latency":572550,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.183821269+02:00","latency":3064656,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.19197642+02:00","latency":645279,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.201533697+02:00","latency":549083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.211581356+02:00","latency":557255,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.221878388+02:00","latency":529301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.231098476+02:00","latency":732310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.240967909+02:00","latency":519196,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.253978209+02:00","latency":3562079,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.261070427+02:00","latency":581040,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.271279228+02:00","latency":734950,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.281521818+02:00","latency":661851,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.291553862+02:00","latency":561546,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.302274842+02:00","latency":546040,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.311361798+02:00","latency":591139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.322365552+02:00","latency":566117,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.331466315+02:00","latency":590528,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.34164081+02:00","latency":695301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.351712235+02:00","latency":576969,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.361781225+02:00","latency":577241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.372659714+02:00","latency":1415054,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.381869022+02:00","latency":698496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.390977576+02:00","latency":562517,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.402630663+02:00","latency":2125056,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.411252791+02:00","latency":656282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.421340135+02:00","latency":655588,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.431397102+02:00","latency":545963,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.442188968+02:00","latency":507159,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.451731674+02:00","latency":1029413,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.462031973+02:00","latency":706314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.473823029+02:00","latency":3265911,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.483727837+02:00","latency":2480221,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.494731227+02:00","latency":3449700,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.501835322+02:00","latency":615359,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.514834175+02:00","latency":3608373,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.520829806+02:00","latency":544745,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.531848264+02:00","latency":611640,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.542748852+02:00","latency":1511764,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.551890391+02:00","latency":527788,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.560819294+02:00","latency":539352,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.5719172+02:00","latency":576286,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.581910975+02:00","latency":1522498,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.59117518+02:00","latency":672462,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.602014142+02:00","latency":825148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.611023543+02:00","latency":549095,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.621154557+02:00","latency":634816,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.631340895+02:00","latency":664645,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.641391224+02:00","latency":597297,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.652748206+02:00","latency":1485347,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.661976334+02:00","latency":655568,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.673784644+02:00","latency":3264803,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.684740107+02:00","latency":3444134,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.691924161+02:00","latency":693701,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.700870626+02:00","latency":552207,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.710843799+02:00","latency":521127,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.720970203+02:00","latency":612857,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.731202568+02:00","latency":687711,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.741325367+02:00","latency":532376,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.751423566+02:00","latency":612687,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.761486423+02:00","latency":578942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.773692093+02:00","latency":2709899,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.781734337+02:00","latency":586441,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.79182789+02:00","latency":609073,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.802014039+02:00","latency":649039,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.811241153+02:00","latency":698555,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.82205564+02:00","latency":756449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.832088397+02:00","latency":1450486,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.842714187+02:00","latency":2138208,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.853698461+02:00","latency":2459721,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.861804577+02:00","latency":624044,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.874717164+02:00","latency":3408639,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.8818558+02:00","latency":622965,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.893742684+02:00","latency":2367663,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.90195383+02:00","latency":616075,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.913399125+02:00","latency":2976048,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.92157887+02:00","latency":607036,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.931248443+02:00","latency":616432,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.941524689+02:00","latency":779491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.951711128+02:00","latency":680139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.962225355+02:00","latency":855422,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.971300056+02:00","latency":556395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.981802374+02:00","latency":726774,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:00.992732139+02:00","latency":2471365,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.001924327+02:00","latency":658898,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.011079681+02:00","latency":607786,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.021226015+02:00","latency":622493,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.031335336+02:00","latency":604950,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.0415695+02:00","latency":719944,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.051842139+02:00","latency":717649,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.060849773+02:00","latency":583276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.071422378+02:00","latency":575750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.084036721+02:00","latency":3171377,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.09131089+02:00","latency":773861,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.101605705+02:00","latency":646177,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.111801359+02:00","latency":626603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.121895931+02:00","latency":622112,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.130978348+02:00","latency":653948,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.141074728+02:00","latency":587405,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.151260409+02:00","latency":703675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.161283868+02:00","latency":543069,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.171291665+02:00","latency":523230,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.181386279+02:00","latency":612236,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.191476521+02:00","latency":623239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.201474582+02:00","latency":551555,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.214636469+02:00","latency":3619377,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.221834204+02:00","latency":631666,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.230991019+02:00","latency":642304,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.247202524+02:00","latency":919627,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.251451976+02:00","latency":593903,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.261622165+02:00","latency":619721,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.272371674+02:00","latency":592083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.281590635+02:00","latency":736875,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.294632586+02:00","latency":3432631,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.301826674+02:00","latency":540051,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.311959162+02:00","latency":629977,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.321065707+02:00","latency":636591,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.333076942+02:00","latency":2096290,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.341249084+02:00","latency":600442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.353115402+02:00","latency":2378241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.364613147+02:00","latency":3963058,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.370829639+02:00","latency":533348,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.380985075+02:00","latency":637957,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.392126263+02:00","latency":1638736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.401000752+02:00","latency":509022,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.412683644+02:00","latency":2192848,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.42368931+02:00","latency":2517670,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.431771266+02:00","latency":609738,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.441884854+02:00","latency":572362,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.454827889+02:00","latency":3509182,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.461884615+02:00","latency":557708,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.471046511+02:00","latency":756695,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.481113588+02:00","latency":607009,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.49304258+02:00","latency":2474565,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.503706801+02:00","latency":3200437,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.514728007+02:00","latency":3524383,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.522699776+02:00","latency":1447434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.531904205+02:00","latency":734802,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.544720985+02:00","latency":3414234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.551974128+02:00","latency":690052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.562728202+02:00","latency":2304614,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.571842173+02:00","latency":632515,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.582272411+02:00","latency":878800,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.591394434+02:00","latency":581142,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.601335148+02:00","latency":597351,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.612708059+02:00","latency":1907427,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.621799918+02:00","latency":622553,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.634876504+02:00","latency":3556786,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.641856299+02:00","latency":567693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.652044392+02:00","latency":693793,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.661136616+02:00","latency":798828,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.671595052+02:00","latency":643301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.681678431+02:00","latency":630373,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.692558692+02:00","latency":1432603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.701748883+02:00","latency":687012,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.713272745+02:00","latency":2120353,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.722482994+02:00","latency":1955880,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.731576695+02:00","latency":632483,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.741669816+02:00","latency":575013,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.753029485+02:00","latency":1853655,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.761181681+02:00","latency":650389,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.771386317+02:00","latency":719827,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.783750312+02:00","latency":2830180,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.794595877+02:00","latency":3479505,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.801829648+02:00","latency":590292,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.814074603+02:00","latency":2765124,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.821796257+02:00","latency":1209752,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.832015001+02:00","latency":668585,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.841095405+02:00","latency":647251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.853690987+02:00","latency":2247740,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.860993441+02:00","latency":609612,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.873746371+02:00","latency":3271211,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.884995752+02:00","latency":3789577,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.891016348+02:00","latency":499312,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.901007653+02:00","latency":641351,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.911203499+02:00","latency":676169,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.92126402+02:00","latency":584884,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.931298447+02:00","latency":553819,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.9413999+02:00","latency":620442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.951428765+02:00","latency":588253,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.961467166+02:00","latency":591555,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.971414805+02:00","latency":584805,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.981523198+02:00","latency":642145,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:01.991577036+02:00","latency":589897,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.001611241+02:00","latency":581638,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.011579819+02:00","latency":600259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.021571356+02:00","latency":570758,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.03168971+02:00","latency":652703,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.041765828+02:00","latency":611671,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.0536249+02:00","latency":2367592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.060960665+02:00","latency":654051,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.071897272+02:00","latency":650098,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.080898111+02:00","latency":590036,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.093731765+02:00","latency":3417257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.101948333+02:00","latency":686427,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.111092968+02:00","latency":683531,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.121255695+02:00","latency":739743,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.131302713+02:00","latency":554921,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.14146111+02:00","latency":713257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.158571453+02:00","latency":698523,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.16151895+02:00","latency":526181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.171776393+02:00","latency":1420002,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.181846168+02:00","latency":593740,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.192413627+02:00","latency":578557,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.203049233+02:00","latency":2207008,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.211134469+02:00","latency":534313,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.221141824+02:00","latency":549193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.233808+02:00","latency":3176849,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.24183946+02:00","latency":555313,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.250852974+02:00","latency":622690,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.260806666+02:00","latency":538118,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.271887581+02:00","latency":593970,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.284740635+02:00","latency":4420222,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.2918279+02:00","latency":581359,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.301905574+02:00","latency":571105,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.310896086+02:00","latency":539470,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.320876947+02:00","latency":603932,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.330880035+02:00","latency":533113,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.340841138+02:00","latency":568289,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.350870122+02:00","latency":555951,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.361118314+02:00","latency":756042,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.371051557+02:00","latency":651659,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.381361207+02:00","latency":819305,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.391190747+02:00","latency":612785,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.401696196+02:00","latency":1115301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.41482557+02:00","latency":3676962,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.420989962+02:00","latency":703678,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.432868662+02:00","latency":2468713,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.441057468+02:00","latency":667084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.451191123+02:00","latency":627918,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.461352149+02:00","latency":646469,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.471409486+02:00","latency":619184,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.484383095+02:00","latency":568109,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.491396692+02:00","latency":579391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.504067827+02:00","latency":3261773,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.511105823+02:00","latency":561957,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.521091525+02:00","latency":528589,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.531204976+02:00","latency":614293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.541195443+02:00","latency":546399,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.551211179+02:00","latency":537520,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.563688402+02:00","latency":3020109,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.571740524+02:00","latency":544840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.581764651+02:00","latency":567069,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.591924237+02:00","latency":630638,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.603728268+02:00","latency":3350709,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.611804077+02:00","latency":543367,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.62201598+02:00","latency":656618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.631391369+02:00","latency":840437,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.641455681+02:00","latency":621826,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.652680912+02:00","latency":1759704,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.661640759+02:00","latency":550830,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.671676535+02:00","latency":570434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.681853743+02:00","latency":694323,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.691711332+02:00","latency":549681,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.701871139+02:00","latency":675292,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.712990755+02:00","latency":1669628,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.720942899+02:00","latency":543716,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.731600165+02:00","latency":655099,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.741573162+02:00","latency":551803,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.751741225+02:00","latency":663911,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.761943723+02:00","latency":631648,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.771180922+02:00","latency":745700,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.781196244+02:00","latency":595148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.791211371+02:00","latency":563863,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.801855326+02:00","latency":811459,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.814735283+02:00","latency":3639815,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.821930819+02:00","latency":645584,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.831028148+02:00","latency":582666,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.84109962+02:00","latency":572279,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.851145874+02:00","latency":534997,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.864068123+02:00","latency":3428391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.871162858+02:00","latency":547707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.881157819+02:00","latency":512293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.891225905+02:00","latency":598999,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.9013858+02:00","latency":624526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.911455232+02:00","latency":593945,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.921480514+02:00","latency":555920,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.931535265+02:00","latency":605988,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.941683644+02:00","latency":676262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.951832207+02:00","latency":731992,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.960882919+02:00","latency":576272,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.970900362+02:00","latency":551840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.9809801+02:00","latency":640067,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:02.991118268+02:00","latency":712129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.001229917+02:00","latency":647606,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.011544164+02:00","latency":607108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.021643573+02:00","latency":622600,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.031514807+02:00","latency":524811,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.041512647+02:00","latency":571987,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.05164239+02:00","latency":595006,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.061838277+02:00","latency":670865,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.071940479+02:00","latency":1667449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.080929061+02:00","latency":511857,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.093089238+02:00","latency":2612043,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.101187178+02:00","latency":542204,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.111243609+02:00","latency":571525,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.121391082+02:00","latency":673039,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.131468852+02:00","latency":568174,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.141555984+02:00","latency":650661,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.154912143+02:00","latency":641918,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.161335685+02:00","latency":773486,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.171247687+02:00","latency":534419,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.18279368+02:00","latency":1803382,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.194626225+02:00","latency":3510663,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.201901649+02:00","latency":608792,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.210911313+02:00","latency":584289,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.220947673+02:00","latency":598070,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.231056824+02:00","latency":640314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.241139442+02:00","latency":519490,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.251264038+02:00","latency":654107,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.261257145+02:00","latency":541303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.271257902+02:00","latency":515146,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.283568209+02:00","latency":1031692,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.291638284+02:00","latency":550751,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.30193688+02:00","latency":793339,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.314709134+02:00","latency":3748221,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.321865308+02:00","latency":611282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.331069759+02:00","latency":772699,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.341156448+02:00","latency":572465,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.351197916+02:00","latency":634348,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.361246507+02:00","latency":585522,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.37140369+02:00","latency":670661,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.381425944+02:00","latency":572223,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.392661494+02:00","latency":1727783,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.40181569+02:00","latency":662028,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.412171631+02:00","latency":675017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.421429765+02:00","latency":714315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.431733465+02:00","latency":748854,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.441845426+02:00","latency":680749,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.454899563+02:00","latency":3737728,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.460920537+02:00","latency":508084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.470912147+02:00","latency":552955,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.481232698+02:00","latency":907307,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.491215105+02:00","latency":557707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.501277036+02:00","latency":593160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.511392418+02:00","latency":657970,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.523786884+02:00","latency":2950257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.531908432+02:00","latency":571206,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.54103317+02:00","latency":669982,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.551185507+02:00","latency":703784,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.561178165+02:00","latency":511952,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.571180047+02:00","latency":548495,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.581370772+02:00","latency":660280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.591709171+02:00","latency":999353,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.601675436+02:00","latency":531922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.612683021+02:00","latency":1633606,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.621789015+02:00","latency":653083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.631866586+02:00","latency":705138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.641804592+02:00","latency":539694,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.651884355+02:00","latency":663112,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.661524232+02:00","latency":839456,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.673744096+02:00","latency":3344625,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.68190585+02:00","latency":682289,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.690919477+02:00","latency":596015,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.701050322+02:00","latency":648637,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.71104515+02:00","latency":629801,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.7218885+02:00","latency":611742,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.731790865+02:00","latency":596394,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.740811717+02:00","latency":556930,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.751993723+02:00","latency":706942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.761145874+02:00","latency":638740,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.771375571+02:00","latency":684458,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.781618744+02:00","latency":815538,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.794838876+02:00","latency":3884590,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.800917598+02:00","latency":627280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.811043999+02:00","latency":693039,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.821322766+02:00","latency":606582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.831343353+02:00","latency":537234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.841404695+02:00","latency":607757,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.852101697+02:00","latency":784128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.861109197+02:00","latency":544355,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.871972421+02:00","latency":1026811,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.881954238+02:00","latency":1657416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.891176639+02:00","latency":747651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.901135106+02:00","latency":536684,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.911215529+02:00","latency":632742,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.921301249+02:00","latency":654704,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.934046607+02:00","latency":3317567,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.941155398+02:00","latency":581466,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.951187804+02:00","latency":572196,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.971997625+02:00","latency":3215165,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.972099934+02:00","latency":632354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.984043063+02:00","latency":627833,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:03.990920406+02:00","latency":603761,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.00103214+02:00","latency":700908,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.011113937+02:00","latency":727332,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.021238623+02:00","latency":661771,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.031045209+02:00","latency":661079,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.041348489+02:00","latency":728797,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.053225939+02:00","latency":2313148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.061269134+02:00","latency":572969,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.071299994+02:00","latency":666591,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.082981439+02:00","latency":2240739,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.093777014+02:00","latency":3355879,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.101753584+02:00","latency":568296,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.112704437+02:00","latency":1480150,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.122930682+02:00","latency":1839280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.133710092+02:00","latency":3362030,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.144714784+02:00","latency":3549834,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.151808147+02:00","latency":635703,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.161893732+02:00","latency":662213,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.171088936+02:00","latency":697133,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.181186155+02:00","latency":620345,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.192824573+02:00","latency":1863920,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.201951771+02:00","latency":620919,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.211386798+02:00","latency":909396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.221363968+02:00","latency":599459,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.231407873+02:00","latency":623724,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.241428684+02:00","latency":586301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.253453351+02:00","latency":594345,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.261766645+02:00","latency":774576,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.271847726+02:00","latency":649203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.281807668+02:00","latency":499515,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.291735715+02:00","latency":523292,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.302609372+02:00","latency":2273797,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.31175845+02:00","latency":865632,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.32512843+02:00","latency":2599285,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.331183537+02:00","latency":585210,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.341564035+02:00","latency":626402,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.351759557+02:00","latency":690406,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.361809591+02:00","latency":598945,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.371896376+02:00","latency":644960,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.380848343+02:00","latency":574276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.391874663+02:00","latency":599345,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.400951776+02:00","latency":690150,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.411019952+02:00","latency":624037,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.423388416+02:00","latency":600946,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.431397272+02:00","latency":610464,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.441166679+02:00","latency":692332,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.451519104+02:00","latency":538321,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.461576987+02:00","latency":572656,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.471720973+02:00","latency":652299,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.481723022+02:00","latency":546882,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.491650864+02:00","latency":493262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.501740731+02:00","latency":656447,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.511786883+02:00","latency":616377,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.521763222+02:00","latency":517434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.531840789+02:00","latency":592095,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.541820855+02:00","latency":571632,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.551845198+02:00","latency":592459,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.561893031+02:00","latency":553034,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.571755762+02:00","latency":1544022,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.582618183+02:00","latency":1364722,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.59200058+02:00","latency":716969,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.601000203+02:00","latency":609435,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.61118859+02:00","latency":722567,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.621330842+02:00","latency":667666,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.632471195+02:00","latency":650208,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.641491465+02:00","latency":595698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.651601411+02:00","latency":696881,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.662624013+02:00","latency":1580054,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.672672929+02:00","latency":1582569,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.681788953+02:00","latency":597819,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.692784082+02:00","latency":1589726,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.701827383+02:00","latency":592403,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.713690562+02:00","latency":2371069,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.721076376+02:00","latency":674961,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.732509007+02:00","latency":1991154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.741221033+02:00","latency":538784,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.751370561+02:00","latency":660287,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.762428896+02:00","latency":704052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.775278543+02:00","latency":4645141,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.786163827+02:00","latency":5446693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.792694512+02:00","latency":566975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.80174714+02:00","latency":646009,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.813383026+02:00","latency":560808,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.823753849+02:00","latency":2948098,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.831555961+02:00","latency":621398,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.843866841+02:00","latency":613684,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.853707027+02:00","latency":3493396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.860987627+02:00","latency":660784,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.871020264+02:00","latency":600222,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.880982949+02:00","latency":553194,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.892409604+02:00","latency":605783,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.901521857+02:00","latency":658603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.911537571+02:00","latency":598773,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.921468351+02:00","latency":532201,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.931390541+02:00","latency":535587,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.941244552+02:00","latency":593954,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.952940852+02:00","latency":613398,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.960927213+02:00","latency":600170,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.970934792+02:00","latency":594771,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.980973147+02:00","latency":643314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:04.991091854+02:00","latency":654132,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.001133424+02:00","latency":631996,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.011165125+02:00","latency":554255,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.021173434+02:00","latency":619225,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.031296727+02:00","latency":633281,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.041238743+02:00","latency":518116,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.051190612+02:00","latency":545568,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.061165047+02:00","latency":590300,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.07114693+02:00","latency":590017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.081246032+02:00","latency":650048,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.091305826+02:00","latency":652047,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.101426515+02:00","latency":602796,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.114717687+02:00","latency":3866138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.121804382+02:00","latency":641384,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.131665967+02:00","latency":576176,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.141656583+02:00","latency":577536,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.152672471+02:00","latency":1651505,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.162749043+02:00","latency":1634611,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.171731827+02:00","latency":620128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.181696433+02:00","latency":557016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.194720855+02:00","latency":3616274,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.202686988+02:00","latency":1541148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.211714877+02:00","latency":615823,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.221773231+02:00","latency":612872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.232702854+02:00","latency":1543126,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.241499935+02:00","latency":554732,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.253875795+02:00","latency":1071482,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.261715761+02:00","latency":1507566,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.271837608+02:00","latency":653460,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.281767982+02:00","latency":609588,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.291753693+02:00","latency":605139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.307702212+02:00","latency":6547892,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.31238516+02:00","latency":1030273,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.328040219+02:00","latency":3238209,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.334697164+02:00","latency":4260606,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.344995144+02:00","latency":4469247,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.351046881+02:00","latency":602315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.360968402+02:00","latency":544813,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.370953506+02:00","latency":599073,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.380855234+02:00","latency":533582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.390910387+02:00","latency":698826,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.400870565+02:00","latency":544894,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.410777038+02:00","latency":561516,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.421690263+02:00","latency":707606,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.433387938+02:00","latency":555888,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.441334934+02:00","latency":518805,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.451365393+02:00","latency":622437,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.463017301+02:00","latency":2241022,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.471008161+02:00","latency":568577,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.481008056+02:00","latency":594584,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.492410107+02:00","latency":600646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.501393832+02:00","latency":552129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.513004773+02:00","latency":2253364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.521044197+02:00","latency":565239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.534697431+02:00","latency":4283663,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.542665529+02:00","latency":2236753,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.551691955+02:00","latency":594936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.562686314+02:00","latency":1941864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.571649481+02:00","latency":559259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.581690634+02:00","latency":622085,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.593994165+02:00","latency":3492331,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.603360378+02:00","latency":550156,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.611283869+02:00","latency":502231,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.625024227+02:00","latency":4318329,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.630818634+02:00","latency":551628,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.644624244+02:00","latency":565819,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.651586347+02:00","latency":570023,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.669736258+02:00","latency":7900417,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.671594657+02:00","latency":533891,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.681593714+02:00","latency":579636,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.691582795+02:00","latency":543820,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.701536496+02:00","latency":519940,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.711403036+02:00","latency":541120,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.721405394+02:00","latency":632399,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.734320587+02:00","latency":2416355,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.746665622+02:00","latency":498728,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.752397551+02:00","latency":1347670,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.763343375+02:00","latency":528146,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.77599157+02:00","latency":2178391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.781072248+02:00","latency":655770,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.791152991+02:00","latency":637860,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.801196904+02:00","latency":598193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.811297118+02:00","latency":657868,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.821359456+02:00","latency":547250,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.831256906+02:00","latency":530491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.845515018+02:00","latency":4822993,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.85157956+02:00","latency":652406,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.861572974+02:00","latency":637404,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.873411384+02:00","latency":602679,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.884398885+02:00","latency":596607,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.892963399+02:00","latency":2703409,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.900902114+02:00","latency":543939,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.910860345+02:00","latency":591616,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.921765353+02:00","latency":523262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.931804164+02:00","latency":631564,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.941939063+02:00","latency":647211,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.950915157+02:00","latency":617069,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.96085368+02:00","latency":527962,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.972279994+02:00","latency":686779,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.981343682+02:00","latency":636790,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:05.991316781+02:00","latency":591359,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.001364725+02:00","latency":632920,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.011453671+02:00","latency":670422,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.021405397+02:00","latency":559923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.031402257+02:00","latency":600985,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.04146922+02:00","latency":676635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.051432999+02:00","latency":572197,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.061386294+02:00","latency":574521,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.071189355+02:00","latency":604781,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.082494701+02:00","latency":693855,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.091557813+02:00","latency":642545,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.101577892+02:00","latency":636060,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.111686712+02:00","latency":633397,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.121614455+02:00","latency":510904,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.131613479+02:00","latency":546320,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.141812879+02:00","latency":769651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.151687294+02:00","latency":599322,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.16110577+02:00","latency":705125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.171066925+02:00","latency":565112,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.181094654+02:00","latency":591372,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.19114891+02:00","latency":630584,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.201164936+02:00","latency":549882,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.211091206+02:00","latency":648500,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.221129409+02:00","latency":650442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.231000663+02:00","latency":563301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.24107259+02:00","latency":655723,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.250959868+02:00","latency":482840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.260851253+02:00","latency":503439,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.271849461+02:00","latency":603748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.281816389+02:00","latency":568195,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.291928901+02:00","latency":696598,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.302253673+02:00","latency":500934,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.311236314+02:00","latency":576915,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.321157237+02:00","latency":519583,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.331143117+02:00","latency":545804,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.34606226+02:00","latency":4238314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.351097307+02:00","latency":567574,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.361150184+02:00","latency":596020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.373830491+02:00","latency":583711,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.382386453+02:00","latency":549149,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.391281957+02:00","latency":512340,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.401342613+02:00","latency":642224,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.417678929+02:00","latency":3884020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.421656853+02:00","latency":560714,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.431660178+02:00","latency":530738,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.44172762+02:00","latency":631134,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.451704134+02:00","latency":562923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.461765566+02:00","latency":632508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.473940608+02:00","latency":2626633,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.481394886+02:00","latency":583350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.492221268+02:00","latency":718412,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.506405596+02:00","latency":2605952,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.511484866+02:00","latency":593897,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.521456653+02:00","latency":581382,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.53233702+02:00","latency":548601,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.541377357+02:00","latency":550710,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.551249275+02:00","latency":647932,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.574605692+02:00","latency":10178053,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.578057201+02:00","latency":2163756,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.584346699+02:00","latency":547068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.595031957+02:00","latency":4237279,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.601206293+02:00","latency":696537,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.61339501+02:00","latency":567756,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.622676408+02:00","latency":721605,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.634543146+02:00","latency":723299,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.641433488+02:00","latency":491566,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.657034584+02:00","latency":5250307,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.661016578+02:00","latency":545021,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.673698096+02:00","latency":3266619,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.687422549+02:00","latency":4588001,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.691401652+02:00","latency":534278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.701342691+02:00","latency":546486,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.711282072+02:00","latency":502154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.725983328+02:00","latency":3488219,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.736714138+02:00","latency":6299172,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.74553978+02:00","latency":647068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.760755361+02:00","latency":9166456,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.761462403+02:00","latency":464367,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.774833231+02:00","latency":1022736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.783927475+02:00","latency":2708265,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.792318531+02:00","latency":500113,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.802255013+02:00","latency":1458183,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.811216144+02:00","latency":523715,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.822384998+02:00","latency":580517,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.835434094+02:00","latency":594181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.842672373+02:00","latency":2079131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.851492257+02:00","latency":511644,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.861508751+02:00","latency":601763,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.871568601+02:00","latency":591811,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.881660783+02:00","latency":627586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.893047492+02:00","latency":2749385,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.901203297+02:00","latency":671921,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.911149833+02:00","latency":464151,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.921098412+02:00","latency":535399,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.931105866+02:00","latency":579686,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.940777194+02:00","latency":493214,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.951681426+02:00","latency":510185,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.970157629+02:00","latency":5346089,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.974052233+02:00","latency":3647742,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:06.981033448+02:00","latency":552185,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.003079551+02:00","latency":2668837,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.005617899+02:00","latency":9808330,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.01164742+02:00","latency":596462,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.021732149+02:00","latency":582201,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.031722549+02:00","latency":568020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.049044023+02:00","latency":5219878,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.056682973+02:00","latency":5943375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.063744391+02:00","latency":2635984,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.071694527+02:00","latency":538504,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.08164143+02:00","latency":539084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.091590786+02:00","latency":541799,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.102338233+02:00","latency":538473,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.111425295+02:00","latency":577791,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.121297531+02:00","latency":493452,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.133308795+02:00","latency":515089,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.141272576+02:00","latency":481443,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.151151438+02:00","latency":475314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.161097628+02:00","latency":543755,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.171346634+02:00","latency":547262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.181415982+02:00","latency":583284,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.191468842+02:00","latency":573870,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.208084365+02:00","latency":4287082,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.211079038+02:00","latency":555732,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.226347932+02:00","latency":555702,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.2312515+02:00","latency":477016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.244989211+02:00","latency":4322967,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.250773306+02:00","latency":544188,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.263957409+02:00","latency":524673,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.279765224+02:00","latency":579932,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.283992904+02:00","latency":2912346,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.291138443+02:00","latency":523680,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.301123332+02:00","latency":569090,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.313341819+02:00","latency":525438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.324990177+02:00","latency":3192005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.330961896+02:00","latency":540456,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.342677332+02:00","latency":1666097,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.351705322+02:00","latency":565398,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.360940146+02:00","latency":622860,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.370935442+02:00","latency":581039,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.380962134+02:00","latency":620029,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.392342465+02:00","latency":548771,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.405019026+02:00","latency":4225313,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.414373242+02:00","latency":563859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.42140327+02:00","latency":615780,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.431505068+02:00","latency":632541,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.44164347+02:00","latency":687213,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.451800341+02:00","latency":685195,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.46386822+02:00","latency":593122,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.470858014+02:00","latency":597671,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.485329484+02:00","latency":518122,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.491409263+02:00","latency":629781,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.50152608+02:00","latency":633736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.511615977+02:00","latency":695782,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.521635356+02:00","latency":606731,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.531505045+02:00","latency":499496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.541469026+02:00","latency":536196,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.551371257+02:00","latency":517322,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.563366876+02:00","latency":562067,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.574631052+02:00","latency":3806214,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.581299393+02:00","latency":539647,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.591339014+02:00","latency":550251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.603307914+02:00","latency":516557,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.612393445+02:00","latency":491856,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.623364256+02:00","latency":521145,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.633487279+02:00","latency":633298,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.64339191+02:00","latency":582023,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.652083735+02:00","latency":1238502,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.662881492+02:00","latency":988872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.67231003+02:00","latency":583319,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.681425794+02:00","latency":600142,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.691352496+02:00","latency":553408,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.704391733+02:00","latency":556839,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.714300636+02:00","latency":508534,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.72082518+02:00","latency":534602,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.730902859+02:00","latency":560038,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.740839134+02:00","latency":573942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.751821607+02:00","latency":630616,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.76184758+02:00","latency":584922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.771873854+02:00","latency":605438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.780833188+02:00","latency":552314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.790892992+02:00","latency":636748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.800919533+02:00","latency":584231,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.811048944+02:00","latency":668643,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.82110934+02:00","latency":596167,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.831164539+02:00","latency":709244,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.841153656+02:00","latency":580466,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.851311576+02:00","latency":700707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.861498378+02:00","latency":648168,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.871005617+02:00","latency":645124,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.881016621+02:00","latency":577910,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.891034261+02:00","latency":587462,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.900969462+02:00","latency":562532,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.910915074+02:00","latency":572456,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.920952763+02:00","latency":673780,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.93102508+02:00","latency":639440,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.941058852+02:00","latency":617873,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.951223148+02:00","latency":706747,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.961269476+02:00","latency":601281,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.971339259+02:00","latency":611515,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.98125849+02:00","latency":534888,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:07.991220676+02:00","latency":564268,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.001273669+02:00","latency":607310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.011469915+02:00","latency":671393,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.020950286+02:00","latency":647349,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.031775262+02:00","latency":933791,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.041840174+02:00","latency":644703,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.050931721+02:00","latency":703272,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.061025413+02:00","latency":729339,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.07119905+02:00","latency":590922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.088084769+02:00","latency":5250395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.098389947+02:00","latency":6533036,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.101758664+02:00","latency":587254,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.113231003+02:00","latency":615167,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.121345585+02:00","latency":644194,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.131386085+02:00","latency":568117,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.141418642+02:00","latency":620554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.150890307+02:00","latency":585130,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.164749056+02:00","latency":4444211,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.17104559+02:00","latency":832315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.181188851+02:00","latency":710829,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.198707771+02:00","latency":7587916,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.201792659+02:00","latency":625234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.21350093+02:00","latency":643689,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.22153036+02:00","latency":551147,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.231521422+02:00","latency":589991,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.241652482+02:00","latency":686523,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.251735677+02:00","latency":647513,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.261735281+02:00","latency":581624,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.271684705+02:00","latency":539904,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.281655101+02:00","latency":566523,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.29403246+02:00","latency":2958325,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.301031767+02:00","latency":551147,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.311015576+02:00","latency":593747,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.321301083+02:00","latency":841953,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.333724536+02:00","latency":644165,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.341620498+02:00","latency":521986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.353364211+02:00","latency":548809,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.363313002+02:00","latency":478439,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.375805561+02:00","latency":5075357,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.381885871+02:00","latency":637001,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.39188676+02:00","latency":618842,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.400953372+02:00","latency":661593,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.411762043+02:00","latency":1409173,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.421644238+02:00","latency":542244,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.431537723+02:00","latency":539533,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.441470101+02:00","latency":544753,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.451677241+02:00","latency":1267625,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.461579837+02:00","latency":496746,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:08.471630919+02:00","latency":577422,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"}]} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_order_status_20251003_173658.txt b/test_results/grpc_load_tests/trading_service_order_status_20251003_173658.txt deleted file mode 100644 index 22d69d222..000000000 --- a/test_results/grpc_load_tests/trading_service_order_status_20251003_173658.txt +++ /dev/null @@ -1,6048 +0,0 @@ -{ - "date": "2025-10-03T17:37:18+02:00", - "endReason": "timeout", - "options": { - "call": "trading.TradingService.GetOrderStatus", - "host": "localhost:50051", - "proto": "/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto", - "import-paths": [ - "/home/jgrusewski/Work/foxhunt/services/trading_service/proto", - "." - ], - "insecure": true, - "rps": 100, - "load-schedule": "const", - "load-start": 0, - "load-end": 0, - "load-step": 0, - "load-step-duration": 0, - "load-max-duration": 0, - "concurrency": 50, - "concurrency-schedule": "const", - "concurrency-start": 0, - "concurrency-end": 0, - "concurrency-step": 1, - "concurrency-step-duration": 0, - "concurrency-max-duration": 0, - "total": 2147483647, - "connections": 10, - "duration": 10000000000, - "timeout": 20000000000, - "dial-timeout": 10000000000, - "data": { - "order_id": "test-order-123" - }, - "binary": false, - "CPUs": 16 - }, - "count": 999, - "total": 10002014924, - "average": 950366, - "fastest": 0, - "slowest": 0, - "rps": 99.87987496428175, - "errorDistribution": { - "rpc error: code = Unauthenticated desc = Valid authentication required": 999 - }, - "statusCodeDistribution": { - "Unauthenticated": 999 - }, - "latencyDistribution": null, - "histogram": null, - "details": [ - { - "timestamp": "2025-10-03T17:37:08.577808233+02:00", - "latency": 2454375, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.585531868+02:00", - "latency": 639247, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.595475363+02:00", - "latency": 622901, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.608407783+02:00", - "latency": 566838, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.615454543+02:00", - "latency": 621479, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.625479088+02:00", - "latency": 699247, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.635583795+02:00", - "latency": 648008, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.64584065+02:00", - "latency": 733525, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.656027494+02:00", - "latency": 728354, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.66607057+02:00", - "latency": 594525, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.676367661+02:00", - "latency": 535802, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.685363555+02:00", - "latency": 576740, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.695462583+02:00", - "latency": 663942, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.705596556+02:00", - "latency": 617933, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.715790281+02:00", - "latency": 642161, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.725754623+02:00", - "latency": 529968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.735877982+02:00", - "latency": 617720, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.745929728+02:00", - "latency": 598293, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.755984216+02:00", - "latency": 614163, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.766146942+02:00", - "latency": 693671, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.778249822+02:00", - "latency": 1301082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.786272689+02:00", - "latency": 617638, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.797811105+02:00", - "latency": 1892436, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.808512715+02:00", - "latency": 619748, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.815645293+02:00", - "latency": 600848, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.829149943+02:00", - "latency": 625324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.836282797+02:00", - "latency": 579758, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.846390362+02:00", - "latency": 616111, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.856408822+02:00", - "latency": 599509, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.865631472+02:00", - "latency": 663623, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.875777107+02:00", - "latency": 623867, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.885872751+02:00", - "latency": 610814, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.895997366+02:00", - "latency": 627817, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.906126741+02:00", - "latency": 636375, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.916268418+02:00", - "latency": 599464, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.926390408+02:00", - "latency": 597864, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.936289126+02:00", - "latency": 651812, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.946733178+02:00", - "latency": 1008184, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.965668507+02:00", - "latency": 9884458, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.96653485+02:00", - "latency": 533854, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.979662815+02:00", - "latency": 4169031, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.985879346+02:00", - "latency": 576891, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:08.9956655+02:00", - "latency": 613512, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.005812797+02:00", - "latency": 641646, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.015938881+02:00", - "latency": 617352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.031724295+02:00", - "latency": 6241578, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.0359352+02:00", - "latency": 682738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.04959617+02:00", - "latency": 646468, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.055807814+02:00", - "latency": 693258, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.066004398+02:00", - "latency": 631416, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.076107344+02:00", - "latency": 597412, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.086268339+02:00", - "latency": 599118, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.096358792+02:00", - "latency": 595866, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.105365704+02:00", - "latency": 592622, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.115356967+02:00", - "latency": 570649, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.125351903+02:00", - "latency": 573614, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.138020772+02:00", - "latency": 3257771, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.146040425+02:00", - "latency": 500911, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.157461164+02:00", - "latency": 1973963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.167504528+02:00", - "latency": 629628, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.175568454+02:00", - "latency": 541603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.185945089+02:00", - "latency": 793186, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.197456472+02:00", - "latency": 558399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.206731578+02:00", - "latency": 522093, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.215466955+02:00", - "latency": 506557, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.226689809+02:00", - "latency": 1742337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.238993319+02:00", - "latency": 3831083, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.245976782+02:00", - "latency": 484658, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.256049528+02:00", - "latency": 552102, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.266323877+02:00", - "latency": 727091, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.275557231+02:00", - "latency": 686590, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.285686502+02:00", - "latency": 579113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.299471975+02:00", - "latency": 590560, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.308928967+02:00", - "latency": 3955567, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.317438141+02:00", - "latency": 521718, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.330394419+02:00", - "latency": 518442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.335519161+02:00", - "latency": 510693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.346718791+02:00", - "latency": 607045, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.355754104+02:00", - "latency": 569689, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.365845199+02:00", - "latency": 584894, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.375974876+02:00", - "latency": 620617, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.386087868+02:00", - "latency": 623876, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.457747622+02:00", - "latency": 1973869, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.458794362+02:00", - "latency": 1853689, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.458834733+02:00", - "latency": 2365903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.458837197+02:00", - "latency": 2173363, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.458865385+02:00", - "latency": 2594878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.458873676+02:00", - "latency": 2826493, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.458896786+02:00", - "latency": 1745023, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.469031941+02:00", - "latency": 3575712, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.4761154+02:00", - "latency": 572833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.486219481+02:00", - "latency": 513009, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.496234096+02:00", - "latency": 529144, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.509691591+02:00", - "latency": 3966153, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.517703321+02:00", - "latency": 2865222, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.525837375+02:00", - "latency": 576650, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.53597753+02:00", - "latency": 640031, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.546122806+02:00", - "latency": 656612, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.556339938+02:00", - "latency": 686677, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.56892559+02:00", - "latency": 4096463, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.576075924+02:00", - "latency": 658861, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.586240535+02:00", - "latency": 670493, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.59629877+02:00", - "latency": 595627, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.606268446+02:00", - "latency": 520434, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.616243941+02:00", - "latency": 492976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.626226117+02:00", - "latency": 539284, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.636442546+02:00", - "latency": 605497, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.645597501+02:00", - "latency": 577506, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.655590528+02:00", - "latency": 581401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.665607313+02:00", - "latency": 581614, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.675644981+02:00", - "latency": 561124, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.686569073+02:00", - "latency": 594761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.698449347+02:00", - "latency": 543796, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.708431292+02:00", - "latency": 547868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.717971057+02:00", - "latency": 2803723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.726117148+02:00", - "latency": 644736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.736221972+02:00", - "latency": 572589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.746342341+02:00", - "latency": 632741, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.755363336+02:00", - "latency": 580777, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.765272058+02:00", - "latency": 495598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.776384169+02:00", - "latency": 616627, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.785419296+02:00", - "latency": 574504, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.795501801+02:00", - "latency": 549412, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.805519301+02:00", - "latency": 562080, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.815582293+02:00", - "latency": 574337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.82561715+02:00", - "latency": 555510, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.835641619+02:00", - "latency": 560079, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.845750578+02:00", - "latency": 614319, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.857264348+02:00", - "latency": 606418, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.866308429+02:00", - "latency": 569209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.8903596+02:00", - "latency": 614377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.89102141+02:00", - "latency": 298878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.89593931+02:00", - "latency": 504808, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.905923703+02:00", - "latency": 551349, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.915969051+02:00", - "latency": 605509, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.925967133+02:00", - "latency": 528685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.935951105+02:00", - "latency": 578882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.946009543+02:00", - "latency": 563334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.956161179+02:00", - "latency": 620536, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.966246868+02:00", - "latency": 605292, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.976280107+02:00", - "latency": 572394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.986369519+02:00", - "latency": 593856, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:09.995758603+02:00", - "latency": 982830, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.005729931+02:00", - "latency": 541007, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.015842063+02:00", - "latency": 622471, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.025968697+02:00", - "latency": 646649, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.036204093+02:00", - "latency": 696927, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.046270384+02:00", - "latency": 579249, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.056302333+02:00", - "latency": 496589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.066418236+02:00", - "latency": 598128, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.075486494+02:00", - "latency": 647180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.08556515+02:00", - "latency": 609102, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.095673858+02:00", - "latency": 604823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.10646532+02:00", - "latency": 1262226, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.115456281+02:00", - "latency": 527931, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.125578378+02:00", - "latency": 624230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.135620354+02:00", - "latency": 542794, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.145661213+02:00", - "latency": 563923, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.155402869+02:00", - "latency": 537140, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.165383377+02:00", - "latency": 535599, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.175359801+02:00", - "latency": 544732, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.186003429+02:00", - "latency": 627408, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.19615611+02:00", - "latency": 635951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.206865976+02:00", - "latency": 1273284, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.215773229+02:00", - "latency": 469210, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.225754175+02:00", - "latency": 545681, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.235793362+02:00", - "latency": 564027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.245829951+02:00", - "latency": 543840, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.255951322+02:00", - "latency": 600956, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.265542369+02:00", - "latency": 622023, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.275691608+02:00", - "latency": 642245, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.285593778+02:00", - "latency": 660697, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.297062809+02:00", - "latency": 1931508, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.306116825+02:00", - "latency": 595799, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.316141164+02:00", - "latency": 535286, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.326098752+02:00", - "latency": 499669, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.337949762+02:00", - "latency": 2358444, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.346056907+02:00", - "latency": 562799, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.356163053+02:00", - "latency": 551218, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.366240486+02:00", - "latency": 598725, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.376229723+02:00", - "latency": 520893, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.386281321+02:00", - "latency": 517563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.39646323+02:00", - "latency": 606681, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.406751199+02:00", - "latency": 1803224, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.416017934+02:00", - "latency": 717780, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.42560794+02:00", - "latency": 683701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.435774827+02:00", - "latency": 631769, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.445783537+02:00", - "latency": 559771, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.455858062+02:00", - "latency": 558040, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.465861942+02:00", - "latency": 536730, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.475882001+02:00", - "latency": 582162, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.486185341+02:00", - "latency": 771759, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.496260306+02:00", - "latency": 544570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.50637353+02:00", - "latency": 631160, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.515872664+02:00", - "latency": 554395, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.525935691+02:00", - "latency": 589036, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.543080683+02:00", - "latency": 4152396, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.546252134+02:00", - "latency": 599806, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.55751398+02:00", - "latency": 596102, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.567498194+02:00", - "latency": 595852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.575699243+02:00", - "latency": 643330, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.585788678+02:00", - "latency": 603364, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.595799586+02:00", - "latency": 553950, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.605654061+02:00", - "latency": 618981, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.615776077+02:00", - "latency": 625089, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.625835866+02:00", - "latency": 574956, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.635462861+02:00", - "latency": 547642, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.646044641+02:00", - "latency": 529423, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.655993859+02:00", - "latency": 470615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.665988673+02:00", - "latency": 511708, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.675940348+02:00", - "latency": 540082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.686241943+02:00", - "latency": 810110, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.69616012+02:00", - "latency": 499583, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.705736833+02:00", - "latency": 887210, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.715757058+02:00", - "latency": 587333, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.725736563+02:00", - "latency": 517327, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.73580352+02:00", - "latency": 581152, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.745795934+02:00", - "latency": 544396, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.755906197+02:00", - "latency": 644269, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.766011652+02:00", - "latency": 614361, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.776093153+02:00", - "latency": 623596, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.789281055+02:00", - "latency": 546138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.796288586+02:00", - "latency": 547684, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.805637869+02:00", - "latency": 546987, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.815735134+02:00", - "latency": 564510, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.825316166+02:00", - "latency": 508295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.836330965+02:00", - "latency": 559091, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.846429197+02:00", - "latency": 625778, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.858486296+02:00", - "latency": 596628, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.865540681+02:00", - "latency": 535582, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.877016207+02:00", - "latency": 2141595, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.88604951+02:00", - "latency": 602502, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.896163095+02:00", - "latency": 687181, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.906249556+02:00", - "latency": 564666, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.918185616+02:00", - "latency": 993707, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.92632954+02:00", - "latency": 584349, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.936442973+02:00", - "latency": 638991, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.948775398+02:00", - "latency": 2965256, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.955760749+02:00", - "latency": 519929, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.965768743+02:00", - "latency": 511105, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.975785758+02:00", - "latency": 544283, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.985812937+02:00", - "latency": 540750, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:10.997698449+02:00", - "latency": 2430104, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.006096252+02:00", - "latency": 607534, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.016194155+02:00", - "latency": 571181, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.026244528+02:00", - "latency": 559503, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.036289912+02:00", - "latency": 558013, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.046384487+02:00", - "latency": 604115, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.057053484+02:00", - "latency": 2215216, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.067733408+02:00", - "latency": 2207627, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.075831085+02:00", - "latency": 571264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.085947412+02:00", - "latency": 605257, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.096433291+02:00", - "latency": 935142, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.105478848+02:00", - "latency": 568687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.115443056+02:00", - "latency": 473991, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.125526598+02:00", - "latency": 603882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.135701743+02:00", - "latency": 649348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.145636775+02:00", - "latency": 553561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.155701548+02:00", - "latency": 700604, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.165643281+02:00", - "latency": 570479, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.179024512+02:00", - "latency": 3975992, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.186971755+02:00", - "latency": 579971, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.196158731+02:00", - "latency": 508430, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.206146814+02:00", - "latency": 497066, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.216240461+02:00", - "latency": 562144, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.226315162+02:00", - "latency": 538035, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.235444473+02:00", - "latency": 654493, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.245698868+02:00", - "latency": 592868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.255808779+02:00", - "latency": 606480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.265960422+02:00", - "latency": 563655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.276144696+02:00", - "latency": 668119, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.289937454+02:00", - "latency": 2732833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.302718445+02:00", - "latency": 5443392, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.305816961+02:00", - "latency": 562521, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.31573062+02:00", - "latency": 517035, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.325745638+02:00", - "latency": 536670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.335912823+02:00", - "latency": 666022, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.346166333+02:00", - "latency": 748807, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.356100753+02:00", - "latency": 561849, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.366711086+02:00", - "latency": 1085245, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.375761903+02:00", - "latency": 582310, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.385948831+02:00", - "latency": 639433, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.395982804+02:00", - "latency": 488656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.407689922+02:00", - "latency": 2217888, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.415952926+02:00", - "latency": 796691, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.42600262+02:00", - "latency": 540795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.438724626+02:00", - "latency": 3285707, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.446713786+02:00", - "latency": 1478506, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.455809173+02:00", - "latency": 627910, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.465837274+02:00", - "latency": 503502, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.475596885+02:00", - "latency": 658201, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.48562569+02:00", - "latency": 585716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.495879881+02:00", - "latency": 720050, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.506045787+02:00", - "latency": 676190, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.516103368+02:00", - "latency": 591153, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.528708208+02:00", - "latency": 3180743, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.535975722+02:00", - "latency": 580271, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.546197516+02:00", - "latency": 772270, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.556317194+02:00", - "latency": 582642, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.565505888+02:00", - "latency": 684786, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.575604703+02:00", - "latency": 610549, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.585629315+02:00", - "latency": 554117, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.595744474+02:00", - "latency": 594167, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.606028995+02:00", - "latency": 659692, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.616228753+02:00", - "latency": 729958, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.626242248+02:00", - "latency": 535662, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.636309274+02:00", - "latency": 529457, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.649370353+02:00", - "latency": 658189, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.657452865+02:00", - "latency": 571480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.666029286+02:00", - "latency": 1190290, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.675970421+02:00", - "latency": 572919, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.687923547+02:00", - "latency": 2487078, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.69692488+02:00", - "latency": 660230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.705757297+02:00", - "latency": 507744, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.716523173+02:00", - "latency": 501227, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.727443313+02:00", - "latency": 545133, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.73567103+02:00", - "latency": 740772, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.745592752+02:00", - "latency": 592791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.755745458+02:00", - "latency": 638788, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.765823724+02:00", - "latency": 596197, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.778665491+02:00", - "latency": 3384749, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.78569275+02:00", - "latency": 564067, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.798755414+02:00", - "latency": 3617363, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.805767643+02:00", - "latency": 536221, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.815896814+02:00", - "latency": 650837, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.82595377+02:00", - "latency": 552821, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.836156379+02:00", - "latency": 661399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.846201115+02:00", - "latency": 586138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.856403388+02:00", - "latency": 633400, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.865305614+02:00", - "latency": 505665, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.876408755+02:00", - "latency": 611160, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.888750996+02:00", - "latency": 768083, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.895804442+02:00", - "latency": 583887, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.905813529+02:00", - "latency": 504336, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.915770851+02:00", - "latency": 595188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.925875494+02:00", - "latency": 600038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.936315508+02:00", - "latency": 603289, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.946421434+02:00", - "latency": 632201, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.95545282+02:00", - "latency": 609619, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.965777851+02:00", - "latency": 791448, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.975597795+02:00", - "latency": 525237, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.985324594+02:00", - "latency": 524635, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:11.995850769+02:00", - "latency": 584792, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.005968084+02:00", - "latency": 652563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.016082345+02:00", - "latency": 666976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.026126616+02:00", - "latency": 560394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.037248803+02:00", - "latency": 1554404, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.046575252+02:00", - "latency": 770757, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.055630436+02:00", - "latency": 579771, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.065765331+02:00", - "latency": 644596, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.075804351+02:00", - "latency": 534900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.096570575+02:00", - "latency": 6642371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.098307058+02:00", - "latency": 1828337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.106487674+02:00", - "latency": 606511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.115481201+02:00", - "latency": 544387, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.125591009+02:00", - "latency": 620510, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.136169306+02:00", - "latency": 588413, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.146419907+02:00", - "latency": 759564, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.155405554+02:00", - "latency": 510635, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.171464433+02:00", - "latency": 563455, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.175438312+02:00", - "latency": 518560, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.18645758+02:00", - "latency": 1528385, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.195541321+02:00", - "latency": 613453, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.205696964+02:00", - "latency": 672400, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.220700852+02:00", - "latency": 2798943, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.225329144+02:00", - "latency": 509242, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.241011182+02:00", - "latency": 4151533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.24632977+02:00", - "latency": 493364, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.261441136+02:00", - "latency": 562545, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.265400457+02:00", - "latency": 524554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.275495434+02:00", - "latency": 619107, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.290702198+02:00", - "latency": 739518, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.295701265+02:00", - "latency": 541318, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.305787461+02:00", - "latency": 642206, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.31607146+02:00", - "latency": 734312, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.325977196+02:00", - "latency": 568224, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.33604442+02:00", - "latency": 573605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.346339895+02:00", - "latency": 882124, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.35534175+02:00", - "latency": 577598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.365451722+02:00", - "latency": 693440, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.375341374+02:00", - "latency": 535532, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.386341508+02:00", - "latency": 559686, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.395340965+02:00", - "latency": 545301, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.406345559+02:00", - "latency": 522496, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.415465897+02:00", - "latency": 566283, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.425771906+02:00", - "latency": 571965, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.43533554+02:00", - "latency": 517533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.446381981+02:00", - "latency": 593443, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.455741198+02:00", - "latency": 923886, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.465609631+02:00", - "latency": 538420, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.475874401+02:00", - "latency": 728146, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.48733025+02:00", - "latency": 576706, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.496457703+02:00", - "latency": 584206, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.505513123+02:00", - "latency": 548490, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.515647927+02:00", - "latency": 667221, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.525792474+02:00", - "latency": 559567, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.535847475+02:00", - "latency": 569438, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.54592367+02:00", - "latency": 627165, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.555975028+02:00", - "latency": 558469, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.565407594+02:00", - "latency": 534489, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.57628009+02:00", - "latency": 511224, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.58632437+02:00", - "latency": 579109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.596316361+02:00", - "latency": 492810, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.606306685+02:00", - "latency": 500239, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.616091237+02:00", - "latency": 1010726, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.625600798+02:00", - "latency": 524728, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.635820598+02:00", - "latency": 726818, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.646674823+02:00", - "latency": 1383092, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.655712107+02:00", - "latency": 526784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.66575047+02:00", - "latency": 541962, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.67573045+02:00", - "latency": 533771, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.685776149+02:00", - "latency": 554326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.69589349+02:00", - "latency": 562593, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.705944052+02:00", - "latency": 581875, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.71618627+02:00", - "latency": 721016, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.726213265+02:00", - "latency": 617086, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.736207222+02:00", - "latency": 526844, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.746120834+02:00", - "latency": 517080, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.755345395+02:00", - "latency": 519722, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.766574456+02:00", - "latency": 584652, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.79312564+02:00", - "latency": 7005066, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.796950579+02:00", - "latency": 619204, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.797202895+02:00", - "latency": 11325371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.810698999+02:00", - "latency": 2277371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.824689921+02:00", - "latency": 3080702, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.83554401+02:00", - "latency": 663127, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.844198181+02:00", - "latency": 19174274, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.846154017+02:00", - "latency": 562262, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.858186947+02:00", - "latency": 1176134, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.868578277+02:00", - "latency": 657787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.888965314+02:00", - "latency": 10753188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.889861671+02:00", - "latency": 567971, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.895898861+02:00", - "latency": 590853, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.90612539+02:00", - "latency": 728456, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.91825053+02:00", - "latency": 635919, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.93363125+02:00", - "latency": 576283, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.94097401+02:00", - "latency": 2903425, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.947770422+02:00", - "latency": 534970, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.958605976+02:00", - "latency": 3033135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.965687556+02:00", - "latency": 594434, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.986778729+02:00", - "latency": 10902734, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:12.987688093+02:00", - "latency": 545663, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.001675006+02:00", - "latency": 4802865, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.007977648+02:00", - "latency": 610877, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.015631889+02:00", - "latency": 611426, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.026721518+02:00", - "latency": 1847823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.040538331+02:00", - "latency": 655224, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.046117401+02:00", - "latency": 635372, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.056194778+02:00", - "latency": 590070, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.066298906+02:00", - "latency": 496920, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.076255468+02:00", - "latency": 513947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.086345009+02:00", - "latency": 610250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.095388047+02:00", - "latency": 588450, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.105400669+02:00", - "latency": 509840, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.115394994+02:00", - "latency": 535819, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.129261512+02:00", - "latency": 3848660, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.139880226+02:00", - "latency": 4689723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.145856849+02:00", - "latency": 537793, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.155666001+02:00", - "latency": 593653, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.165771292+02:00", - "latency": 684431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.175962453+02:00", - "latency": 629897, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.189778834+02:00", - "latency": 4313736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.196270996+02:00", - "latency": 668581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.206367738+02:00", - "latency": 632173, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.21533135+02:00", - "latency": 583658, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.227844088+02:00", - "latency": 768561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.235749699+02:00", - "latency": 619139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.245853702+02:00", - "latency": 700740, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.255907344+02:00", - "latency": 656289, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.268856563+02:00", - "latency": 3282768, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.276063915+02:00", - "latency": 700338, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.286102667+02:00", - "latency": 634494, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.296087771+02:00", - "latency": 565730, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.306077986+02:00", - "latency": 635775, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.316102262+02:00", - "latency": 537359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.32770668+02:00", - "latency": 2182957, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.337164538+02:00", - "latency": 2036128, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.346058928+02:00", - "latency": 627697, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.356141059+02:00", - "latency": 646061, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.366118079+02:00", - "latency": 541668, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.376064418+02:00", - "latency": 571058, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.386092322+02:00", - "latency": 627546, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.396140423+02:00", - "latency": 638109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.40624013+02:00", - "latency": 617806, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.41804628+02:00", - "latency": 2374624, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.426202752+02:00", - "latency": 595009, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.436208917+02:00", - "latency": 580796, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.446297429+02:00", - "latency": 667165, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.458622141+02:00", - "latency": 792050, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.465593969+02:00", - "latency": 566955, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.475632108+02:00", - "latency": 610415, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.485706458+02:00", - "latency": 627922, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.495738047+02:00", - "latency": 638366, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.505800742+02:00", - "latency": 662833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.515804261+02:00", - "latency": 540934, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.526049039+02:00", - "latency": 788046, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.536102992+02:00", - "latency": 619963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.546196904+02:00", - "latency": 644794, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.55869882+02:00", - "latency": 3063823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.565782091+02:00", - "latency": 609144, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.57584489+02:00", - "latency": 628148, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.585846537+02:00", - "latency": 585793, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.595858046+02:00", - "latency": 531834, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.606323727+02:00", - "latency": 606242, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.615517441+02:00", - "latency": 571399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.625470433+02:00", - "latency": 536011, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.635429215+02:00", - "latency": 553258, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.645372725+02:00", - "latency": 526856, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.655395854+02:00", - "latency": 584038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.667426809+02:00", - "latency": 612894, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.675407043+02:00", - "latency": 577452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.686399282+02:00", - "latency": 1632019, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.695381186+02:00", - "latency": 636092, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.707964756+02:00", - "latency": 563746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.716015587+02:00", - "latency": 629005, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.730355018+02:00", - "latency": 542554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.735192519+02:00", - "latency": 468792, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.746138456+02:00", - "latency": 516180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.756297253+02:00", - "latency": 759726, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.766309824+02:00", - "latency": 593724, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.776229852+02:00", - "latency": 497404, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.789434452+02:00", - "latency": 621416, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.795468187+02:00", - "latency": 621661, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.805468461+02:00", - "latency": 583685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.815468934+02:00", - "latency": 585710, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.825449873+02:00", - "latency": 585630, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.836120413+02:00", - "latency": 561342, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.847131183+02:00", - "latency": 597803, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.858321289+02:00", - "latency": 526655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.86637263+02:00", - "latency": 655702, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.875537658+02:00", - "latency": 777636, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.885495177+02:00", - "latency": 678589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.895737881+02:00", - "latency": 840229, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.905673629+02:00", - "latency": 571876, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.915641763+02:00", - "latency": 547673, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.925609546+02:00", - "latency": 588361, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.935925869+02:00", - "latency": 549398, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.945927079+02:00", - "latency": 598706, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.956001511+02:00", - "latency": 599697, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.966000846+02:00", - "latency": 590759, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.975990099+02:00", - "latency": 517970, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.986539474+02:00", - "latency": 683877, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:13.995577836+02:00", - "latency": 624472, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.005365566+02:00", - "latency": 580147, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.015374432+02:00", - "latency": 637267, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.026040184+02:00", - "latency": 851139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.036237851+02:00", - "latency": 741554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.048008524+02:00", - "latency": 2354554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.055906166+02:00", - "latency": 599646, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.065944513+02:00", - "latency": 640377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.075871866+02:00", - "latency": 546058, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.085884529+02:00", - "latency": 627249, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.095974456+02:00", - "latency": 636303, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.106085523+02:00", - "latency": 664028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.116051863+02:00", - "latency": 540550, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.125521896+02:00", - "latency": 685620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.135461018+02:00", - "latency": 515763, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.146044427+02:00", - "latency": 727550, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.155870292+02:00", - "latency": 586647, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.165862001+02:00", - "latency": 522359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.175839028+02:00", - "latency": 542400, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.185422517+02:00", - "latency": 572729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.197394003+02:00", - "latency": 519268, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.205362857+02:00", - "latency": 571947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.215496892+02:00", - "latency": 722922, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.225339395+02:00", - "latency": 592193, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.235412187+02:00", - "latency": 640693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.245442988+02:00", - "latency": 592950, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.255494554+02:00", - "latency": 575629, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.266430147+02:00", - "latency": 542800, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.275398472+02:00", - "latency": 566295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.285415908+02:00", - "latency": 611586, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.295490104+02:00", - "latency": 593485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.307300458+02:00", - "latency": 503802, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.316305935+02:00", - "latency": 499024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.328380975+02:00", - "latency": 533993, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.336662845+02:00", - "latency": 1893572, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.345677815+02:00", - "latency": 536479, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.355793714+02:00", - "latency": 661685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.365862101+02:00", - "latency": 600395, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.380064667+02:00", - "latency": 4778135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.385438212+02:00", - "latency": 538589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.395383703+02:00", - "latency": 564326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.405299645+02:00", - "latency": 548451, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.416302609+02:00", - "latency": 567867, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.428470236+02:00", - "latency": 606603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.435456913+02:00", - "latency": 575633, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.445443852+02:00", - "latency": 556016, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.455444264+02:00", - "latency": 579574, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.467981413+02:00", - "latency": 3105317, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.475838831+02:00", - "latency": 467821, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.487853287+02:00", - "latency": 2600784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.495827478+02:00", - "latency": 551347, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.505795853+02:00", - "latency": 564752, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.517287741+02:00", - "latency": 480146, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.52616297+02:00", - "latency": 483822, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.538637003+02:00", - "latency": 837332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.545564016+02:00", - "latency": 508988, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.555448997+02:00", - "latency": 492821, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.565366027+02:00", - "latency": 505776, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.575371602+02:00", - "latency": 599041, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.585408696+02:00", - "latency": 566417, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.59543114+02:00", - "latency": 565252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.605355352+02:00", - "latency": 490094, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.615267173+02:00", - "latency": 503025, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.627986167+02:00", - "latency": 495122, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.63607513+02:00", - "latency": 601762, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.645998247+02:00", - "latency": 521901, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.655938355+02:00", - "latency": 533694, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.665352633+02:00", - "latency": 560255, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.678488848+02:00", - "latency": 594661, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.685680576+02:00", - "latency": 651299, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.695974095+02:00", - "latency": 690879, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.706436426+02:00", - "latency": 667796, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.717446332+02:00", - "latency": 622333, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.725410809+02:00", - "latency": 516976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.73530863+02:00", - "latency": 523033, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.746374676+02:00", - "latency": 601274, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.755347537+02:00", - "latency": 582716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.765416623+02:00", - "latency": 591521, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.775532769+02:00", - "latency": 574309, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.785874485+02:00", - "latency": 740292, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.795948348+02:00", - "latency": 619263, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.805827615+02:00", - "latency": 452444, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.815820001+02:00", - "latency": 509222, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.825794212+02:00", - "latency": 578718, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.835738376+02:00", - "latency": 502925, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.845705781+02:00", - "latency": 552863, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.855775145+02:00", - "latency": 639830, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.865889514+02:00", - "latency": 608740, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.875951037+02:00", - "latency": 620685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.885395309+02:00", - "latency": 587602, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.896403528+02:00", - "latency": 613420, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.90535719+02:00", - "latency": 556918, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.915358174+02:00", - "latency": 587161, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.925330817+02:00", - "latency": 596463, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.935523517+02:00", - "latency": 712441, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.945510768+02:00", - "latency": 513876, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.955574071+02:00", - "latency": 677573, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.965584998+02:00", - "latency": 599798, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.975651537+02:00", - "latency": 566377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.985568944+02:00", - "latency": 511287, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:14.997071875+02:00", - "latency": 1538394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.006322212+02:00", - "latency": 820064, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.016469318+02:00", - "latency": 715250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.025381866+02:00", - "latency": 512893, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.03571128+02:00", - "latency": 931896, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.046472108+02:00", - "latency": 711549, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.055359182+02:00", - "latency": 571021, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.065970501+02:00", - "latency": 573838, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.078016281+02:00", - "latency": 2221074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.086182243+02:00", - "latency": 664798, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.096207211+02:00", - "latency": 594208, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.106251623+02:00", - "latency": 537334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.11627626+02:00", - "latency": 569024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.126561305+02:00", - "latency": 760587, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.135506692+02:00", - "latency": 551427, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.145353546+02:00", - "latency": 532816, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.155394182+02:00", - "latency": 619828, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.165582671+02:00", - "latency": 735275, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.175511938+02:00", - "latency": 567277, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.185423608+02:00", - "latency": 549706, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.195635838+02:00", - "latency": 735875, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.205520378+02:00", - "latency": 510530, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.215581383+02:00", - "latency": 632074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.225578598+02:00", - "latency": 575586, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.235507151+02:00", - "latency": 526048, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.24541225+02:00", - "latency": 549913, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.256156532+02:00", - "latency": 500159, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.266840574+02:00", - "latency": 1211390, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.275783499+02:00", - "latency": 538025, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.285657509+02:00", - "latency": 489915, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.295517906+02:00", - "latency": 511166, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.305571428+02:00", - "latency": 726940, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.31562603+02:00", - "latency": 539563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.326945298+02:00", - "latency": 1896669, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.335956898+02:00", - "latency": 541042, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.345880232+02:00", - "latency": 502282, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.358836466+02:00", - "latency": 3465018, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.365381226+02:00", - "latency": 573670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.378033414+02:00", - "latency": 3309057, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.386052419+02:00", - "latency": 592848, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.396063693+02:00", - "latency": 584817, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.406958119+02:00", - "latency": 1508186, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.415997003+02:00", - "latency": 622748, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.425967827+02:00", - "latency": 533898, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.438695671+02:00", - "latency": 3290779, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.445646111+02:00", - "latency": 521419, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.457324044+02:00", - "latency": 2223976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.466193709+02:00", - "latency": 502936, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.478698609+02:00", - "latency": 3072800, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.485740602+02:00", - "latency": 559716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.497776849+02:00", - "latency": 2595869, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.505766263+02:00", - "latency": 563703, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.515729326+02:00", - "latency": 511820, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.525778067+02:00", - "latency": 556801, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.5357697+02:00", - "latency": 573388, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.546994845+02:00", - "latency": 959693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.556384963+02:00", - "latency": 531983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.565329917+02:00", - "latency": 520935, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.577997191+02:00", - "latency": 2270088, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.586005771+02:00", - "latency": 590929, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.596161199+02:00", - "latency": 679610, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.60668474+02:00", - "latency": 1097670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.615659196+02:00", - "latency": 611131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.625541449+02:00", - "latency": 564653, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.635445181+02:00", - "latency": 501517, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.64835323+02:00", - "latency": 571857, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.656229313+02:00", - "latency": 511999, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.668808235+02:00", - "latency": 3176326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.675807839+02:00", - "latency": 514903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.685754803+02:00", - "latency": 533569, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.695650291+02:00", - "latency": 542732, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.705614454+02:00", - "latency": 543912, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.71555641+02:00", - "latency": 541452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.725331977+02:00", - "latency": 575576, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.735180222+02:00", - "latency": 501143, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.746006999+02:00", - "latency": 501280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.755864238+02:00", - "latency": 547687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.765869284+02:00", - "latency": 592843, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.775861523+02:00", - "latency": 609817, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.788817393+02:00", - "latency": 3534361, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.795794982+02:00", - "latency": 545472, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.806278415+02:00", - "latency": 754904, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.816290568+02:00", - "latency": 603698, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.826653353+02:00", - "latency": 1098474, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.836271093+02:00", - "latency": 548373, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.849972425+02:00", - "latency": 4264143, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.855980538+02:00", - "latency": 565310, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.865975076+02:00", - "latency": 510907, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.875936478+02:00", - "latency": 566104, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.88730127+02:00", - "latency": 493237, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.899016324+02:00", - "latency": 3311332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.906019382+02:00", - "latency": 625905, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.915994079+02:00", - "latency": 571414, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.925998672+02:00", - "latency": 516438, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.935974913+02:00", - "latency": 566705, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.948735026+02:00", - "latency": 3333250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.955784684+02:00", - "latency": 594084, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.965688954+02:00", - "latency": 534853, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.975891244+02:00", - "latency": 728265, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.985980762+02:00", - "latency": 623159, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:15.996056595+02:00", - "latency": 629503, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.006184719+02:00", - "latency": 672574, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.016269862+02:00", - "latency": 613270, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.030747824+02:00", - "latency": 4938120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.037408211+02:00", - "latency": 591413, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.04551168+02:00", - "latency": 624838, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.055577372+02:00", - "latency": 738061, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.065999559+02:00", - "latency": 487749, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.075930041+02:00", - "latency": 534356, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.087881898+02:00", - "latency": 2528384, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.095983314+02:00", - "latency": 691210, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.105353263+02:00", - "latency": 565788, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.115414676+02:00", - "latency": 635544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.125293009+02:00", - "latency": 550353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.138007034+02:00", - "latency": 2269754, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.145907455+02:00", - "latency": 529689, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.155699333+02:00", - "latency": 530900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.165737145+02:00", - "latency": 636994, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.175703345+02:00", - "latency": 598664, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.185930531+02:00", - "latency": 879880, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.197658731+02:00", - "latency": 2325619, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.205616417+02:00", - "latency": 580894, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.215494492+02:00", - "latency": 533889, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.225833987+02:00", - "latency": 582620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.23589211+02:00", - "latency": 613906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.248687656+02:00", - "latency": 3330377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.255713713+02:00", - "latency": 585437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.265753122+02:00", - "latency": 608878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.275691135+02:00", - "latency": 551659, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.285591606+02:00", - "latency": 502739, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.295521465+02:00", - "latency": 448216, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.309714338+02:00", - "latency": 4841788, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.315655193+02:00", - "latency": 546911, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.325572403+02:00", - "latency": 552598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.335608281+02:00", - "latency": 567027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.345524304+02:00", - "latency": 500212, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.356652114+02:00", - "latency": 1561151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.365744699+02:00", - "latency": 613164, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.375755644+02:00", - "latency": 567640, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.385669802+02:00", - "latency": 571504, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.395652257+02:00", - "latency": 563378, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.40645003+02:00", - "latency": 948632, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.416183024+02:00", - "latency": 967457, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.425854164+02:00", - "latency": 665273, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.435879872+02:00", - "latency": 579775, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.445840523+02:00", - "latency": 555570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.456330966+02:00", - "latency": 518770, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.469296284+02:00", - "latency": 497247, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.476295453+02:00", - "latency": 563387, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.486321908+02:00", - "latency": 502065, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.496462085+02:00", - "latency": 719323, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.509461114+02:00", - "latency": 498505, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.515387701+02:00", - "latency": 536598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.525341879+02:00", - "latency": 581386, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.536293277+02:00", - "latency": 582010, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.546296011+02:00", - "latency": 591552, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.556306469+02:00", - "latency": 602951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.566341099+02:00", - "latency": 695546, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.575383132+02:00", - "latency": 646075, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.585400071+02:00", - "latency": 616034, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.596335382+02:00", - "latency": 623340, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.605304287+02:00", - "latency": 510348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.616307718+02:00", - "latency": 584107, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.627901055+02:00", - "latency": 2153247, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.641828634+02:00", - "latency": 3445818, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.645774435+02:00", - "latency": 571357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.655715407+02:00", - "latency": 505453, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.666092217+02:00", - "latency": 565433, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.676952499+02:00", - "latency": 2140389, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.685984798+02:00", - "latency": 629427, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.695896661+02:00", - "latency": 501088, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.70589935+02:00", - "latency": 564355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.718684858+02:00", - "latency": 3036551, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.725666398+02:00", - "latency": 544431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.735625432+02:00", - "latency": 507228, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.745326407+02:00", - "latency": 561034, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.756268393+02:00", - "latency": 551776, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.766222801+02:00", - "latency": 532908, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.775499282+02:00", - "latency": 543011, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.785594125+02:00", - "latency": 621592, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.799215597+02:00", - "latency": 4294816, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.807196887+02:00", - "latency": 541527, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.816245235+02:00", - "latency": 526315, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.826199754+02:00", - "latency": 567107, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.836647415+02:00", - "latency": 1093956, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.846752909+02:00", - "latency": 1735023, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.855910288+02:00", - "latency": 707844, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.865830652+02:00", - "latency": 584334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.877802137+02:00", - "latency": 2573277, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.885704944+02:00", - "latency": 475620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.895667861+02:00", - "latency": 499532, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.905837926+02:00", - "latency": 748125, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.915974032+02:00", - "latency": 651609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.926640135+02:00", - "latency": 513630, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.935629047+02:00", - "latency": 583727, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.945749365+02:00", - "latency": 702571, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.955723339+02:00", - "latency": 534831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.965744518+02:00", - "latency": 558411, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.977812767+02:00", - "latency": 2666388, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.985869584+02:00", - "latency": 526718, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:16.99585301+02:00", - "latency": 531929, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.005829725+02:00", - "latency": 587266, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.015817412+02:00", - "latency": 572376, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.025941912+02:00", - "latency": 692261, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.040693836+02:00", - "latency": 5406580, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.045640467+02:00", - "latency": 538392, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.055701253+02:00", - "latency": 616949, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.065734034+02:00", - "latency": 615641, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.075764845+02:00", - "latency": 593496, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.085327568+02:00", - "latency": 564324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.095399175+02:00", - "latency": 629215, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.106405329+02:00", - "latency": 911303, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.118473281+02:00", - "latency": 550238, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.125437138+02:00", - "latency": 564692, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.135352282+02:00", - "latency": 579791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.145217258+02:00", - "latency": 526422, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.155335873+02:00", - "latency": 535724, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.165913038+02:00", - "latency": 586249, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.175884969+02:00", - "latency": 552357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.185740989+02:00", - "latency": 500769, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.195787935+02:00", - "latency": 583338, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.206261308+02:00", - "latency": 663738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.216221386+02:00", - "latency": 596318, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.226142361+02:00", - "latency": 512123, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.236523528+02:00", - "latency": 618051, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.245562982+02:00", - "latency": 611951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.255503559+02:00", - "latency": 541254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.265513968+02:00", - "latency": 530541, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.275490423+02:00", - "latency": 558873, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.285402738+02:00", - "latency": 516092, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.295864513+02:00", - "latency": 841251, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.3075245+02:00", - "latency": 685510, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.3156577+02:00", - "latency": 701261, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.325630439+02:00", - "latency": 544155, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.335664603+02:00", - "latency": 593758, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.346327589+02:00", - "latency": 609675, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.355356478+02:00", - "latency": 646414, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.365423173+02:00", - "latency": 693371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.375383925+02:00", - "latency": 558518, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.38543385+02:00", - "latency": 677352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.395539771+02:00", - "latency": 632541, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.405435595+02:00", - "latency": 663948, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.418630492+02:00", - "latency": 3833467, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.426262067+02:00", - "latency": 580989, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.436105191+02:00", - "latency": 532442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.446094473+02:00", - "latency": 593273, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.458125847+02:00", - "latency": 1146996, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.466098493+02:00", - "latency": 558575, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.476183276+02:00", - "latency": 624110, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.486175161+02:00", - "latency": 561900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.49625712+02:00", - "latency": 640362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.506200965+02:00", - "latency": 524955, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.516510006+02:00", - "latency": 771984, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.526841135+02:00", - "latency": 2106759, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.535900567+02:00", - "latency": 572364, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.546017658+02:00", - "latency": 635785, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.556060145+02:00", - "latency": 577261, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.566045019+02:00", - "latency": 515555, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.57607789+02:00", - "latency": 603732, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.586161881+02:00", - "latency": 620972, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.596232957+02:00", - "latency": 628225, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.606186606+02:00", - "latency": 540375, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.617608866+02:00", - "latency": 1938005, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.626312728+02:00", - "latency": 604134, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.636341731+02:00", - "latency": 585763, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.64704691+02:00", - "latency": 2241252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.656090694+02:00", - "latency": 575944, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.666060733+02:00", - "latency": 533176, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.676314478+02:00", - "latency": 734735, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.686339582+02:00", - "latency": 611532, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.696257063+02:00", - "latency": 529766, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.706223869+02:00", - "latency": 531600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.716231946+02:00", - "latency": 576760, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.726236682+02:00", - "latency": 522002, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.736303656+02:00", - "latency": 648430, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.753131407+02:00", - "latency": 6817126, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.766489869+02:00", - "latency": 572113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.767436983+02:00", - "latency": 1474324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.775437958+02:00", - "latency": 621548, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.785384874+02:00", - "latency": 599729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.795354995+02:00", - "latency": 591509, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.805525419+02:00", - "latency": 779841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.815823892+02:00", - "latency": 872133, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.825670629+02:00", - "latency": 598030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.835590649+02:00", - "latency": 524487, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.845540259+02:00", - "latency": 555181, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.855533952+02:00", - "latency": 574262, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.866669845+02:00", - "latency": 1690342, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.875776017+02:00", - "latency": 637612, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.88581177+02:00", - "latency": 614646, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.899855357+02:00", - "latency": 5079139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.905887105+02:00", - "latency": 629787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.915911585+02:00", - "latency": 594616, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.926177565+02:00", - "latency": 822289, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.937003148+02:00", - "latency": 1492490, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.946162446+02:00", - "latency": 722353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.956210794+02:00", - "latency": 549889, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.966192196+02:00", - "latency": 543329, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.975425352+02:00", - "latency": 560246, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.985422062+02:00", - "latency": 554633, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:17.995460996+02:00", - "latency": 633765, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.005575256+02:00", - "latency": 682907, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.015573315+02:00", - "latency": 633533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.028714139+02:00", - "latency": 3718599, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.035856376+02:00", - "latency": 684059, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.045921701+02:00", - "latency": 645098, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.057137644+02:00", - "latency": 2109133, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.066249785+02:00", - "latency": 602078, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.076316685+02:00", - "latency": 691886, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.08646459+02:00", - "latency": 761229, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.09589099+02:00", - "latency": 775094, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.106002468+02:00", - "latency": 698389, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.118705195+02:00", - "latency": 3363105, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.125830953+02:00", - "latency": 659281, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.13583064+02:00", - "latency": 567401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.145924535+02:00", - "latency": 665179, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.155812991+02:00", - "latency": 551895, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.165927547+02:00", - "latency": 669875, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.176542257+02:00", - "latency": 952393, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.185715937+02:00", - "latency": 744890, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.195694281+02:00", - "latency": 593761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.205601221+02:00", - "latency": 513613, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.216753207+02:00", - "latency": 1744598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.225726066+02:00", - "latency": 550547, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.235729954+02:00", - "latency": 632306, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.246671758+02:00", - "latency": 1561424, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.256665757+02:00", - "latency": 1565442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.265699114+02:00", - "latency": 612605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.275693142+02:00", - "latency": 525357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.28593129+02:00", - "latency": 698160, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.295978741+02:00", - "latency": 712212, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.306791967+02:00", - "latency": 1383868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.316137247+02:00", - "latency": 825522, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.32991719+02:00", - "latency": 4504923, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.335843361+02:00", - "latency": 542047, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.345828397+02:00", - "latency": 582840, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.355775568+02:00", - "latency": 550261, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.365803457+02:00", - "latency": 620264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.376029542+02:00", - "latency": 734419, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.388977853+02:00", - "latency": 3485327, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.39605745+02:00", - "latency": 570634, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.410714027+02:00", - "latency": 5264579, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.41567638+02:00", - "latency": 534778, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.425749914+02:00", - "latency": 660617, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.435730964+02:00", - "latency": 602117, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.447712408+02:00", - "latency": 2555285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.455780682+02:00", - "latency": 603178, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.465698625+02:00", - "latency": 533295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.477711166+02:00", - "latency": 2609656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.485672318+02:00", - "latency": 515515, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.495640019+02:00", - "latency": 594848, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.505600075+02:00", - "latency": 577539, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.515589909+02:00", - "latency": 580520, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.525606794+02:00", - "latency": 599801, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.535716747+02:00", - "latency": 690131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.545641475+02:00", - "latency": 563732, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:18.555710691+02:00", - "latency": 616729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - } - ] -} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.json b/test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.json deleted file mode 100644 index 758f7aaf9..000000000 --- a/test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.json +++ /dev/null @@ -1 +0,0 @@ -{"date":"2025-10-03T17:38:09+02:00","endReason":"timeout","options":{"call":"trading.TradingService.GetOrderBook","host":"localhost:50051","proto":"/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto","import-paths":["/home/jgrusewski/Work/foxhunt/services/trading_service/proto","."],"insecure":true,"rps":100,"load-schedule":"const","load-start":0,"load-end":0,"load-step":0,"load-step-duration":0,"load-max-duration":0,"concurrency":50,"concurrency-schedule":"const","concurrency-start":0,"concurrency-end":0,"concurrency-step":1,"concurrency-step-duration":0,"concurrency-max-duration":0,"total":2147483647,"connections":10,"duration":10000000000,"timeout":20000000000,"dial-timeout":10000000000,"data":{"depth":10,"symbol":"AAPL"},"binary":false,"CPUs":16},"count":999,"total":10007674812,"average":1077373,"fastest":0,"slowest":0,"rps":99.8233874268296,"errorDistribution":{"rpc error: code = Unauthenticated desc = Valid authentication required":999},"statusCodeDistribution":{"Unauthenticated":999},"latencyDistribution":null,"histogram":null,"details":[{"timestamp":"2025-10-03T17:37:59.110746775+02:00","latency":6351360,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.113753237+02:00","latency":564712,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.124471445+02:00","latency":596291,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.133390354+02:00","latency":689923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.143495185+02:00","latency":613583,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.153508089+02:00","latency":592618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.163593546+02:00","latency":703380,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.17383605+02:00","latency":768278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.183951197+02:00","latency":658596,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.229950863+02:00","latency":27035042,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.230041242+02:00","latency":16726146,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.231897475+02:00","latency":8913622,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.233771885+02:00","latency":40340531,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.236014092+02:00","latency":2708975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.244061411+02:00","latency":567958,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.254225368+02:00","latency":1140496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.268516581+02:00","latency":620217,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.275359755+02:00","latency":500201,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.283464375+02:00","latency":597619,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.293441484+02:00","latency":523597,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.303585212+02:00","latency":633237,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.31378986+02:00","latency":657870,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.32386235+02:00","latency":614017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.333896839+02:00","latency":572645,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.343979589+02:00","latency":624779,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.354124003+02:00","latency":649883,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.363216356+02:00","latency":627141,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.373464884+02:00","latency":767245,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.383595292+02:00","latency":639714,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.393732371+02:00","latency":573164,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.403864431+02:00","latency":687420,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.414050622+02:00","latency":660600,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.424118926+02:00","latency":585352,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.435538427+02:00","latency":616261,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.443647282+02:00","latency":542590,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.453791458+02:00","latency":642928,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.468672235+02:00","latency":5338877,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.473769385+02:00","latency":565378,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.483880537+02:00","latency":635114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.495507074+02:00","latency":590421,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.503459103+02:00","latency":529911,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.513495031+02:00","latency":548628,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.525504087+02:00","latency":584029,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.534003775+02:00","latency":683750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.544128687+02:00","latency":613662,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.553221269+02:00","latency":625646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.564272832+02:00","latency":580241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.573579194+02:00","latency":552017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.590786704+02:00","latency":591492,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.598663629+02:00","latency":5386355,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.605439318+02:00","latency":2434444,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.617676734+02:00","latency":4905422,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.623894977+02:00","latency":535957,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.63907246+02:00","latency":5638540,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.644019843+02:00","latency":1433943,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.654139982+02:00","latency":553025,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.663228249+02:00","latency":604416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.673322279+02:00","latency":534754,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.683349354+02:00","latency":543094,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.693362809+02:00","latency":509768,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.703416502+02:00","latency":575223,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.713388518+02:00","latency":462857,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.723419602+02:00","latency":539463,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.733391161+02:00","latency":450979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.743284719+02:00","latency":477314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.753232916+02:00","latency":454717,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.763170656+02:00","latency":504583,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.778656922+02:00","latency":4339941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.783723563+02:00","latency":598110,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.793728421+02:00","latency":531815,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.803748363+02:00","latency":506354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.817372483+02:00","latency":519685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.823390639+02:00","latency":524282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.8330852+02:00","latency":465725,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.843120844+02:00","latency":591537,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.853695351+02:00","latency":1080882,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.863841732+02:00","latency":546580,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.873901454+02:00","latency":556639,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.883955337+02:00","latency":588979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.894074864+02:00","latency":562872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.904129913+02:00","latency":559437,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.913081598+02:00","latency":489836,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.924017791+02:00","latency":452764,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.941029527+02:00","latency":5527493,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.943237345+02:00","latency":381316,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.953317322+02:00","latency":615900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.96340396+02:00","latency":631976,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.973410175+02:00","latency":544231,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.983690468+02:00","latency":637204,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:59.993779879+02:00","latency":531578,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.004334157+02:00","latency":1064962,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.013263357+02:00","latency":460787,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.025805152+02:00","latency":3078099,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.03460488+02:00","latency":1291143,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.043658642+02:00","latency":549121,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.053715247+02:00","latency":570665,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.065094083+02:00","latency":1178364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.0733097+02:00","latency":685260,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.086801319+02:00","latency":3926874,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.093897088+02:00","latency":554629,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.105702578+02:00","latency":815720,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.114370924+02:00","latency":837718,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.123643992+02:00","latency":636650,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.136743468+02:00","latency":3486221,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.144983638+02:00","latency":1645057,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.154097022+02:00","latency":581564,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.163140209+02:00","latency":597904,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.177649965+02:00","latency":5045946,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.183644514+02:00","latency":563283,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.193675374+02:00","latency":512566,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.203815594+02:00","latency":629042,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.21438484+02:00","latency":503618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.22566949+02:00","latency":2796585,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.235691025+02:00","latency":2490674,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.24375745+02:00","latency":503147,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.258179279+02:00","latency":4852364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.264049771+02:00","latency":514510,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.273902613+02:00","latency":519810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.283136127+02:00","latency":544477,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.311687332+02:00","latency":19105239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.313127947+02:00","latency":1098443,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.313461466+02:00","latency":392463,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.324430521+02:00","latency":513593,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.333443246+02:00","latency":577467,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.349962896+02:00","latency":637462,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.35350859+02:00","latency":525915,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.363754105+02:00","latency":525060,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.374066099+02:00","latency":536643,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.383213761+02:00","latency":620943,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.393278047+02:00","latency":528688,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.403412509+02:00","latency":575625,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.420826998+02:00","latency":7870130,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.425649055+02:00","latency":689686,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.433740325+02:00","latency":561143,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.443897256+02:00","latency":550590,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.456060929+02:00","latency":2634480,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.464097212+02:00","latency":530290,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.474052313+02:00","latency":479085,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.484086114+02:00","latency":547215,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.493075416+02:00","latency":567616,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.503538834+02:00","latency":568681,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.513643773+02:00","latency":612792,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.523812302+02:00","latency":647323,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.533862108+02:00","latency":563049,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.543916299+02:00","latency":532975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.55397867+02:00","latency":564171,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.563258082+02:00","latency":652061,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.573379899+02:00","latency":611349,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.660659312+02:00","latency":1256553,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.660709289+02:00","latency":1042854,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.66071752+02:00","latency":856582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.660755129+02:00","latency":698292,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.660793739+02:00","latency":1411937,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.660989887+02:00","latency":514508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.661214768+02:00","latency":249150,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.661368242+02:00","latency":1092708,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.663131037+02:00","latency":347113,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.673144714+02:00","latency":604939,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.6840968+02:00","latency":538671,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.694109433+02:00","latency":523899,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.703070421+02:00","latency":541749,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.714056369+02:00","latency":594363,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.724260326+02:00","latency":667099,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.733316844+02:00","latency":575149,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.743404161+02:00","latency":539434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.753536487+02:00","latency":659830,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.763452616+02:00","latency":567924,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.773603237+02:00","latency":655905,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.786109495+02:00","latency":3010006,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.793149736+02:00","latency":557923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.803370296+02:00","latency":603004,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.813325678+02:00","latency":519892,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.823463876+02:00","latency":562468,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.833540075+02:00","latency":585936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.843644152+02:00","latency":620031,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.853701999+02:00","latency":543438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.871785283+02:00","latency":2867864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.873576151+02:00","latency":485748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.892701513+02:00","latency":8637081,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.898595539+02:00","latency":2433466,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.903418228+02:00","latency":704679,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.926133922+02:00","latency":9767217,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.927072429+02:00","latency":531395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.938881254+02:00","latency":669390,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.943245279+02:00","latency":591174,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.962416267+02:00","latency":6499209,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.968145519+02:00","latency":5379355,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.973198188+02:00","latency":562388,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.987142407+02:00","latency":3164586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:00.998756174+02:00","latency":6076951,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.003492632+02:00","latency":639888,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.013503717+02:00","latency":556128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.02527673+02:00","latency":1206280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.039424098+02:00","latency":6588240,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.045689491+02:00","latency":2828047,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.056455249+02:00","latency":563232,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.063482993+02:00","latency":541615,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.07502506+02:00","latency":1190626,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.08412215+02:00","latency":553937,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.093154734+02:00","latency":546680,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.103189356+02:00","latency":596380,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.113366227+02:00","latency":599916,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.123570523+02:00","latency":595133,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.133653795+02:00","latency":576816,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.143790822+02:00","latency":609570,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.153915395+02:00","latency":594409,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.16400777+02:00","latency":564069,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.174144288+02:00","latency":514092,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.18317007+02:00","latency":570586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.193242097+02:00","latency":537280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.203218502+02:00","latency":471261,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.21325226+02:00","latency":504572,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.223283924+02:00","latency":553381,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.233293368+02:00","latency":491876,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.243379819+02:00","latency":559674,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.253435001+02:00","latency":524470,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.263370117+02:00","latency":501702,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.275013521+02:00","latency":2018754,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.2840093+02:00","latency":517622,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.293997884+02:00","latency":498278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.304054532+02:00","latency":570971,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.314065986+02:00","latency":523097,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.324116101+02:00","latency":508406,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.333102965+02:00","latency":602154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.344168275+02:00","latency":597865,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.353189102+02:00","latency":583086,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.36305907+02:00","latency":525078,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.375983727+02:00","latency":2337241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.384044925+02:00","latency":534214,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.394041051+02:00","latency":475806,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.404000149+02:00","latency":483002,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.414055884+02:00","latency":519991,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.424046928+02:00","latency":535898,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.436894989+02:00","latency":3377213,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.443895019+02:00","latency":484587,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.453908864+02:00","latency":539083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.464035423+02:00","latency":563795,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.474075912+02:00","latency":552879,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.484000326+02:00","latency":471296,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.493922604+02:00","latency":461691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.507328476+02:00","latency":433458,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.513280753+02:00","latency":489385,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.523303511+02:00","latency":528831,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.534617742+02:00","latency":1632416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.544670699+02:00","latency":1584769,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.553761215+02:00","latency":570628,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.564608015+02:00","latency":1274557,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.573821715+02:00","latency":507307,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.583872547+02:00","latency":584944,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.593945741+02:00","latency":573538,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.603903595+02:00","latency":436178,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.613872359+02:00","latency":469527,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.624023554+02:00","latency":641317,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.638388045+02:00","latency":515168,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.645359571+02:00","latency":488357,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.653354882+02:00","latency":524669,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.66535174+02:00","latency":486060,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.673379201+02:00","latency":508806,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.683315618+02:00","latency":447117,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.693288373+02:00","latency":504436,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.703363401+02:00","latency":582197,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.720947452+02:00","latency":3688908,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.724069443+02:00","latency":615926,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.734058991+02:00","latency":491097,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.744151963+02:00","latency":573840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.753181117+02:00","latency":608949,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.764895585+02:00","latency":2247369,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.773949393+02:00","latency":571704,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.783873581+02:00","latency":482554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.793857796+02:00","latency":489577,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.803802493+02:00","latency":491551,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.814634815+02:00","latency":1283140,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.823750441+02:00","latency":579682,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.833689011+02:00","latency":429180,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.843760221+02:00","latency":600888,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.853744497+02:00","latency":506899,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.863703657+02:00","latency":484889,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.873656782+02:00","latency":503885,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.888918517+02:00","latency":2045614,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.896822762+02:00","latency":964796,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.90390559+02:00","latency":592260,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.913948101+02:00","latency":589342,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.924045951+02:00","latency":612764,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.934138334+02:00","latency":530825,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.948754517+02:00","latency":6147643,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.953929146+02:00","latency":540608,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.965954525+02:00","latency":468900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.974133949+02:00","latency":471837,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.983843028+02:00","latency":1258915,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:01.993297896+02:00","latency":684045,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.013120694+02:00","latency":3190691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.013827529+02:00","latency":395991,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.026017722+02:00","latency":1862911,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.038200203+02:00","latency":2053949,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.045129967+02:00","latency":501230,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.053040316+02:00","latency":430210,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.06389151+02:00","latency":431312,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.074484296+02:00","latency":477527,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.083198419+02:00","latency":567512,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.093147192+02:00","latency":508593,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.105148388+02:00","latency":2216875,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.113182008+02:00","latency":567204,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.125838876+02:00","latency":570443,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.138735033+02:00","latency":3730185,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.14380539+02:00","latency":571574,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.155077112+02:00","latency":1835386,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.164110212+02:00","latency":584707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.177326387+02:00","latency":526917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.18361677+02:00","latency":577426,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.193562755+02:00","latency":474151,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.203528985+02:00","latency":476126,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.213599076+02:00","latency":570038,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.223637808+02:00","latency":587578,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.233656816+02:00","latency":490845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.243558294+02:00","latency":439816,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.2535618+02:00","latency":500572,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.263564263+02:00","latency":530878,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.273514757+02:00","latency":499932,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.283506014+02:00","latency":461899,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.293537013+02:00","latency":440114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.304825281+02:00","latency":1817685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.31452571+02:00","latency":491605,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.326937397+02:00","latency":4037522,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.333404528+02:00","latency":431228,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.3433914+02:00","latency":459063,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.354891811+02:00","latency":2014568,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.377321157+02:00","latency":3883844,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.378754038+02:00","latency":7899773,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.388619671+02:00","latency":824800,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.396728411+02:00","latency":592190,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.406393581+02:00","latency":516710,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.413293081+02:00","latency":454957,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.42326685+02:00","latency":427278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.433186352+02:00","latency":480050,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.443213688+02:00","latency":518520,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.453165126+02:00","latency":525487,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.463142887+02:00","latency":477641,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.473187425+02:00","latency":523690,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.483851736+02:00","latency":720498,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.493148961+02:00","latency":513850,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.503095872+02:00","latency":513548,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.513141663+02:00","latency":559183,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.529712344+02:00","latency":6338003,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.539969852+02:00","latency":6623673,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.543344281+02:00","latency":518775,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.553918861+02:00","latency":523369,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.566275331+02:00","latency":458845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.574872539+02:00","latency":540576,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.583864276+02:00","latency":546845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.593832455+02:00","latency":523354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.603814169+02:00","latency":513079,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.613929357+02:00","latency":603424,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.623909213+02:00","latency":619739,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.634198387+02:00","latency":686582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.643174306+02:00","latency":466814,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.653053866+02:00","latency":486163,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.664066397+02:00","latency":535567,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.674096624+02:00","latency":519833,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.684127144+02:00","latency":515226,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.69312647+02:00","latency":583330,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.704110819+02:00","latency":1298167,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.713143594+02:00","latency":631104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.723114408+02:00","latency":587877,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.733253684+02:00","latency":653693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.744155324+02:00","latency":1404191,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.753301781+02:00","latency":648820,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.763303774+02:00","latency":514325,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.773408686+02:00","latency":565241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.783459452+02:00","latency":596092,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.794380258+02:00","latency":599570,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.803437635+02:00","latency":549994,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.814028122+02:00","latency":1101506,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.824133164+02:00","latency":620127,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.833113656+02:00","latency":529797,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.843777524+02:00","latency":510114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.853880065+02:00","latency":612352,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.864757872+02:00","latency":1404554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.873770564+02:00","latency":526998,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.883940743+02:00","latency":656460,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.893793234+02:00","latency":506698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.903460153+02:00","latency":518561,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.91563699+02:00","latency":2394794,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.924400833+02:00","latency":1056004,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.933431015+02:00","latency":559586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.945208974+02:00","latency":2267209,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.953195597+02:00","latency":529393,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.963219749+02:00","latency":545948,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.973197135+02:00","latency":494149,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.983232861+02:00","latency":532595,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:02.995821105+02:00","latency":3012859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.003801753+02:00","latency":492790,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.01697034+02:00","latency":3628282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.023935618+02:00","latency":508975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.033929987+02:00","latency":474801,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.044027137+02:00","latency":579781,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.054043258+02:00","latency":549266,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.064062207+02:00","latency":532787,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.074142031+02:00","latency":538551,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.08304245+02:00","latency":523544,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.094189247+02:00","latency":572373,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.103258311+02:00","latency":612449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.114107847+02:00","latency":507026,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.124076086+02:00","latency":472116,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.13425826+02:00","latency":530968,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.14331202+02:00","latency":555243,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.153330366+02:00","latency":493215,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.163413788+02:00","latency":590733,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.176822034+02:00","latency":3958513,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.183867055+02:00","latency":518113,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.193857744+02:00","latency":517046,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.203847913+02:00","latency":517682,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.214605958+02:00","latency":1234344,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.223885783+02:00","latency":558083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.235360857+02:00","latency":489463,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.246144215+02:00","latency":3261801,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.253067614+02:00","latency":495057,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.263257789+02:00","latency":617278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.273123242+02:00","latency":534489,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.283211466+02:00","latency":618657,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.293314397+02:00","latency":645621,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.303345623+02:00","latency":602548,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.313278622+02:00","latency":474290,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.323234793+02:00","latency":501117,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.333334481+02:00","latency":566889,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.343511785+02:00","latency":635519,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.353443061+02:00","latency":520783,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.363601209+02:00","latency":663208,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.373673152+02:00","latency":547651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.383641415+02:00","latency":508601,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.39318498+02:00","latency":554139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.40327357+02:00","latency":631366,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.413234482+02:00","latency":534936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.423326295+02:00","latency":580238,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.433417923+02:00","latency":622251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.443413375+02:00","latency":557994,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.453948819+02:00","latency":703826,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.465299117+02:00","latency":504915,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.473296421+02:00","latency":579954,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.483439499+02:00","latency":598809,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.493473861+02:00","latency":521476,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.503571903+02:00","latency":622504,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.516384876+02:00","latency":494319,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.525249775+02:00","latency":2393450,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.533334425+02:00","latency":568567,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.543318691+02:00","latency":502252,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.554989991+02:00","latency":2312913,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.564052038+02:00","latency":524050,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.573847058+02:00","latency":653019,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.583812027+02:00","latency":572630,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.593838735+02:00","latency":490523,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.605612286+02:00","latency":2275687,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.613812073+02:00","latency":538637,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.628696656+02:00","latency":5383391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.634023896+02:00","latency":872580,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.644127911+02:00","latency":567320,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.65305944+02:00","latency":554903,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.664074757+02:00","latency":549278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.673121568+02:00","latency":517987,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.683113892+02:00","latency":579338,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.693225737+02:00","latency":682047,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.703159058+02:00","latency":492035,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.713250987+02:00","latency":629668,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.723257087+02:00","latency":505988,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.733247939+02:00","latency":506541,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.745686541+02:00","latency":2923203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.753634417+02:00","latency":472064,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.763366865+02:00","latency":496807,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.773578968+02:00","latency":678871,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.784782613+02:00","latency":461554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.816565039+02:00","latency":482071,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.817100013+02:00","latency":313388,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.817252975+02:00","latency":24002226,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.824743288+02:00","latency":2037129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.833741212+02:00","latency":526485,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.843997513+02:00","latency":673860,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.853828654+02:00","latency":584684,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.866194211+02:00","latency":527933,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.893860098+02:00","latency":497439,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.894041127+02:00","latency":941696,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.894220283+02:00","latency":1423670,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.903148341+02:00","latency":474290,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.914676835+02:00","latency":2142594,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.92378356+02:00","latency":614304,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.933750474+02:00","latency":505267,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.943762771+02:00","latency":523161,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.95668713+02:00","latency":3445314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.963627366+02:00","latency":473503,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.973607435+02:00","latency":508472,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.983608968+02:00","latency":512431,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:03.99358515+02:00","latency":521327,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.003599928+02:00","latency":557707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.013635394+02:00","latency":543536,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.023827803+02:00","latency":600206,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.034667328+02:00","latency":1373129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.043831414+02:00","latency":579218,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.054535505+02:00","latency":612474,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.063638614+02:00","latency":623800,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.073677011+02:00","latency":562245,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.083936257+02:00","latency":697205,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.093924424+02:00","latency":593750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.10390347+02:00","latency":605626,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.113905355+02:00","latency":501707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.123951916+02:00","latency":575845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.136406004+02:00","latency":536036,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.14332574+02:00","latency":478690,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.153357029+02:00","latency":572570,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.163323458+02:00","latency":513951,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.173422203+02:00","latency":593061,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.18352642+02:00","latency":525494,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.193638127+02:00","latency":561838,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.203644616+02:00","latency":521210,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.213683806+02:00","latency":518107,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.226825042+02:00","latency":571854,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.233938348+02:00","latency":654193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.243947788+02:00","latency":596251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.255300751+02:00","latency":1895108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.2636857+02:00","latency":595338,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.273872922+02:00","latency":648507,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.283946035+02:00","latency":561981,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.293957989+02:00","latency":514055,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.304043897+02:00","latency":610541,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.313648277+02:00","latency":704685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.323772031+02:00","latency":577989,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.333752589+02:00","latency":539779,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.345990742+02:00","latency":2748241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.354034288+02:00","latency":546208,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.363931243+02:00","latency":477778,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.373994299+02:00","latency":573234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.38572443+02:00","latency":596743,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.393893856+02:00","latency":583154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.404032146+02:00","latency":644181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.415631136+02:00","latency":669682,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.428726701+02:00","latency":5626444,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.433826018+02:00","latency":571654,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.443854866+02:00","latency":535115,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.453895928+02:00","latency":557881,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.463958132+02:00","latency":585773,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.474055607+02:00","latency":477262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.483235422+02:00","latency":579301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.493383819+02:00","latency":603058,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.503468358+02:00","latency":480998,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.513187542+02:00","latency":582067,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.523380457+02:00","latency":677003,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.533748186+02:00","latency":725653,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.544881603+02:00","latency":690077,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.553971182+02:00","latency":732126,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.56402961+02:00","latency":679713,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.574113368+02:00","latency":531880,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.583279244+02:00","latency":632592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.593126135+02:00","latency":540872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.606071189+02:00","latency":3477087,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.614225044+02:00","latency":620410,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.623226631+02:00","latency":505652,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.633233649+02:00","latency":509980,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.643614054+02:00","latency":899028,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.65380712+02:00","latency":729554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.665270685+02:00","latency":2133665,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.673357207+02:00","latency":619895,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.684192421+02:00","latency":1397385,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.693218554+02:00","latency":505619,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.703458046+02:00","latency":683975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.714982842+02:00","latency":504369,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.724027076+02:00","latency":639654,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.734647694+02:00","latency":1151452,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.743714535+02:00","latency":510248,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.754623788+02:00","latency":778309,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.76343485+02:00","latency":536326,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.773465369+02:00","latency":601017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.78350673+02:00","latency":526977,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.793559671+02:00","latency":566727,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.803544272+02:00","latency":486529,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.813678084+02:00","latency":596703,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.823782628+02:00","latency":647100,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.834814851+02:00","latency":548163,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.843871313+02:00","latency":607016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.854032101+02:00","latency":687685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.864445346+02:00","latency":592981,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.87344599+02:00","latency":541628,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.883629212+02:00","latency":647084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.893842183+02:00","latency":692353,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.904135186+02:00","latency":729048,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.913109266+02:00","latency":598731,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.92598263+02:00","latency":3399733,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.934151793+02:00","latency":646330,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.944718491+02:00","latency":1874399,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.953964665+02:00","latency":652466,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.964182908+02:00","latency":678657,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.973305583+02:00","latency":717774,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.983501684+02:00","latency":660279,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:04.993881802+02:00","latency":829758,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.004003068+02:00","latency":586380,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.0142474+02:00","latency":644041,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.023538019+02:00","latency":688571,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.033959955+02:00","latency":850785,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.044015582+02:00","latency":611442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.054180411+02:00","latency":693098,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.064285217+02:00","latency":755378,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.073306155+02:00","latency":567065,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.083380305+02:00","latency":632341,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.093711404+02:00","latency":684496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.10384018+02:00","latency":590850,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.113991691+02:00","latency":649006,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.124157133+02:00","latency":689823,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.134390916+02:00","latency":717073,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.143850273+02:00","latency":892155,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.165480414+02:00","latency":6443530,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.166620412+02:00","latency":679189,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.176775354+02:00","latency":782003,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.185018744+02:00","latency":673583,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.193267561+02:00","latency":673764,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.203440978+02:00","latency":672597,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.213605654+02:00","latency":677246,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.223909342+02:00","latency":725282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.234177827+02:00","latency":765212,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.243195642+02:00","latency":620125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.255460094+02:00","latency":2740060,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.263587193+02:00","latency":653512,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.273740882+02:00","latency":724893,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.283969676+02:00","latency":736176,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.294039085+02:00","latency":676027,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.304461053+02:00","latency":823233,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.313534411+02:00","latency":630104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.323765127+02:00","latency":796559,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.333744985+02:00","latency":605144,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.343857942+02:00","latency":665673,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.354494266+02:00","latency":1183141,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.363684457+02:00","latency":783766,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.375597704+02:00","latency":619343,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.383742114+02:00","latency":618955,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.394037635+02:00","latency":859508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.40442867+02:00","latency":1076479,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.414510665+02:00","latency":608547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.423508599+02:00","latency":617253,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.433590684+02:00","latency":669864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.443542492+02:00","latency":579676,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.453533351+02:00","latency":637801,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.463420638+02:00","latency":581509,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.473538189+02:00","latency":595814,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.483952853+02:00","latency":1093035,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.495048989+02:00","latency":652126,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.504028964+02:00","latency":557542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.514089276+02:00","latency":652843,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.524532239+02:00","latency":811259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.533557183+02:00","latency":588467,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.543533894+02:00","latency":574154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.554190946+02:00","latency":1096015,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.563112775+02:00","latency":622853,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.573139832+02:00","latency":602212,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.58634211+02:00","latency":3782542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.593310639+02:00","latency":620745,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.603339096+02:00","latency":626751,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.61335808+02:00","latency":586660,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.623724635+02:00","latency":909193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.633995984+02:00","latency":784414,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.644039742+02:00","latency":632404,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.654076816+02:00","latency":581110,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.663994938+02:00","latency":551598,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.67400765+02:00","latency":598331,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.687993272+02:00","latency":4502731,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.694082291+02:00","latency":576129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.705071727+02:00","latency":1437145,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.717019392+02:00","latency":4552547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.726499335+02:00","latency":578381,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.733178674+02:00","latency":586052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.743850703+02:00","latency":1300156,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.754005901+02:00","latency":600116,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.766343291+02:00","latency":531981,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.775139977+02:00","latency":2003748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.786358651+02:00","latency":555205,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.793979583+02:00","latency":583216,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.8040133+02:00","latency":581096,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.813961784+02:00","latency":543709,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.825416839+02:00","latency":606090,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.835571421+02:00","latency":2773998,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.843498664+02:00","latency":559635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.853484879+02:00","latency":569303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.863641258+02:00","latency":503868,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.873575519+02:00","latency":513076,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.883690589+02:00","latency":624350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.893631284+02:00","latency":534205,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.903702252+02:00","latency":662164,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.91372752+02:00","latency":564276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.924322679+02:00","latency":1194948,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.933537125+02:00","latency":696933,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.943512084+02:00","latency":550198,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.957335304+02:00","latency":1978895,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.963288387+02:00","latency":550303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.973314099+02:00","latency":541396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.983311137+02:00","latency":588153,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:05.99511118+02:00","latency":2248046,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.004477655+02:00","latency":1081178,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.013986763+02:00","latency":982381,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.023282664+02:00","latency":517458,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.033288651+02:00","latency":600159,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.04569332+02:00","latency":2842907,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.054560857+02:00","latency":662198,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.063597901+02:00","latency":622536,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.073605751+02:00","latency":605140,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.083668226+02:00","latency":650143,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.093686765+02:00","latency":579114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.103329285+02:00","latency":523775,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.113353255+02:00","latency":567752,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.123419431+02:00","latency":563041,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.133335965+02:00","latency":542185,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.144233864+02:00","latency":569838,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.153135144+02:00","latency":530183,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.163165662+02:00","latency":633804,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.173077893+02:00","latency":603824,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.183994675+02:00","latency":546108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.193222902+02:00","latency":648111,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.203586182+02:00","latency":704643,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.213717365+02:00","latency":633759,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.223730825+02:00","latency":623355,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.233840467+02:00","latency":686830,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.246051636+02:00","latency":2736900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.254166093+02:00","latency":669694,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.264097449+02:00","latency":614653,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.274985095+02:00","latency":1547872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.28405756+02:00","latency":657607,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.29405034+02:00","latency":634884,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.303959318+02:00","latency":566695,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.314001002+02:00","latency":680452,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.323076546+02:00","latency":632896,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.333035054+02:00","latency":575433,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.343955159+02:00","latency":558966,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.354096911+02:00","latency":754675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.365030347+02:00","latency":1262587,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.373644396+02:00","latency":648675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.385075863+02:00","latency":643527,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.393413365+02:00","latency":482795,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.403176025+02:00","latency":622193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.413352458+02:00","latency":590333,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.42326336+02:00","latency":517350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.436604037+02:00","latency":3608645,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.443587992+02:00","latency":544473,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.454056674+02:00","latency":1033920,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.464018746+02:00","latency":542830,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.475048199+02:00","latency":1566390,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.484046498+02:00","latency":650217,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.496980423+02:00","latency":604910,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.505786005+02:00","latency":2143266,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.513275606+02:00","latency":555836,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.523290697+02:00","latency":613353,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.533486048+02:00","latency":636016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.543474944+02:00","latency":584285,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.556067067+02:00","latency":2940292,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.567357918+02:00","latency":542739,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.573320978+02:00","latency":550068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.583836812+02:00","latency":544242,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.593909909+02:00","latency":549464,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.603623086+02:00","latency":609000,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.614974924+02:00","latency":1986255,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.623985452+02:00","latency":603235,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.633973693+02:00","latency":550281,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.644056021+02:00","latency":688847,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.653146911+02:00","latency":605659,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.663146733+02:00","latency":668207,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.673265697+02:00","latency":635410,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.684464588+02:00","latency":645736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.693465627+02:00","latency":632630,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.70353345+02:00","latency":637635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.713862392+02:00","latency":798624,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.724094013+02:00","latency":679310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.734380103+02:00","latency":725858,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.743629806+02:00","latency":647466,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.753701142+02:00","latency":670987,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.763837341+02:00","latency":579930,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.776455826+02:00","latency":3168085,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.783529267+02:00","latency":759086,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.793575277+02:00","latency":524262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.803808625+02:00","latency":793181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.814038995+02:00","latency":593568,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.824125752+02:00","latency":540187,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.836118014+02:00","latency":3540744,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.843205138+02:00","latency":610860,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.853426765+02:00","latency":749793,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.863536645+02:00","latency":591542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.874839442+02:00","latency":1661534,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.883875421+02:00","latency":530455,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.893748081+02:00","latency":644972,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.903786808+02:00","latency":622656,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.913840999+02:00","latency":525686,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.923848628+02:00","latency":571498,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.935383892+02:00","latency":1750810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.94315234+02:00","latency":643119,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.953385805+02:00","latency":784107,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.963639856+02:00","latency":569620,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.973856992+02:00","latency":659487,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.983871481+02:00","latency":636946,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:06.993482509+02:00","latency":591980,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.006366462+02:00","latency":546023,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.01342228+02:00","latency":609137,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.023452067+02:00","latency":591099,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.03377438+02:00","latency":796212,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.043812468+02:00","latency":586052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.053619696+02:00","latency":506450,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.064283374+02:00","latency":717942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.073294546+02:00","latency":551007,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.085709652+02:00","latency":3006343,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.097003308+02:00","latency":3778944,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.108149834+02:00","latency":4593731,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.113494786+02:00","latency":549631,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.123682687+02:00","latency":692464,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.135038773+02:00","latency":1947015,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.144216447+02:00","latency":702425,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.153172964+02:00","latency":533065,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.165621359+02:00","latency":2943177,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.173366452+02:00","latency":633780,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.183893198+02:00","latency":1164174,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.193883806+02:00","latency":562972,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.20392862+02:00","latency":633026,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.215329804+02:00","latency":1888235,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.223442799+02:00","latency":615053,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.23366562+02:00","latency":532073,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.243721787+02:00","latency":578253,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.255033296+02:00","latency":1909868,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.263980143+02:00","latency":564376,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.273873249+02:00","latency":548031,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.283694295+02:00","latency":522343,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.293725297+02:00","latency":621265,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.303727647+02:00","latency":559131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.314644143+02:00","latency":1562817,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.323666555+02:00","latency":584520,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.333625721+02:00","latency":512759,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.343607692+02:00","latency":619257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.353648572+02:00","latency":622655,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.363491391+02:00","latency":607256,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.376435018+02:00","latency":570862,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.384182821+02:00","latency":502157,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.393229158+02:00","latency":625849,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.403243713+02:00","latency":540143,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.413305717+02:00","latency":552798,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.423363877+02:00","latency":588484,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.433685721+02:00","latency":934662,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.444193732+02:00","latency":681020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.453176296+02:00","latency":562965,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.463232373+02:00","latency":622727,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.473198884+02:00","latency":590559,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.48323427+02:00","latency":605342,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.493264394+02:00","latency":606140,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.503345136+02:00","latency":660418,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.513563601+02:00","latency":754062,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.524648526+02:00","latency":1603775,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.533846581+02:00","latency":781285,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.543809822+02:00","latency":641534,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.553908482+02:00","latency":608237,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.564284587+02:00","latency":983552,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.573717759+02:00","latency":638160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.583870753+02:00","latency":682077,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.594081033+02:00","latency":677760,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.604114518+02:00","latency":643954,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.613329562+02:00","latency":754936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.623195556+02:00","latency":585291,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.63368084+02:00","latency":940631,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.643776073+02:00","latency":551445,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.653796588+02:00","latency":635214,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.663872649+02:00","latency":595987,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.673999116+02:00","latency":673420,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.684108566+02:00","latency":640419,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.693220954+02:00","latency":579698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.703064139+02:00","latency":582239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.714150265+02:00","latency":684796,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.723939669+02:00","latency":486148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.733899908+02:00","latency":568360,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.744407728+02:00","latency":1171836,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.753411969+02:00","latency":496810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.763473852+02:00","latency":627980,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.773442397+02:00","latency":543239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.783482686+02:00","latency":709979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.793449078+02:00","latency":600419,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.803522104+02:00","latency":602475,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.813442329+02:00","latency":442497,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.823738121+02:00","latency":928100,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.833635733+02:00","latency":489893,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.843637381+02:00","latency":583091,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.853558172+02:00","latency":499123,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.866996375+02:00","latency":3850158,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.873888544+02:00","latency":488100,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.883898357+02:00","latency":588524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.894358761+02:00","latency":556999,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.904172528+02:00","latency":890375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.913057089+02:00","latency":596408,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.924149886+02:00","latency":640405,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.932954553+02:00","latency":484808,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.944976541+02:00","latency":782068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.954018998+02:00","latency":607068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.964259954+02:00","latency":1045491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.974448713+02:00","latency":1258342,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.983832435+02:00","latency":873400,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:07.994244176+02:00","latency":621516,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.003008832+02:00","latency":533401,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.01518387+02:00","latency":1301290,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.024425985+02:00","latency":1388535,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.034186338+02:00","latency":1194618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.043424158+02:00","latency":591378,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.054612399+02:00","latency":1068818,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.064600758+02:00","latency":596231,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.073966891+02:00","latency":619817,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.084705681+02:00","latency":1874449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.094292918+02:00","latency":1524966,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.103371952+02:00","latency":614910,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.113444627+02:00","latency":565440,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.123482192+02:00","latency":628724,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.133525045+02:00","latency":687442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.143710099+02:00","latency":840856,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.154399769+02:00","latency":1032192,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.165623683+02:00","latency":680213,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.173975207+02:00","latency":627934,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.183249782+02:00","latency":810808,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.193352344+02:00","latency":641225,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.205866313+02:00","latency":2628524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.213990593+02:00","latency":642181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.223981418+02:00","latency":600902,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.234096894+02:00","latency":598308,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.244144004+02:00","latency":584012,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.253164649+02:00","latency":557254,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.263219369+02:00","latency":571756,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.275581398+02:00","latency":2891525,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.283866277+02:00","latency":762730,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.294080208+02:00","latency":697635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.303099386+02:00","latency":583762,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.315426498+02:00","latency":669956,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.323544093+02:00","latency":641678,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.333378187+02:00","latency":540380,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.343309788+02:00","latency":528900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.353303716+02:00","latency":605579,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.364289709+02:00","latency":1298515,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.373281214+02:00","latency":610550,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.383227907+02:00","latency":561375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.393265696+02:00","latency":498711,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.403194669+02:00","latency":504723,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.413244068+02:00","latency":609627,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.423193908+02:00","latency":574759,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.4332585+02:00","latency":566300,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.443305673+02:00","latency":487773,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.453554522+02:00","latency":837280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.463500826+02:00","latency":544665,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.473602294+02:00","latency":645708,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.48361488+02:00","latency":605270,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.493754258+02:00","latency":676068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.503823938+02:00","latency":490266,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.513851235+02:00","latency":657322,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.523898415+02:00","latency":667369,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.5341686+02:00","latency":788412,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.543214709+02:00","latency":687247,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.55323496+02:00","latency":643814,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.563191646+02:00","latency":677403,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.573879313+02:00","latency":1021023,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.584023543+02:00","latency":638374,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.593995861+02:00","latency":542914,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.603994153+02:00","latency":1489095,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.614020612+02:00","latency":528955,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.62413102+02:00","latency":701796,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.634117338+02:00","latency":645932,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.644010513+02:00","latency":600371,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.654012241+02:00","latency":1576450,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.664447574+02:00","latency":630286,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.673711999+02:00","latency":687358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.683609502+02:00","latency":578491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.694182481+02:00","latency":1137470,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.70417124+02:00","latency":738096,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.715386924+02:00","latency":571638,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.723377485+02:00","latency":614889,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.733445475+02:00","latency":627634,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.743296329+02:00","latency":612773,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.753275545+02:00","latency":565659,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.763439511+02:00","latency":741538,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.773104718+02:00","latency":586260,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.783210326+02:00","latency":669631,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.793984595+02:00","latency":1295434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.804112923+02:00","latency":654891,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.813705693+02:00","latency":511641,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.823778023+02:00","latency":566986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.839085226+02:00","latency":5773188,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.843063363+02:00","latency":589533,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.854120178+02:00","latency":599428,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.86311732+02:00","latency":633851,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.874375011+02:00","latency":1932821,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.883682647+02:00","latency":584673,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.894855777+02:00","latency":1694825,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.903838184+02:00","latency":540203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.913956289+02:00","latency":651303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.924755749+02:00","latency":778193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.937385518+02:00","latency":620123,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.943471274+02:00","latency":612084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.9532841+02:00","latency":530905,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.963352006+02:00","latency":549531,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.974846263+02:00","latency":1994908,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.987113232+02:00","latency":3883393,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:08.993078533+02:00","latency":590967,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.004302104+02:00","latency":758623,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.013340291+02:00","latency":607346,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.02371885+02:00","latency":803733,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.036375929+02:00","latency":566257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.043517624+02:00","latency":681048,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.053478466+02:00","latency":558887,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.064733783+02:00","latency":663205,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.080209215+02:00","latency":3988631,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:38:09.083308132+02:00","latency":650739,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"}]} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.txt b/test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.txt deleted file mode 100644 index 20e625c1e..000000000 --- a/test_results/grpc_load_tests/trading_service_orderbook_20251003_173658.txt +++ /dev/null @@ -1,6037 +0,0 @@ -{ - "date": "2025-10-03T17:38:19+02:00", - "endReason": "timeout", - "options": { - "call": "trading.TradingService.GetOrderBook", - "host": "localhost:50051", - "proto": "/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto", - "import-paths": [ - "/home/jgrusewski/Work/foxhunt/services/trading_service/proto", - "." - ], - "insecure": true, - "rps": 100, - "load-schedule": "const", - "load-start": 0, - "load-end": 0, - "load-step": 0, - "load-step-duration": 0, - "load-max-duration": 0, - "concurrency": 50, - "concurrency-schedule": "const", - "concurrency-start": 0, - "concurrency-end": 0, - "concurrency-step": 1, - "concurrency-step-duration": 0, - "concurrency-max-duration": 0, - "total": 2147483647, - "connections": 10, - "duration": 10000000000, - "timeout": 20000000000, - "dial-timeout": 10000000000, - "data": { - "depth": 10, - "symbol": "AAPL" - }, - "binary": false, - "CPUs": 16 - }, - "count": 997, - "total": 10014665212, - "average": 1187497, - "fastest": 0, - "slowest": 0, - "rps": 99.55400194560193, - "errorDistribution": { - "rpc error: code = Unauthenticated desc = Valid authentication required": 997 - }, - "statusCodeDistribution": { - "Unauthenticated": 997 - }, - "latencyDistribution": null, - "histogram": null, - "details": [ - { - "timestamp": "2025-10-03T17:38:09.171923709+02:00", - "latency": 1061416, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.181349369+02:00", - "latency": 649764, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.191345628+02:00", - "latency": 718171, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.201476185+02:00", - "latency": 577512, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.211582297+02:00", - "latency": 610047, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.221634621+02:00", - "latency": 580087, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.231569368+02:00", - "latency": 504150, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.244161223+02:00", - "latency": 2260816, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.252310887+02:00", - "latency": 656816, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.261602159+02:00", - "latency": 741956, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.272270162+02:00", - "latency": 602928, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.281264885+02:00", - "latency": 580839, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.291636378+02:00", - "latency": 836028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.301539779+02:00", - "latency": 579483, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.311714126+02:00", - "latency": 637785, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.321816707+02:00", - "latency": 623218, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.33185705+02:00", - "latency": 569942, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.34203629+02:00", - "latency": 705404, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.352334625+02:00", - "latency": 683331, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.361359481+02:00", - "latency": 676472, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.371529977+02:00", - "latency": 707825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.381878088+02:00", - "latency": 759515, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.3921739+02:00", - "latency": 764627, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.401935081+02:00", - "latency": 1247137, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.412043927+02:00", - "latency": 670560, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.425224247+02:00", - "latency": 3761533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.431921507+02:00", - "latency": 1037062, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.442756497+02:00", - "latency": 755568, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.4538045+02:00", - "latency": 825045, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.462057411+02:00", - "latency": 634544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.472366164+02:00", - "latency": 613798, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.481654118+02:00", - "latency": 738661, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.491764804+02:00", - "latency": 594640, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.504164016+02:00", - "latency": 2916240, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.511303883+02:00", - "latency": 564717, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.521638416+02:00", - "latency": 581203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.532534515+02:00", - "latency": 1262091, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.54168354+02:00", - "latency": 599717, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.551786035+02:00", - "latency": 640528, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.562012341+02:00", - "latency": 605531, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.572405924+02:00", - "latency": 703342, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.581531525+02:00", - "latency": 653100, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.591771336+02:00", - "latency": 660159, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.605368276+02:00", - "latency": 3852656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.611622943+02:00", - "latency": 605796, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.62372702+02:00", - "latency": 700644, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.631880704+02:00", - "latency": 557459, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.642176217+02:00", - "latency": 634511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.651235441+02:00", - "latency": 536099, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.661312477+02:00", - "latency": 633459, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.671461561+02:00", - "latency": 639491, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.681747687+02:00", - "latency": 732378, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.691710836+02:00", - "latency": 632271, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.701808895+02:00", - "latency": 625280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.711938943+02:00", - "latency": 692425, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.722502038+02:00", - "latency": 1127617, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.731315227+02:00", - "latency": 555065, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.741372593+02:00", - "latency": 615006, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.751500782+02:00", - "latency": 688046, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.761707683+02:00", - "latency": 714704, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.771895659+02:00", - "latency": 650201, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.782061748+02:00", - "latency": 704133, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.79316193+02:00", - "latency": 1246920, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.802276535+02:00", - "latency": 676720, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.811534566+02:00", - "latency": 750574, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.824069281+02:00", - "latency": 3073984, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.832074653+02:00", - "latency": 474987, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.844363035+02:00", - "latency": 2571675, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.851827653+02:00", - "latency": 572018, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.861876583+02:00", - "latency": 507165, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.873367166+02:00", - "latency": 1999189, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.881369045+02:00", - "latency": 462563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.893818832+02:00", - "latency": 3029165, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.901892289+02:00", - "latency": 550735, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.911976969+02:00", - "latency": 493839, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.921890367+02:00", - "latency": 493538, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.932044886+02:00", - "latency": 521115, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.942717852+02:00", - "latency": 1218808, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.952879071+02:00", - "latency": 1393746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.961957599+02:00", - "latency": 556326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.972140264+02:00", - "latency": 711219, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.982285012+02:00", - "latency": 595726, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:09.992264425+02:00", - "latency": 1404299, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.001391414+02:00", - "latency": 559581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.011825639+02:00", - "latency": 755332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.021924111+02:00", - "latency": 535517, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.033505473+02:00", - "latency": 628122, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.041534188+02:00", - "latency": 541431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.052023871+02:00", - "latency": 808863, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.06201274+02:00", - "latency": 541906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.072048025+02:00", - "latency": 569105, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.082624809+02:00", - "latency": 1025724, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.092134879+02:00", - "latency": 546669, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.102642103+02:00", - "latency": 1021384, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.111811361+02:00", - "latency": 688156, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.122761589+02:00", - "latency": 1593758, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.131821369+02:00", - "latency": 598113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.141882219+02:00", - "latency": 681052, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.153925627+02:00", - "latency": 2701234, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.16189023+02:00", - "latency": 530565, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.172049815+02:00", - "latency": 736609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.184794368+02:00", - "latency": 2379350, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.193710581+02:00", - "latency": 2395362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.202743281+02:00", - "latency": 598370, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.211809865+02:00", - "latency": 630924, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.223078121+02:00", - "latency": 2187133, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.232651897+02:00", - "latency": 721939, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.241649484+02:00", - "latency": 578658, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.252107435+02:00", - "latency": 757015, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.262153703+02:00", - "latency": 649431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.272263728+02:00", - "latency": 635242, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.284961377+02:00", - "latency": 801814, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.292107606+02:00", - "latency": 672731, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.302669791+02:00", - "latency": 738399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.311972022+02:00", - "latency": 721542, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.322722872+02:00", - "latency": 1361130, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.331898627+02:00", - "latency": 604323, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.343758699+02:00", - "latency": 2278527, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.352793819+02:00", - "latency": 1503015, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.361535208+02:00", - "latency": 725615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.37146503+02:00", - "latency": 535861, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.381520092+02:00", - "latency": 584026, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.391599668+02:00", - "latency": 613659, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.402697362+02:00", - "latency": 1623761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.412100066+02:00", - "latency": 597159, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.425437877+02:00", - "latency": 546502, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.431617782+02:00", - "latency": 584028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.441975725+02:00", - "latency": 847437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.451404751+02:00", - "latency": 595108, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.46141362+02:00", - "latency": 546399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.471888853+02:00", - "latency": 552926, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.482154799+02:00", - "latency": 738576, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.492862004+02:00", - "latency": 629841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.502087387+02:00", - "latency": 621884, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.513889581+02:00", - "latency": 2314631, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.522126891+02:00", - "latency": 655406, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.532069172+02:00", - "latency": 548570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.544725992+02:00", - "latency": 2829496, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.555144731+02:00", - "latency": 3780170, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.561491377+02:00", - "latency": 598204, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.571996095+02:00", - "latency": 700625, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.582145825+02:00", - "latency": 635149, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.592248481+02:00", - "latency": 569903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.601215301+02:00", - "latency": 551671, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.611291656+02:00", - "latency": 541712, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.621314368+02:00", - "latency": 482352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.631443044+02:00", - "latency": 579605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.641466897+02:00", - "latency": 530048, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.651509459+02:00", - "latency": 566784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.661582181+02:00", - "latency": 603855, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.671650076+02:00", - "latency": 553977, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.681744911+02:00", - "latency": 579870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.691863191+02:00", - "latency": 605007, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.702030148+02:00", - "latency": 609698, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.712195099+02:00", - "latency": 684561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.721184395+02:00", - "latency": 578507, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.732616131+02:00", - "latency": 2001758, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.742252767+02:00", - "latency": 619514, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.751312902+02:00", - "latency": 637437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.761730121+02:00", - "latency": 796190, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.771823652+02:00", - "latency": 581369, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.781908313+02:00", - "latency": 612727, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.792016391+02:00", - "latency": 652920, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.801980661+02:00", - "latency": 606498, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.812176136+02:00", - "latency": 655737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.821366701+02:00", - "latency": 755690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.850003555+02:00", - "latency": 1855542, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.850226557+02:00", - "latency": 2336250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.851854665+02:00", - "latency": 292963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.861699648+02:00", - "latency": 1029810, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.871734758+02:00", - "latency": 556338, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.886700617+02:00", - "latency": 5477006, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.893989146+02:00", - "latency": 2756894, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.901954077+02:00", - "latency": 517076, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.911844221+02:00", - "latency": 490574, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.921843831+02:00", - "latency": 536901, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.931680188+02:00", - "latency": 495225, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.942844465+02:00", - "latency": 1503120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.951850224+02:00", - "latency": 514751, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.962000696+02:00", - "latency": 689655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.972129949+02:00", - "latency": 564279, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.981154492+02:00", - "latency": 486471, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:10.992146806+02:00", - "latency": 519413, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.001491019+02:00", - "latency": 548977, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.014406852+02:00", - "latency": 531422, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.0219734+02:00", - "latency": 562721, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.032028295+02:00", - "latency": 502743, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.042022373+02:00", - "latency": 540856, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.052794608+02:00", - "latency": 1351637, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.061841786+02:00", - "latency": 550849, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.072010805+02:00", - "latency": 601341, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.082065525+02:00", - "latency": 573188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.092197196+02:00", - "latency": 585603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.101149988+02:00", - "latency": 537508, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.113403171+02:00", - "latency": 508690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.121473935+02:00", - "latency": 533729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.13151631+02:00", - "latency": 514567, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.141556296+02:00", - "latency": 560272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.151564202+02:00", - "latency": 535277, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.161698041+02:00", - "latency": 538836, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.172088862+02:00", - "latency": 968378, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.182208787+02:00", - "latency": 580795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.191096195+02:00", - "latency": 500925, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.202127848+02:00", - "latency": 502080, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.211221145+02:00", - "latency": 521257, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.224012306+02:00", - "latency": 3317757, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.231969764+02:00", - "latency": 532781, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.242081231+02:00", - "latency": 563721, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.251938768+02:00", - "latency": 557727, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.263241105+02:00", - "latency": 1318485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.271817277+02:00", - "latency": 610250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.282007214+02:00", - "latency": 611609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.29200889+02:00", - "latency": 527527, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.302091585+02:00", - "latency": 554276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.312049782+02:00", - "latency": 509903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.321383273+02:00", - "latency": 537038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.331634409+02:00", - "latency": 723575, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.342092845+02:00", - "latency": 565766, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.356982634+02:00", - "latency": 5358871, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.361368762+02:00", - "latency": 507017, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.371357924+02:00", - "latency": 520223, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.381337958+02:00", - "latency": 517067, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.391274369+02:00", - "latency": 467867, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.404468011+02:00", - "latency": 3587735, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.414949956+02:00", - "latency": 3825425, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.422009593+02:00", - "latency": 583295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.432039318+02:00", - "latency": 514666, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.44240937+02:00", - "latency": 723298, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.451497643+02:00", - "latency": 474069, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.467486767+02:00", - "latency": 6518215, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.471491853+02:00", - "latency": 485974, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.481541524+02:00", - "latency": 653036, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.49370951+02:00", - "latency": 2720667, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.501847498+02:00", - "latency": 599565, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.51178371+02:00", - "latency": 488472, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.521897988+02:00", - "latency": 600403, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.531981279+02:00", - "latency": 594083, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.54212415+02:00", - "latency": 606254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.552406485+02:00", - "latency": 533222, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.567472319+02:00", - "latency": 5506111, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.573739719+02:00", - "latency": 642379, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.581914891+02:00", - "latency": 452905, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.591892317+02:00", - "latency": 549014, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.602111788+02:00", - "latency": 688363, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.612318411+02:00", - "latency": 696168, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.623648843+02:00", - "latency": 2833897, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.631693339+02:00", - "latency": 520874, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.641828351+02:00", - "latency": 675515, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.651899572+02:00", - "latency": 572252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.661937752+02:00", - "latency": 569190, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.672073307+02:00", - "latency": 656793, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.682206002+02:00", - "latency": 614893, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.691370781+02:00", - "latency": 617846, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.703871075+02:00", - "latency": 1005101, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.711894241+02:00", - "latency": 524397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.722128129+02:00", - "latency": 716505, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.732002335+02:00", - "latency": 492497, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.742085878+02:00", - "latency": 612813, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.752881771+02:00", - "latency": 1345141, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.761892046+02:00", - "latency": 517905, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.774087101+02:00", - "latency": 2665925, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.782233502+02:00", - "latency": 634834, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.791239495+02:00", - "latency": 575922, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.803939612+02:00", - "latency": 3212795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.812205857+02:00", - "latency": 614039, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.821923562+02:00", - "latency": 1184042, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.832322429+02:00", - "latency": 761181, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.841308251+02:00", - "latency": 671962, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.851415665+02:00", - "latency": 601823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.86161124+02:00", - "latency": 719824, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.871839885+02:00", - "latency": 740152, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.88205173+02:00", - "latency": 742742, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.892317389+02:00", - "latency": 861149, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.901478557+02:00", - "latency": 641845, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.911656873+02:00", - "latency": 650869, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.921679078+02:00", - "latency": 664646, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.931789386+02:00", - "latency": 706402, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.94202367+02:00", - "latency": 675388, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.952651699+02:00", - "latency": 953125, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.961736356+02:00", - "latency": 590098, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.971858495+02:00", - "latency": 698207, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.982005812+02:00", - "latency": 606024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:11.992265318+02:00", - "latency": 752909, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.001314858+02:00", - "latency": 676714, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.011675721+02:00", - "latency": 775087, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.022069568+02:00", - "latency": 756843, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.032152493+02:00", - "latency": 542570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.042470592+02:00", - "latency": 794939, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.051622768+02:00", - "latency": 614999, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.061772199+02:00", - "latency": 677223, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.071952485+02:00", - "latency": 650208, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.082424423+02:00", - "latency": 538818, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.092498372+02:00", - "latency": 616246, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.101642636+02:00", - "latency": 586290, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.111661804+02:00", - "latency": 545696, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.121761775+02:00", - "latency": 580947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.131842825+02:00", - "latency": 560465, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.141902689+02:00", - "latency": 543690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.152013866+02:00", - "latency": 607940, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.16206888+02:00", - "latency": 573497, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.172096381+02:00", - "latency": 661688, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.18158308+02:00", - "latency": 550014, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.191839553+02:00", - "latency": 540992, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.201957424+02:00", - "latency": 802764, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.212149465+02:00", - "latency": 695893, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.221106005+02:00", - "latency": 464792, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.232509397+02:00", - "latency": 1633971, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.243714526+02:00", - "latency": 1777032, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.252749133+02:00", - "latency": 1557611, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.262134343+02:00", - "latency": 708253, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.271466268+02:00", - "latency": 505908, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.28162317+02:00", - "latency": 659842, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.29183125+02:00", - "latency": 752452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.302357651+02:00", - "latency": 563928, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.312300379+02:00", - "latency": 500432, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.321498476+02:00", - "latency": 672410, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.331674142+02:00", - "latency": 702492, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.341742541+02:00", - "latency": 608956, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.354371295+02:00", - "latency": 3137442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.361838094+02:00", - "latency": 531586, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.371990391+02:00", - "latency": 700804, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.38206114+02:00", - "latency": 570791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.392035006+02:00", - "latency": 528987, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.403219813+02:00", - "latency": 1731622, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.411269903+02:00", - "latency": 571579, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.421315294+02:00", - "latency": 475226, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.43164414+02:00", - "latency": 880978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.441827059+02:00", - "latency": 595779, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.451901658+02:00", - "latency": 567804, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.462198683+02:00", - "latency": 804664, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.473989382+02:00", - "latency": 2042162, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.482168762+02:00", - "latency": 669989, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.492227371+02:00", - "latency": 1374481, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.501349867+02:00", - "latency": 544232, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.51204106+02:00", - "latency": 1237345, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.522164073+02:00", - "latency": 565118, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.532255799+02:00", - "latency": 551901, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.541494588+02:00", - "latency": 575176, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.551630198+02:00", - "latency": 560006, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.561829868+02:00", - "latency": 638104, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.574211282+02:00", - "latency": 2856015, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.581318749+02:00", - "latency": 586891, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.591443061+02:00", - "latency": 596716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.601872822+02:00", - "latency": 791112, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.611873509+02:00", - "latency": 578730, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.624620261+02:00", - "latency": 3313824, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.631767787+02:00", - "latency": 610941, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.643853262+02:00", - "latency": 567841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.652028291+02:00", - "latency": 637406, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.662808641+02:00", - "latency": 1003610, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.671892658+02:00", - "latency": 604265, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.684381118+02:00", - "latency": 517508, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.691583097+02:00", - "latency": 690393, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.701781721+02:00", - "latency": 611711, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.711841046+02:00", - "latency": 577307, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.72505554+02:00", - "latency": 3729305, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.732134358+02:00", - "latency": 596180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.741731918+02:00", - "latency": 732773, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.751600802+02:00", - "latency": 528659, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.761691404+02:00", - "latency": 592587, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.77175996+02:00", - "latency": 611908, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.782051315+02:00", - "latency": 773783, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.792066237+02:00", - "latency": 615589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.802188691+02:00", - "latency": 618635, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.812179262+02:00", - "latency": 685290, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.821208544+02:00", - "latency": 568593, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.831258959+02:00", - "latency": 649408, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.841314272+02:00", - "latency": 580614, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.851521653+02:00", - "latency": 744845, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.861505245+02:00", - "latency": 551269, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.871595858+02:00", - "latency": 651534, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.881771441+02:00", - "latency": 726988, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.891866337+02:00", - "latency": 603513, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.902012573+02:00", - "latency": 692471, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.912339434+02:00", - "latency": 960359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.921532613+02:00", - "latency": 699977, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.931555051+02:00", - "latency": 557035, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.941695027+02:00", - "latency": 674089, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.951730226+02:00", - "latency": 645577, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.961809155+02:00", - "latency": 638968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.971972233+02:00", - "latency": 714387, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.982255395+02:00", - "latency": 713630, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:12.991327293+02:00", - "latency": 670577, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.001523201+02:00", - "latency": 689082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.011844829+02:00", - "latency": 909031, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.021930554+02:00", - "latency": 653819, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.03222311+02:00", - "latency": 637157, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.04135637+02:00", - "latency": 700131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.051411243+02:00", - "latency": 631402, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.061620425+02:00", - "latency": 704468, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.071647688+02:00", - "latency": 511106, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.081776992+02:00", - "latency": 667197, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.091766291+02:00", - "latency": 589512, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.102159653+02:00", - "latency": 827270, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.111494379+02:00", - "latency": 828387, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.121658332+02:00", - "latency": 699254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.131769107+02:00", - "latency": 743600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.141898134+02:00", - "latency": 651359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.152378084+02:00", - "latency": 718391, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.161592912+02:00", - "latency": 644784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.171809468+02:00", - "latency": 631380, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.181990617+02:00", - "latency": 674825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.192065596+02:00", - "latency": 589259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.202127885+02:00", - "latency": 619933, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.212213195+02:00", - "latency": 642135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.221532243+02:00", - "latency": 796140, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.231739363+02:00", - "latency": 731325, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.241931586+02:00", - "latency": 700696, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.252073822+02:00", - "latency": 678276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.262469656+02:00", - "latency": 826860, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.271628131+02:00", - "latency": 647614, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.281825422+02:00", - "latency": 662690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.291905053+02:00", - "latency": 580591, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.302175644+02:00", - "latency": 656156, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.311398253+02:00", - "latency": 663397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.321624573+02:00", - "latency": 615366, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.331746986+02:00", - "latency": 600764, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.341874152+02:00", - "latency": 620038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.352057424+02:00", - "latency": 622485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.362275582+02:00", - "latency": 587064, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.372637999+02:00", - "latency": 1738726, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.38172539+02:00", - "latency": 607784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.391344385+02:00", - "latency": 538325, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.401353209+02:00", - "latency": 549637, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.411637605+02:00", - "latency": 699136, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.422624093+02:00", - "latency": 1158720, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.433135477+02:00", - "latency": 601771, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.442020497+02:00", - "latency": 492394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.452170909+02:00", - "latency": 539174, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.4610894+02:00", - "latency": 502593, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.472166432+02:00", - "latency": 633778, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.48221002+02:00", - "latency": 575934, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.494236297+02:00", - "latency": 1612492, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.501208985+02:00", - "latency": 485295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.511466971+02:00", - "latency": 607672, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.521856895+02:00", - "latency": 846905, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.531941061+02:00", - "latency": 564516, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.544990726+02:00", - "latency": 3635626, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.552354706+02:00", - "latency": 668492, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.561567954+02:00", - "latency": 672489, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.571801025+02:00", - "latency": 619329, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.581893266+02:00", - "latency": 664816, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.592007913+02:00", - "latency": 588768, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.602098054+02:00", - "latency": 620709, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.612139337+02:00", - "latency": 554147, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.622302929+02:00", - "latency": 623355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.63136589+02:00", - "latency": 560220, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.641403835+02:00", - "latency": 529283, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.651413811+02:00", - "latency": 545492, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.661582817+02:00", - "latency": 654619, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.671768942+02:00", - "latency": 626735, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.681820571+02:00", - "latency": 559776, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.692019787+02:00", - "latency": 710679, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.701999624+02:00", - "latency": 517209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.725696981+02:00", - "latency": 2001156, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.726088824+02:00", - "latency": 1999561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.73216817+02:00", - "latency": 645749, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.741364119+02:00", - "latency": 708437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.751575785+02:00", - "latency": 731833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.762338382+02:00", - "latency": 1040937, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.771408145+02:00", - "latency": 627800, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.781614262+02:00", - "latency": 795112, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.791748939+02:00", - "latency": 719979, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.801788917+02:00", - "latency": 816651, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.811877094+02:00", - "latency": 749028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.822108441+02:00", - "latency": 817255, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.832310841+02:00", - "latency": 712342, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.841492096+02:00", - "latency": 708142, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.851575099+02:00", - "latency": 755397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.861959816+02:00", - "latency": 772958, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.872133772+02:00", - "latency": 740340, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.882407992+02:00", - "latency": 824854, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.891459897+02:00", - "latency": 666091, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.901652428+02:00", - "latency": 690418, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.911852526+02:00", - "latency": 809177, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.92404502+02:00", - "latency": 2004010, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.932069993+02:00", - "latency": 616170, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.94201084+02:00", - "latency": 619550, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.952105379+02:00", - "latency": 638991, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.96212066+02:00", - "latency": 599623, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.972279434+02:00", - "latency": 736831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.981346727+02:00", - "latency": 561203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:13.99142347+02:00", - "latency": 661660, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.001542069+02:00", - "latency": 697895, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.011579354+02:00", - "latency": 684534, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.022571396+02:00", - "latency": 1411175, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.031558707+02:00", - "latency": 570657, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.041457838+02:00", - "latency": 576952, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.051374563+02:00", - "latency": 586766, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.061506093+02:00", - "latency": 644974, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.071698379+02:00", - "latency": 823560, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.08185533+02:00", - "latency": 614980, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.092406905+02:00", - "latency": 870930, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.101540684+02:00", - "latency": 643474, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.111564397+02:00", - "latency": 656635, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.121626285+02:00", - "latency": 683190, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.131694344+02:00", - "latency": 649971, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.141802156+02:00", - "latency": 676203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.15252864+02:00", - "latency": 1202145, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.161597748+02:00", - "latency": 646452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.171580828+02:00", - "latency": 630779, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.18187049+02:00", - "latency": 830543, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.191802201+02:00", - "latency": 633888, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.201903843+02:00", - "latency": 647393, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.212163913+02:00", - "latency": 746714, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.222065433+02:00", - "latency": 1489189, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.232118038+02:00", - "latency": 558734, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.242314533+02:00", - "latency": 658584, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.251252846+02:00", - "latency": 574149, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.261273817+02:00", - "latency": 672700, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.271102317+02:00", - "latency": 559904, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.282123237+02:00", - "latency": 619480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.292081579+02:00", - "latency": 606736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.302043789+02:00", - "latency": 569682, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.312070981+02:00", - "latency": 608451, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.322062344+02:00", - "latency": 594300, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.332133055+02:00", - "latency": 709336, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.342061384+02:00", - "latency": 550659, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.352125824+02:00", - "latency": 661580, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.362168384+02:00", - "latency": 710310, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.372134472+02:00", - "latency": 592434, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.38197231+02:00", - "latency": 541607, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.391817304+02:00", - "latency": 497735, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.402061262+02:00", - "latency": 592380, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.414452337+02:00", - "latency": 2336144, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.422442649+02:00", - "latency": 629032, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.433703936+02:00", - "latency": 2612335, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.442001818+02:00", - "latency": 745338, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.451986678+02:00", - "latency": 637272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.462161079+02:00", - "latency": 795435, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.471620616+02:00", - "latency": 891254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.48444993+02:00", - "latency": 1869324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.491483674+02:00", - "latency": 832667, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.503433961+02:00", - "latency": 611205, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.511375933+02:00", - "latency": 563371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.521710224+02:00", - "latency": 784204, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.531767822+02:00", - "latency": 637350, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.54343343+02:00", - "latency": 615066, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.551672991+02:00", - "latency": 798755, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.56171039+02:00", - "latency": 755897, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.57191963+02:00", - "latency": 712411, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.582107584+02:00", - "latency": 762052, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.591978308+02:00", - "latency": 631194, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.602160611+02:00", - "latency": 769316, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.611128433+02:00", - "latency": 532772, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.623603622+02:00", - "latency": 2065685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.631704068+02:00", - "latency": 691983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.641959474+02:00", - "latency": 764350, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.651867408+02:00", - "latency": 592049, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.661900465+02:00", - "latency": 613855, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.672332709+02:00", - "latency": 514962, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.681376468+02:00", - "latency": 613981, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.691309687+02:00", - "latency": 576921, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.704785056+02:00", - "latency": 4112274, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.711781279+02:00", - "latency": 565606, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.721801172+02:00", - "latency": 623389, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.731691+02:00", - "latency": 532886, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.741784256+02:00", - "latency": 699024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.751822984+02:00", - "latency": 589050, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.762897211+02:00", - "latency": 1642209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.771797785+02:00", - "latency": 520464, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.781854325+02:00", - "latency": 688633, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.791882917+02:00", - "latency": 565931, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.801944433+02:00", - "latency": 681948, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.812001496+02:00", - "latency": 614194, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.822613855+02:00", - "latency": 873377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.831335321+02:00", - "latency": 578759, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.841367237+02:00", - "latency": 617263, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.852846182+02:00", - "latency": 2025209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.862172436+02:00", - "latency": 754636, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.872262365+02:00", - "latency": 860833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.88174593+02:00", - "latency": 835848, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.89211871+02:00", - "latency": 944381, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.902487342+02:00", - "latency": 699932, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.91200644+02:00", - "latency": 924320, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.921566573+02:00", - "latency": 737000, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.933696567+02:00", - "latency": 2721081, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.941668508+02:00", - "latency": 553334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.951665092+02:00", - "latency": 605345, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.961655621+02:00", - "latency": 557996, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.971478353+02:00", - "latency": 481058, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.981415047+02:00", - "latency": 555808, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:14.991322721+02:00", - "latency": 522027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.001330778+02:00", - "latency": 589674, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.011506571+02:00", - "latency": 774139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.021366127+02:00", - "latency": 568490, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.0313986+02:00", - "latency": 652275, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.041380334+02:00", - "latency": 603745, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.054720538+02:00", - "latency": 3950483, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.061697449+02:00", - "latency": 528680, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.071662333+02:00", - "latency": 544201, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.081681998+02:00", - "latency": 544024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.09177391+02:00", - "latency": 574136, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.101729545+02:00", - "latency": 562914, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.111917903+02:00", - "latency": 733341, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.121861464+02:00", - "latency": 659241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.131873386+02:00", - "latency": 521640, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.141842743+02:00", - "latency": 607881, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.151262057+02:00", - "latency": 613341, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.162086468+02:00", - "latency": 571561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.172457318+02:00", - "latency": 546040, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.183716027+02:00", - "latency": 2753581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.192473659+02:00", - "latency": 518744, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.201182192+02:00", - "latency": 529862, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.211657674+02:00", - "latency": 566446, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.221592863+02:00", - "latency": 571900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.231736966+02:00", - "latency": 713345, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.241700378+02:00", - "latency": 608508, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.251184926+02:00", - "latency": 564188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.261165729+02:00", - "latency": 643428, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.27493922+02:00", - "latency": 3471305, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.282020609+02:00", - "latency": 683440, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.292056387+02:00", - "latency": 607151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.302178643+02:00", - "latency": 698879, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.31409993+02:00", - "latency": 2596846, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.321649688+02:00", - "latency": 788606, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.331793817+02:00", - "latency": 807421, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.341702701+02:00", - "latency": 522858, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.351886234+02:00", - "latency": 741303, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.361710566+02:00", - "latency": 556592, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.373967046+02:00", - "latency": 2819130, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.38193456+02:00", - "latency": 548316, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.391892877+02:00", - "latency": 496130, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.404836542+02:00", - "latency": 3548333, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.411853658+02:00", - "latency": 575496, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.422620726+02:00", - "latency": 1290367, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.432891656+02:00", - "latency": 1548874, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.441913087+02:00", - "latency": 571837, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.45184985+02:00", - "latency": 524188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.464125219+02:00", - "latency": 2752331, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.471987149+02:00", - "latency": 564600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.481989103+02:00", - "latency": 580997, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.492059515+02:00", - "latency": 616141, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.501508382+02:00", - "latency": 807427, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.511277134+02:00", - "latency": 494882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.526156119+02:00", - "latency": 1194857, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.532119494+02:00", - "latency": 564273, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.542768377+02:00", - "latency": 1248461, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.551995459+02:00", - "latency": 669030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.563280767+02:00", - "latency": 477697, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.571208256+02:00", - "latency": 520680, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.58132133+02:00", - "latency": 629225, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.591454412+02:00", - "latency": 635996, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.601570864+02:00", - "latency": 601293, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.611474964+02:00", - "latency": 526241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.621334903+02:00", - "latency": 476235, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.631361599+02:00", - "latency": 584341, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.641381129+02:00", - "latency": 527501, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.651660706+02:00", - "latency": 808395, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.661640597+02:00", - "latency": 507194, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.67167658+02:00", - "latency": 604716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.681597687+02:00", - "latency": 513279, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.691756947+02:00", - "latency": 695315, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.701808989+02:00", - "latency": 576634, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.711883139+02:00", - "latency": 623868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.721853917+02:00", - "latency": 555125, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.735050871+02:00", - "latency": 3747625, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.742043015+02:00", - "latency": 529270, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.751974399+02:00", - "latency": 523760, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.7621131+02:00", - "latency": 623524, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.7743088+02:00", - "latency": 523268, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.781350835+02:00", - "latency": 617473, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.791358262+02:00", - "latency": 491470, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.801537579+02:00", - "latency": 653271, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.811678346+02:00", - "latency": 707601, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.821702901+02:00", - "latency": 549126, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.831619938+02:00", - "latency": 524357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.841646693+02:00", - "latency": 553809, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.851353549+02:00", - "latency": 673945, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.861356569+02:00", - "latency": 586853, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.871380328+02:00", - "latency": 615599, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.881417369+02:00", - "latency": 548488, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.891425599+02:00", - "latency": 543796, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.90144656+02:00", - "latency": 585122, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.911588533+02:00", - "latency": 662497, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.922849686+02:00", - "latency": 1893197, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.931803601+02:00", - "latency": 529600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.941909533+02:00", - "latency": 676866, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.99269817+02:00", - "latency": 1540906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.993164465+02:00", - "latency": 1458738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.993207422+02:00", - "latency": 1645154, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.993239128+02:00", - "latency": 1409334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:15.993505696+02:00", - "latency": 2107716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.00173171+02:00", - "latency": 605137, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.019815668+02:00", - "latency": 3816718, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.021588241+02:00", - "latency": 433736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.036711098+02:00", - "latency": 3887431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.046808853+02:00", - "latency": 5607894, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.053357771+02:00", - "latency": 561758, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.068142485+02:00", - "latency": 2922847, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.080735961+02:00", - "latency": 7857051, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.095452137+02:00", - "latency": 608338, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.118360862+02:00", - "latency": 3398303, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.118440713+02:00", - "latency": 37459658, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.129353809+02:00", - "latency": 500201, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.136002486+02:00", - "latency": 35135731, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.139729162+02:00", - "latency": 3805114, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.144540306+02:00", - "latency": 953182, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.165019612+02:00", - "latency": 12172696, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.16574888+02:00", - "latency": 485382, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.17168234+02:00", - "latency": 519107, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.190020468+02:00", - "latency": 6135394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.19529254+02:00", - "latency": 474167, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.207070019+02:00", - "latency": 4243561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.211450808+02:00", - "latency": 480201, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.221578329+02:00", - "latency": 594293, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.232117971+02:00", - "latency": 576916, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.242102009+02:00", - "latency": 526135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.252233175+02:00", - "latency": 505216, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.261118515+02:00", - "latency": 458885, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.271649643+02:00", - "latency": 476168, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.281506068+02:00", - "latency": 499651, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.291339511+02:00", - "latency": 473303, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.301260354+02:00", - "latency": 507990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.314372261+02:00", - "latency": 553367, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.324289848+02:00", - "latency": 501584, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.331143906+02:00", - "latency": 533094, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.342284852+02:00", - "latency": 742298, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.351520325+02:00", - "latency": 572356, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.362521633+02:00", - "latency": 492047, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.371443061+02:00", - "latency": 495739, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.383623943+02:00", - "latency": 807240, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.391256568+02:00", - "latency": 614062, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.401330942+02:00", - "latency": 465241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.413384089+02:00", - "latency": 537334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.423419965+02:00", - "latency": 556435, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.431432869+02:00", - "latency": 576540, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.441510359+02:00", - "latency": 498971, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.452481948+02:00", - "latency": 513950, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.461293204+02:00", - "latency": 450088, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.471187975+02:00", - "latency": 474685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.481064682+02:00", - "latency": 465879, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.49166942+02:00", - "latency": 544768, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.501667374+02:00", - "latency": 603783, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.511612114+02:00", - "latency": 494834, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.521671108+02:00", - "latency": 638069, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.531584848+02:00", - "latency": 490898, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.541400127+02:00", - "latency": 454703, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.551357681+02:00", - "latency": 513851, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.561304877+02:00", - "latency": 531004, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.571165372+02:00", - "latency": 494337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.582333298+02:00", - "latency": 502356, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.591211708+02:00", - "latency": 480903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.60105039+02:00", - "latency": 509407, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.611373408+02:00", - "latency": 620399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.621419494+02:00", - "latency": 598014, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.631857788+02:00", - "latency": 484918, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.641889583+02:00", - "latency": 562935, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.651815083+02:00", - "latency": 535441, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.661668066+02:00", - "latency": 408207, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.671513682+02:00", - "latency": 457620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.681292349+02:00", - "latency": 427964, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.691273323+02:00", - "latency": 434998, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.701190117+02:00", - "latency": 487442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.711091446+02:00", - "latency": 522191, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.721983824+02:00", - "latency": 500723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.73540237+02:00", - "latency": 428392, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.741302439+02:00", - "latency": 494779, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.751222257+02:00", - "latency": 512096, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.761105769+02:00", - "latency": 536478, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.772425596+02:00", - "latency": 672252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.784287801+02:00", - "latency": 494422, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.793962551+02:00", - "latency": 3015558, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.803964485+02:00", - "latency": 2575718, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.816014384+02:00", - "latency": 4616741, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.821659934+02:00", - "latency": 490909, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.835435859+02:00", - "latency": 622088, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.841399187+02:00", - "latency": 484583, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.851223539+02:00", - "latency": 430113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.861208363+02:00", - "latency": 615471, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.871246336+02:00", - "latency": 679406, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.883343754+02:00", - "latency": 535130, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.891216888+02:00", - "latency": 445407, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.902168561+02:00", - "latency": 1546810, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.912069147+02:00", - "latency": 454394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.924433322+02:00", - "latency": 2657674, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.931275444+02:00", - "latency": 510642, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.946431608+02:00", - "latency": 5832306, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.953311034+02:00", - "latency": 515225, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.964357657+02:00", - "latency": 528581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.971472697+02:00", - "latency": 533259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.981723645+02:00", - "latency": 574624, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:16.991665351+02:00", - "latency": 537104, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.004413869+02:00", - "latency": 585990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.013484048+02:00", - "latency": 649597, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.021513712+02:00", - "latency": 532125, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.031388271+02:00", - "latency": 485379, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.041278856+02:00", - "latency": 487032, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.051194927+02:00", - "latency": 507983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.063582477+02:00", - "latency": 583000, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.072430031+02:00", - "latency": 547225, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.084409283+02:00", - "latency": 567558, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.091169272+02:00", - "latency": 438323, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.101215639+02:00", - "latency": 605877, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.11141742+02:00", - "latency": 574258, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.123951407+02:00", - "latency": 2938527, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.132539357+02:00", - "latency": 509945, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.141464269+02:00", - "latency": 506239, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.154799937+02:00", - "latency": 4211193, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.16442054+02:00", - "latency": 578095, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.17111524+02:00", - "latency": 503121, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.182092673+02:00", - "latency": 502107, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.191207918+02:00", - "latency": 590340, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.201285441+02:00", - "latency": 666168, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.22215443+02:00", - "latency": 11527043, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.226742034+02:00", - "latency": 4334796, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.236367228+02:00", - "latency": 543822, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.259690727+02:00", - "latency": 14349978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.263699631+02:00", - "latency": 3196880, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.263761327+02:00", - "latency": 3814360, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.274386089+02:00", - "latency": 570252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.285478727+02:00", - "latency": 635485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.291524004+02:00", - "latency": 488811, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.312484524+02:00", - "latency": 679772, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.312751995+02:00", - "latency": 3943706, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.321834629+02:00", - "latency": 564946, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.331776517+02:00", - "latency": 518470, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.341702393+02:00", - "latency": 522160, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.355354527+02:00", - "latency": 535199, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.3614534+02:00", - "latency": 660569, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.373674673+02:00", - "latency": 564004, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.381990699+02:00", - "latency": 580679, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.391915304+02:00", - "latency": 506614, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.401807983+02:00", - "latency": 466456, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.411669667+02:00", - "latency": 471706, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.422379405+02:00", - "latency": 567746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.431266566+02:00", - "latency": 492046, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.441116502+02:00", - "latency": 414421, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.452001551+02:00", - "latency": 497478, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.461835503+02:00", - "latency": 448998, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.47215788+02:00", - "latency": 733056, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.481779596+02:00", - "latency": 498864, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.493858935+02:00", - "latency": 459139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.501823088+02:00", - "latency": 537659, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.511955815+02:00", - "latency": 610797, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.521837687+02:00", - "latency": 473898, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.53171199+02:00", - "latency": 477139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.541631258+02:00", - "latency": 478125, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.551448515+02:00", - "latency": 439627, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.561352265+02:00", - "latency": 459259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.571327113+02:00", - "latency": 479191, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.581112622+02:00", - "latency": 406312, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.591963719+02:00", - "latency": 482599, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.602106746+02:00", - "latency": 744224, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.61209773+02:00", - "latency": 516605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.622039147+02:00", - "latency": 541124, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.631277112+02:00", - "latency": 483841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.641239583+02:00", - "latency": 504830, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.651182187+02:00", - "latency": 601238, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.662830257+02:00", - "latency": 1549308, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.671726055+02:00", - "latency": 557604, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.683673127+02:00", - "latency": 2519143, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.691589837+02:00", - "latency": 495109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.701376078+02:00", - "latency": 517326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.711718144+02:00", - "latency": 650626, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.721838276+02:00", - "latency": 728724, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.731935106+02:00", - "latency": 615032, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.741721125+02:00", - "latency": 509424, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.751775386+02:00", - "latency": 585509, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.761633092+02:00", - "latency": 481135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.771509282+02:00", - "latency": 477308, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.781384772+02:00", - "latency": 502868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.795625264+02:00", - "latency": 2200112, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.802124885+02:00", - "latency": 693176, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.812155473+02:00", - "latency": 567570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.822162433+02:00", - "latency": 528968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.832236821+02:00", - "latency": 631911, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.841106092+02:00", - "latency": 470093, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.852146543+02:00", - "latency": 620123, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.863628513+02:00", - "latency": 1951250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.871565018+02:00", - "latency": 651096, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.883804543+02:00", - "latency": 2880644, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.891809159+02:00", - "latency": 571941, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.901757324+02:00", - "latency": 466171, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.911736624+02:00", - "latency": 537183, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.921677905+02:00", - "latency": 471401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.931726264+02:00", - "latency": 618768, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.941595827+02:00", - "latency": 473248, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.951262951+02:00", - "latency": 471228, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.962274428+02:00", - "latency": 534949, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.974173339+02:00", - "latency": 3488351, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.981094687+02:00", - "latency": 550304, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:17.992109946+02:00", - "latency": 598972, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.00236331+02:00", - "latency": 545326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.011450976+02:00", - "latency": 638511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.023354286+02:00", - "latency": 551613, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.031259877+02:00", - "latency": 464659, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.041206268+02:00", - "latency": 504874, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.051208508+02:00", - "latency": 586074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.061211932+02:00", - "latency": 581518, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.078005314+02:00", - "latency": 7258400, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.081376962+02:00", - "latency": 560901, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.091201917+02:00", - "latency": 586604, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.113106781+02:00", - "latency": 7217699, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.124006534+02:00", - "latency": 10615852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.134756062+02:00", - "latency": 10826653, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.138713407+02:00", - "latency": 3667509, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.154724305+02:00", - "latency": 2501736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.157068486+02:00", - "latency": 5027339, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.161966102+02:00", - "latency": 501756, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.171863861+02:00", - "latency": 519407, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.181816749+02:00", - "latency": 564802, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.192530926+02:00", - "latency": 566689, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.201480908+02:00", - "latency": 748264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.218042703+02:00", - "latency": 4171252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.221161966+02:00", - "latency": 521863, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.231802738+02:00", - "latency": 594340, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.241747141+02:00", - "latency": 529697, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.251779473+02:00", - "latency": 510449, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.261699536+02:00", - "latency": 510738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.271637206+02:00", - "latency": 521903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.289918126+02:00", - "latency": 8101415, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.292327122+02:00", - "latency": 460528, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.309339962+02:00", - "latency": 631265, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.327711901+02:00", - "latency": 17035441, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.337985164+02:00", - "latency": 10014822, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.340139931+02:00", - "latency": 2267089, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.341840579+02:00", - "latency": 412000, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.361669756+02:00", - "latency": 11071019, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.3624752+02:00", - "latency": 555906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.373605107+02:00", - "latency": 2727711, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.38163128+02:00", - "latency": 571829, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.415679753+02:00", - "latency": 18489131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.415750939+02:00", - "latency": 14704549, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.416442052+02:00", - "latency": 486181, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.423321521+02:00", - "latency": 509954, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.432748107+02:00", - "latency": 463919, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.449337243+02:00", - "latency": 6489568, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.462740274+02:00", - "latency": 12099324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.463346337+02:00", - "latency": 416285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.47123778+02:00", - "latency": 517208, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.4811666+02:00", - "latency": 510489, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.492866136+02:00", - "latency": 499147, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.504724715+02:00", - "latency": 3427729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.514046436+02:00", - "latency": 535337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.525688109+02:00", - "latency": 4644185, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.531604055+02:00", - "latency": 527476, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.5458416+02:00", - "latency": 630756, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.552091172+02:00", - "latency": 611353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.564454152+02:00", - "latency": 2935873, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.571828031+02:00", - "latency": 554229, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.583403949+02:00", - "latency": 566563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.591300319+02:00", - "latency": 468395, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.601226589+02:00", - "latency": 491365, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.611083062+02:00", - "latency": 503569, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.621966293+02:00", - "latency": 481408, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.635347459+02:00", - "latency": 684004, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.643325716+02:00", - "latency": 492764, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.651621823+02:00", - "latency": 565884, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.671361392+02:00", - "latency": 2577412, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.67229109+02:00", - "latency": 659657, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.681430132+02:00", - "latency": 618565, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.692444701+02:00", - "latency": 591852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.704672088+02:00", - "latency": 3994968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.714410616+02:00", - "latency": 595264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.722937645+02:00", - "latency": 611578, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.736644436+02:00", - "latency": 3849446, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.742026545+02:00", - "latency": 631216, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.751824083+02:00", - "latency": 503340, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.761401794+02:00", - "latency": 543410, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.771314753+02:00", - "latency": 509701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.781393424+02:00", - "latency": 635373, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.791433137+02:00", - "latency": 628912, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.806989739+02:00", - "latency": 6312072, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.811971166+02:00", - "latency": 521020, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.822021345+02:00", - "latency": 655310, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.832145582+02:00", - "latency": 614687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.842107642+02:00", - "latency": 513890, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.852064312+02:00", - "latency": 520679, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.861894279+02:00", - "latency": 520598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.872005123+02:00", - "latency": 656770, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.882208913+02:00", - "latency": 631754, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.891049506+02:00", - "latency": 505915, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.902357067+02:00", - "latency": 536564, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.911274706+02:00", - "latency": 528409, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.921274781+02:00", - "latency": 602731, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.931209996+02:00", - "latency": 536252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.94118854+02:00", - "latency": 591254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.951300208+02:00", - "latency": 651130, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.962466051+02:00", - "latency": 597329, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.975930081+02:00", - "latency": 519554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.981934446+02:00", - "latency": 597831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:18.991824388+02:00", - "latency": 488700, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.002251132+02:00", - "latency": 625609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.011483068+02:00", - "latency": 775442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.021166047+02:00", - "latency": 455748, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.032187378+02:00", - "latency": 485307, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.041129708+02:00", - "latency": 525280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.060091219+02:00", - "latency": 8576790, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.061905876+02:00", - "latency": 468274, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.080614931+02:00", - "latency": 719392, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.099997611+02:00", - "latency": 4167580, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.104102138+02:00", - "latency": 483451, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.12285811+02:00", - "latency": 42397352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.123592266+02:00", - "latency": 587033, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.126477018+02:00", - "latency": 3610635, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:38:19.132446779+02:00", - "latency": 612396, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - } - ] -} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.json b/test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.json deleted file mode 100644 index 787913352..000000000 --- a/test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.json +++ /dev/null @@ -1 +0,0 @@ -{"date":"2025-10-03T17:37:48+02:00","endReason":"timeout","options":{"call":"trading.TradingService.GetPortfolioSummary","host":"localhost:50051","proto":"/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto","import-paths":["/home/jgrusewski/Work/foxhunt/services/trading_service/proto","."],"insecure":true,"rps":100,"load-schedule":"const","load-start":0,"load-end":0,"load-step":0,"load-step-duration":0,"load-max-duration":0,"concurrency":50,"concurrency-schedule":"const","concurrency-start":0,"concurrency-end":0,"concurrency-step":1,"concurrency-step-duration":0,"concurrency-max-duration":0,"total":2147483647,"connections":10,"duration":10000000000,"timeout":20000000000,"dial-timeout":10000000000,"data":{"account_id":"test-account"},"binary":false,"CPUs":16},"count":998,"total":10003026748,"average":1280029,"fastest":0,"slowest":0,"rps":99.76980219507456,"errorDistribution":{"rpc error: code = Unauthenticated desc = Valid authentication required":998},"statusCodeDistribution":{"Unauthenticated":998},"latencyDistribution":null,"histogram":null,"details":[{"timestamp":"2025-10-03T17:37:38.898844907+02:00","latency":1697264,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.907952445+02:00","latency":593416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.91796301+02:00","latency":656409,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.928067758+02:00","latency":624149,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.941029495+02:00","latency":3613496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.948032647+02:00","latency":543585,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.958143792+02:00","latency":662675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.969767664+02:00","latency":2192888,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.977917263+02:00","latency":646239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.988050233+02:00","latency":600276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:38.997960961+02:00","latency":533268,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.008005849+02:00","latency":574619,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.020112905+02:00","latency":2658300,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.028162344+02:00","latency":618156,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.039655927+02:00","latency":2054565,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.047785827+02:00","latency":576692,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.060865103+02:00","latency":3531504,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.067998717+02:00","latency":688315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.077973508+02:00","latency":579733,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.088074653+02:00","latency":645228,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.098002409+02:00","latency":619526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.108691289+02:00","latency":1188406,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.117928146+02:00","latency":706820,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.129690659+02:00","latency":2311658,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.138122921+02:00","latency":727150,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.14812587+02:00","latency":548247,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.159119482+02:00","latency":1094241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.168154533+02:00","latency":569252,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.178842903+02:00","latency":1179073,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.187904294+02:00","latency":578104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.19792734+02:00","latency":531614,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.208029986+02:00","latency":604131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.219016691+02:00","latency":1413699,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.228360505+02:00","latency":581537,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.238442826+02:00","latency":515936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.25007455+02:00","latency":3126983,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.258508115+02:00","latency":627596,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.267848344+02:00","latency":610751,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.277945884+02:00","latency":588848,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.288146226+02:00","latency":642264,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.300190901+02:00","latency":603768,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.30837268+02:00","latency":664929,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.317397693+02:00","latency":570637,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.327480977+02:00","latency":593952,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.337626312+02:00","latency":673310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.347706997+02:00","latency":605916,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.35801367+02:00","latency":741016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.368989391+02:00","latency":1495238,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.378338964+02:00","latency":745635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.388575665+02:00","latency":700590,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.397570975+02:00","latency":550315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.407899946+02:00","latency":803966,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.417961607+02:00","latency":589731,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.428152024+02:00","latency":582317,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.43826029+02:00","latency":607706,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.448524872+02:00","latency":759046,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.457931692+02:00","latency":609331,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.473850966+02:00","latency":6312376,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.477910698+02:00","latency":583050,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.490733932+02:00","latency":3260142,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.497836952+02:00","latency":596508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.507905771+02:00","latency":594013,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.520665816+02:00","latency":3316358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.527722566+02:00","latency":547275,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.539923349+02:00","latency":2669821,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.54804185+02:00","latency":640193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.560671169+02:00","latency":3131990,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.567818012+02:00","latency":654562,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.578688821+02:00","latency":1417825,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.588083332+02:00","latency":783802,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.598328743+02:00","latency":738544,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.607462063+02:00","latency":644418,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.620853444+02:00","latency":3913856,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.627769429+02:00","latency":541508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.638263813+02:00","latency":983587,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.651929382+02:00","latency":4229517,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.659121686+02:00","latency":1632544,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.668705624+02:00","latency":1595715,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.678185531+02:00","latency":865115,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.687514175+02:00","latency":658588,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.703670485+02:00","latency":6800286,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.708963067+02:00","latency":1780876,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.720680633+02:00","latency":3159850,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.727900562+02:00","latency":738608,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.737932233+02:00","latency":606782,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.748057418+02:00","latency":602410,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.758130789+02:00","latency":511516,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.768188355+02:00","latency":556066,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.778246933+02:00","latency":545797,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.788288044+02:00","latency":546579,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.79914117+02:00","latency":757255,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.808216438+02:00","latency":569999,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.818400208+02:00","latency":592859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.82874277+02:00","latency":636038,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.837802665+02:00","latency":539740,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.848383066+02:00","latency":510622,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.860043816+02:00","latency":3233756,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.868084917+02:00","latency":544866,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.878157043+02:00","latency":586945,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.888402964+02:00","latency":634590,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.897418148+02:00","latency":612553,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.908526316+02:00","latency":613352,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.920662473+02:00","latency":3643312,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.930684859+02:00","latency":3193666,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.937881317+02:00","latency":683295,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.950387238+02:00","latency":641118,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.958407926+02:00","latency":564866,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.968173029+02:00","latency":422500,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.980283844+02:00","latency":2672396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.987337984+02:00","latency":490758,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:39.998412555+02:00","latency":644603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.007474926+02:00","latency":533224,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.017643268+02:00","latency":695977,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.027742028+02:00","latency":643869,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.037880759+02:00","latency":572417,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.047845059+02:00","latency":516542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.058046847+02:00","latency":695935,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.068789971+02:00","latency":1012318,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.08207143+02:00","latency":4180917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.088197982+02:00","latency":559816,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.098827067+02:00","latency":780424,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.170232275+02:00","latency":13041174,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.170789811+02:00","latency":53451338,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.170842543+02:00","latency":391853,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.172325103+02:00","latency":35494596,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.172325105+02:00","latency":45191145,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.172347153+02:00","latency":23822730,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.177410346+02:00","latency":70292819,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.179152715+02:00","latency":899581,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.196974427+02:00","latency":9362104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.197416543+02:00","latency":526658,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.207394423+02:00","latency":585394,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.238088788+02:00","latency":905465,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.238414651+02:00","latency":5222675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.238600662+02:00","latency":646547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.2500256+02:00","latency":2940539,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.258156685+02:00","latency":629916,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.268294124+02:00","latency":576675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.278362959+02:00","latency":596343,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.290694767+02:00","latency":3896921,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.297753014+02:00","latency":591957,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.307894268+02:00","latency":639967,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.317912868+02:00","latency":558553,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.327982907+02:00","latency":577447,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.33811847+02:00","latency":644877,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.348189293+02:00","latency":604884,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.358266655+02:00","latency":584157,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.368310762+02:00","latency":538604,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.37832779+02:00","latency":610596,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.388419082+02:00","latency":595296,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.398987694+02:00","latency":2116764,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.412752917+02:00","latency":693760,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.421971422+02:00","latency":2113139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.428193011+02:00","latency":606100,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.43837944+02:00","latency":607486,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.447381498+02:00","latency":553412,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.457442755+02:00","latency":579199,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.467526722+02:00","latency":622653,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.477635748+02:00","latency":625364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.487758387+02:00","latency":665654,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.497795714+02:00","latency":539033,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.507768944+02:00","latency":563591,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.518025195+02:00","latency":704972,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.528034421+02:00","latency":542140,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.537604637+02:00","latency":665801,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.547620653+02:00","latency":675735,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.557807312+02:00","latency":687371,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.567858043+02:00","latency":541688,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.577930388+02:00","latency":620716,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.58869204+02:00","latency":1346997,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.597719108+02:00","latency":539503,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.60776109+02:00","latency":592293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.617522038+02:00","latency":625108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.627719904+02:00","latency":667031,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.637523865+02:00","latency":641675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.64747772+02:00","latency":544124,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.657505247+02:00","latency":628579,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.667734214+02:00","latency":769227,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.677711895+02:00","latency":634398,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.687942385+02:00","latency":686817,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.698057303+02:00","latency":743123,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.708662441+02:00","latency":525072,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.717708884+02:00","latency":615922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.728280426+02:00","latency":864836,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.738031369+02:00","latency":566635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.75095588+02:00","latency":3371128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.757702731+02:00","latency":808625,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.769936639+02:00","latency":2732458,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.778165383+02:00","latency":653108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.788332809+02:00","latency":691320,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.798461673+02:00","latency":535303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.809587337+02:00","latency":2659952,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.820805764+02:00","latency":3720079,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.827867763+02:00","latency":567064,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.838018261+02:00","latency":631998,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.84813147+02:00","latency":587426,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.858259507+02:00","latency":650251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.868631422+02:00","latency":663680,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.877779497+02:00","latency":619430,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.887807159+02:00","latency":587701,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.898154475+02:00","latency":828149,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.909699746+02:00","latency":2334213,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.917731045+02:00","latency":573973,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.927827371+02:00","latency":646009,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.93807572+02:00","latency":668276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.948169002+02:00","latency":593252,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.958211156+02:00","latency":580126,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.968717289+02:00","latency":1064402,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.979689988+02:00","latency":2421191,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.987793269+02:00","latency":623421,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:40.997851318+02:00","latency":604975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.007994104+02:00","latency":658685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.018050093+02:00","latency":522393,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.028722283+02:00","latency":1128012,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.03975821+02:00","latency":2191293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.048370374+02:00","latency":711048,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.0607349+02:00","latency":2912684,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.067921392+02:00","latency":656439,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.077924196+02:00","latency":595474,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.088032197+02:00","latency":633660,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.098161635+02:00","latency":639022,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.108177824+02:00","latency":661782,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.118234917+02:00","latency":553288,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.127805723+02:00","latency":844084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.137568881+02:00","latency":681900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.149514846+02:00","latency":632004,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.15763431+02:00","latency":612838,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.16768533+02:00","latency":610688,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.177858775+02:00","latency":678483,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.187775109+02:00","latency":483943,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.197781771+02:00","latency":563859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.207856879+02:00","latency":621719,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.217954098+02:00","latency":749744,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.227883465+02:00","latency":561591,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.237823441+02:00","latency":533864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.249983578+02:00","latency":2770627,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.2577584+02:00","latency":747882,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.267739598+02:00","latency":637830,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.277819766+02:00","latency":661509,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.28797685+02:00","latency":735106,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.298062479+02:00","latency":614959,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.307913407+02:00","latency":534784,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.318786567+02:00","latency":1415727,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.328990813+02:00","latency":1713077,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.33985888+02:00","latency":3010846,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.347745512+02:00","latency":572278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.357728239+02:00","latency":537438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.367846726+02:00","latency":639304,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.379717025+02:00","latency":2421266,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.387730804+02:00","latency":578346,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.402684525+02:00","latency":5482210,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.409695829+02:00","latency":2524637,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.417849271+02:00","latency":673496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.427870307+02:00","latency":632454,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.437835357+02:00","latency":525519,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.450813738+02:00","latency":3532992,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.457892913+02:00","latency":611009,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.467867431+02:00","latency":516503,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.478426923+02:00","latency":585259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.487552831+02:00","latency":628995,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.49767488+02:00","latency":594848,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.507914678+02:00","latency":708236,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.517981781+02:00","latency":534686,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.530961964+02:00","latency":3555468,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.537901278+02:00","latency":553243,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.547891093+02:00","latency":574552,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.557401855+02:00","latency":532081,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.567379624+02:00","latency":570298,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.577412612+02:00","latency":594970,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.588403705+02:00","latency":558477,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.598383332+02:00","latency":570154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.607335922+02:00","latency":561057,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.621615234+02:00","latency":3689336,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.628606943+02:00","latency":1572748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.63763636+02:00","latency":579984,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.647752397+02:00","latency":627583,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.657871476+02:00","latency":616672,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.668079406+02:00","latency":598432,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.67799112+02:00","latency":519887,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.688062461+02:00","latency":581660,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.698080773+02:00","latency":514949,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.708072709+02:00","latency":512705,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.718076388+02:00","latency":474598,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.728144713+02:00","latency":579162,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.739405778+02:00","latency":552350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.747416357+02:00","latency":592417,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.757405414+02:00","latency":577116,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.767352568+02:00","latency":527797,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.780806894+02:00","latency":2959558,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.787837375+02:00","latency":520966,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.798620788+02:00","latency":1265225,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.807985177+02:00","latency":531983,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.817954214+02:00","latency":504623,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.828026715+02:00","latency":589326,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.838082347+02:00","latency":584582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.848174766+02:00","latency":505958,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.85815646+02:00","latency":511334,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.868180823+02:00","latency":532535,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.878265659+02:00","latency":586588,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.89201598+02:00","latency":4263900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.898080009+02:00","latency":543604,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.908173734+02:00","latency":550546,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.918216387+02:00","latency":524125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.928268985+02:00","latency":570662,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.938472968+02:00","latency":582568,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.947479623+02:00","latency":540334,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.959970766+02:00","latency":3027356,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.968149964+02:00","latency":592990,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.980840785+02:00","latency":3892678,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.987852225+02:00","latency":509740,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:41.99788456+02:00","latency":507208,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.007889928+02:00","latency":535056,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.017958359+02:00","latency":586799,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.028106967+02:00","latency":581846,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.038201388+02:00","latency":550038,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.048855382+02:00","latency":1893039,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.057892261+02:00","latency":581078,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.068040056+02:00","latency":674730,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.078266218+02:00","latency":558396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.088271376+02:00","latency":530969,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.098355857+02:00","latency":604027,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.108384653+02:00","latency":511079,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.117604433+02:00","latency":690181,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.127588463+02:00","latency":638080,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.137607501+02:00","latency":552601,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.15259729+02:00","latency":543320,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.157771663+02:00","latency":592374,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.167902278+02:00","latency":647882,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.178586644+02:00","latency":1093261,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.188165301+02:00","latency":532651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.198192168+02:00","latency":553992,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.208404648+02:00","latency":745030,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.217585924+02:00","latency":590280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.227726676+02:00","latency":646884,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.237461656+02:00","latency":595293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.249695536+02:00","latency":2709925,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.257830383+02:00","latency":632614,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.268009374+02:00","latency":672846,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.278271493+02:00","latency":677453,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.290687189+02:00","latency":2939741,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.2977791+02:00","latency":547328,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.307854036+02:00","latency":599241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.318019108+02:00","latency":682232,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.328108765+02:00","latency":625697,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.338189055+02:00","latency":571352,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.350410694+02:00","latency":564981,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.357451823+02:00","latency":624989,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.367481878+02:00","latency":535293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.37751401+02:00","latency":550250,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.387595944+02:00","latency":572505,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.397662485+02:00","latency":576594,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.409144376+02:00","latency":1928087,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.418235348+02:00","latency":590090,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.42840356+02:00","latency":636161,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.439617226+02:00","latency":2708501,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.447644483+02:00","latency":577905,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.457663478+02:00","latency":532526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.470726157+02:00","latency":3532945,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.477892201+02:00","latency":629641,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.488064376+02:00","latency":635233,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.498234082+02:00","latency":660472,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.508517789+02:00","latency":696052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.517443797+02:00","latency":542050,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.527391151+02:00","latency":549113,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.537392635+02:00","latency":581038,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.547448691+02:00","latency":617128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.557574937+02:00","latency":630935,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.567788077+02:00","latency":672318,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.577976468+02:00","latency":682129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.588605578+02:00","latency":1077277,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.598911259+02:00","latency":1270502,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.608006001+02:00","latency":601044,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.61810166+02:00","latency":589755,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.628205502+02:00","latency":604490,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.638383437+02:00","latency":611109,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.647512588+02:00","latency":598787,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.657550611+02:00","latency":597015,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.667740818+02:00","latency":663604,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.678670742+02:00","latency":1454862,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.688641049+02:00","latency":1459565,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.698673322+02:00","latency":1519646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.710986333+02:00","latency":3708993,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.718282653+02:00","latency":756201,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.728340269+02:00","latency":630326,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.738496748+02:00","latency":552104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.747707086+02:00","latency":676202,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.759842086+02:00","latency":2656134,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.769688465+02:00","latency":2313956,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.780708795+02:00","latency":3505758,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.787800883+02:00","latency":587335,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.797926321+02:00","latency":641324,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.808510838+02:00","latency":687496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.817641001+02:00","latency":615800,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.831630715+02:00","latency":2143568,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.838279465+02:00","latency":769791,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.848293759+02:00","latency":524455,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.858364058+02:00","latency":566258,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.871698344+02:00","latency":4652408,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.879741673+02:00","latency":2531765,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.887827459+02:00","latency":583218,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.897836034+02:00","latency":526558,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.907983725+02:00","latency":658031,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.918171963+02:00","latency":718185,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.931445526+02:00","latency":1772813,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.937570168+02:00","latency":610496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.949146637+02:00","latency":623768,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.958298338+02:00","latency":569986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.9683461+02:00","latency":566782,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.978431259+02:00","latency":585246,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.987615449+02:00","latency":664275,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:42.997751669+02:00","latency":537540,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.007858975+02:00","latency":611623,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.018926025+02:00","latency":1551256,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.028104599+02:00","latency":697158,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.038209594+02:00","latency":583975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.048437539+02:00","latency":627047,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.059645203+02:00","latency":2708585,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.0684635+02:00","latency":1424551,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.077536702+02:00","latency":583315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.087474898+02:00","latency":604683,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.097436234+02:00","latency":571416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.107453498+02:00","latency":602337,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.117532247+02:00","latency":573947,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.12763578+02:00","latency":640594,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.137750211+02:00","latency":595599,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.147805234+02:00","latency":592358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.16102516+02:00","latency":1166838,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.169524921+02:00","latency":2226373,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.177562172+02:00","latency":516993,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.189996515+02:00","latency":2933244,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.198051188+02:00","latency":564326,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.208144049+02:00","latency":609436,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.218185319+02:00","latency":497535,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.228280761+02:00","latency":573028,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.240707425+02:00","latency":2896691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.247759901+02:00","latency":524009,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.257830442+02:00","latency":557318,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.267780674+02:00","latency":496256,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.277911957+02:00","latency":545158,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.287915651+02:00","latency":511117,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.297668441+02:00","latency":620467,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.30816358+02:00","latency":543153,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.318173009+02:00","latency":512342,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.329062971+02:00","latency":1499186,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.338047819+02:00","latency":512878,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.349013735+02:00","latency":1388415,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.359697377+02:00","latency":2189501,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.367796875+02:00","latency":569961,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.378021165+02:00","latency":668943,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.388110984+02:00","latency":570885,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.398160254+02:00","latency":570447,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.408319095+02:00","latency":592857,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.419070242+02:00","latency":2122076,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.429772378+02:00","latency":2235698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.438953717+02:00","latency":1312148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.451736863+02:00","latency":4205930,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.465826345+02:00","latency":6060849,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.47685095+02:00","latency":646366,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.478965017+02:00","latency":1639940,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.489817764+02:00","latency":2146369,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.498007532+02:00","latency":761051,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.509061658+02:00","latency":1590045,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.520793096+02:00","latency":602840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.527829631+02:00","latency":621829,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.539995155+02:00","latency":2719986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.548198924+02:00","latency":694541,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.558299952+02:00","latency":601252,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.568422497+02:00","latency":635282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.577890805+02:00","latency":749648,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.587876967+02:00","latency":554426,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.599832205+02:00","latency":2360492,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.610722286+02:00","latency":3909849,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.617757538+02:00","latency":591182,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.628075133+02:00","latency":952490,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.641831967+02:00","latency":4417822,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.648975197+02:00","latency":1706626,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.658129728+02:00","latency":643427,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.668357806+02:00","latency":757627,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.678463191+02:00","latency":570729,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.687567765+02:00","latency":657069,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.697833816+02:00","latency":775391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.70782979+02:00","latency":578065,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.718645192+02:00","latency":802449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.727798586+02:00","latency":749648,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.737921616+02:00","latency":658486,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.748661128+02:00","latency":1400283,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.759475686+02:00","latency":581444,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.767592975+02:00","latency":697131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.777576863+02:00","latency":557993,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.790433362+02:00","latency":658028,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.797462717+02:00","latency":600966,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.807561389+02:00","latency":661984,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.820402211+02:00","latency":578743,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.827305263+02:00","latency":537789,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.83837268+02:00","latency":623785,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.848441789+02:00","latency":630150,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.861068298+02:00","latency":4263932,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.868139877+02:00","latency":626495,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.878155597+02:00","latency":592678,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.888153617+02:00","latency":574671,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.899089576+02:00","latency":2302018,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.908088155+02:00","latency":592714,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.920707793+02:00","latency":3172534,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.927798112+02:00","latency":660248,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.93777157+02:00","latency":653358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.94787753+02:00","latency":677036,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.957934583+02:00","latency":647967,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.969392699+02:00","latency":2036434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.977539917+02:00","latency":643871,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.987784977+02:00","latency":703778,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:43.997775796+02:00","latency":590155,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.007412413+02:00","latency":569445,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.017540375+02:00","latency":677220,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.027617905+02:00","latency":596532,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.037435016+02:00","latency":627975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.047383688+02:00","latency":577408,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.057858477+02:00","latency":837842,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.067637981+02:00","latency":549900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.077395093+02:00","latency":574909,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.087575005+02:00","latency":736999,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.099187891+02:00","latency":2001460,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.111140308+02:00","latency":3446084,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.118282227+02:00","latency":659013,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.12829527+02:00","latency":572735,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.138339837+02:00","latency":620685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.147448131+02:00","latency":613275,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.157354393+02:00","latency":566421,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.168390716+02:00","latency":629504,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.178615779+02:00","latency":1788965,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.187408148+02:00","latency":639982,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.197496868+02:00","latency":687624,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.208657471+02:00","latency":1720166,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.217779368+02:00","latency":684986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.227777064+02:00","latency":587005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.238653228+02:00","latency":1428015,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.247758019+02:00","latency":684067,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.257741691+02:00","latency":578157,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.267797182+02:00","latency":634693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.277473505+02:00","latency":673970,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.28766069+02:00","latency":866866,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.297767908+02:00","latency":601790,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.307934929+02:00","latency":737230,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.317814506+02:00","latency":646260,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.327982672+02:00","latency":607540,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.339712597+02:00","latency":2319395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.348598826+02:00","latency":733799,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.357841751+02:00","latency":799267,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.369461079+02:00","latency":627204,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.377577784+02:00","latency":686200,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.387514605+02:00","latency":558248,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.39905432+02:00","latency":2032219,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.408055913+02:00","latency":664691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.418264756+02:00","latency":645143,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.42838722+02:00","latency":645018,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.437439297+02:00","latency":672460,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.448407823+02:00","latency":625827,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.457494537+02:00","latency":609197,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.467482323+02:00","latency":608656,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.47756961+02:00","latency":695589,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.487743778+02:00","latency":682006,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.498666072+02:00","latency":1593180,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.508681628+02:00","latency":1586543,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.520686962+02:00","latency":3552961,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.527722614+02:00","latency":622980,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.537829545+02:00","latency":710356,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.548087384+02:00","latency":711321,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.558232969+02:00","latency":658627,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.57073785+02:00","latency":3081311,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.577790304+02:00","latency":604383,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.587920799+02:00","latency":688315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.598002282+02:00","latency":580803,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.60970394+02:00","latency":2055229,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.618814414+02:00","latency":1718962,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.6279931+02:00","latency":703369,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.637920437+02:00","latency":593236,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.649732626+02:00","latency":2897281,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.660030748+02:00","latency":2933385,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.670725836+02:00","latency":3218703,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.677792373+02:00","latency":631852,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.688041581+02:00","latency":634507,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.698215111+02:00","latency":727095,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.708199327+02:00","latency":601589,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.720732567+02:00","latency":3069917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.727851137+02:00","latency":673983,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.740468926+02:00","latency":548425,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.749028317+02:00","latency":2043696,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.758270165+02:00","latency":777478,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.768204741+02:00","latency":683563,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.778220525+02:00","latency":596131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.788224946+02:00","latency":596014,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.798492795+02:00","latency":759363,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.807510389+02:00","latency":614822,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.817569057+02:00","latency":653365,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.827701054+02:00","latency":719432,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.837884003+02:00","latency":638154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.848176299+02:00","latency":806537,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.858262939+02:00","latency":608206,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.868428506+02:00","latency":742549,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.877577982+02:00","latency":765736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.887667943+02:00","latency":700041,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.900745003+02:00","latency":3625526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.907771682+02:00","latency":580076,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.91797046+02:00","latency":725419,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.928131002+02:00","latency":702524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.938248457+02:00","latency":591259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.948185125+02:00","latency":527339,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.958229611+02:00","latency":664072,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.971883643+02:00","latency":3970576,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.978527841+02:00","latency":844917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.987894026+02:00","latency":880061,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:44.999815887+02:00","latency":2648431,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.007829763+02:00","latency":590877,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.018244365+02:00","latency":708954,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.02848763+02:00","latency":617730,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.037472345+02:00","latency":633627,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.049297136+02:00","latency":546891,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.058201092+02:00","latency":515345,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.068263284+02:00","latency":592832,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.078292119+02:00","latency":651148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.088287505+02:00","latency":562763,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.098365109+02:00","latency":574407,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.108722198+02:00","latency":954693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.117761178+02:00","latency":690550,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.127834492+02:00","latency":657680,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.137818434+02:00","latency":546310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.147970096+02:00","latency":657426,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.16103197+02:00","latency":3655647,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.168079322+02:00","latency":619895,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.178030086+02:00","latency":552093,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.187933318+02:00","latency":522230,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.198009316+02:00","latency":636710,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.207914657+02:00","latency":504735,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.22106303+02:00","latency":3784387,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.228046784+02:00","latency":568132,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.237993121+02:00","latency":568062,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.248154588+02:00","latency":719676,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.258107149+02:00","latency":551965,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.267649752+02:00","latency":742792,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.277680248+02:00","latency":652280,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.28776805+02:00","latency":563924,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.297679202+02:00","latency":508811,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.310567102+02:00","latency":539062,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.317411648+02:00","latency":597690,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.328496962+02:00","latency":521445,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.341422545+02:00","latency":917991,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.347467374+02:00","latency":630542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.358413265+02:00","latency":1125329,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.367345613+02:00","latency":432080,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.378378694+02:00","latency":579876,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.387262027+02:00","latency":452461,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.398181329+02:00","latency":531041,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.408070257+02:00","latency":529034,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.418061505+02:00","latency":582387,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.427996412+02:00","latency":514780,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.437880408+02:00","latency":494562,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.447848673+02:00","latency":605481,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.458020836+02:00","latency":728815,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.470717062+02:00","latency":3313180,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.477640781+02:00","latency":509194,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.487580267+02:00","latency":545557,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.497623815+02:00","latency":623829,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.507688975+02:00","latency":648625,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.5188493+02:00","latency":1733908,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.527933742+02:00","latency":580485,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.538760608+02:00","latency":1559423,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.549620888+02:00","latency":2350760,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.557777818+02:00","latency":785686,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.568141325+02:00","latency":727218,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.59195583+02:00","latency":12797513,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.592213481+02:00","latency":4858526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.598373102+02:00","latency":555421,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.608799727+02:00","latency":2027969,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.617885295+02:00","latency":625407,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.627825438+02:00","latency":562618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.639758882+02:00","latency":2497748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.647650197+02:00","latency":595469,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.657682443+02:00","latency":596314,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.66780917+02:00","latency":651306,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.677894715+02:00","latency":601443,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.687946273+02:00","latency":618942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.697875347+02:00","latency":495365,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.707781034+02:00","latency":515998,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.718683162+02:00","latency":1495715,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.727733713+02:00","latency":602332,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.737971187+02:00","latency":831516,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.747999587+02:00","latency":762301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.758213995+02:00","latency":805279,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.768471547+02:00","latency":884472,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.77778199+02:00","latency":779926,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.790929733+02:00","latency":3574377,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.797940958+02:00","latency":603850,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.808674403+02:00","latency":1334415,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.817434686+02:00","latency":620537,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.827606118+02:00","latency":669211,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.838118439+02:00","latency":555297,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.848175551+02:00","latency":620678,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.858172463+02:00","latency":495237,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.86807647+02:00","latency":545618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.880004961+02:00","latency":647017,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.888022887+02:00","latency":588477,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.898024229+02:00","latency":524619,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.908042904+02:00","latency":566400,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.918163702+02:00","latency":626596,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.928147065+02:00","latency":566013,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.938188991+02:00","latency":528990,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.948206465+02:00","latency":606220,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.958138885+02:00","latency":526138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.968031674+02:00","latency":504543,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.977979001+02:00","latency":552963,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.989003913+02:00","latency":1735749,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:45.999948936+02:00","latency":2468629,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.008014474+02:00","latency":583490,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.018063434+02:00","latency":580745,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.028234746+02:00","latency":732172,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.038266687+02:00","latency":618255,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.047990112+02:00","latency":730658,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.059005712+02:00","latency":1430941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.068104899+02:00","latency":671654,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.079812294+02:00","latency":2289864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.087886884+02:00","latency":577974,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.097857688+02:00","latency":566287,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.109661831+02:00","latency":2446096,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.117611965+02:00","latency":554915,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.127719194+02:00","latency":610157,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.137637068+02:00","latency":525645,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.147697192+02:00","latency":662077,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.157835583+02:00","latency":705436,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.167949721+02:00","latency":600994,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.17803995+02:00","latency":634328,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.188261293+02:00","latency":560459,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.19967806+02:00","latency":1942131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.207698748+02:00","latency":561923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.217585726+02:00","latency":485779,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.22758183+02:00","latency":602573,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.237648338+02:00","latency":636508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.247533061+02:00","latency":549328,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.25758871+02:00","latency":627940,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.26750211+02:00","latency":496169,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.281329574+02:00","latency":3530223,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.289251906+02:00","latency":1487731,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.298236436+02:00","latency":534700,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.308842202+02:00","latency":1200540,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.317749005+02:00","latency":518293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.327659502+02:00","latency":538348,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.338816123+02:00","latency":1781707,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.348708563+02:00","latency":1458350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.357709134+02:00","latency":544441,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.367738362+02:00","latency":556646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.37779447+02:00","latency":652828,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.388225485+02:00","latency":940943,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.39814772+02:00","latency":665172,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.408200836+02:00","latency":624306,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.418231195+02:00","latency":557518,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.430710215+02:00","latency":3084467,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.437792528+02:00","latency":641775,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.447780565+02:00","latency":564944,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.457730306+02:00","latency":555318,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.468714716+02:00","latency":1604140,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.477666483+02:00","latency":603450,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.487801329+02:00","latency":758472,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.500719373+02:00","latency":3464289,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.507712348+02:00","latency":557664,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.518110474+02:00","latency":1005310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.528001774+02:00","latency":534559,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.538064135+02:00","latency":607498,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.547961159+02:00","latency":548534,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.557966847+02:00","latency":604344,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.568016317+02:00","latency":592274,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.578055589+02:00","latency":553582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.5885323+02:00","latency":1014840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.603017463+02:00","latency":2590988,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.607875309+02:00","latency":502739,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.621022824+02:00","latency":3846586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.628356299+02:00","latency":567524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.637325834+02:00","latency":577137,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.648398831+02:00","latency":537923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.657310563+02:00","latency":542950,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.668365564+02:00","latency":608673,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.677409573+02:00","latency":675250,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.687463449+02:00","latency":574295,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.697469971+02:00","latency":585787,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.707849549+02:00","latency":671001,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.717714174+02:00","latency":633869,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.727686135+02:00","latency":547807,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.739689079+02:00","latency":2609410,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.748184007+02:00","latency":1079660,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.758065425+02:00","latency":547122,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.770018849+02:00","latency":2644462,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.778030559+02:00","latency":618059,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.788081361+02:00","latency":589775,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.798166598+02:00","latency":631939,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.808122652+02:00","latency":548864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.81809914+02:00","latency":570191,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.828064456+02:00","latency":575209,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.838014338+02:00","latency":548500,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.848106895+02:00","latency":615171,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.858039023+02:00","latency":586316,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.868056701+02:00","latency":545994,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.87803934+02:00","latency":592804,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.887996212+02:00","latency":539810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.898035596+02:00","latency":617546,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.908023701+02:00","latency":578804,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.917936321+02:00","latency":516673,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.927947252+02:00","latency":565073,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.937923148+02:00","latency":552928,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.950808708+02:00","latency":3425308,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.957777937+02:00","latency":546333,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.967762357+02:00","latency":579265,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.977793335+02:00","latency":631297,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.989921471+02:00","latency":2788726,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:46.998071159+02:00","latency":653859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.008012872+02:00","latency":602496,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.018120915+02:00","latency":657881,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.028542242+02:00","latency":820333,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.039440686+02:00","latency":1672259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.047417465+02:00","latency":630582,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.059807+02:00","latency":2916657,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.067801083+02:00","latency":597322,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.080736953+02:00","latency":3522021,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.087781363+02:00","latency":620750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.097778078+02:00","latency":535993,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.107738182+02:00","latency":603976,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.120721164+02:00","latency":3506715,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.127729571+02:00","latency":573944,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.13772435+02:00","latency":550693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.147714619+02:00","latency":544166,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.157721436+02:00","latency":565988,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.167801013+02:00","latency":670554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.177759842+02:00","latency":588263,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.187829371+02:00","latency":682824,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.197906295+02:00","latency":647130,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.208224613+02:00","latency":707339,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.217433948+02:00","latency":609160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.227443774+02:00","latency":599343,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.237550884+02:00","latency":637016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.247346082+02:00","latency":550230,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.25872526+02:00","latency":800523,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.267739622+02:00","latency":612407,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.277717897+02:00","latency":563520,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.287603533+02:00","latency":567349,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.297634761+02:00","latency":618813,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.309792206+02:00","latency":2726973,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.3207457+02:00","latency":3562362,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.327729051+02:00","latency":561977,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.338189859+02:00","latency":1087459,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.348005406+02:00","latency":551562,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.358106584+02:00","latency":644555,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.367969078+02:00","latency":574909,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.377968781+02:00","latency":578248,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.388076985+02:00","latency":684752,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.398041649+02:00","latency":569714,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.408104631+02:00","latency":539298,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.418725839+02:00","latency":1187353,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.427567073+02:00","latency":553884,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.43763488+02:00","latency":552766,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.447539857+02:00","latency":534030,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.459712589+02:00","latency":2723936,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.46765722+02:00","latency":567088,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.477658655+02:00","latency":521939,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.487586752+02:00","latency":562225,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.497624863+02:00","latency":629983,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.507694236+02:00","latency":541820,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.517880128+02:00","latency":786509,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.536297196+02:00","latency":498066,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.537901153+02:00","latency":377431,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.547783173+02:00","latency":490323,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.557697155+02:00","latency":530945,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.567723017+02:00","latency":590168,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.577755127+02:00","latency":615053,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.58787302+02:00","latency":622195,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.59791052+02:00","latency":625772,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.610615175+02:00","latency":3289920,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.617989444+02:00","latency":628442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.627996499+02:00","latency":589233,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.639683461+02:00","latency":2276637,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.65070819+02:00","latency":3598276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.659625936+02:00","latency":2447951,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.667561125+02:00","latency":546659,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.677523637+02:00","latency":578618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.687507626+02:00","latency":616259,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.697419269+02:00","latency":517695,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.708360771+02:00","latency":1605306,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.720031026+02:00","latency":3248668,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.727945025+02:00","latency":574615,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.738008812+02:00","latency":630068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.747944085+02:00","latency":556410,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.757926585+02:00","latency":584502,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.76788164+02:00","latency":578153,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.777976531+02:00","latency":674389,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.788047536+02:00","latency":607896,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.798092015+02:00","latency":587372,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.808119269+02:00","latency":646354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.81813473+02:00","latency":604700,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.827458065+02:00","latency":607215,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.83742912+02:00","latency":528405,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.84785831+02:00","latency":817013,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.857824023+02:00","latency":716032,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.867736838+02:00","latency":554895,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.88072051+02:00","latency":3557470,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.887781895+02:00","latency":608445,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.897801896+02:00","latency":600470,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.907853437+02:00","latency":605113,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.917880353+02:00","latency":604091,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.927826854+02:00","latency":543006,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.937900498+02:00","latency":645730,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.947878+02:00","latency":588602,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.958103089+02:00","latency":772881,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.967999472+02:00","latency":601272,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.977975745+02:00","latency":585224,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.988090942+02:00","latency":709067,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:47.998020604+02:00","latency":562732,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.007945121+02:00","latency":585307,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.018961025+02:00","latency":1590138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.027965164+02:00","latency":561427,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.039838275+02:00","latency":2491933,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.048084269+02:00","latency":711822,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.058223665+02:00","latency":734208,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.06821882+02:00","latency":566397,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.078877049+02:00","latency":974922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.088931218+02:00","latency":1741115,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.109614744+02:00","latency":4756674,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.110489531+02:00","latency":466491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.117391364+02:00","latency":579048,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.128399965+02:00","latency":595023,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.139977492+02:00","latency":1916596,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.147992996+02:00","latency":621798,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.16169334+02:00","latency":4296335,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.168660484+02:00","latency":1484708,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.178667034+02:00","latency":1572609,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.190699751+02:00","latency":3553270,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.197683221+02:00","latency":612031,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.207688963+02:00","latency":591281,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.217751762+02:00","latency":563748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.227628749+02:00","latency":555504,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.240702809+02:00","latency":3659983,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.247725732+02:00","latency":569138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.257781086+02:00","latency":617555,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.267858755+02:00","latency":577734,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.277894631+02:00","latency":608360,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.287911764+02:00","latency":591329,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.298020952+02:00","latency":648659,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.308052453+02:00","latency":604052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.318144123+02:00","latency":591475,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.328244382+02:00","latency":670818,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.33831483+02:00","latency":702641,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.348562075+02:00","latency":775713,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.359313926+02:00","latency":2330262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.370722079+02:00","latency":2947297,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.377928278+02:00","latency":751296,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.388829711+02:00","latency":1545209,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.39792076+02:00","latency":630893,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.40900589+02:00","latency":1647762,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.418062048+02:00","latency":615382,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.428195688+02:00","latency":696291,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.438147215+02:00","latency":520756,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.448184863+02:00","latency":649923,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.45893102+02:00","latency":1291008,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.468121314+02:00","latency":799052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.477918772+02:00","latency":589559,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.487855765+02:00","latency":546865,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.497440597+02:00","latency":627593,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.507946584+02:00","latency":1116217,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.517988119+02:00","latency":641425,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.530980265+02:00","latency":3472460,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.53796807+02:00","latency":583867,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.5480361+02:00","latency":638449,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.558319078+02:00","latency":957579,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.568411063+02:00","latency":762578,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.578406138+02:00","latency":654080,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.58861003+02:00","latency":752293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.597669063+02:00","latency":648082,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.608056784+02:00","latency":861548,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.618074441+02:00","latency":617083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.629782774+02:00","latency":2158217,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.63772977+02:00","latency":554236,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.650731727+02:00","latency":3632120,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.657887894+02:00","latency":710810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.668064046+02:00","latency":651176,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.678214952+02:00","latency":717513,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.68834585+02:00","latency":674068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.69793123+02:00","latency":1122158,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.710815022+02:00","latency":3497444,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.718605832+02:00","latency":729397,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.727713203+02:00","latency":783880,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.741710515+02:00","latency":4873542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.748744553+02:00","latency":587885,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.757800644+02:00","latency":559687,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.767777751+02:00","latency":607672,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.777748061+02:00","latency":644521,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.788444108+02:00","latency":587551,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.797600563+02:00","latency":662797,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.811480254+02:00","latency":659227,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.817835452+02:00","latency":532459,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.827857764+02:00","latency":610207,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.837862128+02:00","latency":605725,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.84786824+02:00","latency":615372,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.857945601+02:00","latency":657033,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:48.867974646+02:00","latency":634240,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"}]} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.txt b/test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.txt deleted file mode 100644 index 1024559e4..000000000 --- a/test_results/grpc_load_tests/trading_service_portfolio_20251003_173658.txt +++ /dev/null @@ -1,6048 +0,0 @@ -{ - "date": "2025-10-03T17:37:58+02:00", - "endReason": "timeout", - "options": { - "call": "trading.TradingService.GetPortfolioSummary", - "host": "localhost:50051", - "proto": "/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto", - "import-paths": [ - "/home/jgrusewski/Work/foxhunt/services/trading_service/proto", - "." - ], - "insecure": true, - "rps": 100, - "load-schedule": "const", - "load-start": 0, - "load-end": 0, - "load-step": 0, - "load-step-duration": 0, - "load-max-duration": 0, - "concurrency": 50, - "concurrency-schedule": "const", - "concurrency-start": 0, - "concurrency-end": 0, - "concurrency-step": 1, - "concurrency-step-duration": 0, - "concurrency-max-duration": 0, - "total": 2147483647, - "connections": 10, - "duration": 10000000000, - "timeout": 20000000000, - "dial-timeout": 10000000000, - "data": { - "account_id": "test-account" - }, - "binary": false, - "CPUs": 16 - }, - "count": 999, - "total": 10017411096, - "average": 983569, - "fastest": 0, - "slowest": 0, - "rps": 99.72636546771106, - "errorDistribution": { - "rpc error: code = Unauthenticated desc = Valid authentication required": 999 - }, - "statusCodeDistribution": { - "Unauthenticated": 999 - }, - "latencyDistribution": null, - "histogram": null, - "details": [ - { - "timestamp": "2025-10-03T17:37:48.963942959+02:00", - "latency": 4852737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:48.970155804+02:00", - "latency": 630503, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:48.97959907+02:00", - "latency": 659000, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:48.989789751+02:00", - "latency": 724000, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:48.999826412+02:00", - "latency": 567419, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.010005547+02:00", - "latency": 708374, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.024017006+02:00", - "latency": 4551653, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.031999238+02:00", - "latency": 2524762, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.040067652+02:00", - "latency": 660950, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.05011324+02:00", - "latency": 617434, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.061656841+02:00", - "latency": 2064031, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.069673239+02:00", - "latency": 576480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.081660433+02:00", - "latency": 2536851, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.089837755+02:00", - "latency": 662640, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.100056161+02:00", - "latency": 728638, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.112019292+02:00", - "latency": 2532132, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.120140092+02:00", - "latency": 630564, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.133049985+02:00", - "latency": 3447807, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.142061344+02:00", - "latency": 2507487, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.150153207+02:00", - "latency": 596111, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.160226388+02:00", - "latency": 587762, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.170294182+02:00", - "latency": 608997, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.180361813+02:00", - "latency": 625233, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.190443662+02:00", - "latency": 612770, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.200440672+02:00", - "latency": 562162, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.210507352+02:00", - "latency": 570193, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.219515486+02:00", - "latency": 610500, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.230759747+02:00", - "latency": 1841308, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.23952163+02:00", - "latency": 635528, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.249474432+02:00", - "latency": 613995, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.260544484+02:00", - "latency": 639491, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.269510261+02:00", - "latency": 588170, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.280494565+02:00", - "latency": 606600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.29206209+02:00", - "latency": 3152280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.300091998+02:00", - "latency": 519290, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.311156349+02:00", - "latency": 1512206, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.322009592+02:00", - "latency": 2368837, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.330116159+02:00", - "latency": 591041, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.340225456+02:00", - "latency": 588022, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.350313262+02:00", - "latency": 568856, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.361925122+02:00", - "latency": 2060205, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.370062515+02:00", - "latency": 663780, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.381454266+02:00", - "latency": 1591715, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.390429182+02:00", - "latency": 567679, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.400927651+02:00", - "latency": 1077124, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.410125631+02:00", - "latency": 624761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.420217217+02:00", - "latency": 541508, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.430284199+02:00", - "latency": 655353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.440536574+02:00", - "latency": 663828, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.449826426+02:00", - "latency": 763399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.459951408+02:00", - "latency": 589055, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.469961482+02:00", - "latency": 602598, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.483927796+02:00", - "latency": 3952552, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.491688206+02:00", - "latency": 2771231, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.499786755+02:00", - "latency": 589131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.512985959+02:00", - "latency": 3744205, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.520136965+02:00", - "latency": 578329, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.533872489+02:00", - "latency": 4282041, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.540265992+02:00", - "latency": 689817, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.550306286+02:00", - "latency": 537285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.560423404+02:00", - "latency": 587714, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.570598836+02:00", - "latency": 695684, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.579684402+02:00", - "latency": 590624, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.5898864+02:00", - "latency": 647165, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.600121551+02:00", - "latency": 708737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.610229015+02:00", - "latency": 598908, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.620356966+02:00", - "latency": 606890, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.631085039+02:00", - "latency": 2139602, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.641646756+02:00", - "latency": 2063417, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.649665581+02:00", - "latency": 534401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.659717294+02:00", - "latency": 545351, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.669840745+02:00", - "latency": 636942, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.679966208+02:00", - "latency": 639386, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.690053355+02:00", - "latency": 564203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.705265676+02:00", - "latency": 5748272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.711051244+02:00", - "latency": 1319357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.720306061+02:00", - "latency": 624781, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.73044163+02:00", - "latency": 625452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.741755753+02:00", - "latency": 2846863, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.749983798+02:00", - "latency": 667976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.760137048+02:00", - "latency": 599142, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.773089544+02:00", - "latency": 3415211, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.78012812+02:00", - "latency": 524660, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.790223132+02:00", - "latency": 598155, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.803823358+02:00", - "latency": 4091874, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.809983941+02:00", - "latency": 649868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.820060113+02:00", - "latency": 595566, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.830554471+02:00", - "latency": 734684, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.839674578+02:00", - "latency": 642187, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.85167341+02:00", - "latency": 2548557, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.859818752+02:00", - "latency": 647864, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.869905597+02:00", - "latency": 592513, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.880428633+02:00", - "latency": 766493, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.890503093+02:00", - "latency": 571888, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.900964568+02:00", - "latency": 2065795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.910125422+02:00", - "latency": 662716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.92149294+02:00", - "latency": 1878374, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.929769047+02:00", - "latency": 828857, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.939530166+02:00", - "latency": 615965, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.950200196+02:00", - "latency": 805250, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.963153661+02:00", - "latency": 3424057, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.970277868+02:00", - "latency": 595010, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.981935033+02:00", - "latency": 2122167, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:49.990231345+02:00", - "latency": 846797, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.000160838+02:00", - "latency": 641533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.010575018+02:00", - "latency": 645535, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.019668859+02:00", - "latency": 628175, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.030575918+02:00", - "latency": 878563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.04268289+02:00", - "latency": 3651877, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.049783306+02:00", - "latency": 614075, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.059903545+02:00", - "latency": 649703, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.069987591+02:00", - "latency": 562072, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.082946694+02:00", - "latency": 3416587, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.090098284+02:00", - "latency": 614795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.100236524+02:00", - "latency": 637693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.110286001+02:00", - "latency": 570535, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.119907386+02:00", - "latency": 699200, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.130913913+02:00", - "latency": 573725, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.139980943+02:00", - "latency": 540809, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.150021328+02:00", - "latency": 583648, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.160139343+02:00", - "latency": 599634, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.17493895+02:00", - "latency": 5238610, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.180068073+02:00", - "latency": 658286, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.190261977+02:00", - "latency": 590211, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.200301721+02:00", - "latency": 592332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.210455819+02:00", - "latency": 573737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.219645802+02:00", - "latency": 717831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.229469339+02:00", - "latency": 564297, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.2394461+02:00", - "latency": 545554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.249603626+02:00", - "latency": 600105, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.260624822+02:00", - "latency": 1391120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.269905029+02:00", - "latency": 598631, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.280065738+02:00", - "latency": 632889, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.290754047+02:00", - "latency": 1132909, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.299657302+02:00", - "latency": 547728, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.309727493+02:00", - "latency": 624996, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.319853774+02:00", - "latency": 619114, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.33014954+02:00", - "latency": 776748, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.341174111+02:00", - "latency": 1554179, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.364779754+02:00", - "latency": 3735334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.365786641+02:00", - "latency": 626953, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.369921322+02:00", - "latency": 612759, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.379575849+02:00", - "latency": 565511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.389671164+02:00", - "latency": 580478, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.401441106+02:00", - "latency": 564235, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.410538111+02:00", - "latency": 595667, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.421468094+02:00", - "latency": 574767, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.429504472+02:00", - "latency": 571947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.439508422+02:00", - "latency": 633684, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.451966446+02:00", - "latency": 2813010, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.460254603+02:00", - "latency": 601216, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.470906858+02:00", - "latency": 1073882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.480081449+02:00", - "latency": 647960, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.490774758+02:00", - "latency": 668640, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.499605+02:00", - "latency": 630717, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.509514233+02:00", - "latency": 550844, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.520983037+02:00", - "latency": 1485836, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.538321037+02:00", - "latency": 2499481, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.542082943+02:00", - "latency": 637221, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.552708348+02:00", - "latency": 3176207, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.560544428+02:00", - "latency": 1150654, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.569820866+02:00", - "latency": 637204, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.580047786+02:00", - "latency": 625580, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.59009937+02:00", - "latency": 580091, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.600137419+02:00", - "latency": 573766, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.610231061+02:00", - "latency": 597472, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.620322912+02:00", - "latency": 591197, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.63051914+02:00", - "latency": 705328, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.639459961+02:00", - "latency": 550630, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.650509238+02:00", - "latency": 706239, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.660472957+02:00", - "latency": 574006, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.669942505+02:00", - "latency": 973258, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.680568492+02:00", - "latency": 1038855, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.689636749+02:00", - "latency": 609042, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.69980255+02:00", - "latency": 620461, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.709920973+02:00", - "latency": 971113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.719893309+02:00", - "latency": 646896, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.732036795+02:00", - "latency": 633548, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.740903393+02:00", - "latency": 598922, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.755047189+02:00", - "latency": 3988499, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.763101124+02:00", - "latency": 3531263, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.769489222+02:00", - "latency": 530526, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.780680827+02:00", - "latency": 1794727, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.789758267+02:00", - "latency": 568740, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.799859737+02:00", - "latency": 591321, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.810429328+02:00", - "latency": 555304, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.820471066+02:00", - "latency": 611142, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.832045105+02:00", - "latency": 2098195, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.843988249+02:00", - "latency": 1707261, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.851377679+02:00", - "latency": 519965, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.860431579+02:00", - "latency": 581466, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.86960924+02:00", - "latency": 602634, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.880761176+02:00", - "latency": 686348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.889933383+02:00", - "latency": 629989, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.900274833+02:00", - "latency": 1334922, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.909522664+02:00", - "latency": 495990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.920519874+02:00", - "latency": 573583, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.929503605+02:00", - "latency": 573652, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.942467057+02:00", - "latency": 561210, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.949543486+02:00", - "latency": 636464, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.962688785+02:00", - "latency": 2265518, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.969956693+02:00", - "latency": 575560, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.980126987+02:00", - "latency": 651970, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:50.996693837+02:00", - "latency": 7087347, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.0015089+02:00", - "latency": 613948, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.009583929+02:00", - "latency": 613559, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.019551248+02:00", - "latency": 547052, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.029686374+02:00", - "latency": 680990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.039772329+02:00", - "latency": 621388, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.049973587+02:00", - "latency": 676408, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.061497288+02:00", - "latency": 1977045, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.0707046+02:00", - "latency": 1263620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.079726189+02:00", - "latency": 562914, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.089931458+02:00", - "latency": 660821, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.100215002+02:00", - "latency": 701798, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.109919076+02:00", - "latency": 675596, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.121120296+02:00", - "latency": 592408, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.130402176+02:00", - "latency": 633481, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.140089242+02:00", - "latency": 1147118, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.149886518+02:00", - "latency": 567936, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.159430184+02:00", - "latency": 561371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.176709258+02:00", - "latency": 3744993, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.17979783+02:00", - "latency": 648100, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.189882147+02:00", - "latency": 566542, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.199837364+02:00", - "latency": 542974, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.209922213+02:00", - "latency": 613213, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.220041245+02:00", - "latency": 636722, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.230250867+02:00", - "latency": 707936, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.240410134+02:00", - "latency": 572296, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.250484215+02:00", - "latency": 623670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.260118586+02:00", - "latency": 838232, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.27006946+02:00", - "latency": 703332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.280132319+02:00", - "latency": 549120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.290176517+02:00", - "latency": 573387, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.300230042+02:00", - "latency": 545970, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.310296424+02:00", - "latency": 555070, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.320351735+02:00", - "latency": 563217, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.329386432+02:00", - "latency": 451841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.340286334+02:00", - "latency": 571123, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.350319694+02:00", - "latency": 514476, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.362028944+02:00", - "latency": 2142157, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.370078873+02:00", - "latency": 517534, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.385729702+02:00", - "latency": 6038607, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.389806133+02:00", - "latency": 604421, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.399907689+02:00", - "latency": 575743, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.409979941+02:00", - "latency": 610767, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.420422296+02:00", - "latency": 836693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.429475392+02:00", - "latency": 579549, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.439600097+02:00", - "latency": 604865, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.453337602+02:00", - "latency": 490069, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.460387813+02:00", - "latency": 536581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.470898395+02:00", - "latency": 866383, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.479820584+02:00", - "latency": 526215, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.489929252+02:00", - "latency": 609645, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.500120701+02:00", - "latency": 692025, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.511710468+02:00", - "latency": 2832777, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.519763974+02:00", - "latency": 579241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.52989976+02:00", - "latency": 577477, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.539912597+02:00", - "latency": 523013, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.550005065+02:00", - "latency": 608927, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.560215316+02:00", - "latency": 710516, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.570423378+02:00", - "latency": 546941, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.579668149+02:00", - "latency": 718561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.589698696+02:00", - "latency": 610002, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.599939199+02:00", - "latency": 759832, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.609947513+02:00", - "latency": 569074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.620039079+02:00", - "latency": 572999, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.62947709+02:00", - "latency": 587359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.640692711+02:00", - "latency": 606532, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.649714857+02:00", - "latency": 754882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.659895352+02:00", - "latency": 536354, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.670011325+02:00", - "latency": 656721, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.679715057+02:00", - "latency": 804981, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.689904626+02:00", - "latency": 634265, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.700124487+02:00", - "latency": 636927, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.712653897+02:00", - "latency": 705108, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.722525396+02:00", - "latency": 503700, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.729595854+02:00", - "latency": 542511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.739623816+02:00", - "latency": 601192, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.749577073+02:00", - "latency": 558125, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.759587602+02:00", - "latency": 714560, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.769814662+02:00", - "latency": 673480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.780024312+02:00", - "latency": 763882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.7899895+02:00", - "latency": 659477, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.799953143+02:00", - "latency": 509884, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.810010021+02:00", - "latency": 557951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.820019934+02:00", - "latency": 573882, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.833008973+02:00", - "latency": 3476443, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.840007454+02:00", - "latency": 539752, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.850677778+02:00", - "latency": 1251713, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.86068416+02:00", - "latency": 1532310, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.870277799+02:00", - "latency": 1112252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.880528297+02:00", - "latency": 685287, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.890422059+02:00", - "latency": 545694, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.900612017+02:00", - "latency": 680538, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.909720398+02:00", - "latency": 571875, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.919703438+02:00", - "latency": 598903, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.929793608+02:00", - "latency": 604199, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.942092116+02:00", - "latency": 2813879, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.950145008+02:00", - "latency": 560028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.960369649+02:00", - "latency": 682252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.970525374+02:00", - "latency": 677337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.979611291+02:00", - "latency": 593424, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.989596842+02:00", - "latency": 663253, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:51.999662285+02:00", - "latency": 570802, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.009807722+02:00", - "latency": 593243, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.020999111+02:00", - "latency": 2077756, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.030039908+02:00", - "latency": 554896, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.040034706+02:00", - "latency": 563802, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.05013332+02:00", - "latency": 594533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.060347002+02:00", - "latency": 716362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.07169764+02:00", - "latency": 2010693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.082741451+02:00", - "latency": 3584523, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.091856573+02:00", - "latency": 2636520, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.099932359+02:00", - "latency": 556206, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.110103787+02:00", - "latency": 608311, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.120057753+02:00", - "latency": 536204, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.13022523+02:00", - "latency": 659630, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.141468225+02:00", - "latency": 562685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.1504409+02:00", - "latency": 565583, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.159502073+02:00", - "latency": 557441, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.169716614+02:00", - "latency": 733295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.181703491+02:00", - "latency": 2531198, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.189859755+02:00", - "latency": 593006, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.199985352+02:00", - "latency": 618138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.210079732+02:00", - "latency": 629483, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.220987651+02:00", - "latency": 1449600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.231679145+02:00", - "latency": 2223991, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.240714289+02:00", - "latency": 1480851, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.252848554+02:00", - "latency": 3590615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.259510146+02:00", - "latency": 551298, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.272010322+02:00", - "latency": 1701129, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.279754888+02:00", - "latency": 564273, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.289867443+02:00", - "latency": 584853, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.300733446+02:00", - "latency": 1411832, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.309990389+02:00", - "latency": 655109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.320157403+02:00", - "latency": 685826, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.330188301+02:00", - "latency": 614544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.342979462+02:00", - "latency": 3224685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.350033463+02:00", - "latency": 593473, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.360038276+02:00", - "latency": 593067, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.370342283+02:00", - "latency": 587650, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.380377797+02:00", - "latency": 575463, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.390362995+02:00", - "latency": 558979, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.400929226+02:00", - "latency": 1076254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.409947754+02:00", - "latency": 558021, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.419470056+02:00", - "latency": 586264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.430574634+02:00", - "latency": 627195, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.439652254+02:00", - "latency": 600073, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.449778769+02:00", - "latency": 587878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.459869918+02:00", - "latency": 630077, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.470021113+02:00", - "latency": 628010, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.480182766+02:00", - "latency": 590105, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.490290666+02:00", - "latency": 590533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.500401195+02:00", - "latency": 618248, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.510683053+02:00", - "latency": 710574, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.519801261+02:00", - "latency": 618661, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.529801603+02:00", - "latency": 653144, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.539854952+02:00", - "latency": 593567, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.549500692+02:00", - "latency": 608656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.559604079+02:00", - "latency": 682359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.569659769+02:00", - "latency": 533304, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.57969127+02:00", - "latency": 576073, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.589688403+02:00", - "latency": 563240, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.599690418+02:00", - "latency": 552585, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.609803508+02:00", - "latency": 591607, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.619758187+02:00", - "latency": 697288, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.629830826+02:00", - "latency": 578573, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.639923387+02:00", - "latency": 628375, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.65008196+02:00", - "latency": 667943, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.660195575+02:00", - "latency": 641547, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.670332718+02:00", - "latency": 633027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.680417367+02:00", - "latency": 618951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.69058254+02:00", - "latency": 685305, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.699569413+02:00", - "latency": 670696, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.709712374+02:00", - "latency": 711147, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.719780121+02:00", - "latency": 605784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.730139594+02:00", - "latency": 869825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.7424377+02:00", - "latency": 763626, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.75061728+02:00", - "latency": 667825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.761658467+02:00", - "latency": 2493597, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.769927656+02:00", - "latency": 636193, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.780153985+02:00", - "latency": 718720, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.790285404+02:00", - "latency": 625988, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.800442023+02:00", - "latency": 603755, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.810708032+02:00", - "latency": 1842319, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.820170126+02:00", - "latency": 707203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.833919209+02:00", - "latency": 561657, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.843517641+02:00", - "latency": 598355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.849917998+02:00", - "latency": 625766, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.860105747+02:00", - "latency": 702923, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.870223563+02:00", - "latency": 548790, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.881606598+02:00", - "latency": 1844378, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.893859204+02:00", - "latency": 573557, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.900065951+02:00", - "latency": 564462, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.910097052+02:00", - "latency": 576687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.920304001+02:00", - "latency": 621429, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.931386582+02:00", - "latency": 595101, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.940719085+02:00", - "latency": 759217, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.952145184+02:00", - "latency": 2893269, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.960297709+02:00", - "latency": 623001, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.970450164+02:00", - "latency": 623805, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.985832298+02:00", - "latency": 622551, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:52.989960783+02:00", - "latency": 630001, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.0000003+02:00", - "latency": 578209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.01515322+02:00", - "latency": 5616673, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.020387459+02:00", - "latency": 577815, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.033702877+02:00", - "latency": 3833256, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.039577152+02:00", - "latency": 599173, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.049567155+02:00", - "latency": 563074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.061613173+02:00", - "latency": 627777, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.070015088+02:00", - "latency": 712968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.081755852+02:00", - "latency": 2692660, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.090333589+02:00", - "latency": 642887, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.100413984+02:00", - "latency": 578436, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.110495123+02:00", - "latency": 607813, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.120204931+02:00", - "latency": 625143, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.130174593+02:00", - "latency": 542044, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.140190742+02:00", - "latency": 555055, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.150336525+02:00", - "latency": 1016922, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.16046178+02:00", - "latency": 606728, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.169670375+02:00", - "latency": 707180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.181988095+02:00", - "latency": 2668607, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.190108146+02:00", - "latency": 541762, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.20027132+02:00", - "latency": 732548, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.21036981+02:00", - "latency": 631015, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.224753968+02:00", - "latency": 4878615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.229999912+02:00", - "latency": 551327, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.240117661+02:00", - "latency": 630937, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.250174009+02:00", - "latency": 535008, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.26018466+02:00", - "latency": 527120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.273628607+02:00", - "latency": 3960412, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.279611403+02:00", - "latency": 500232, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.28963547+02:00", - "latency": 545209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.299731+02:00", - "latency": 532895, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.309751651+02:00", - "latency": 560321, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.31989307+02:00", - "latency": 560597, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.331609516+02:00", - "latency": 1446021, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.33990452+02:00", - "latency": 659652, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.351396758+02:00", - "latency": 1605022, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.360350082+02:00", - "latency": 620090, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.370405836+02:00", - "latency": 543293, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.381428596+02:00", - "latency": 555346, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.390505763+02:00", - "latency": 632855, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.401463232+02:00", - "latency": 572603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.409477494+02:00", - "latency": 588631, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.421274302+02:00", - "latency": 2386192, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.430286413+02:00", - "latency": 532751, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.440448926+02:00", - "latency": 629936, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.450554767+02:00", - "latency": 632974, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.459643921+02:00", - "latency": 636755, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.469786477+02:00", - "latency": 606658, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.479890682+02:00", - "latency": 629535, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.489898535+02:00", - "latency": 568965, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.499976324+02:00", - "latency": 566656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.509975827+02:00", - "latency": 527106, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.520571687+02:00", - "latency": 658074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.531607206+02:00", - "latency": 633769, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.539517943+02:00", - "latency": 608751, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.552439953+02:00", - "latency": 557417, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.559523725+02:00", - "latency": 648897, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.569759317+02:00", - "latency": 739064, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.579889685+02:00", - "latency": 586639, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.590013725+02:00", - "latency": 637271, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.600060002+02:00", - "latency": 586109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.610110171+02:00", - "latency": 583629, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.620269885+02:00", - "latency": 627932, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.630571346+02:00", - "latency": 601335, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.639595066+02:00", - "latency": 594043, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.652707345+02:00", - "latency": 3670583, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.659918125+02:00", - "latency": 641656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.67110503+02:00", - "latency": 1729141, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.680031057+02:00", - "latency": 569008, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.690095192+02:00", - "latency": 578767, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.700207531+02:00", - "latency": 664510, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.711721192+02:00", - "latency": 1973575, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.722728764+02:00", - "latency": 3434223, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.730194457+02:00", - "latency": 977166, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.740155455+02:00", - "latency": 553724, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.752173956+02:00", - "latency": 579646, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.760267577+02:00", - "latency": 597030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.77026692+02:00", - "latency": 536872, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.780844811+02:00", - "latency": 595170, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.789956548+02:00", - "latency": 607208, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.800165168+02:00", - "latency": 653421, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.8101689+02:00", - "latency": 565527, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.820298478+02:00", - "latency": 669949, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.830408014+02:00", - "latency": 674754, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.840620031+02:00", - "latency": 714298, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.84967812+02:00", - "latency": 589484, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.859472711+02:00", - "latency": 603285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.871497958+02:00", - "latency": 2169184, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.882432805+02:00", - "latency": 547629, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.891041187+02:00", - "latency": 1057680, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.903061483+02:00", - "latency": 3817239, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.90963902+02:00", - "latency": 667352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.919840945+02:00", - "latency": 703216, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.929947188+02:00", - "latency": 625443, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.939531338+02:00", - "latency": 558511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.949672235+02:00", - "latency": 656539, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.959692193+02:00", - "latency": 532333, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.969671987+02:00", - "latency": 526779, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.979699589+02:00", - "latency": 487252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.989808229+02:00", - "latency": 668100, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:53.999698228+02:00", - "latency": 539403, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.009722389+02:00", - "latency": 562263, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.023147409+02:00", - "latency": 637485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.03115296+02:00", - "latency": 546842, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.039702138+02:00", - "latency": 714061, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.049734918+02:00", - "latency": 581084, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.059813509+02:00", - "latency": 575678, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.069804215+02:00", - "latency": 563854, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.079915109+02:00", - "latency": 635054, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.092520139+02:00", - "latency": 573981, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.099591091+02:00", - "latency": 609402, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.110222986+02:00", - "latency": 486990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.122867276+02:00", - "latency": 2811139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.137788306+02:00", - "latency": 7716734, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.142224849+02:00", - "latency": 2154904, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.150817342+02:00", - "latency": 531870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.160219604+02:00", - "latency": 551982, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.187158803+02:00", - "latency": 1025062, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.190565592+02:00", - "latency": 683810, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.197140536+02:00", - "latency": 454747, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.200705087+02:00", - "latency": 1021803, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.21508705+02:00", - "latency": 1264225, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.226144291+02:00", - "latency": 5075939, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.2295937+02:00", - "latency": 521215, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.244978807+02:00", - "latency": 4181985, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.252640744+02:00", - "latency": 570049, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.260228866+02:00", - "latency": 585381, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.271117489+02:00", - "latency": 516828, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.281065637+02:00", - "latency": 544219, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.290366026+02:00", - "latency": 523633, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.300595722+02:00", - "latency": 736254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.310139794+02:00", - "latency": 517616, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.320328337+02:00", - "latency": 573371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.330587055+02:00", - "latency": 610446, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.339684761+02:00", - "latency": 609775, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.349689361+02:00", - "latency": 515322, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.359528717+02:00", - "latency": 588263, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.369645984+02:00", - "latency": 619402, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.379719669+02:00", - "latency": 522196, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.389803089+02:00", - "latency": 564935, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.399858418+02:00", - "latency": 540620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.409936173+02:00", - "latency": 599110, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.419836063+02:00", - "latency": 538049, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.429828726+02:00", - "latency": 551870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.43998917+02:00", - "latency": 657841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.450145176+02:00", - "latency": 574807, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.46029435+02:00", - "latency": 616670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.470329533+02:00", - "latency": 495723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.480305067+02:00", - "latency": 643462, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.490446848+02:00", - "latency": 564051, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.50058563+02:00", - "latency": 608754, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.509589348+02:00", - "latency": 501326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.519655675+02:00", - "latency": 589823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.529894095+02:00", - "latency": 661342, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.539990024+02:00", - "latency": 584711, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.550240314+02:00", - "latency": 577259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.560204983+02:00", - "latency": 497181, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.570362089+02:00", - "latency": 694431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.58269089+02:00", - "latency": 537872, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.589816442+02:00", - "latency": 796978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.599570664+02:00", - "latency": 564480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.609611363+02:00", - "latency": 595455, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.619633951+02:00", - "latency": 538016, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.629676345+02:00", - "latency": 566296, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.639726573+02:00", - "latency": 595634, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.64979203+02:00", - "latency": 579426, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.659789305+02:00", - "latency": 542478, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.669831861+02:00", - "latency": 617261, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.680360496+02:00", - "latency": 514727, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.691692146+02:00", - "latency": 713639, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.700003014+02:00", - "latency": 663026, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.70999852+02:00", - "latency": 481391, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.72007771+02:00", - "latency": 611990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.72996973+02:00", - "latency": 520483, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.740323677+02:00", - "latency": 744426, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.750283066+02:00", - "latency": 509335, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.763580336+02:00", - "latency": 2342492, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.769913026+02:00", - "latency": 644051, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.77999084+02:00", - "latency": 615085, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.790044479+02:00", - "latency": 559737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.800606262+02:00", - "latency": 976952, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.810160956+02:00", - "latency": 515476, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.820289263+02:00", - "latency": 493151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.833423701+02:00", - "latency": 3657904, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.839469035+02:00", - "latency": 557824, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.849399782+02:00", - "latency": 543421, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.861127567+02:00", - "latency": 1272149, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.869638972+02:00", - "latency": 629058, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.879694308+02:00", - "latency": 511484, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.889840905+02:00", - "latency": 673460, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.900488352+02:00", - "latency": 562061, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.909431174+02:00", - "latency": 499906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.920585576+02:00", - "latency": 691438, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.929747434+02:00", - "latency": 579729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.939909191+02:00", - "latency": 628134, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.950011089+02:00", - "latency": 546690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.9600347+02:00", - "latency": 559509, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.970161689+02:00", - "latency": 567737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.980225516+02:00", - "latency": 580164, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.991494286+02:00", - "latency": 2189543, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:54.999591972+02:00", - "latency": 545772, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.00965135+02:00", - "latency": 585495, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.01960463+02:00", - "latency": 697719, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.029704918+02:00", - "latency": 599897, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.04030246+02:00", - "latency": 985452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.050220013+02:00", - "latency": 608829, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.072828105+02:00", - "latency": 701713, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.078783959+02:00", - "latency": 17725360, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.079787231+02:00", - "latency": 539001, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.090525847+02:00", - "latency": 664854, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.103785263+02:00", - "latency": 3898116, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.109955875+02:00", - "latency": 627505, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.120121996+02:00", - "latency": 740738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.130313632+02:00", - "latency": 680531, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.140718437+02:00", - "latency": 751298, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.149767353+02:00", - "latency": 625577, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.159913606+02:00", - "latency": 688833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.170093391+02:00", - "latency": 672885, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.180223184+02:00", - "latency": 697295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.19034909+02:00", - "latency": 663777, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.200560813+02:00", - "latency": 682188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.209632147+02:00", - "latency": 650493, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.219866971+02:00", - "latency": 779693, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.230047614+02:00", - "latency": 687232, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.240104061+02:00", - "latency": 579736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.250183271+02:00", - "latency": 665372, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.260577197+02:00", - "latency": 828690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.270333848+02:00", - "latency": 519499, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.280607767+02:00", - "latency": 821220, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.289453629+02:00", - "latency": 500884, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.300386915+02:00", - "latency": 562005, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.311065081+02:00", - "latency": 652434, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.320074047+02:00", - "latency": 637590, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.330119071+02:00", - "latency": 581042, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.340156759+02:00", - "latency": 595621, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.350796175+02:00", - "latency": 1963324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.35983468+02:00", - "latency": 581943, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.369875115+02:00", - "latency": 619180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.37994949+02:00", - "latency": 615488, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.391968678+02:00", - "latency": 2575463, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.400009296+02:00", - "latency": 621158, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.410013366+02:00", - "latency": 560028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.420028489+02:00", - "latency": 574415, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.430362361+02:00", - "latency": 821609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.444076947+02:00", - "latency": 4159080, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.450180593+02:00", - "latency": 663525, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.460283337+02:00", - "latency": 689260, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.470427724+02:00", - "latency": 624750, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.48065332+02:00", - "latency": 750223, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.489895441+02:00", - "latency": 716221, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.499896092+02:00", - "latency": 530230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.509931209+02:00", - "latency": 593698, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.520101343+02:00", - "latency": 686315, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.531123303+02:00", - "latency": 1521904, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.540198409+02:00", - "latency": 576393, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.550189898+02:00", - "latency": 567474, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.56101277+02:00", - "latency": 2123166, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.570025799+02:00", - "latency": 568095, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.580113822+02:00", - "latency": 618285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.590116987+02:00", - "latency": 493365, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.600094494+02:00", - "latency": 557527, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.609995909+02:00", - "latency": 553783, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.620062861+02:00", - "latency": 646687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.630095728+02:00", - "latency": 573403, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.641397331+02:00", - "latency": 601522, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.650454031+02:00", - "latency": 636153, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.659371896+02:00", - "latency": 555278, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.669958146+02:00", - "latency": 1061241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.680069982+02:00", - "latency": 608800, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.690091133+02:00", - "latency": 573458, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.700123846+02:00", - "latency": 594791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.710551905+02:00", - "latency": 912362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.719614033+02:00", - "latency": 648015, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.731990488+02:00", - "latency": 2806825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.739963201+02:00", - "latency": 560886, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.750102145+02:00", - "latency": 757026, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.760091031+02:00", - "latency": 668151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.770176491+02:00", - "latency": 686958, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.782837277+02:00", - "latency": 3245402, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.790019042+02:00", - "latency": 750592, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.80005228+02:00", - "latency": 634913, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.810607316+02:00", - "latency": 857001, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.820409949+02:00", - "latency": 1122411, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.831671031+02:00", - "latency": 2813362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.841749015+02:00", - "latency": 2588417, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.8498323+02:00", - "latency": 628048, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.859956691+02:00", - "latency": 647180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.870021526+02:00", - "latency": 617272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.880162633+02:00", - "latency": 724408, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.890236009+02:00", - "latency": 725043, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.9044578+02:00", - "latency": 4602475, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.911018211+02:00", - "latency": 763713, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.92002407+02:00", - "latency": 562815, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.931312532+02:00", - "latency": 504582, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.94035424+02:00", - "latency": 610911, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.950476166+02:00", - "latency": 596544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.959368765+02:00", - "latency": 480288, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.971759122+02:00", - "latency": 1961102, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.979667343+02:00", - "latency": 475294, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.989580805+02:00", - "latency": 744653, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:55.999486092+02:00", - "latency": 531700, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.00942096+02:00", - "latency": 603301, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.022388134+02:00", - "latency": 553030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.030410183+02:00", - "latency": 585987, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.040332599+02:00", - "latency": 536592, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.050486494+02:00", - "latency": 593565, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.059451739+02:00", - "latency": 575230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.069394751+02:00", - "latency": 566037, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.082974711+02:00", - "latency": 3095365, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.090035699+02:00", - "latency": 594470, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.100060491+02:00", - "latency": 631485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.110069419+02:00", - "latency": 589834, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.120077914+02:00", - "latency": 581769, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.130096923+02:00", - "latency": 591783, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.14048103+02:00", - "latency": 615723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.14942476+02:00", - "latency": 595782, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.162671554+02:00", - "latency": 2810243, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.169577805+02:00", - "latency": 524253, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.179491849+02:00", - "latency": 552007, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.189435354+02:00", - "latency": 572241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.200946295+02:00", - "latency": 2137102, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.209969961+02:00", - "latency": 607799, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.22001211+02:00", - "latency": 577955, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.23007407+02:00", - "latency": 602803, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.241986615+02:00", - "latency": 2506263, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.250011569+02:00", - "latency": 588428, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.259987162+02:00", - "latency": 545983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.269927343+02:00", - "latency": 485280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.279923398+02:00", - "latency": 563938, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.292651905+02:00", - "latency": 3226469, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.305587761+02:00", - "latency": 974178, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.309653028+02:00", - "latency": 639430, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.319782778+02:00", - "latency": 658989, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.345348194+02:00", - "latency": 668986, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.345618828+02:00", - "latency": 748885, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.351622146+02:00", - "latency": 2560951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.359689135+02:00", - "latency": 626601, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.369632885+02:00", - "latency": 525741, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.380969547+02:00", - "latency": 1480075, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.393922554+02:00", - "latency": 600859, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.40008133+02:00", - "latency": 683313, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.416107052+02:00", - "latency": 4248582, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.420224875+02:00", - "latency": 689612, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.432273992+02:00", - "latency": 2454589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.44261771+02:00", - "latency": 537480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.451712976+02:00", - "latency": 2653386, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.459500427+02:00", - "latency": 532579, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.469584085+02:00", - "latency": 565044, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.479790806+02:00", - "latency": 745384, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.494143459+02:00", - "latency": 4899130, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.500263721+02:00", - "latency": 537968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.512830443+02:00", - "latency": 1003368, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.520000814+02:00", - "latency": 711007, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.529967386+02:00", - "latency": 529990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.540080443+02:00", - "latency": 627473, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.550044305+02:00", - "latency": 544512, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.560139938+02:00", - "latency": 669095, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.572354543+02:00", - "latency": 543535, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.581328418+02:00", - "latency": 510694, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.590392407+02:00", - "latency": 532537, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.600429681+02:00", - "latency": 585563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.613248708+02:00", - "latency": 3347024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.620339769+02:00", - "latency": 603928, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.630406075+02:00", - "latency": 622324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.642334725+02:00", - "latency": 504961, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.650368126+02:00", - "latency": 594397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.664012796+02:00", - "latency": 2182050, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.67024571+02:00", - "latency": 676671, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.680300784+02:00", - "latency": 595498, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.690195601+02:00", - "latency": 556051, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.700253686+02:00", - "latency": 576085, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.710329679+02:00", - "latency": 613512, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.720314994+02:00", - "latency": 579798, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.73016587+02:00", - "latency": 460251, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.740133078+02:00", - "latency": 510861, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.749700866+02:00", - "latency": 609090, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.763254384+02:00", - "latency": 609272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.770324503+02:00", - "latency": 528218, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.780292696+02:00", - "latency": 551026, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.790274597+02:00", - "latency": 554141, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.800320401+02:00", - "latency": 592117, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.810351078+02:00", - "latency": 539202, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.821424529+02:00", - "latency": 532543, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.829570135+02:00", - "latency": 660288, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.843927114+02:00", - "latency": 4846485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.850014979+02:00", - "latency": 584533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.860034626+02:00", - "latency": 589639, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.870031696+02:00", - "latency": 620799, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.87997464+02:00", - "latency": 570765, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.890003002+02:00", - "latency": 587451, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.900064368+02:00", - "latency": 653122, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.910015685+02:00", - "latency": 540504, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.920121418+02:00", - "latency": 622918, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.9301278+02:00", - "latency": 617705, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.940230888+02:00", - "latency": 646552, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.950232825+02:00", - "latency": 514136, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.960191361+02:00", - "latency": 536640, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.970196755+02:00", - "latency": 577728, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.982054963+02:00", - "latency": 2400041, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.990057235+02:00", - "latency": 580541, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:56.999931082+02:00", - "latency": 482372, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.009969364+02:00", - "latency": 607787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.020738657+02:00", - "latency": 1345059, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.031624363+02:00", - "latency": 2339582, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.039773127+02:00", - "latency": 568536, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.049729824+02:00", - "latency": 559858, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.059825542+02:00", - "latency": 650025, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.069781108+02:00", - "latency": 576716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.079357967+02:00", - "latency": 529264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.090419702+02:00", - "latency": 618400, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.100427503+02:00", - "latency": 616418, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.109392515+02:00", - "latency": 570150, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.120097148+02:00", - "latency": 491436, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.132146806+02:00", - "latency": 2441254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.140284548+02:00", - "latency": 654785, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.150160792+02:00", - "latency": 524762, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.163064888+02:00", - "latency": 4230294, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.170097835+02:00", - "latency": 525649, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.180100561+02:00", - "latency": 593878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.19003733+02:00", - "latency": 502592, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.199952535+02:00", - "latency": 549673, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.209967269+02:00", - "latency": 651611, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.219872764+02:00", - "latency": 584023, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.229945017+02:00", - "latency": 605715, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.239948071+02:00", - "latency": 625999, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.2498765+02:00", - "latency": 541020, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.262407092+02:00", - "latency": 564905, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.272765125+02:00", - "latency": 488657, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.27969606+02:00", - "latency": 541381, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.289799353+02:00", - "latency": 588017, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.299592992+02:00", - "latency": 485352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.310436223+02:00", - "latency": 543128, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.3207972+02:00", - "latency": 506866, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.329750937+02:00", - "latency": 526163, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.339756082+02:00", - "latency": 606052, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.3497431+02:00", - "latency": 557732, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.359732101+02:00", - "latency": 510132, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.37209905+02:00", - "latency": 2922391, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.379846776+02:00", - "latency": 562776, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.389813855+02:00", - "latency": 547849, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.401471192+02:00", - "latency": 2246060, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.409287083+02:00", - "latency": 464931, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.422784262+02:00", - "latency": 2954826, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.429746622+02:00", - "latency": 500217, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.440747869+02:00", - "latency": 1523707, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.450310342+02:00", - "latency": 531737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.459661875+02:00", - "latency": 542080, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.46964029+02:00", - "latency": 550789, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.480668487+02:00", - "latency": 646978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.489790931+02:00", - "latency": 630479, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.499837077+02:00", - "latency": 601795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.510011266+02:00", - "latency": 720787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.522028707+02:00", - "latency": 1748987, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.530162373+02:00", - "latency": 661361, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.540474128+02:00", - "latency": 788720, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.550412693+02:00", - "latency": 604179, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.56140936+02:00", - "latency": 580119, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.569836499+02:00", - "latency": 495950, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.579540084+02:00", - "latency": 639242, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.58970827+02:00", - "latency": 695139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.599817437+02:00", - "latency": 625444, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.609973202+02:00", - "latency": 660027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.620203958+02:00", - "latency": 679267, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.629699995+02:00", - "latency": 758295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.639786674+02:00", - "latency": 659299, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.666244038+02:00", - "latency": 5092740, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.67075204+02:00", - "latency": 676760, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.672751241+02:00", - "latency": 16085919, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.679920901+02:00", - "latency": 685784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.689997269+02:00", - "latency": 607424, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.700107831+02:00", - "latency": 640222, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.710205125+02:00", - "latency": 620960, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.722948718+02:00", - "latency": 639285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.736711489+02:00", - "latency": 7656425, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.739758211+02:00", - "latency": 592738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.75674039+02:00", - "latency": 7818023, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.759464938+02:00", - "latency": 567846, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.769879972+02:00", - "latency": 929555, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.780153605+02:00", - "latency": 588597, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.79068233+02:00", - "latency": 929974, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.80018769+02:00", - "latency": 700963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.810175665+02:00", - "latency": 454687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.820354446+02:00", - "latency": 698299, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.830453962+02:00", - "latency": 618352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.839566812+02:00", - "latency": 675701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.850950575+02:00", - "latency": 1760519, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.860585744+02:00", - "latency": 811723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.869832482+02:00", - "latency": 703647, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.879901902+02:00", - "latency": 645076, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.890271776+02:00", - "latency": 730592, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.902452499+02:00", - "latency": 617135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.909344016+02:00", - "latency": 520014, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.920404673+02:00", - "latency": 619280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.932543505+02:00", - "latency": 569220, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.94074144+02:00", - "latency": 1723763, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.950006091+02:00", - "latency": 642462, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.960240061+02:00", - "latency": 685364, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.970289093+02:00", - "latency": 604787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.980481765+02:00", - "latency": 740907, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:57.992399133+02:00", - "latency": 549397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.000505292+02:00", - "latency": 623671, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.00967813+02:00", - "latency": 697563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.019706731+02:00", - "latency": 560690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.029835364+02:00", - "latency": 695385, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.039838506+02:00", - "latency": 557276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.04993143+02:00", - "latency": 647006, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.061486296+02:00", - "latency": 745656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.069573737+02:00", - "latency": 575117, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.080320471+02:00", - "latency": 1399710, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.090413734+02:00", - "latency": 592151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.100036303+02:00", - "latency": 575109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.115557416+02:00", - "latency": 690721, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.119413375+02:00", - "latency": 590430, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.129633969+02:00", - "latency": 628558, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.139609708+02:00", - "latency": 551739, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.149635704+02:00", - "latency": 571273, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.159629529+02:00", - "latency": 522597, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.171378461+02:00", - "latency": 545211, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.18361379+02:00", - "latency": 3795262, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.190425274+02:00", - "latency": 595960, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.199445561+02:00", - "latency": 577155, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.209406323+02:00", - "latency": 599354, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.219517042+02:00", - "latency": 651563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.229516865+02:00", - "latency": 554984, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.239601498+02:00", - "latency": 637870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.249609553+02:00", - "latency": 558035, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.259615138+02:00", - "latency": 596179, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.269618738+02:00", - "latency": 578858, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.279610384+02:00", - "latency": 561591, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.289838615+02:00", - "latency": 696452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.299891798+02:00", - "latency": 539019, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.309946536+02:00", - "latency": 579067, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.320042399+02:00", - "latency": 695334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.330068164+02:00", - "latency": 591963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.340093719+02:00", - "latency": 571888, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.350109566+02:00", - "latency": 588820, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.362460316+02:00", - "latency": 591123, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.369399734+02:00", - "latency": 541028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.380335934+02:00", - "latency": 526115, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.391478981+02:00", - "latency": 622952, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.39969847+02:00", - "latency": 836947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.409779753+02:00", - "latency": 630885, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.419981683+02:00", - "latency": 663602, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.430000575+02:00", - "latency": 587479, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.440003268+02:00", - "latency": 809398, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.450340287+02:00", - "latency": 778828, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.460386062+02:00", - "latency": 553897, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.470329746+02:00", - "latency": 501313, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.480319417+02:00", - "latency": 567783, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.49036809+02:00", - "latency": 607253, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.499364392+02:00", - "latency": 551003, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.510635603+02:00", - "latency": 1519723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.520468444+02:00", - "latency": 614031, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.529494081+02:00", - "latency": 569603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.539450893+02:00", - "latency": 614772, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.549602688+02:00", - "latency": 648024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.559993865+02:00", - "latency": 716442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.575056343+02:00", - "latency": 1762355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.58086137+02:00", - "latency": 560174, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.59359882+02:00", - "latency": 753631, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.599797612+02:00", - "latency": 735096, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.612680794+02:00", - "latency": 3763596, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.619502927+02:00", - "latency": 613341, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.629561863+02:00", - "latency": 589165, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.640460499+02:00", - "latency": 641625, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.649502494+02:00", - "latency": 650084, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.659586713+02:00", - "latency": 648675, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.669702472+02:00", - "latency": 636255, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.679954989+02:00", - "latency": 719357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.689811293+02:00", - "latency": 481164, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.699790338+02:00", - "latency": 609925, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.709717355+02:00", - "latency": 497398, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.719566599+02:00", - "latency": 465787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.72949798+02:00", - "latency": 549184, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.739279472+02:00", - "latency": 473701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.750250121+02:00", - "latency": 547851, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.760151798+02:00", - "latency": 520188, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.77019027+02:00", - "latency": 621647, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.780204983+02:00", - "latency": 508635, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.792044025+02:00", - "latency": 2440569, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.800110582+02:00", - "latency": 589254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.810098612+02:00", - "latency": 595292, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.820108247+02:00", - "latency": 575414, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.8300778+02:00", - "latency": 574454, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.840220363+02:00", - "latency": 686675, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.850109188+02:00", - "latency": 576900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.859715558+02:00", - "latency": 805187, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.869665291+02:00", - "latency": 540099, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.879696522+02:00", - "latency": 643961, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.891409608+02:00", - "latency": 587549, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.899413966+02:00", - "latency": 595590, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.910959945+02:00", - "latency": 513396, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.919973062+02:00", - "latency": 605090, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.930008551+02:00", - "latency": 482359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:58.939976852+02:00", - "latency": 557531, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - } - ] -} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_positions_20251003_173658.json b/test_results/grpc_load_tests/trading_service_positions_20251003_173658.json deleted file mode 100644 index 31feafb0a..000000000 --- a/test_results/grpc_load_tests/trading_service_positions_20251003_173658.json +++ /dev/null @@ -1 +0,0 @@ -{"date":"2025-10-03T17:37:28+02:00","endReason":"timeout","options":{"call":"trading.TradingService.GetPositions","host":"localhost:50051","proto":"/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto","import-paths":["/home/jgrusewski/Work/foxhunt/services/trading_service/proto","."],"insecure":true,"rps":100,"load-schedule":"const","load-start":0,"load-end":0,"load-step":0,"load-step-duration":0,"load-max-duration":0,"concurrency":50,"concurrency-schedule":"const","concurrency-start":0,"concurrency-end":0,"concurrency-step":1,"concurrency-step-duration":0,"concurrency-max-duration":0,"total":2147483647,"connections":10,"duration":10000000000,"timeout":20000000000,"dial-timeout":10000000000,"data":{"account_id":"test-account","symbol":"AAPL"},"binary":false,"CPUs":16},"count":999,"total":10006283864,"average":1036402,"fastest":0,"slowest":0,"rps":99.83726362132714,"errorDistribution":{"rpc error: code = Unauthenticated desc = Valid authentication required":999},"statusCodeDistribution":{"Unauthenticated":999},"latencyDistribution":null,"histogram":null,"details":[{"timestamp":"2025-10-03T17:37:18.661747721+02:00","latency":835438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.670816655+02:00","latency":671766,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.679995948+02:00","latency":671629,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.69015586+02:00","latency":743375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.703434179+02:00","latency":3983872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.710579511+02:00","latency":669111,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.7206554+02:00","latency":591572,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.730679505+02:00","latency":585110,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.740804124+02:00","latency":747375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.749875634+02:00","latency":646646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.760011935+02:00","latency":663636,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.770066882+02:00","latency":608182,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.78022403+02:00","latency":640677,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.790255369+02:00","latency":564177,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.800656905+02:00","latency":617377,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.812857624+02:00","latency":2833963,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.820676757+02:00","latency":1287440,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.832538506+02:00","latency":656907,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.840754483+02:00","latency":694554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.851877719+02:00","latency":622507,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.859966682+02:00","latency":604541,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.870100895+02:00","latency":631162,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.880294904+02:00","latency":671285,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.890407227+02:00","latency":597801,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.900470829+02:00","latency":584984,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.910661983+02:00","latency":698025,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.920698055+02:00","latency":524559,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.930763638+02:00","latency":604136,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.939882775+02:00","latency":662481,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.950103827+02:00","latency":763706,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.960089684+02:00","latency":580546,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.970153696+02:00","latency":559824,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.98026839+02:00","latency":599118,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:18.990505133+02:00","latency":706910,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.000752854+02:00","latency":753520,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.010711922+02:00","latency":1510417,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.020834246+02:00","latency":663401,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.029925826+02:00","latency":568454,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.040132974+02:00","latency":783168,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.0501466+02:00","latency":556434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.060155831+02:00","latency":582618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.070766094+02:00","latency":767947,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.080819018+02:00","latency":1423114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.092020412+02:00","latency":631806,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.100074795+02:00","latency":564514,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.110172397+02:00","latency":568745,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.120368833+02:00","latency":659974,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.130668609+02:00","latency":634679,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.143667511+02:00","latency":3493813,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.150943029+02:00","latency":731483,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.160271237+02:00","latency":789887,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.170460039+02:00","latency":632828,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.181663268+02:00","latency":1731552,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.191669362+02:00","latency":1491583,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.201656163+02:00","latency":2499309,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.213059542+02:00","latency":2871796,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.222676936+02:00","latency":3072736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.230861163+02:00","latency":694299,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.240138119+02:00","latency":643651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.251699135+02:00","latency":2158854,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.264014502+02:00","latency":3849366,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.270220494+02:00","latency":626091,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.283837403+02:00","latency":3830927,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.290656926+02:00","latency":1296229,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.299978592+02:00","latency":793035,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.309874444+02:00","latency":583261,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.320135102+02:00","latency":796210,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.330113792+02:00","latency":685062,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.340692365+02:00","latency":1197951,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.349983217+02:00","latency":757007,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.36165738+02:00","latency":2216308,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.370677274+02:00","latency":544198,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.37976017+02:00","latency":567270,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.389845781+02:00","latency":591505,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.399967332+02:00","latency":555717,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.41009165+02:00","latency":660677,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.420123748+02:00","latency":642170,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.432430563+02:00","latency":550931,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.442679293+02:00","latency":2779532,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.452677431+02:00","latency":3512467,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.460038974+02:00","latency":909661,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.470125869+02:00","latency":553821,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.480089958+02:00","latency":590524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.490267042+02:00","latency":686028,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.500720629+02:00","latency":526651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.509775636+02:00","latency":603121,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.519752039+02:00","latency":551844,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.532365969+02:00","latency":530190,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.540416337+02:00","latency":562507,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.550419571+02:00","latency":562903,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.561821508+02:00","latency":1955398,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.569806507+02:00","latency":551446,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.580631753+02:00","latency":616479,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.591747028+02:00","latency":1628093,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.600725055+02:00","latency":567836,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.610774796+02:00","latency":593180,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.620090924+02:00","latency":935935,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.630024575+02:00","latency":448924,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.642676409+02:00","latency":3188662,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.650760011+02:00","latency":569093,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.710682795+02:00","latency":421832,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.7110182+02:00","latency":1691415,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.711028098+02:00","latency":1505416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.711039017+02:00","latency":1740885,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.71106644+02:00","latency":1768315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.711156145+02:00","latency":1484237,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.721973504+02:00","latency":2471148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.729963907+02:00","latency":462064,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.739979812+02:00","latency":480751,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.749788018+02:00","latency":457592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.760793457+02:00","latency":623926,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.769800968+02:00","latency":607856,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.779853237+02:00","latency":501820,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.789843773+02:00","latency":586068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.799861814+02:00","latency":592395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.810130917+02:00","latency":670240,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.820267024+02:00","latency":541226,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.830292828+02:00","latency":567693,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.842667848+02:00","latency":2971882,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.850729452+02:00","latency":584557,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.859697771+02:00","latency":589428,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.87070349+02:00","latency":543390,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.880812298+02:00","latency":633228,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.88971285+02:00","latency":512845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.900502841+02:00","latency":594835,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.910542366+02:00","latency":624902,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.920562047+02:00","latency":538297,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.933785121+02:00","latency":3752725,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.939864879+02:00","latency":568011,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.950093139+02:00","latency":715877,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.960015074+02:00","latency":533160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.970105255+02:00","latency":528619,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.980239726+02:00","latency":595303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:19.990228183+02:00","latency":531764,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.000378234+02:00","latency":594921,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.010348486+02:00","latency":499702,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.02052445+02:00","latency":694443,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.030635784+02:00","latency":614610,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.040740409+02:00","latency":580016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.049931202+02:00","latency":710539,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.060254167+02:00","latency":902106,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.070431667+02:00","latency":605664,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.080458573+02:00","latency":519232,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.090741686+02:00","latency":731277,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.101645588+02:00","latency":1525460,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.112428128+02:00","latency":563018,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.120556433+02:00","latency":659373,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.13049644+02:00","latency":533147,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.14171698+02:00","latency":1802814,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.149843907+02:00","latency":643757,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.162694817+02:00","latency":3417498,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.169942362+02:00","latency":769910,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.179956699+02:00","latency":542790,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.190837736+02:00","latency":1423558,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.19980991+02:00","latency":551062,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.209886139+02:00","latency":638080,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.221713913+02:00","latency":2430666,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.232712867+02:00","latency":3524547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.23983246+02:00","latency":653492,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.249789449+02:00","latency":525522,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.259922445+02:00","latency":639883,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.269893457+02:00","latency":535253,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.280048345+02:00","latency":693917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.290682642+02:00","latency":1236524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.300808702+02:00","latency":653116,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.310780222+02:00","latency":586602,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.319794426+02:00","latency":565267,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.329981406+02:00","latency":683187,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.340071051+02:00","latency":654232,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.350252367+02:00","latency":639629,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.360464427+02:00","latency":646552,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.370652914+02:00","latency":627632,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.379877508+02:00","latency":604514,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.393692731+02:00","latency":3773023,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.399762535+02:00","latency":617657,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.409951358+02:00","latency":653230,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.41976811+02:00","latency":508643,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.429962326+02:00","latency":561170,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.442711013+02:00","latency":3261364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.450733679+02:00","latency":1538916,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.461714851+02:00","latency":1549088,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.46977941+02:00","latency":614871,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.479854774+02:00","latency":574508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.489974089+02:00","latency":645648,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.500066188+02:00","latency":606125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.510100838+02:00","latency":587849,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.520248162+02:00","latency":660527,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.530251924+02:00","latency":589093,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.54053561+02:00","latency":788323,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.550684991+02:00","latency":602152,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.56007699+02:00","latency":749258,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.570277615+02:00","latency":691549,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.580473107+02:00","latency":561450,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.590581119+02:00","latency":640927,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.59985449+02:00","latency":606539,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.610137706+02:00","latency":911250,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.620158015+02:00","latency":568434,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.631958648+02:00","latency":2096579,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.640529146+02:00","latency":721260,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.653707514+02:00","latency":3702284,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.659770943+02:00","latency":614493,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.67001716+02:00","latency":689476,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.680136136+02:00","latency":636395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.690579009+02:00","latency":629222,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.700877721+02:00","latency":761114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.709976326+02:00","latency":625816,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.72018011+02:00","latency":714895,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.730235186+02:00","latency":546834,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.740344065+02:00","latency":595787,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.750622558+02:00","latency":806256,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.760761217+02:00","latency":629438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.770597438+02:00","latency":861464,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.782431945+02:00","latency":633223,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.790602237+02:00","latency":655813,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.800779373+02:00","latency":655861,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.810012787+02:00","latency":715197,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.820059055+02:00","latency":575497,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.830089428+02:00","latency":565676,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.84023905+02:00","latency":645631,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.850527836+02:00","latency":737020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.860746632+02:00","latency":759954,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.871035213+02:00","latency":1861974,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.880123177+02:00","latency":587818,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.890257978+02:00","latency":664358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.900785431+02:00","latency":838228,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.91332571+02:00","latency":868172,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.920937+02:00","latency":805070,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.930051278+02:00","latency":548820,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.940057353+02:00","latency":534533,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.951014494+02:00","latency":1487600,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.960040383+02:00","latency":549122,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.970093674+02:00","latency":572229,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.983076236+02:00","latency":3361251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:20.990224406+02:00","latency":606874,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.000417483+02:00","latency":659750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.01062412+02:00","latency":738139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.020711535+02:00","latency":600917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.029770496+02:00","latency":599202,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.041007019+02:00","latency":1803249,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.050284307+02:00","latency":803922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.060341324+02:00","latency":655307,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.070673439+02:00","latency":827198,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.080604585+02:00","latency":601301,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.09023988+02:00","latency":683028,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.101020899+02:00","latency":975938,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.110135073+02:00","latency":605354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.120335195+02:00","latency":685395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.130352794+02:00","latency":520241,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.140403323+02:00","latency":571101,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.15046963+02:00","latency":616144,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.164083728+02:00","latency":4054828,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.170159289+02:00","latency":551298,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.18028077+02:00","latency":657620,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.190245008+02:00","latency":598855,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.201005299+02:00","latency":1106814,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.21053589+02:00","latency":1035083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.222988557+02:00","latency":3026133,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.230058708+02:00","latency":552219,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.240086715+02:00","latency":563613,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.250700269+02:00","latency":1068501,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.259831042+02:00","latency":602034,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.269836364+02:00","latency":568416,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.28195017+02:00","latency":2618565,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.290026636+02:00","latency":592818,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.300200171+02:00","latency":657548,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.310522951+02:00","latency":848347,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.321011753+02:00","latency":963128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.329907346+02:00","latency":707624,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.341834316+02:00","latency":2395523,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.349961803+02:00","latency":633363,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.363728507+02:00","latency":4293493,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.36991142+02:00","latency":603257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.382723752+02:00","latency":3263401,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.389965342+02:00","latency":697419,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.400118423+02:00","latency":600329,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.410344817+02:00","latency":651195,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.420427388+02:00","latency":586772,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.430470912+02:00","latency":523404,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.440559551+02:00","latency":586968,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.45061397+02:00","latency":602347,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.46095597+02:00","latency":456808,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.470247188+02:00","latency":609175,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.480334889+02:00","latency":588866,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.49034759+02:00","latency":540831,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.500382511+02:00","latency":547007,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.510494767+02:00","latency":584979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.520637475+02:00","latency":723759,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.530508071+02:00","latency":583775,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.543008998+02:00","latency":3074249,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.549860508+02:00","latency":629292,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.560300184+02:00","latency":662507,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.570903222+02:00","latency":640213,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.581688608+02:00","latency":2320821,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.594414621+02:00","latency":527495,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.600598172+02:00","latency":638319,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.610709316+02:00","latency":627141,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.621397444+02:00","latency":510604,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.630458248+02:00","latency":595066,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.643364139+02:00","latency":550375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.650361063+02:00","latency":515010,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.660464215+02:00","latency":737785,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.671192597+02:00","latency":1444391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.680444709+02:00","latency":572242,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.691430195+02:00","latency":572064,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.700378634+02:00","latency":464926,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.710354412+02:00","latency":524845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.72035462+02:00","latency":499524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.732694052+02:00","latency":2848429,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.740763308+02:00","latency":596948,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.749703881+02:00","latency":573558,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.7606749+02:00","latency":591388,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.770696658+02:00","latency":563029,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.779683438+02:00","latency":568749,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.790792872+02:00","latency":637376,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.799974215+02:00","latency":726367,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.81168097+02:00","latency":2485525,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.819750973+02:00","latency":642804,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.82983277+02:00","latency":635873,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.844443695+02:00","latency":547477,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.850476757+02:00","latency":565462,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.860460002+02:00","latency":592859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.870449474+02:00","latency":535354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.880543557+02:00","latency":629926,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.890482039+02:00","latency":516925,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.902702163+02:00","latency":2779749,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.909718285+02:00","latency":591020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.920662501+02:00","latency":539716,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.932922298+02:00","latency":2784985,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.939929048+02:00","latency":570626,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.950562663+02:00","latency":607122,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.960587516+02:00","latency":587302,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.972810985+02:00","latency":2707062,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.979946429+02:00","latency":625197,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:21.990138123+02:00","latency":672930,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.000290574+02:00","latency":570118,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.010419538+02:00","latency":555862,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.020507626+02:00","latency":563793,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.038347321+02:00","latency":500364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.040039146+02:00","latency":351060,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.050494655+02:00","latency":500849,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.060479311+02:00","latency":557075,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.071389312+02:00","latency":510474,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.080309086+02:00","latency":462810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.089969355+02:00","latency":500375,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.100238685+02:00","latency":457825,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.11025124+02:00","latency":500386,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.120238137+02:00","latency":529856,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.13470736+02:00","latency":4928104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.141675342+02:00","latency":2522236,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.150748349+02:00","latency":541121,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.161680539+02:00","latency":2506376,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.170690993+02:00","latency":496888,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.180680542+02:00","latency":601646,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.190950097+02:00","latency":807385,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.200919406+02:00","latency":1482791,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.209958165+02:00","latency":567606,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.219968798+02:00","latency":516160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.229975204+02:00","latency":542363,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.240124495+02:00","latency":645298,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.250123864+02:00","latency":505616,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.260041779+02:00","latency":470043,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.271674361+02:00","latency":2154002,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.28074454+02:00","latency":540955,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.291752346+02:00","latency":2590902,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.300693884+02:00","latency":532081,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.31072727+02:00","latency":502617,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.319722903+02:00","latency":568010,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.329640713+02:00","latency":504400,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.34062123+02:00","latency":501309,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.350589467+02:00","latency":508074,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.360605257+02:00","latency":551231,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.372952708+02:00","latency":2900795,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.379967897+02:00","latency":522364,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.390463005+02:00","latency":560235,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.40044003+02:00","latency":525356,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.410516297+02:00","latency":587343,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.420570622+02:00","latency":526086,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.430653279+02:00","latency":615768,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.44076104+02:00","latency":657845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.449686907+02:00","latency":535698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.460844364+02:00","latency":591350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.469811361+02:00","latency":521245,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.48099803+02:00","latency":1721254,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.490083051+02:00","latency":579854,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.500244317+02:00","latency":567685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.510403062+02:00","latency":623810,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.523147402+02:00","latency":3444005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.530137893+02:00","latency":572728,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.540281511+02:00","latency":641651,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.55371927+02:00","latency":3984404,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.559745859+02:00","latency":584018,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.572698309+02:00","latency":3536900,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.579751752+02:00","latency":593005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.589674426+02:00","latency":515391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.60270635+02:00","latency":2535455,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.610694624+02:00","latency":1557563,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.621050021+02:00","latency":888158,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.630110587+02:00","latency":581278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.640135978+02:00","latency":559020,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.651015755+02:00","latency":984941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.663620632+02:00","latency":4137550,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.670089278+02:00","latency":524430,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.680980825+02:00","latency":947716,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.689990036+02:00","latency":554864,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.700025042+02:00","latency":575138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.710173497+02:00","latency":641285,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.72021116+02:00","latency":528491,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.733696069+02:00","latency":3983558,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.739677679+02:00","latency":540119,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.751701174+02:00","latency":1528430,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.760962677+02:00","latency":783772,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.773845307+02:00","latency":3952276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.780538673+02:00","latency":516594,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.790390874+02:00","latency":527798,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.800386181+02:00","latency":527947,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.810426189+02:00","latency":579948,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.820408149+02:00","latency":504174,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.830367686+02:00","latency":516325,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.840429944+02:00","latency":609256,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.850449328+02:00","latency":536662,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.860475842+02:00","latency":538624,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.870374794+02:00","latency":545806,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.880343636+02:00","latency":517656,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.89040388+02:00","latency":578143,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.900405134+02:00","latency":539251,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.91042623+02:00","latency":542799,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.920464565+02:00","latency":557395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.930444994+02:00","latency":532469,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.94044775+02:00","latency":536542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.949739388+02:00","latency":556189,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.95971199+02:00","latency":551414,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.972635234+02:00","latency":3363227,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.979803022+02:00","latency":486798,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.989781208+02:00","latency":464713,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:22.999786571+02:00","latency":592695,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.011419331+02:00","latency":527263,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.020442852+02:00","latency":512918,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.032690447+02:00","latency":1825702,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.040837034+02:00","latency":634626,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.049825947+02:00","latency":637029,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.059898656+02:00","latency":607472,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.069869664+02:00","latency":553207,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.079910421+02:00","latency":552575,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.089934468+02:00","latency":588719,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.100182102+02:00","latency":671812,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.110592809+02:00","latency":633374,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.120842169+02:00","latency":631271,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.129857855+02:00","latency":552211,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.140314497+02:00","latency":535382,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.150930369+02:00","latency":1021599,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.159906779+02:00","latency":497688,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.171778969+02:00","latency":2363848,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.179777908+02:00","latency":492310,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.189713404+02:00","latency":488545,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.200767184+02:00","latency":581603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.209774596+02:00","latency":619595,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.223156708+02:00","latency":3935276,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.230181632+02:00","latency":522411,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.240145368+02:00","latency":488030,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.250151332+02:00","latency":529857,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.260252254+02:00","latency":596065,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.270301961+02:00","latency":500104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.280381386+02:00","latency":568349,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.290448243+02:00","latency":492166,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.300501983+02:00","latency":532438,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.310487983+02:00","latency":517293,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.3204565+02:00","latency":517011,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.330619109+02:00","latency":694883,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.340619194+02:00","latency":555019,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.351783275+02:00","latency":1710476,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.359884683+02:00","latency":650985,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.370543254+02:00","latency":1327508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.380758268+02:00","latency":699858,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.390715003+02:00","latency":597163,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.400676011+02:00","latency":1554875,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.410756503+02:00","latency":593790,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.41975896+02:00","latency":607452,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.430641891+02:00","latency":1401193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.443791887+02:00","latency":4497480,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.449802836+02:00","latency":586528,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.459902678+02:00","latency":528005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.470603177+02:00","latency":1261524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.483657889+02:00","latency":3565690,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.490927476+02:00","latency":785635,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.500946204+02:00","latency":1518215,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.509914129+02:00","latency":515215,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.519938555+02:00","latency":534307,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.531641161+02:00","latency":2211203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.541681908+02:00","latency":1544215,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.550668869+02:00","latency":518089,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.560581887+02:00","latency":490526,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.570588214+02:00","latency":517380,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.580543253+02:00","latency":517051,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.591775481+02:00","latency":1780358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.599691269+02:00","latency":486919,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.612694745+02:00","latency":2600761,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.620685214+02:00","latency":1559125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.630772896+02:00","latency":604595,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.639819824+02:00","latency":619135,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.649989947+02:00","latency":613542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.660242803+02:00","latency":830811,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.670680415+02:00","latency":1157013,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.681663939+02:00","latency":1443748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.691670669+02:00","latency":1512363,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.702735885+02:00","latency":3526941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.713265063+02:00","latency":4048793,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.720207838+02:00","latency":598860,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.730175659+02:00","latency":567806,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.740196615+02:00","latency":521411,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.750286862+02:00","latency":602224,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.760464447+02:00","latency":620386,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.770503384+02:00","latency":552949,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.780611824+02:00","latency":576675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.790625602+02:00","latency":532127,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.800909049+02:00","latency":633823,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.810417274+02:00","latency":509529,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.820327146+02:00","latency":497822,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.830569116+02:00","latency":544026,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.84053201+02:00","latency":515531,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.850575502+02:00","latency":587832,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.862687825+02:00","latency":2699246,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.869778607+02:00","latency":605706,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.879677448+02:00","latency":516691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.889793093+02:00","latency":667862,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.899866151+02:00","latency":608738,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.910613641+02:00","latency":1179772,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.920050443+02:00","latency":586832,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.930208415+02:00","latency":646918,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.943637423+02:00","latency":3820083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.954186591+02:00","latency":4156679,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.960429572+02:00","latency":662834,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.970689117+02:00","latency":690303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.980757008+02:00","latency":570125,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:23.989708171+02:00","latency":551955,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.000774526+02:00","latency":593161,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.009954363+02:00","latency":661005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.026289415+02:00","latency":539935,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.030252851+02:00","latency":487613,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.043835526+02:00","latency":3854447,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.049976683+02:00","latency":618580,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.060053725+02:00","latency":575411,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.070226529+02:00","latency":671388,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.081851235+02:00","latency":2122545,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.08996365+02:00","latency":552603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.099999932+02:00","latency":520542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.112339256+02:00","latency":499079,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.120398612+02:00","latency":534581,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.130396881+02:00","latency":535148,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.140492247+02:00","latency":508254,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.15042189+02:00","latency":493976,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.16242352+02:00","latency":556153,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.170532293+02:00","latency":604872,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.180606484+02:00","latency":531638,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.190574626+02:00","latency":499941,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.200605087+02:00","latency":509547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.210665014+02:00","latency":578546,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.220817369+02:00","latency":638973,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.229814962+02:00","latency":517255,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.239899966+02:00","latency":607287,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.251508697+02:00","latency":630874,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.260499259+02:00","latency":498691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.270502998+02:00","latency":579337,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.281771781+02:00","latency":1821194,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.289799053+02:00","latency":515172,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.299839228+02:00","latency":536482,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.309921049+02:00","latency":575203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.320136212+02:00","latency":755387,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.330707419+02:00","latency":723501,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.340896623+02:00","latency":735374,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.350568561+02:00","latency":602615,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.360621907+02:00","latency":625875,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.370716604+02:00","latency":540234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.379807669+02:00","latency":669969,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.389796547+02:00","latency":639661,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.411147783+02:00","latency":8530979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.414698898+02:00","latency":3868244,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.426643527+02:00","latency":818303,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.432927221+02:00","latency":2693502,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.439835821+02:00","latency":627464,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.449864512+02:00","latency":575104,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.459950183+02:00","latency":626747,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.469872068+02:00","latency":584400,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.479852826+02:00","latency":699068,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.489875247+02:00","latency":592852,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.499925929+02:00","latency":604421,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.509664578+02:00","latency":500979,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.520780361+02:00","latency":611433,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.52975555+02:00","latency":614282,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.543685416+02:00","latency":3890128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.551658059+02:00","latency":1555362,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.560674159+02:00","latency":590915,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.570786169+02:00","latency":655740,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.579800384+02:00","latency":581344,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.591790693+02:00","latency":2574600,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.599918261+02:00","latency":671609,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.61059763+02:00","latency":699916,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.646621925+02:00","latency":844171,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.646826322+02:00","latency":4843191,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.646965161+02:00","latency":562714,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.649907774+02:00","latency":554708,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.659943159+02:00","latency":580206,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.66996463+02:00","latency":617835,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.680469234+02:00","latency":531851,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.69045678+02:00","latency":525114,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.70046032+02:00","latency":600478,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.710490764+02:00","latency":529841,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.720439225+02:00","latency":575865,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.730473356+02:00","latency":572586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.740448831+02:00","latency":566070,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.750450634+02:00","latency":572863,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.76071718+02:00","latency":628978,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.769966296+02:00","latency":643363,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.779947414+02:00","latency":509054,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.790184049+02:00","latency":804682,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.800031093+02:00","latency":726676,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.810080118+02:00","latency":601468,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.819989467+02:00","latency":521139,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.830025765+02:00","latency":576418,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.840331544+02:00","latency":593514,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.849633918+02:00","latency":526033,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.86067911+02:00","latency":901539,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.870629168+02:00","latency":556949,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.88064024+02:00","latency":1241262,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.893083819+02:00","latency":3585732,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.900286203+02:00","latency":512748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.913987755+02:00","latency":558884,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.922766171+02:00","latency":3340506,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.931315104+02:00","latency":497134,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.944998928+02:00","latency":4191925,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.949943613+02:00","latency":511540,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.959886669+02:00","latency":536515,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.969945788+02:00","latency":660477,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.979875325+02:00","latency":520064,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.989789792+02:00","latency":566946,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:24.999579738+02:00","latency":503190,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.010751126+02:00","latency":656791,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.019742068+02:00","latency":643206,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.03201381+02:00","latency":2943156,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.039996356+02:00","latency":570395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.049976373+02:00","latency":554547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.059875468+02:00","latency":510759,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.069847473+02:00","latency":577010,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.080010576+02:00","latency":676470,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.089890062+02:00","latency":600120,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.099951955+02:00","latency":549730,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.110354654+02:00","latency":962021,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.120605905+02:00","latency":789129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.132740166+02:00","latency":2990529,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.140990977+02:00","latency":1157657,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.149912067+02:00","latency":522328,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.159982073+02:00","latency":642046,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.171460491+02:00","latency":543557,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.180509102+02:00","latency":631950,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.190563931+02:00","latency":590394,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.20063229+02:00","latency":692160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.212329831+02:00","latency":528645,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.223163165+02:00","latency":526260,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.230131583+02:00","latency":563716,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.242592875+02:00","latency":3015986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.249693976+02:00","latency":566342,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.26306089+02:00","latency":1038629,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.270083406+02:00","latency":614174,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.280162437+02:00","latency":619217,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.290146315+02:00","latency":553597,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.30017004+02:00","latency":592922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.311354266+02:00","latency":553319,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.3203489+02:00","latency":552508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.330402987+02:00","latency":562198,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.340285065+02:00","latency":503160,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.350138321+02:00","latency":542117,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.360229096+02:00","latency":592617,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.372007134+02:00","latency":2272751,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.380005467+02:00","latency":553098,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.389975475+02:00","latency":563402,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.399951252+02:00","latency":582354,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.409982209+02:00","latency":590618,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.420442624+02:00","latency":1009425,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.429933438+02:00","latency":558954,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.43987225+02:00","latency":555234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.450249876+02:00","latency":572859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.460309347+02:00","latency":634395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.470228921+02:00","latency":499877,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.480252697+02:00","latency":586378,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.490260094+02:00","latency":592859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.500251921+02:00","latency":568200,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.510228201+02:00","latency":545092,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.521746708+02:00","latency":1345848,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.530296888+02:00","latency":937176,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.540328804+02:00","latency":538129,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.550299333+02:00","latency":549942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.563990018+02:00","latency":4051154,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.56992957+02:00","latency":520629,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.579898062+02:00","latency":526667,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.58984717+02:00","latency":563518,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.599822111+02:00","latency":551879,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.609774983+02:00","latency":537115,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.619732666+02:00","latency":633128,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.629683094+02:00","latency":580007,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.640628242+02:00","latency":529108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.650569916+02:00","latency":497136,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.660494552+02:00","latency":536442,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.670363921+02:00","latency":506630,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.680280831+02:00","latency":497379,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.690130908+02:00","latency":464182,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.700313428+02:00","latency":717586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.710092243+02:00","latency":485681,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.720023206+02:00","latency":527203,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.730028411+02:00","latency":540874,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.740516068+02:00","latency":1106838,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.751261647+02:00","latency":1041342,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.760036092+02:00","latency":677227,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.770036301+02:00","latency":721452,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.780951445+02:00","latency":1558982,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.791959169+02:00","latency":2534234,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.800477931+02:00","latency":547399,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.810452665+02:00","latency":552701,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.82058811+02:00","latency":677866,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.830579182+02:00","latency":499170,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.84126474+02:00","latency":1236239,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.852828522+02:00","latency":2923917,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.859832936+02:00","latency":536522,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.869861431+02:00","latency":632393,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.880526745+02:00","latency":706087,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.890292987+02:00","latency":496963,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.901017574+02:00","latency":1254961,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.909996225+02:00","latency":566840,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.920001698+02:00","latency":655285,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.929859603+02:00","latency":505229,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.940766913+02:00","latency":1494709,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.950057605+02:00","latency":598822,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.963768416+02:00","latency":4316558,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.970850558+02:00","latency":730780,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.979872268+02:00","latency":602391,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:25.98994758+02:00","latency":614547,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.000164062+02:00","latency":740004,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.010312867+02:00","latency":674425,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.020266439+02:00","latency":569115,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.030486115+02:00","latency":720414,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.040559454+02:00","latency":747808,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.051605359+02:00","latency":1589975,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.060692386+02:00","latency":564711,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.07178759+02:00","latency":1682448,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.079682575+02:00","latency":591563,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.090605208+02:00","latency":543325,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.100146191+02:00","latency":665003,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.110121685+02:00","latency":566465,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.12025801+02:00","latency":608499,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.130237445+02:00","latency":580845,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.140258526+02:00","latency":615658,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.150240867+02:00","latency":601396,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.160259434+02:00","latency":626700,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.171797011+02:00","latency":2107035,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.17977808+02:00","latency":576546,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.189842686+02:00","latency":584965,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.199829021+02:00","latency":614315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.21067275+02:00","latency":1435395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.221248942+02:00","latency":1172694,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.2300943+02:00","latency":654005,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.24207643+02:00","latency":2245177,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.249855289+02:00","latency":501002,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.260605984+02:00","latency":734870,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.270617363+02:00","latency":606175,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.28370896+02:00","latency":3586246,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.289743051+02:00","latency":645942,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.29966529+02:00","latency":580237,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.311000688+02:00","latency":816182,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.319592293+02:00","latency":539586,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.331134938+02:00","latency":788311,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.340156839+02:00","latency":618830,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.350514474+02:00","latency":775085,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.360362342+02:00","latency":582505,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.373487592+02:00","latency":695669,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.380509651+02:00","latency":624805,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.393715209+02:00","latency":3770493,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.399681345+02:00","latency":554358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.410689922+02:00","latency":582118,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.421623454+02:00","latency":1446956,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.429825549+02:00","latency":618660,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.439912086+02:00","latency":577656,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.449954853+02:00","latency":592789,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.460591278+02:00","latency":843393,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.470540316+02:00","latency":629420,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.480567367+02:00","latency":695327,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.489959601+02:00","latency":670014,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.504941968+02:00","latency":3130074,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.510018218+02:00","latency":588905,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.520137637+02:00","latency":673171,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.532691236+02:00","latency":3151897,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.539765076+02:00","latency":688336,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.550632019+02:00","latency":530685,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.560662743+02:00","latency":636927,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.570695369+02:00","latency":609894,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.579766557+02:00","latency":656739,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.589750089+02:00","latency":596672,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.600671931+02:00","latency":1553729,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.611051565+02:00","latency":933672,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.620183101+02:00","latency":627499,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.630156188+02:00","latency":556111,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.640148638+02:00","latency":630407,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.65010595+02:00","latency":587257,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.660169036+02:00","latency":657371,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.672782437+02:00","latency":3141675,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.68066336+02:00","latency":1399548,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.690993214+02:00","latency":978034,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.699945428+02:00","latency":577466,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.710274244+02:00","latency":638266,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.720310302+02:00","latency":601015,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.730382345+02:00","latency":611792,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.74771526+02:00","latency":7909360,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.749526382+02:00","latency":479040,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.760641201+02:00","latency":696035,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.770705932+02:00","latency":601655,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.779702074+02:00","latency":576741,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.803674737+02:00","latency":7863225,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.819194059+02:00","latency":6149002,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.819575055+02:00","latency":447358,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.820982357+02:00","latency":2631444,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.830000831+02:00","latency":552086,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.840067223+02:00","latency":658630,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.849955002+02:00","latency":568187,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.860018253+02:00","latency":736570,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.870571714+02:00","latency":686437,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.880741247+02:00","latency":577587,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.890695873+02:00","latency":576345,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.900854215+02:00","latency":651244,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.90992457+02:00","latency":685711,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.922682278+02:00","latency":948701,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.930405162+02:00","latency":595986,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.940335839+02:00","latency":540064,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.951165228+02:00","latency":1303008,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.961697758+02:00","latency":2197025,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.969656188+02:00","latency":588789,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.98068357+02:00","latency":610450,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:26.990654722+02:00","latency":552330,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.000765809+02:00","latency":596552,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.009816578+02:00","latency":727607,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.021900773+02:00","latency":2785016,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.02995005+02:00","latency":590480,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.041985584+02:00","latency":2601715,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.051729227+02:00","latency":638395,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.059658383+02:00","latency":584805,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.070670106+02:00","latency":537051,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.0807166+02:00","latency":636038,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.091706037+02:00","latency":2607424,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.099717002+02:00","latency":650275,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.109670807+02:00","latency":580762,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.120855126+02:00","latency":660445,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.129888414+02:00","latency":618302,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.140668273+02:00","latency":1363866,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.150815188+02:00","latency":685116,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.159927489+02:00","latency":662225,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.170041223+02:00","latency":588390,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.180930934+02:00","latency":1475126,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.190655559+02:00","latency":1339985,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.201017824+02:00","latency":942612,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.210386052+02:00","latency":596414,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.220340147+02:00","latency":537007,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.232709768+02:00","latency":2955891,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.239691163+02:00","latency":604832,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.251186691+02:00","latency":1065350,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.260973816+02:00","latency":1533168,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.270059441+02:00","latency":610922,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.28015878+02:00","latency":684745,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.291860524+02:00","latency":2267012,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.299880293+02:00","latency":592184,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.312695066+02:00","latency":2830141,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.319628193+02:00","latency":545839,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.331983504+02:00","latency":1936681,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.3416786+02:00","latency":1803019,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.353719315+02:00","latency":3634357,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.360686526+02:00","latency":1533372,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.370689224+02:00","latency":598536,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.382352942+02:00","latency":612193,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.390405749+02:00","latency":670984,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.400380772+02:00","latency":580822,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.410359685+02:00","latency":551770,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.420358505+02:00","latency":687195,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.430421223+02:00","latency":632643,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.443609311+02:00","latency":3736185,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.45003841+02:00","latency":642669,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.460041789+02:00","latency":593052,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.472835907+02:00","latency":3398508,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.479761235+02:00","latency":545644,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.490615844+02:00","latency":595432,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.500602825+02:00","latency":653561,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.514696379+02:00","latency":4670694,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.519755242+02:00","latency":636592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.529804241+02:00","latency":662509,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.539804534+02:00","latency":592010,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.54970789+02:00","latency":589290,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.560658388+02:00","latency":608994,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.571021405+02:00","latency":1010859,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.580278082+02:00","latency":607603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.593721167+02:00","latency":4004817,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.599639705+02:00","latency":538928,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.610619111+02:00","latency":539197,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.62070572+02:00","latency":584191,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.63080962+02:00","latency":721042,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.639681059+02:00","latency":596149,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.654638942+02:00","latency":4511108,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.660142371+02:00","latency":657440,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.670087871+02:00","latency":560734,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.680159362+02:00","latency":630091,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.690180321+02:00","latency":576374,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.700129342+02:00","latency":562653,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.714143936+02:00","latency":4202574,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.720146325+02:00","latency":567049,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.730247425+02:00","latency":620457,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.740263007+02:00","latency":599180,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.750270451+02:00","latency":585709,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.762114849+02:00","latency":2518353,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.769998049+02:00","latency":569865,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.781989302+02:00","latency":2639599,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.790139824+02:00","latency":567552,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.800148665+02:00","latency":581210,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.812706794+02:00","latency":3116573,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.819655955+02:00","latency":562349,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.83067834+02:00","latency":570653,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.841095978+02:00","latency":834359,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.850975436+02:00","latency":1611865,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.861832702+02:00","latency":2470522,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.872727745+02:00","latency":3453542,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.879805323+02:00","latency":669355,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.889661768+02:00","latency":574824,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.900669586+02:00","latency":576078,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.912691704+02:00","latency":2572177,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.920695579+02:00","latency":562963,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.930687577+02:00","latency":572131,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.940653644+02:00","latency":541524,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.950542454+02:00","latency":501664,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.960607014+02:00","latency":635780,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.97072696+02:00","latency":709426,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.980663413+02:00","latency":575579,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:27.992710138+02:00","latency":2611315,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.000672882+02:00","latency":1565507,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.013730211+02:00","latency":3635804,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.020175967+02:00","latency":1048390,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.030025161+02:00","latency":586691,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.040031428+02:00","latency":589698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.050055277+02:00","latency":614504,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.060089208+02:00","latency":579834,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.071030127+02:00","latency":1204930,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.080978746+02:00","latency":1464278,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.090066592+02:00","latency":632736,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.101715018+02:00","latency":2261573,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.112806324+02:00","latency":3627082,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.137973013+02:00","latency":503379,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.138182081+02:00","latency":365625,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.13980054+02:00","latency":409786,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.150500713+02:00","latency":554388,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.163773938+02:00","latency":3909277,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.169904096+02:00","latency":684254,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.182970425+02:00","latency":3648554,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.192664924+02:00","latency":3232809,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.200757558+02:00","latency":635295,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.209890474+02:00","latency":732039,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.220090501+02:00","latency":845326,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.231978797+02:00","latency":2595428,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.240121727+02:00","latency":704603,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.250317904+02:00","latency":674164,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.262716071+02:00","latency":2970847,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.27073115+02:00","latency":1586585,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.279913261+02:00","latency":673728,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.289886943+02:00","latency":624592,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.302135524+02:00","latency":2912637,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.310155327+02:00","latency":655030,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.321689713+02:00","latency":2080083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.329653927+02:00","latency":556748,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.342713833+02:00","latency":2629243,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.350700414+02:00","latency":1571784,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.363721061+02:00","latency":4583680,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.372727506+02:00","latency":3619609,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.380713696+02:00","latency":1580750,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.390330095+02:00","latency":667029,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.400351807+02:00","latency":589758,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.410362906+02:00","latency":607267,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.420312531+02:00","latency":579862,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.430474961+02:00","latency":696083,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.440480023+02:00","latency":609550,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.450495209+02:00","latency":623677,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.460525008+02:00","latency":586141,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.470496363+02:00","latency":549333,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.480692462+02:00","latency":785511,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.490813589+02:00","latency":650158,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.499818044+02:00","latency":547680,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.509877334+02:00","latency":613412,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.522679582+02:00","latency":3369610,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.530787609+02:00","latency":589138,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.544750815+02:00","latency":5636698,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.549824432+02:00","latency":610606,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.561711356+02:00","latency":2336361,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.56972877+02:00","latency":612766,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.579769315+02:00","latency":628321,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.592730971+02:00","latency":3498650,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.599684627+02:00","latency":530387,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.613645672+02:00","latency":3502851,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.619813841+02:00","latency":613277,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.630071611+02:00","latency":920999,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"},{"timestamp":"2025-10-03T17:37:28.640076167+02:00","latency":620953,"error":"rpc error: code = Unauthenticated desc = Valid authentication required","status":"Unauthenticated"}]} \ No newline at end of file diff --git a/test_results/grpc_load_tests/trading_service_positions_20251003_173658.txt b/test_results/grpc_load_tests/trading_service_positions_20251003_173658.txt deleted file mode 100644 index ec5b45cf3..000000000 --- a/test_results/grpc_load_tests/trading_service_positions_20251003_173658.txt +++ /dev/null @@ -1,6051 +0,0 @@ -{ - "date": "2025-10-03T17:37:38+02:00", - "endReason": "timeout", - "options": { - "call": "trading.TradingService.GetPositions", - "host": "localhost:50051", - "proto": "/home/jgrusewski/Work/foxhunt/services/trading_service/proto/trading.proto", - "import-paths": [ - "/home/jgrusewski/Work/foxhunt/services/trading_service/proto", - "." - ], - "insecure": true, - "rps": 100, - "load-schedule": "const", - "load-start": 0, - "load-end": 0, - "load-step": 0, - "load-step-duration": 0, - "load-max-duration": 0, - "concurrency": 50, - "concurrency-schedule": "const", - "concurrency-start": 0, - "concurrency-end": 0, - "concurrency-step": 1, - "concurrency-step-duration": 0, - "concurrency-max-duration": 0, - "total": 2147483647, - "connections": 10, - "duration": 10000000000, - "timeout": 20000000000, - "dial-timeout": 10000000000, - "data": { - "account_id": "test-account", - "symbol": "AAPL" - }, - "binary": false, - "CPUs": 16 - }, - "count": 999, - "total": 10060658780, - "average": 1008851, - "fastest": 0, - "slowest": 0, - "rps": 99.29767243333542, - "errorDistribution": { - "rpc error: code = Unauthenticated desc = Valid authentication required": 998, - "rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:43008-\u003e127.0.0.1:50051: use of closed network connection": 1 - }, - "statusCodeDistribution": { - "Unauthenticated": 998, - "Unavailable": 1 - }, - "latencyDistribution": null, - "histogram": null, - "details": [ - { - "timestamp": "2025-10-03T17:37:28.749683562+02:00", - "latency": 777241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.757822836+02:00", - "latency": 725230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.767882933+02:00", - "latency": 640789, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.777955944+02:00", - "latency": 616665, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.791718491+02:00", - "latency": 3855611, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.799813319+02:00", - "latency": 2651791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.807976312+02:00", - "latency": 691281, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.820676888+02:00", - "latency": 3221398, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.82780121+02:00", - "latency": 703524, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.837882534+02:00", - "latency": 618978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.847929355+02:00", - "latency": 604747, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.858017852+02:00", - "latency": 599390, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.868000396+02:00", - "latency": 599351, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.878095663+02:00", - "latency": 675301, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.888070024+02:00", - "latency": 581083, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.898164109+02:00", - "latency": 601970, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.908281593+02:00", - "latency": 639355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.919787924+02:00", - "latency": 2014151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.927791277+02:00", - "latency": 587849, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.937873643+02:00", - "latency": 639266, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.947933122+02:00", - "latency": 628413, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.957934504+02:00", - "latency": 576037, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.968163615+02:00", - "latency": 706544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.97819892+02:00", - "latency": 575123, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.988481546+02:00", - "latency": 623778, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:28.998697559+02:00", - "latency": 757218, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.008137883+02:00", - "latency": 695364, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.018356823+02:00", - "latency": 689806, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.031033172+02:00", - "latency": 1149299, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.039449216+02:00", - "latency": 1861831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.048574873+02:00", - "latency": 625927, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.058659621+02:00", - "latency": 715025, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.068919082+02:00", - "latency": 722568, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.077836426+02:00", - "latency": 604865, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.088064769+02:00", - "latency": 676276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.098199251+02:00", - "latency": 656836, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.108341663+02:00", - "latency": 620309, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.118451424+02:00", - "latency": 613033, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.128421854+02:00", - "latency": 540676, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.140685026+02:00", - "latency": 2833137, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.14866948+02:00", - "latency": 1551662, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.160898445+02:00", - "latency": 2473701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.167995876+02:00", - "latency": 605963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.178047387+02:00", - "latency": 627480, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.188337801+02:00", - "latency": 824978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.198436892+02:00", - "latency": 621951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.209453114+02:00", - "latency": 1344737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.219439613+02:00", - "latency": 739618, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.231657778+02:00", - "latency": 3697975, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.237995227+02:00", - "latency": 678551, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.248062525+02:00", - "latency": 570120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.258175984+02:00", - "latency": 606497, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.268327107+02:00", - "latency": 609576, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.278434317+02:00", - "latency": 578685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.289793868+02:00", - "latency": 1809368, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.297839943+02:00", - "latency": 562146, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.310810087+02:00", - "latency": 3458971, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.317951413+02:00", - "latency": 606787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.328090342+02:00", - "latency": 616200, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.341826752+02:00", - "latency": 3803595, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.34789541+02:00", - "latency": 553600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.358597042+02:00", - "latency": 601888, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.36871788+02:00", - "latency": 651717, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.377707148+02:00", - "latency": 591171, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.387650229+02:00", - "latency": 568027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.398572042+02:00", - "latency": 696233, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.408659388+02:00", - "latency": 563416, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.417690318+02:00", - "latency": 611529, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.429642276+02:00", - "latency": 2411025, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.43799073+02:00", - "latency": 605505, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.452273279+02:00", - "latency": 3899473, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.458289623+02:00", - "latency": 551397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.468361928+02:00", - "latency": 572483, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.482189731+02:00", - "latency": 4390281, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.48821653+02:00", - "latency": 548502, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.498301548+02:00", - "latency": 572375, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.511832592+02:00", - "latency": 4060467, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.520191633+02:00", - "latency": 2841946, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.531815853+02:00", - "latency": 4061761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.537886842+02:00", - "latency": 571417, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.54863304+02:00", - "latency": 632325, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.558792491+02:00", - "latency": 722564, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.567865646+02:00", - "latency": 619677, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.578172342+02:00", - "latency": 576374, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.588217439+02:00", - "latency": 566275, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.600663412+02:00", - "latency": 2974863, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.607758606+02:00", - "latency": 645276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.617721403+02:00", - "latency": 578923, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.627888045+02:00", - "latency": 648800, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.64067277+02:00", - "latency": 3396857, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.649908263+02:00", - "latency": 2796162, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.657921203+02:00", - "latency": 541203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.668064514+02:00", - "latency": 582532, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.678247455+02:00", - "latency": 582939, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.688358371+02:00", - "latency": 629252, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.699767611+02:00", - "latency": 1938491, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.707728422+02:00", - "latency": 544764, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.718045886+02:00", - "latency": 538985, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.728075742+02:00", - "latency": 553358, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.738555735+02:00", - "latency": 625515, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.748587369+02:00", - "latency": 589795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.76067616+02:00", - "latency": 2611516, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.768029801+02:00", - "latency": 940871, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.777941429+02:00", - "latency": 536675, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.787972685+02:00", - "latency": 565140, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.798939916+02:00", - "latency": 1518995, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.807989083+02:00", - "latency": 588491, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.818440229+02:00", - "latency": 510823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.828416563+02:00", - "latency": 496085, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.838375285+02:00", - "latency": 558747, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.850675243+02:00", - "latency": 2780005, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.858646574+02:00", - "latency": 1528514, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.867689471+02:00", - "latency": 572191, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.877777065+02:00", - "latency": 605714, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.888672015+02:00", - "latency": 1299581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.897695335+02:00", - "latency": 549138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.908575986+02:00", - "latency": 706209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.918626902+02:00", - "latency": 606791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.927754077+02:00", - "latency": 595926, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.937893614+02:00", - "latency": 665667, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.948057675+02:00", - "latency": 646416, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.9580432+02:00", - "latency": 626365, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.968057421+02:00", - "latency": 549872, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.978045073+02:00", - "latency": 569110, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.991997409+02:00", - "latency": 4072207, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:29.99806749+02:00", - "latency": 574582, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.008699151+02:00", - "latency": 726853, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.019676343+02:00", - "latency": 2560822, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.027869714+02:00", - "latency": 662486, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.038008975+02:00", - "latency": 656350, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.048191024+02:00", - "latency": 746228, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.058324424+02:00", - "latency": 568232, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.068689172+02:00", - "latency": 592791, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.07792489+02:00", - "latency": 614155, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.088663709+02:00", - "latency": 665612, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.098813839+02:00", - "latency": 1669747, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.107919346+02:00", - "latency": 590700, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.117998122+02:00", - "latency": 635589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.128124231+02:00", - "latency": 689474, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.140848599+02:00", - "latency": 3190288, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.147976977+02:00", - "latency": 617311, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.158155303+02:00", - "latency": 654544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.168343729+02:00", - "latency": 747558, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.178606885+02:00", - "latency": 738906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.1894909+02:00", - "latency": 595297, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.204703458+02:00", - "latency": 6731353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.209389005+02:00", - "latency": 2288434, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.219679996+02:00", - "latency": 1809003, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.228026469+02:00", - "latency": 752000, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.238205998+02:00", - "latency": 623742, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.248403298+02:00", - "latency": 645332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.258487889+02:00", - "latency": 649197, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.268843807+02:00", - "latency": 790243, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.278156315+02:00", - "latency": 896098, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.288767976+02:00", - "latency": 666719, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.300023703+02:00", - "latency": 2773348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.308295731+02:00", - "latency": 527529, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.318223593+02:00", - "latency": 493519, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.328577005+02:00", - "latency": 618824, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.340704938+02:00", - "latency": 2647631, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.34777934+02:00", - "latency": 586933, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.358000501+02:00", - "latency": 701184, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.367982618+02:00", - "latency": 626859, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.378148702+02:00", - "latency": 626959, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.38824862+02:00", - "latency": 612538, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.398675731+02:00", - "latency": 841579, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.408309354+02:00", - "latency": 730179, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.418623506+02:00", - "latency": 974466, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.432008087+02:00", - "latency": 3912701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.438122947+02:00", - "latency": 582324, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.448208878+02:00", - "latency": 614056, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.458289649+02:00", - "latency": 616825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.468368452+02:00", - "latency": 624567, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.479955661+02:00", - "latency": 2098276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.488000989+02:00", - "latency": 566152, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.498367772+02:00", - "latency": 857656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.508213949+02:00", - "latency": 557723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.518179442+02:00", - "latency": 516029, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.5281827+02:00", - "latency": 513464, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.540802568+02:00", - "latency": 3141363, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.547844035+02:00", - "latency": 566158, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.557965703+02:00", - "latency": 687533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.568070716+02:00", - "latency": 709746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.577941961+02:00", - "latency": 514570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.587898713+02:00", - "latency": 513900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.602053919+02:00", - "latency": 4152787, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.608292807+02:00", - "latency": 709799, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.622767878+02:00", - "latency": 4908905, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.627697027+02:00", - "latency": 567050, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.637781065+02:00", - "latency": 526554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.647772914+02:00", - "latency": 594578, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.657925311+02:00", - "latency": 585755, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.667936931+02:00", - "latency": 517049, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.678161492+02:00", - "latency": 531629, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.688632831+02:00", - "latency": 684943, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.702216353+02:00", - "latency": 593292, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.708233094+02:00", - "latency": 535254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.718320623+02:00", - "latency": 557026, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.729426425+02:00", - "latency": 572314, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.738495983+02:00", - "latency": 637168, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.748575809+02:00", - "latency": 549736, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.759422586+02:00", - "latency": 548790, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.768357779+02:00", - "latency": 515151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.778391464+02:00", - "latency": 503112, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.788481553+02:00", - "latency": 641172, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.798468536+02:00", - "latency": 519359, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.80841956+02:00", - "latency": 514991, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.818494234+02:00", - "latency": 661837, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.828625663+02:00", - "latency": 618396, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.83874084+02:00", - "latency": 605288, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.847990282+02:00", - "latency": 720396, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.859803301+02:00", - "latency": 2476638, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.867895985+02:00", - "latency": 584795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.880813946+02:00", - "latency": 2841830, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.88785893+02:00", - "latency": 561538, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.897937809+02:00", - "latency": 601827, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.907949176+02:00", - "latency": 552964, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.918783592+02:00", - "latency": 1311152, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.927770147+02:00", - "latency": 496833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.939685124+02:00", - "latency": 2468805, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.94962165+02:00", - "latency": 2484056, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.95778848+02:00", - "latency": 578687, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.967933742+02:00", - "latency": 648438, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.978004502+02:00", - "latency": 554079, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.988280588+02:00", - "latency": 709374, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:30.998372062+02:00", - "latency": 582348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.010632473+02:00", - "latency": 2719195, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.018689765+02:00", - "latency": 581117, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.028455469+02:00", - "latency": 562383, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.038523208+02:00", - "latency": 612351, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.048589038+02:00", - "latency": 585564, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.058673549+02:00", - "latency": 604027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.067917865+02:00", - "latency": 696217, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.077983675+02:00", - "latency": 581832, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.088000132+02:00", - "latency": 563500, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.098007418+02:00", - "latency": 546007, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.108174141+02:00", - "latency": 613013, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.118493592+02:00", - "latency": 782566, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.128580469+02:00", - "latency": 713494, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.138597583+02:00", - "latency": 565059, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.148674467+02:00", - "latency": 569246, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.15812868+02:00", - "latency": 531136, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.168702901+02:00", - "latency": 569566, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.177720144+02:00", - "latency": 532518, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.187751758+02:00", - "latency": 634595, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.198050962+02:00", - "latency": 966358, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.208971521+02:00", - "latency": 1508859, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.218062372+02:00", - "latency": 627218, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.229912028+02:00", - "latency": 2486474, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.241042036+02:00", - "latency": 3573412, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.248297549+02:00", - "latency": 474336, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.25829057+02:00", - "latency": 531298, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.268253717+02:00", - "latency": 621272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.278265052+02:00", - "latency": 525789, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.291728615+02:00", - "latency": 4017923, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.300818468+02:00", - "latency": 3593729, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.30791415+02:00", - "latency": 556531, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.318035553+02:00", - "latency": 698030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.330698008+02:00", - "latency": 3212731, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.338754149+02:00", - "latency": 1591715, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.347783393+02:00", - "latency": 587811, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.357881667+02:00", - "latency": 612437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.367798156+02:00", - "latency": 530336, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.377821857+02:00", - "latency": 523274, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.387828008+02:00", - "latency": 552142, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.39781167+02:00", - "latency": 544662, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.407874138+02:00", - "latency": 616983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.417957474+02:00", - "latency": 632713, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.428058551+02:00", - "latency": 631842, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.438048852+02:00", - "latency": 566401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.448115473+02:00", - "latency": 559896, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.458135538+02:00", - "latency": 570587, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.468274928+02:00", - "latency": 647540, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.47825978+02:00", - "latency": 489724, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.488489618+02:00", - "latency": 701078, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.498363842+02:00", - "latency": 539839, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.508371622+02:00", - "latency": 639662, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.518415353+02:00", - "latency": 579710, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.528417044+02:00", - "latency": 566272, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.538372482+02:00", - "latency": 525347, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.550351477+02:00", - "latency": 493952, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.558420417+02:00", - "latency": 597678, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.570810601+02:00", - "latency": 2843484, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.577874108+02:00", - "latency": 536077, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.587944308+02:00", - "latency": 561694, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.597951102+02:00", - "latency": 547681, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.608976464+02:00", - "latency": 1541072, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.618040846+02:00", - "latency": 572135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.628072067+02:00", - "latency": 503178, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.63805817+02:00", - "latency": 469039, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.648022109+02:00", - "latency": 501770, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.658004841+02:00", - "latency": 558883, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.667986194+02:00", - "latency": 479284, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.679674642+02:00", - "latency": 1664177, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.687726292+02:00", - "latency": 625698, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.700717476+02:00", - "latency": 3445655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.707812173+02:00", - "latency": 502585, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.718685173+02:00", - "latency": 1361511, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.727641809+02:00", - "latency": 548231, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.737621047+02:00", - "latency": 558297, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.748625706+02:00", - "latency": 553067, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.758608034+02:00", - "latency": 523600, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.775686368+02:00", - "latency": 7641535, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.77759025+02:00", - "latency": 515368, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.788659703+02:00", - "latency": 552175, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.797532464+02:00", - "latency": 502151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.808586843+02:00", - "latency": 518148, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.818586029+02:00", - "latency": 574861, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.828650175+02:00", - "latency": 589998, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.838729999+02:00", - "latency": 1598082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.847750658+02:00", - "latency": 535900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.85779461+02:00", - "latency": 547925, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.867850177+02:00", - "latency": 518950, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.877844198+02:00", - "latency": 514353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.887822578+02:00", - "latency": 530113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.899414492+02:00", - "latency": 560096, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.908435242+02:00", - "latency": 531100, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.91848943+02:00", - "latency": 580078, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.928588162+02:00", - "latency": 600807, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.939716218+02:00", - "latency": 1633172, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.948035199+02:00", - "latency": 883618, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.957977221+02:00", - "latency": 625126, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.968038013+02:00", - "latency": 663315, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.978271675+02:00", - "latency": 732356, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:31.988454771+02:00", - "latency": 631955, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.004586567+02:00", - "latency": 6681373, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.007910151+02:00", - "latency": 511924, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.017882137+02:00", - "latency": 550832, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.027898858+02:00", - "latency": 543713, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.037868992+02:00", - "latency": 549255, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.04787119+02:00", - "latency": 530769, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.057899015+02:00", - "latency": 556423, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.068114453+02:00", - "latency": 652070, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.078155805+02:00", - "latency": 542617, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.091628929+02:00", - "latency": 3712852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.098622941+02:00", - "latency": 582198, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.108620865+02:00", - "latency": 514500, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.122069554+02:00", - "latency": 3947120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.12811851+02:00", - "latency": 551446, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.138142348+02:00", - "latency": 536087, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.148428619+02:00", - "latency": 865870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.158597418+02:00", - "latency": 662478, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.168667738+02:00", - "latency": 528353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.177624891+02:00", - "latency": 558027, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.187775295+02:00", - "latency": 651032, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.1977869+02:00", - "latency": 569611, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.207812535+02:00", - "latency": 606930, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.217839753+02:00", - "latency": 574030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.228011139+02:00", - "latency": 628205, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.238053958+02:00", - "latency": 645334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.2481792+02:00", - "latency": 585133, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.258289095+02:00", - "latency": 572704, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.268605766+02:00", - "latency": 755334, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.278658902+02:00", - "latency": 576650, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.287859277+02:00", - "latency": 673594, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.297913647+02:00", - "latency": 533763, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.30794496+02:00", - "latency": 610695, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.318658421+02:00", - "latency": 978442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.328329898+02:00", - "latency": 707029, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.341625838+02:00", - "latency": 3751748, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.349509195+02:00", - "latency": 1536080, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.358495663+02:00", - "latency": 554113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.368688888+02:00", - "latency": 687120, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.377727185+02:00", - "latency": 544220, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.387841842+02:00", - "latency": 601074, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.398073338+02:00", - "latency": 686551, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.408143658+02:00", - "latency": 576997, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.418601384+02:00", - "latency": 664576, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.428903634+02:00", - "latency": 774655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.43906462+02:00", - "latency": 1810780, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.448334538+02:00", - "latency": 693348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.458375079+02:00", - "latency": 559830, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.468499481+02:00", - "latency": 625543, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.478611248+02:00", - "latency": 609326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.49067583+02:00", - "latency": 2506870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.497669943+02:00", - "latency": 597605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.507694192+02:00", - "latency": 573754, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.518659362+02:00", - "latency": 573920, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.527651066+02:00", - "latency": 576732, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.537775217+02:00", - "latency": 658053, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.549841695+02:00", - "latency": 2516849, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.557940413+02:00", - "latency": 567221, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.569689781+02:00", - "latency": 2271178, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.577685916+02:00", - "latency": 530521, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.589035308+02:00", - "latency": 1933756, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.598064588+02:00", - "latency": 541603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.608405189+02:00", - "latency": 584517, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.61840902+02:00", - "latency": 565495, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.628470983+02:00", - "latency": 561895, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.638569539+02:00", - "latency": 585674, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.648443586+02:00", - "latency": 601594, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.660629168+02:00", - "latency": 2585401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.668791432+02:00", - "latency": 620092, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.677804948+02:00", - "latency": 547499, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.690694573+02:00", - "latency": 3365983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.698804251+02:00", - "latency": 1579681, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.707948224+02:00", - "latency": 672711, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.718040801+02:00", - "latency": 597105, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.728271513+02:00", - "latency": 765417, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.738290881+02:00", - "latency": 628776, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.748440198+02:00", - "latency": 603214, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.758597347+02:00", - "latency": 676524, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.768740126+02:00", - "latency": 629827, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.777959912+02:00", - "latency": 755145, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.787888537+02:00", - "latency": 548530, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.798090961+02:00", - "latency": 757174, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.808166651+02:00", - "latency": 632082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.818617829+02:00", - "latency": 765485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.827987818+02:00", - "latency": 795414, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.838687651+02:00", - "latency": 751131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.847885266+02:00", - "latency": 638121, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.861729785+02:00", - "latency": 3799852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.868891097+02:00", - "latency": 1648655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.877895058+02:00", - "latency": 598590, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.888574118+02:00", - "latency": 1150095, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.899632993+02:00", - "latency": 1594817, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.909843529+02:00", - "latency": 2679063, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.917938416+02:00", - "latency": 612894, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.927844023+02:00", - "latency": 514773, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.93776263+02:00", - "latency": 553433, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.949453426+02:00", - "latency": 618355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.959017703+02:00", - "latency": 791180, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.968182231+02:00", - "latency": 606609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.978228989+02:00", - "latency": 621845, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.988785362+02:00", - "latency": 620988, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:32.997903569+02:00", - "latency": 576716, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.01073732+02:00", - "latency": 3371909, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.017836167+02:00", - "latency": 592018, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.027975801+02:00", - "latency": 681172, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.04172781+02:00", - "latency": 4196038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.050035751+02:00", - "latency": 2742680, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.058341475+02:00", - "latency": 713982, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.068408363+02:00", - "latency": 589563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.078448586+02:00", - "latency": 590138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.088516528+02:00", - "latency": 597209, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.09868292+02:00", - "latency": 754016, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.109276535+02:00", - "latency": 732831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.118374327+02:00", - "latency": 645229, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.128669591+02:00", - "latency": 825490, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.137614489+02:00", - "latency": 524705, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.151699021+02:00", - "latency": 3798039, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.157817258+02:00", - "latency": 618190, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.169715445+02:00", - "latency": 2408482, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.18070036+02:00", - "latency": 3509706, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.187909363+02:00", - "latency": 512210, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.199934461+02:00", - "latency": 2551871, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.207964581+02:00", - "latency": 557859, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.218071708+02:00", - "latency": 566226, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.231871789+02:00", - "latency": 4293526, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.238007559+02:00", - "latency": 575226, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.249706002+02:00", - "latency": 2100307, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.259015314+02:00", - "latency": 1820520, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.270954635+02:00", - "latency": 3534029, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.278626071+02:00", - "latency": 594285, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.290731161+02:00", - "latency": 3635549, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.297793834+02:00", - "latency": 583418, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.308734883+02:00", - "latency": 1449089, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.31780911+02:00", - "latency": 592859, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.327992283+02:00", - "latency": 604940, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.338091962+02:00", - "latency": 568909, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.34969431+02:00", - "latency": 2154388, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.359698233+02:00", - "latency": 2513257, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.367759591+02:00", - "latency": 559523, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.378583233+02:00", - "latency": 563427, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.388659494+02:00", - "latency": 640351, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.399041145+02:00", - "latency": 1876284, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.40970248+02:00", - "latency": 2135322, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.42075936+02:00", - "latency": 3554829, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.428001436+02:00", - "latency": 732973, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.437991968+02:00", - "latency": 610437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.448075835+02:00", - "latency": 628160, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.458368097+02:00", - "latency": 749618, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.468420704+02:00", - "latency": 598222, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.47907386+02:00", - "latency": 1246356, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.489677525+02:00", - "latency": 561830, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.497841325+02:00", - "latency": 741737, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.508132948+02:00", - "latency": 677973, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.518258118+02:00", - "latency": 661219, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.531028793+02:00", - "latency": 3054059, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.53870339+02:00", - "latency": 1165928, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.547859903+02:00", - "latency": 647901, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.55807934+02:00", - "latency": 654371, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.568193484+02:00", - "latency": 703108, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.578112464+02:00", - "latency": 520145, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.588285372+02:00", - "latency": 685330, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.598490315+02:00", - "latency": 779477, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.610452567+02:00", - "latency": 573836, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.618437022+02:00", - "latency": 503335, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.628563569+02:00", - "latency": 579289, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.638256055+02:00", - "latency": 899653, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.64893944+02:00", - "latency": 573038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.657983808+02:00", - "latency": 523030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.667984471+02:00", - "latency": 530823, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.678043954+02:00", - "latency": 567362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.688127336+02:00", - "latency": 575655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.698162867+02:00", - "latency": 551620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.708243156+02:00", - "latency": 565761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.720711628+02:00", - "latency": 2674200, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.727837108+02:00", - "latency": 595262, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.73809882+02:00", - "latency": 668063, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.748082166+02:00", - "latency": 538442, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.758013434+02:00", - "latency": 509800, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.767963495+02:00", - "latency": 522481, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.778020424+02:00", - "latency": 578924, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.788213663+02:00", - "latency": 635145, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.798303257+02:00", - "latency": 592429, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.80882742+02:00", - "latency": 836762, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.819155445+02:00", - "latency": 729352, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.828288945+02:00", - "latency": 613629, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.838480735+02:00", - "latency": 697807, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.848534664+02:00", - "latency": 582961, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.858303465+02:00", - "latency": 858449, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.868778382+02:00", - "latency": 742868, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.87861958+02:00", - "latency": 564900, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.889997794+02:00", - "latency": 1792570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.898056497+02:00", - "latency": 568934, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.908172099+02:00", - "latency": 583198, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.918259798+02:00", - "latency": 550110, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.928368687+02:00", - "latency": 565230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.938446701+02:00", - "latency": 586187, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.948642812+02:00", - "latency": 667157, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.965667802+02:00", - "latency": 4275194, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.979994533+02:00", - "latency": 2008276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.981637754+02:00", - "latency": 3244720, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.987708817+02:00", - "latency": 568662, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:33.997918039+02:00", - "latency": 772332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.007988338+02:00", - "latency": 646100, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.018081903+02:00", - "latency": 651552, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.030054557+02:00", - "latency": 2578584, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.038370554+02:00", - "latency": 793761, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.048537047+02:00", - "latency": 667885, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.058743394+02:00", - "latency": 610325, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.067859624+02:00", - "latency": 662556, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.078024711+02:00", - "latency": 709351, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.088168077+02:00", - "latency": 688082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.098312847+02:00", - "latency": 686443, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.108413072+02:00", - "latency": 644343, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.118463018+02:00", - "latency": 544543, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.131691348+02:00", - "latency": 3797819, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.137752749+02:00", - "latency": 602005, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.148597071+02:00", - "latency": 615264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.158760307+02:00", - "latency": 692160, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.167780516+02:00", - "latency": 597517, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.17800589+02:00", - "latency": 736068, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.188118086+02:00", - "latency": 733392, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.200834323+02:00", - "latency": 3318554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.207874576+02:00", - "latency": 606862, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.217939701+02:00", - "latency": 603212, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.227971729+02:00", - "latency": 635790, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.238353453+02:00", - "latency": 962394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.249701275+02:00", - "latency": 1914518, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.260732628+02:00", - "latency": 3593833, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.267813626+02:00", - "latency": 619843, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.280866121+02:00", - "latency": 3648107, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.287841569+02:00", - "latency": 534723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.297822312+02:00", - "latency": 579280, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.307830543+02:00", - "latency": 582401, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.317839354+02:00", - "latency": 597109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.331001323+02:00", - "latency": 3742362, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.337957675+02:00", - "latency": 588564, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.348012704+02:00", - "latency": 612192, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.358016892+02:00", - "latency": 585834, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.367990174+02:00", - "latency": 559381, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.378668584+02:00", - "latency": 1276331, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.387669147+02:00", - "latency": 618203, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.397522971+02:00", - "latency": 518816, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.408478661+02:00", - "latency": 583870, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.418497823+02:00", - "latency": 638721, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.428434423+02:00", - "latency": 560099, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.438324474+02:00", - "latency": 548033, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.451718963+02:00", - "latency": 4082500, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.457778192+02:00", - "latency": 617422, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.46775177+02:00", - "latency": 562658, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.47766848+02:00", - "latency": 514991, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.487591641+02:00", - "latency": 582515, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.498612249+02:00", - "latency": 597795, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.508506761+02:00", - "latency": 488715, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.519681355+02:00", - "latency": 1798544, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.52766593+02:00", - "latency": 630768, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.538581691+02:00", - "latency": 548690, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.547797869+02:00", - "latency": 566045, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.55787064+02:00", - "latency": 667593, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.56805062+02:00", - "latency": 771961, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.578165419+02:00", - "latency": 640131, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.588302845+02:00", - "latency": 594923, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.59833352+02:00", - "latency": 595018, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.608355457+02:00", - "latency": 595615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.618308986+02:00", - "latency": 542832, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.628282616+02:00", - "latency": 536790, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.639579128+02:00", - "latency": 567333, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.649650604+02:00", - "latency": 1649331, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.658688661+02:00", - "latency": 1660983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.667676873+02:00", - "latency": 656247, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.679715646+02:00", - "latency": 2451342, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.687757988+02:00", - "latency": 619381, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.697795703+02:00", - "latency": 557236, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.707801991+02:00", - "latency": 601403, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.718661898+02:00", - "latency": 1446475, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.72762672+02:00", - "latency": 529182, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.737659161+02:00", - "latency": 568135, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.749806356+02:00", - "latency": 2706571, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.757769853+02:00", - "latency": 537239, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.767867475+02:00", - "latency": 634719, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.777827847+02:00", - "latency": 545677, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.788623127+02:00", - "latency": 1486058, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.797790866+02:00", - "latency": 778259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.807664692+02:00", - "latency": 629264, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.817622934+02:00", - "latency": 577986, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.829012994+02:00", - "latency": 632615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.838154662+02:00", - "latency": 682718, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.850721251+02:00", - "latency": 3137326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.857735088+02:00", - "latency": 613937, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.867734942+02:00", - "latency": 611843, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.877669831+02:00", - "latency": 534139, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.887703366+02:00", - "latency": 658377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.897784366+02:00", - "latency": 707644, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.908797808+02:00", - "latency": 1652114, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.917819707+02:00", - "latency": 573906, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.92982835+02:00", - "latency": 2596424, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.937739261+02:00", - "latency": 506007, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.951107361+02:00", - "latency": 4119792, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.958079476+02:00", - "latency": 508609, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.970395693+02:00", - "latency": 562886, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.978396869+02:00", - "latency": 599326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.988333144+02:00", - "latency": 542161, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:34.998452896+02:00", - "latency": 643217, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.008468941+02:00", - "latency": 590917, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.01851519+02:00", - "latency": 536297, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.029023284+02:00", - "latency": 1096313, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.037981304+02:00", - "latency": 528471, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.047951821+02:00", - "latency": 573874, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.058063186+02:00", - "latency": 692504, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.068813262+02:00", - "latency": 1377616, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.077792437+02:00", - "latency": 554323, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.087962585+02:00", - "latency": 736649, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.097916763+02:00", - "latency": 783610, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.107742295+02:00", - "latency": 592242, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.117700768+02:00", - "latency": 569707, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.128112578+02:00", - "latency": 709839, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.140007298+02:00", - "latency": 2640873, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.148030898+02:00", - "latency": 596315, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.158049753+02:00", - "latency": 556416, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.168032743+02:00", - "latency": 592872, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.178042448+02:00", - "latency": 596681, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.188033569+02:00", - "latency": 583193, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.198054368+02:00", - "latency": 624345, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.208035183+02:00", - "latency": 579746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.218086646+02:00", - "latency": 646555, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.2287015+02:00", - "latency": 1042797, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.238368945+02:00", - "latency": 525533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.248678609+02:00", - "latency": 526303, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.257628621+02:00", - "latency": 539458, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.26861216+02:00", - "latency": 571855, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.278588638+02:00", - "latency": 511656, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.288670805+02:00", - "latency": 592148, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.297659257+02:00", - "latency": 638570, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.311024368+02:00", - "latency": 4017388, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.317966604+02:00", - "latency": 533503, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.328126302+02:00", - "latency": 747113, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.338220807+02:00", - "latency": 655612, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.348095775+02:00", - "latency": 555462, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.358166378+02:00", - "latency": 633679, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.368220676+02:00", - "latency": 627738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.378318797+02:00", - "latency": 596603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.388277+02:00", - "latency": 550205, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.398620836+02:00", - "latency": 884138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.408696457+02:00", - "latency": 743145, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.417979956+02:00", - "latency": 779533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.427637869+02:00", - "latency": 515603, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.441688523+02:00", - "latency": 3662399, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.447726085+02:00", - "latency": 576605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.457715848+02:00", - "latency": 621914, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.467697109+02:00", - "latency": 559919, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.477676819+02:00", - "latency": 600561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.488047374+02:00", - "latency": 765629, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.498636984+02:00", - "latency": 741348, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.511693752+02:00", - "latency": 3728685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.517676042+02:00", - "latency": 553457, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.527938874+02:00", - "latency": 758058, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.537847488+02:00", - "latency": 552237, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.547777585+02:00", - "latency": 540038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.557762429+02:00", - "latency": 568383, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.567778131+02:00", - "latency": 617377, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.577799343+02:00", - "latency": 596293, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.587820968+02:00", - "latency": 622898, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.59780047+02:00", - "latency": 568505, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.610876672+02:00", - "latency": 3474752, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.617909493+02:00", - "latency": 605151, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.627877045+02:00", - "latency": 591561, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.637846793+02:00", - "latency": 571349, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.647918123+02:00", - "latency": 646478, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.657862709+02:00", - "latency": 572764, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.667819761+02:00", - "latency": 584506, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.677826933+02:00", - "latency": 612355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.688007456+02:00", - "latency": 534106, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.698380646+02:00", - "latency": 943196, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.70833858+02:00", - "latency": 541713, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.718397976+02:00", - "latency": 668485, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.728308426+02:00", - "latency": 569002, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.740714615+02:00", - "latency": 2956781, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.747726724+02:00", - "latency": 569449, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.757741793+02:00", - "latency": 543318, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.767690737+02:00", - "latency": 539636, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.77778933+02:00", - "latency": 674024, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.787738023+02:00", - "latency": 556194, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.797751163+02:00", - "latency": 514825, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.807714041+02:00", - "latency": 519355, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.81772379+02:00", - "latency": 681744, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.827538837+02:00", - "latency": 548382, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.838628127+02:00", - "latency": 621707, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.848483537+02:00", - "latency": 546893, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.858118068+02:00", - "latency": 505804, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.868127012+02:00", - "latency": 514794, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.878118578+02:00", - "latency": 561873, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.888039404+02:00", - "latency": 742666, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.897898567+02:00", - "latency": 546777, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.907932597+02:00", - "latency": 637913, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.917867495+02:00", - "latency": 558056, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.927793218+02:00", - "latency": 530096, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.938696858+02:00", - "latency": 1220877, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.947931676+02:00", - "latency": 754332, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.958072288+02:00", - "latency": 787670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.968659518+02:00", - "latency": 1303579, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.978955844+02:00", - "latency": 1658811, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:35.988468166+02:00", - "latency": 1002747, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.001038268+02:00", - "latency": 3170874, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.008008177+02:00", - "latency": 566286, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.017901983+02:00", - "latency": 536831, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.027826146+02:00", - "latency": 545749, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.038588196+02:00", - "latency": 795397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.048387857+02:00", - "latency": 527415, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.059036867+02:00", - "latency": 919329, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.068573057+02:00", - "latency": 710166, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.082047073+02:00", - "latency": 4115360, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.088115044+02:00", - "latency": 664812, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.098066475+02:00", - "latency": 549976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.107987625+02:00", - "latency": 544336, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.117895819+02:00", - "latency": 544714, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.130716727+02:00", - "latency": 3411522, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.137678107+02:00", - "latency": 543038, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.147648147+02:00", - "latency": 623784, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.157619117+02:00", - "latency": 591707, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.168659577+02:00", - "latency": 633615, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.177793046+02:00", - "latency": 677685, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.187707769+02:00", - "latency": 559978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.197942373+02:00", - "latency": 771588, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.207804531+02:00", - "latency": 522924, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.218396448+02:00", - "latency": 539581, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.231744833+02:00", - "latency": 3950997, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.237810562+02:00", - "latency": 599190, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.247675797+02:00", - "latency": 559305, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.257599093+02:00", - "latency": 564390, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.269156258+02:00", - "latency": 716951, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.278063286+02:00", - "latency": 584836, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.289070199+02:00", - "latency": 1636202, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.298957263+02:00", - "latency": 1403694, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.310686276+02:00", - "latency": 3298464, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.318002907+02:00", - "latency": 942448, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.32797388+02:00", - "latency": 555152, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.338973949+02:00", - "latency": 1598918, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.347805737+02:00", - "latency": 501034, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.357941812+02:00", - "latency": 738021, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.371699509+02:00", - "latency": 4449694, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.377693959+02:00", - "latency": 575254, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.387793453+02:00", - "latency": 755455, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.398669182+02:00", - "latency": 1667086, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.407661264+02:00", - "latency": 595318, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.417688658+02:00", - "latency": 654622, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.427684048+02:00", - "latency": 610347, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.437838171+02:00", - "latency": 748952, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.451726899+02:00", - "latency": 4465343, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.459701318+02:00", - "latency": 2563310, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.467681213+02:00", - "latency": 556276, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.477653803+02:00", - "latency": 602516, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.487715377+02:00", - "latency": 573259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.497941969+02:00", - "latency": 899357, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.507963973+02:00", - "latency": 616196, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.517997255+02:00", - "latency": 556697, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.527969191+02:00", - "latency": 596137, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.538030108+02:00", - "latency": 604775, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.548227178+02:00", - "latency": 703488, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.558265683+02:00", - "latency": 659963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.571081148+02:00", - "latency": 3447973, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.578031693+02:00", - "latency": 528555, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.588091888+02:00", - "latency": 630661, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.599018736+02:00", - "latency": 605913, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.60801612+02:00", - "latency": 525061, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.619688112+02:00", - "latency": 2165749, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.629619713+02:00", - "latency": 2004199, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.638178153+02:00", - "latency": 607441, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.648301761+02:00", - "latency": 607990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.658312241+02:00", - "latency": 602230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.671992488+02:00", - "latency": 4163032, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.677961364+02:00", - "latency": 593488, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.68806997+02:00", - "latency": 653114, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.698085905+02:00", - "latency": 632524, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.710068912+02:00", - "latency": 2553924, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.718166272+02:00", - "latency": 601801, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.728159958+02:00", - "latency": 578079, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.739800192+02:00", - "latency": 2186978, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.747982859+02:00", - "latency": 741380, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.757957415+02:00", - "latency": 563266, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.767999618+02:00", - "latency": 592086, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.778047971+02:00", - "latency": 643378, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.788076774+02:00", - "latency": 570663, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.79814575+02:00", - "latency": 660326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.808163972+02:00", - "latency": 571307, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.820635761+02:00", - "latency": 2976353, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.830720911+02:00", - "latency": 3044140, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.837882777+02:00", - "latency": 698366, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.847838689+02:00", - "latency": 592554, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.857840316+02:00", - "latency": 496102, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.867848112+02:00", - "latency": 625459, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.877945617+02:00", - "latency": 648947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.888095263+02:00", - "latency": 623575, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.90170682+02:00", - "latency": 4182220, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.90767187+02:00", - "latency": 553589, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.917697057+02:00", - "latency": 590437, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.929050585+02:00", - "latency": 1398997, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.938157829+02:00", - "latency": 600046, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.948205385+02:00", - "latency": 675892, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.959698599+02:00", - "latency": 2017670, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.967661962+02:00", - "latency": 534777, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.978531757+02:00", - "latency": 564769, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.988416751+02:00", - "latency": 550746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:36.9985982+02:00", - "latency": 680605, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.008607713+02:00", - "latency": 597260, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.018407685+02:00", - "latency": 531089, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.028282005+02:00", - "latency": 530563, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.041824204+02:00", - "latency": 4131532, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.047829858+02:00", - "latency": 553273, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.058597199+02:00", - "latency": 1354662, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.067784321+02:00", - "latency": 550030, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.078990584+02:00", - "latency": 1956010, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.089723789+02:00", - "latency": 2310326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.097631845+02:00", - "latency": 483015, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.108561626+02:00", - "latency": 575087, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.120640539+02:00", - "latency": 2590513, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.127990645+02:00", - "latency": 594414, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.137859736+02:00", - "latency": 576634, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.147867911+02:00", - "latency": 564617, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.157931457+02:00", - "latency": 635803, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.167920435+02:00", - "latency": 564369, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.177873082+02:00", - "latency": 543654, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.187802866+02:00", - "latency": 617463, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.199046305+02:00", - "latency": 1405455, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.208059515+02:00", - "latency": 490526, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.217926083+02:00", - "latency": 530746, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.227845723+02:00", - "latency": 587440, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.237936454+02:00", - "latency": 587582, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.250994738+02:00", - "latency": 3584426, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.257925978+02:00", - "latency": 500824, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.267888132+02:00", - "latency": 556857, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.27780405+02:00", - "latency": 589431, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.287709714+02:00", - "latency": 525424, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.302034604+02:00", - "latency": 3991620, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.307948836+02:00", - "latency": 517975, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.317871002+02:00", - "latency": 538990, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.32785576+02:00", - "latency": 568136, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.337933133+02:00", - "latency": 595745, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.347758972+02:00", - "latency": 557397, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.357779311+02:00", - "latency": 621266, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.367928177+02:00", - "latency": 564300, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.377801031+02:00", - "latency": 570326, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.387739726+02:00", - "latency": 534350, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.397744253+02:00", - "latency": 543956, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.407820238+02:00", - "latency": 690460, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.417800105+02:00", - "latency": 580481, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.427758424+02:00", - "latency": 635865, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.437741492+02:00", - "latency": 591643, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.447741294+02:00", - "latency": 606028, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.457784772+02:00", - "latency": 646389, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.467774744+02:00", - "latency": 631543, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.478004797+02:00", - "latency": 774497, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.487692018+02:00", - "latency": 557597, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.497599669+02:00", - "latency": 583960, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.508694128+02:00", - "latency": 676223, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.519693841+02:00", - "latency": 2521186, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.527713451+02:00", - "latency": 583065, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.53776333+02:00", - "latency": 618265, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.547818416+02:00", - "latency": 666852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.557817574+02:00", - "latency": 591968, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.567776423+02:00", - "latency": 559230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.578091191+02:00", - "latency": 713771, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.588196147+02:00", - "latency": 601818, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.598127289+02:00", - "latency": 611271, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.608040886+02:00", - "latency": 525856, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.620975021+02:00", - "latency": 3553259, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.627945642+02:00", - "latency": 511944, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.637850059+02:00", - "latency": 571702, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.647872905+02:00", - "latency": 693792, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.657869083+02:00", - "latency": 570845, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.668698315+02:00", - "latency": 826841, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.678648925+02:00", - "latency": 1580174, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.687608297+02:00", - "latency": 554337, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.698883446+02:00", - "latency": 628717, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.709643225+02:00", - "latency": 2242965, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.719374362+02:00", - "latency": 548725, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.728561065+02:00", - "latency": 678471, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.740975048+02:00", - "latency": 2648760, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.748110546+02:00", - "latency": 594792, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.75810327+02:00", - "latency": 517119, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.768297311+02:00", - "latency": 643655, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.778236297+02:00", - "latency": 530708, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.788245552+02:00", - "latency": 533852, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.798232856+02:00", - "latency": 570523, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.80858495+02:00", - "latency": 648001, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.819063039+02:00", - "latency": 1076723, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.828023483+02:00", - "latency": 567452, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.840332323+02:00", - "latency": 2884728, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.848242601+02:00", - "latency": 546902, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.858205608+02:00", - "latency": 570884, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.870623442+02:00", - "latency": 2868983, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.87834593+02:00", - "latency": 523701, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.888338899+02:00", - "latency": 546228, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.898396872+02:00", - "latency": 613684, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.908428091+02:00", - "latency": 636197, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.91839427+02:00", - "latency": 532391, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.930305578+02:00", - "latency": 492156, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.938275728+02:00", - "latency": 557064, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.948251723+02:00", - "latency": 548134, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.958347889+02:00", - "latency": 657289, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.968312856+02:00", - "latency": 522878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.980335508+02:00", - "latency": 528539, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.988326108+02:00", - "latency": 550741, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:37.998316433+02:00", - "latency": 566996, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.008339728+02:00", - "latency": 592908, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.02072939+02:00", - "latency": 2961079, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.027764177+02:00", - "latency": 597341, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.03767796+02:00", - "latency": 555794, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.047798566+02:00", - "latency": 736108, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.057888491+02:00", - "latency": 648378, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.067737676+02:00", - "latency": 653599, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.077770559+02:00", - "latency": 628976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.087748004+02:00", - "latency": 552227, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.097881302+02:00", - "latency": 684661, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.107736773+02:00", - "latency": 552507, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.119949124+02:00", - "latency": 2737248, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.127925165+02:00", - "latency": 552262, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.137905559+02:00", - "latency": 579738, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.147846975+02:00", - "latency": 535533, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.160363434+02:00", - "latency": 560856, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.171387079+02:00", - "latency": 571569, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.17827632+02:00", - "latency": 492636, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.188272901+02:00", - "latency": 567663, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.19823825+02:00", - "latency": 549686, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.211693164+02:00", - "latency": 4019500, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.217728006+02:00", - "latency": 529947, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.227826417+02:00", - "latency": 675099, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.237772209+02:00", - "latency": 525082, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.247723759+02:00", - "latency": 551019, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.257709061+02:00", - "latency": 518109, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.270836836+02:00", - "latency": 3810387, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.277849345+02:00", - "latency": 543671, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.289339117+02:00", - "latency": 576878, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.298502064+02:00", - "latency": 714734, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.308345001+02:00", - "latency": 495976, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.318313513+02:00", - "latency": 549888, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.328318055+02:00", - "latency": 533003, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.341078386+02:00", - "latency": 3303255, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.348182229+02:00", - "latency": 649559, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.358270408+02:00", - "latency": 621798, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.368304272+02:00", - "latency": 572295, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.378347288+02:00", - "latency": 601584, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.38835522+02:00", - "latency": 525483, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.398465692+02:00", - "latency": 662241, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.408396296+02:00", - "latency": 524278, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.421378868+02:00", - "latency": 571290, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.428353858+02:00", - "latency": 553698, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.438353805+02:00", - "latency": 566300, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.448439303+02:00", - "latency": 482751, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.462048917+02:00", - "latency": 4187639, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.468085513+02:00", - "latency": 544491, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.478388769+02:00", - "latency": 768425, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.488393681+02:00", - "latency": 597394, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.498410024+02:00", - "latency": 608176, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.509766276+02:00", - "latency": 1857339, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.517725703+02:00", - "latency": 531844, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.527597568+02:00", - "latency": 548785, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.538690062+02:00", - "latency": 611939, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.547572859+02:00", - "latency": 533571, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.558573838+02:00", - "latency": 536158, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.568660789+02:00", - "latency": 610138, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.577588559+02:00", - "latency": 596801, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.588672687+02:00", - "latency": 650963, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.597611901+02:00", - "latency": 520445, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.608530798+02:00", - "latency": 517984, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.620952902+02:00", - "latency": 2936911, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.627966679+02:00", - "latency": 580851, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.637954157+02:00", - "latency": 583072, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.647940294+02:00", - "latency": 543624, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.65788138+02:00", - "latency": 490863, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.668704231+02:00", - "latency": 654920, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.677647543+02:00", - "latency": 532230, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.688660366+02:00", - "latency": 634314, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.697679597+02:00", - "latency": 671234, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.70857039+02:00", - "latency": 573226, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.717672199+02:00", - "latency": 674752, - "error": "rpc error: code = Unauthenticated desc = Valid authentication required", - "status": "Unauthenticated" - }, - { - "timestamp": "2025-10-03T17:37:38.727230698+02:00", - "latency": 291886, - "error": "rpc error: code = Unavailable desc = error reading from server: read tcp 127.0.0.1:43008-\u003e127.0.0.1:50051: use of closed network connection", - "status": "Unavailable" - } - ] -} \ No newline at end of file diff --git a/check_parquet_rows.rs b/tests/standalone/check_parquet_rows.rs similarity index 100% rename from check_parquet_rows.rs rename to tests/standalone/check_parquet_rows.rs diff --git a/inspect_schema.rs b/tests/standalone/inspect_schema.rs similarity index 100% rename from inspect_schema.rs rename to tests/standalone/inspect_schema.rs diff --git a/inspect_schema_simple.rs b/tests/standalone/inspect_schema_simple.rs similarity index 100% rename from inspect_schema_simple.rs rename to tests/standalone/inspect_schema_simple.rs diff --git a/position_manager_fixes.rs b/tests/standalone/position_manager_fixes.rs similarity index 100% rename from position_manager_fixes.rs rename to tests/standalone/position_manager_fixes.rs diff --git a/redis_validation_test.rs b/tests/standalone/redis_validation_test.rs similarity index 100% rename from redis_validation_test.rs rename to tests/standalone/redis_validation_test.rs diff --git a/test_dbn_debug.rs b/tests/standalone/test_dbn_debug.rs similarity index 100% rename from test_dbn_debug.rs rename to tests/standalone/test_dbn_debug.rs diff --git a/test_dbn_decoder.rs b/tests/standalone/test_dbn_decoder.rs similarity index 100% rename from test_dbn_decoder.rs rename to tests/standalone/test_dbn_decoder.rs diff --git a/test_dqn_imports.rs b/tests/standalone/test_dqn_imports.rs similarity index 100% rename from test_dqn_imports.rs rename to tests/standalone/test_dqn_imports.rs diff --git a/test_hurst_div_zero.rs b/tests/standalone/test_hurst_div_zero.rs similarity index 100% rename from test_hurst_div_zero.rs rename to tests/standalone/test_hurst_div_zero.rs diff --git a/test_regime_db_integration.rs b/tests/standalone/test_regime_db_integration.rs similarity index 100% rename from test_regime_db_integration.rs rename to tests/standalone/test_regime_db_integration.rs diff --git a/validate_14ns_claims.rs b/tests/standalone/validate_14ns_claims.rs similarity index 100% rename from validate_14ns_claims.rs rename to tests/standalone/validate_14ns_claims.rs diff --git a/verify_normalization.rs b/tests/standalone/verify_normalization.rs similarity index 100% rename from verify_normalization.rs rename to tests/standalone/verify_normalization.rs diff --git a/verify_tft_lru_fix.rs b/tests/standalone/verify_tft_lru_fix.rs similarity index 100% rename from verify_tft_lru_fix.rs rename to tests/standalone/verify_tft_lru_fix.rs diff --git a/watch_tuning_progress_updated.rs b/tests/standalone/watch_tuning_progress_updated.rs similarity index 100% rename from watch_tuning_progress_updated.rs rename to tests/standalone/watch_tuning_progress_updated.rs diff --git a/verify_normalization b/verify_normalization deleted file mode 100755 index 62c581f5ec828fb93a55fb3492508e2f73252f91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3876608 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ⅇypRrU7O{q`m2z*3=aM5@{oIpCEku0 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

-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 z z$_Fli__Eu2xm&`FPmJ;HA~OMrA^|zV*qnXG#^FaQeUdxSTnoh7;YUJQggC2@uEdXo zaFWI5$P>*H-bVteb^zg65!(+xGE4!ZaRi9%A5&0AK;G?N(bKRaRQU{qj;Z{(w_MTv z^DI%-5;1tiPjtZ#Y2PD|sqf1pDK@vtV-Mj+=EIxaymL{kSZTZm00&`d@mSkD?C4uA z9|mZ14jfg(aC6!pdpHo|Q*-rjK)U~{4+qM%}}u3h4F`txrm$(HnR#{PO`N*kjaOwR$)%IwUOYD5r>oR1}`PKj~9C% zss7|GAEjHZ9!iO;*-SOvYFB76-D)P$Al=?fg6<|d_21eR3QU^i1UX~UEY~4_c+#Bv zJcJbc0IY_w=@Xv30DXWmdd58=1JgrfWR~F}7k7LH2c6-04o`80Vooa+=Ps!@fjcuj zojy6DbozeFU13bsrA|Kp#TjYTatx_g-37%dH^5#V*do%2*Xt@H*};L0Gr?jtOQ%@l zy}ll6&!9tg%d1-L0B$1Gpi&o*@HDTYqx6x_r$taaS zAPSEnVIdL!+F-|fv}qLuo>9xv?7*QQu=F6fv9I|eU&KDb&JWimN8D$n507}VqI~`X z@+j5vNdAE3hJ0LlCob=r*O)H~F;pEf(6(aN;~3UA3mtL9^YFPHd@aY|aYO@){jy^` zA$H^xjxGMTTX+Ks`X6fHwd|&TrG?97bOQsRWy@u>Bvgxzy(eSJ7F|lWYSGfIT6DDv zBX>;bRxP@oZq=e=>>1Qrh3Ufm z*6R2F&&*lwk^t7e_WQm!KN8NFd1mIBXP#}ISx$mPO9uIR5+;|pZ?J?jA1fu##2 zZ+IaRva(0Ld9z(&D~-S=$a14A@pKfKwF#RD9)<9%A)mreSO5U|ya`I*Z3&(xLyg3x znQluU6D>=ylU4M^Cq>&OofMzQmxOT38EhjV+#+52gm8;YOYo0J>$2{Z4CzNFOb1d0 zPWQn--Y3fe{g)JOk-^lj$7iQ0`e{B}$v4eseAaxHcLDg$`jMJ>lsCs*i^PpAFd8on zFt&W*UJvswZm&T85jl$77#E6IDul_)8{!FT$w+!T}_QgE${$P`fG@5`B$ zkV?{<1kHT|)CNcNu7%M4o&V<%r|w|uoRuj&rj8CLHVdaLiKOIE5>MH^g>E)@e{z_zlkd2GI2QHX}H7(9|IXo(BBRMoeMU zPH-B3ity{5v9X}gGZ7kc_7OR!9O?6XrZdUT8aIUIC~M69!Aa4U?sASVg2#lwI2hC^K+(w zfg?JTMsxNR{XC=A=!D}X^r?VeiQn1VbG{4cdvzvPK4#+x@2H(RLE(2fAfG4nUvf$T zZSCWf(0?72KpRvce`esXcgFiDHU@`QXYb73QtZ=N9?x_<&E91Un}_PGA8y^Ao#o{h zqDD`CLZ!~ef8H5~p@JG#Qs5m}hZ0*Lljf@a+V7$ZWVS?BON|@610U zpLTxsRowwWqaQnROq=7zq32M12+)EH!&<5y?N%qU*l~8-|qAd z#Aex9hvE_lt2M-nP<9%wS-T9sv)AQ}eA~)WpGTbi%b|(TcL4HZ1wDSx&^fbEFMrqN zOxZJZBOsqp&?oL0`ZYivbf&EP^gTnL0LUv8^x1oc{skaEQ_xj=hR!>y&{KwA^s+G( z^n~9jvMPEv0rIyB`W!$Xtuq-7k@T*MgWd+ngJ#RRF9r1SL}+WkRNcn|@)`wwJ)o^0 zyKXB%LEi&N<7`>?ZF`0u1;{1^{i{7gUk}JXE9i&!4E-e_XHsRZ%Xwq2B@I@pEL|f8R57B_JPC(8gb^q3Bi8UI*lqa#?rY zo}tSC`6~rIJOwm71)3z^wWD#|{J&1bvg&O7sac-uIr|E)`&lU@Zp4s3X;Qtsov#$q z*%OjZy`DXBUrxPde?HmKskF*xA37>$*00fs{s}PcntV^vQP{I5f4fIvpMACUuS)KlR^5UT8oxI@uJ=v[v#@4PCk`gnVwyIghQ9h2zS-M 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 z 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

IBPkK*V0G~0x-zp=PLQJ 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

JxB!`v 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%0AL+Y1HDIAL*R*o%8r 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

ZC{Cy^_b({| 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≺P%tOl?2{qtrI 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