From a996f2f797404309f643d1e77ed0b2a81bae42b3 Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 14 Jul 2026 09:58:16 +0300 Subject: [PATCH] feat: add market data architecture and complete migration through build 039 --- app/scripts/get_ticker_24hr.py | 71 + app/src/integrations/exchange/market_cache.py | 116 +- .../exchange/market_data_runner.py | 36 +- .../integrations/exchange/market_stream.py | 32 +- app/src/integrations/exchange/mock_data.py | 31 +- app/src/integrations/exchange/models.py | 36 +- app/src/integrations/exchange/service.py | 671 ++-- app/src/integrations/exchange/status.py | 69 +- app/src/integrations/exchange/symbol_utils.py | 27 +- app/src/market_data/__init__.py | 0 app/src/market_data/acquisition/__init__.py | 0 .../acquisition/adapters/__init__.py | 0 .../acquisition/adapters/dzengi/__init__.py | 0 .../acquisition/adapters/dzengi/auth.py | 0 .../acquisition/adapters/dzengi/mapper.py | 316 ++ .../acquisition/adapters/dzengi/models.py | 133 + .../acquisition/adapters/dzengi/parser.py | 716 ++++ .../acquisition/adapters/dzengi/rest.py | 106 + .../acquisition/adapters/dzengi/websocket.py | 37 + app/src/market_data/acquisition/exceptions.py | 67 + .../market_data/acquisition/feeds/__init__.py | 0 .../acquisition/feeds/candles_feed.py | 0 .../acquisition/feeds/derivatives_feed.py | 0 .../acquisition/feeds/index_feed.py | 0 .../acquisition/feeds/instrument_feed.py | 33 + .../acquisition/feeds/orderbook_feed.py | 0 .../acquisition/feeds/quotes_feed.py | 36 + .../acquisition/feeds/status_feed.py | 2 + .../acquisition/feeds/time_feed.py | 0 .../acquisition/feeds/trades_feed.py | 0 .../acquisition/handlers/__init__.py | 0 .../acquisition/handlers/candles_handler.py | 0 .../handlers/derivatives_handler.py | 0 .../acquisition/handlers/index_handler.py | 0 .../handlers/instrument_handler.py | 32 + .../acquisition/handlers/orderbook_handler.py | 0 .../acquisition/handlers/quotes_handler.py | 35 + .../acquisition/handlers/status_handler.py | 2 + .../acquisition/handlers/time_handler.py | 0 .../acquisition/handlers/trades_handler.py | 0 .../acquisition/models/__init__.py | 1 + .../market_data/acquisition/models/candle.py | 0 .../acquisition/models/derivative.py | 0 .../market_data/acquisition/models/index.py | 0 .../acquisition/models/instrument.py | 38 + .../acquisition/models/orderbook.py | 0 .../market_data/acquisition/models/quote.py | 22 + .../market_data/acquisition/models/status.py | 88 + .../market_data/acquisition/models/time.py | 0 .../market_data/acquisition/models/trade.py | 0 app/src/market_data/acquisition/protocol.py | 86 + app/src/market_data/acquisition/registry.py | 142 + .../acquisition/runtime/__init__.py | 0 .../acquisition/runtime/heartbeat.py | 0 .../acquisition/runtime/reconnect.py | 0 .../acquisition/runtime/scheduler.py | 0 .../acquisition/runtime/supervisor.py | 0 app/src/market_data/acquisition/service.py | 62 + app/src/market_data/acquisition/symbols.py | 45 + .../acquisition/validation/__init__.py | 0 .../acquisition/validation/schema.py | 400 ++ .../acquisition/validation/sequence.py | 1 + .../acquisition/validation/values.py | 551 +++ app/src/storage/exceptions.py | 18 + app/src/storage/instrument_store.py | 110 + app/src/storage/quote_store.py | 215 + .../storage/repositories/balance_snapshots.py | 4 +- app/src/storage/session.py | 2 +- app/src/telegram/handlers/__init__.py | 4 +- app/src/telegram/handlers/auto/ui.py | 50 +- app/src/telegram/handlers/debug_auto/ui.py | 71 +- app/src/telegram/handlers/market.py | 505 --- app/src/telegram/ui/currency_ui.py | 100 +- app/src/trading/auto/execution_quality.py | 45 +- app/src/trading/auto/signal_runtime.py | 15 +- app/src/trading/debug/execution.py | 82 +- app/src/trading/decision/__init__.py | 1 + app/src/trading/decision/exceptions.py | 19 + app/src/trading/decision/models.py | 67 + app/src/trading/decision/protocol.py | 19 + app/src/trading/decision/rules.py | 65 + app/src/trading/decision/service.py | 29 + app/src/trading/decision/validation.py | 51 + app/src/trading/diagnostics/snapshot.py | 8 +- .../trading/market_intelligence/__init__.py | 1 + .../market_intelligence/common/__init__.py | 1 + .../market_intelligence/common/checks.py | 130 + .../market_intelligence/common/constants.py | 125 + .../market_intelligence/common/enums.py | 152 + .../market_intelligence/common/events.py | 110 + .../market_intelligence/common/models.py | 333 ++ .../market_intelligence/common/payloads.py | 173 + .../market_intelligence/common/reasons.py | 153 + .../market_intelligence/common/scores.py | 263 ++ .../market_intelligence/common/snapshots.py | 79 + .../market_intelligence/common/timeframes.py | 169 + .../market_intelligence/common/types.py | 112 + .../market_intelligence/common/validation.py | 258 ++ .../coordinator/__init__.py | 2 + .../coordinator/exceptions.py | 19 + .../coordinator/protocol.py | 21 + .../market_intelligence/coordinator/rules.py | 108 + .../coordinator/service.py | 31 + .../coordinator/validation.py | 51 + .../market_intelligence/cycle/__init__.py | 1 + .../market_intelligence/cycle/calculators.py | 1 + .../market_intelligence/cycle/checks.py | 0 .../market_intelligence/cycle/constants.py | 1 + .../market_intelligence/cycle/engine.py | 1 + .../market_intelligence/cycle/evaluators.py | 1 + .../market_intelligence/cycle/models.py | 1 + .../market_intelligence/cycle/payloads.py | 1 + .../market_intelligence/engine/__init__.py | 2 + .../market_intelligence/engine/base.py | 130 + .../market_intelligence/engine/exceptions.py | 23 + .../market_intelligence/engine/protocol.py | 27 + .../market_intelligence/liquidity/__init__.py | 1 + .../liquidity/calculators.py | 1 + .../market_intelligence/liquidity/checks.py | 0 .../liquidity/constants.py | 1 + .../market_intelligence/liquidity/engine.py | 1 + .../liquidity/evaluators.py | 1 + .../market_intelligence/liquidity/models.py | 1 + .../market_intelligence/liquidity/payloads.py | 1 + .../market_intelligence/momentum/__init__.py | 1 + .../momentum/calculators.py | 1 + .../market_intelligence/momentum/checks.py | 0 .../market_intelligence/momentum/constants.py | 1 + .../market_intelligence/momentum/engine.py | 1 + .../momentum/evaluators.py | 1 + .../market_intelligence/momentum/models.py | 1 + .../market_intelligence/momentum/payloads.py | 1 + .../market_intelligence/regime/__init__.py | 1 + .../market_intelligence/regime/calculators.py | 1 + .../market_intelligence/regime/checks.py | 0 .../market_intelligence/regime/constants.py | 1 + .../market_intelligence/regime/engine.py | 1 + .../market_intelligence/regime/evaluators.py | 1 + .../market_intelligence/regime/models.py | 1 + .../market_intelligence/regime/payloads.py | 1 + .../market_intelligence/runtime/__init__.py | 1 + .../runtime/dependencies.py | 116 + .../market_intelligence/runtime/exceptions.py | 43 + .../market_intelligence/runtime/protocol.py | 44 + .../market_intelligence/runtime/registry.py | 56 + .../market_intelligence/runtime/runner.py | 23 + .../market_intelligence/runtime/service.py | 82 + .../market_intelligence/runtime/validation.py | 79 + .../market_intelligence/structure/__init__.py | 1 + .../structure/calculators.py | 1 + .../market_intelligence/structure/checks.py | 0 .../structure/constants.py | 1 + .../market_intelligence/structure/engine.py | 1 + .../structure/evaluators.py | 1 + .../market_intelligence/structure/models.py | 1 + .../market_intelligence/structure/payloads.py | 1 + .../market_intelligence/trend/__init__.py | 1 + .../market_intelligence/trend/calculators.py | 1 + .../market_intelligence/trend/checks.py | 0 .../market_intelligence/trend/constants.py | 1 + .../market_intelligence/trend/engine.py | 1 + .../market_intelligence/trend/evaluators.py | 1 + .../market_intelligence/trend/models.py | 1 + .../market_intelligence/trend/payloads.py | 1 + .../volatility/__init__.py | 1 + .../volatility/calculators.py | 1 + .../market_intelligence/volatility/checks.py | 0 .../volatility/constants.py | 1 + .../market_intelligence/volatility/engine.py | 1 + .../volatility/evaluators.py | 1 + .../market_intelligence/volatility/models.py | 1 + .../volatility/payloads.py | 1 + .../market_intelligence/wave/__init__.py | 1 + .../market_intelligence/wave/calculators.py | 1 + .../market_intelligence/wave/checks.py | 0 .../market_intelligence/wave/constants.py | 1 + .../market_intelligence/wave/engine.py | 1 + .../market_intelligence/wave/evaluators.py | 1 + .../market_intelligence/wave/models.py | 1 + .../market_intelligence/wave/payloads.py | 1 + app/src/trading/strategies/scalp.py | 40 +- app/src/trading/strategies/trend.py | 73 +- .../exchange/test_market_cache.py | 74 + .../exchange/test_market_data_runner.py | 327 ++ .../exchange/test_market_stream.py | 450 +++ .../exchange/test_service_execution_quote.py | 108 + .../exchange/test_service_instruments.py | 453 +++ .../exchange/test_service_quote.py | 118 + .../test_service_symbol_runtime_status.py | 646 +++ .../exchange/test_service_validate_symbol.py | 530 +++ .../unit/integrations/exchange/test_status.py | 173 + .../exchange/test_symbol_utils.py | 74 + .../adapters/dzengi/test_mapper.py | 407 ++ .../adapters/dzengi/test_models.py | 206 + .../adapters/dzengi/test_parser.py | 405 ++ .../adapters/dzengi/test_quote_mapper.py | 128 + .../adapters/dzengi/test_quote_parser.py | 77 + .../acquisition/adapters/dzengi/test_rest.py | 371 ++ .../dzengi/test_websocket_quote_adapter.py | 34 + .../dzengi/test_websocket_quote_mapper.py | 48 + .../dzengi/test_websocket_quote_parser.py | 52 + .../acquisition/feeds/test_instrument_feed.py | 328 ++ .../acquisition/feeds/test_quotes_feed.py | 220 ++ .../handlers/test_instrument_handler.py | 220 ++ .../handlers/test_quotes_handler.py | 156 + .../acquisition/models/test_instrument.py | 164 + .../models/test_instrument_status.py | 127 + .../market_data/acquisition/test_protocol.py | 159 + .../market_data/acquisition/test_registry.py | 433 ++ .../market_data/acquisition/test_service.py | 475 +++ .../market_data/acquisition/test_symbols.py | 366 ++ .../validation/test_quote_schema.py | 72 + .../validation/test_quote_values.py | 91 + .../acquisition/validation/test_schema.py | 268 ++ .../acquisition/validation/test_values.py | 481 +++ .../validation/test_websocket_quote_schema.py | 46 + .../validation/test_websocket_quote_values.py | 41 + .../unit/storage/test_instrument_store.py | 463 +++ app/tests/unit/storage/test_quote_store.py | 448 +++ app/tests/unit/telegram/__init__.py | 0 app/tests/unit/telegram/ui/__init__.py | 0 .../unit/telegram/ui/test_currency_ui.py | 607 +++ .../trading/auto/test_execution_quality.py | 83 + .../trading/auto/test_signal_runtime_quote.py | 75 + .../unit/trading/debug/test_execution.py | 49 + .../trading/strategies/test_scalp_quote.py | 52 + .../trading/strategies/test_trend_quote.py | 52 + app/ticker_24hr_response.json | 18 + app/tools/dzengi_probe/README.md | 0 app/tools/dzengi_probe/__init__.py | 0 .../ask_price_equivalence_probe.py | 170 + app/tools/dzengi_probe/config.py | 49 + app/tools/dzengi_probe/connection_probe.py | 118 + app/tools/dzengi_probe/probe.py | 150 + app/tools/dzengi_probe/reports/README.md | 0 app/tools/dzengi_probe/response_store.py | 23 + app/tools/dzengi_probe/rest_probe.py | 195 + .../dzengi_probe/runtime_samples/README.md | 0 ...ask_price_equivalence_BTC_USD_LEVERAGE.csv | 31 + .../ticker24hr_update_BTC_USD_LEVERAGE.csv | 301 ++ .../rest/aggTrades/BTC_USD.json | 0 .../rest/aggTrades/BTC_USD_LEVERAGE.json | 142 + .../rest/depth/BTC_USD_LEVERAGE.json | 15 + .../rest/exchangeInfo/all.json | 2313 +++++++++++ .../rest/klines/BTC_USD_1m.json | 0 .../rest/klines/BTC_USD_LEVERAGE_1m.json | 82 + .../rest/ticker24hr/BTC_USD_LEVERAGE.json | 18 + .../runtime_samples/rest/ticker24hr/README.md | 11 + .../runtime_samples/rest/time/response.json | 3 + .../websocket/depth/BTC_USD_LEVERAGE.jsonl | 1 + .../depthMarketData.subscribe/BTC_USD.jsonl | 0 .../BTC_USD_LEVERAGE.empty-symbols.jsonl | 1 + ...TC_USD_LEVERAGE.empty-symbols.request.json | 7 + ...BTC_USD_LEVERAGE.invalid-destination.jsonl | 1 + ..._LEVERAGE.invalid-destination.request.json | 9 + .../BTC_USD_LEVERAGE.invalid-symbol.jsonl | 1 + ...C_USD_LEVERAGE.invalid-symbol.request.json | 9 + .../BTC_USD_LEVERAGE.jsonl | 854 ++++ .../BTC_USD_LEVERAGE.missing-payload.jsonl | 1 + ..._USD_LEVERAGE.missing-payload.request.json | 4 + .../BTC_USD_LEVERAGE.missing-symbols.jsonl | 1 + ..._USD_LEVERAGE.missing-symbols.request.json | 5 + .../BTC_USD_LEVERAGE.multi-symbol.jsonl | 148 + ...BTC_USD_LEVERAGE.multi-symbol.request.json | 10 + .../BTC_USD_LEVERAGE.request.json | 9 + .../BTC_USD_LEVERAGE.unsubscribe.jsonl | 1 + .../BTC_USD_LEVERAGE.unsubscribe.request.json | 9 + .../BTC_USD_LEVERAGE.valid.jsonl | 171 + .../BTC_USD_LEVERAGE.valid.request.json | 9 + .../websocket/trades.subscribe/BTC_USD.jsonl | 0 .../stream_depth_market_data_probe.py | 2 + .../dzengi_probe/stream_market_data_probe.py | 216 + .../stream_ohlc_market_data_probe.py | 2 + app/tools/dzengi_probe/stream_trades_probe.py | 2 + .../dzengi_probe/ticker24hr_update_probe.py | 135 + app/tools/dzengi_probe/websocket_probe.py | 134 + docs/market_intelligence/README.md | 115 + .../architecture_principles.md | 1610 ++++++++ docs/market_intelligence/build_history.md | 296 ++ .../builds/build-001-common-enums.md | 77 + .../builds/build-002-common-types.md | 96 + .../builds/build-003-common-constants.md | 126 + .../builds/build-004-common-reasons.md | 101 + .../builds/build-005-common-scores.md | 88 + ...ild-006-1-common-models-engine-metadata.md | 268 ++ .../builds/build-006-common-models.md | 263 ++ .../builds/build-007-common-validation.md | 248 ++ .../builds/build-008-common-checks.md | 248 ++ .../builds/build-009-common-payloads.md | 187 + .../builds/build-010-common-snapshots.md | 193 + .../builds/build-011-common-events.md | 205 + .../builds/build-012-common-timeframes.md | 226 ++ .../builds/build-013-runtime-architecture.md | 215 + .../builds/build-014-1-engine-protocol.md | 243 ++ .../builds/build-014-2-engine-base.md | 272 ++ .../builds/build-014-3-engine-exceptions.md | 235 ++ ...uild-015-1-common-models-runtime-result.md | 237 ++ .../builds/build-015-2-runtime-protocol.md | 284 ++ ...015-3-common-models-engine-registration.md | 296 ++ .../builds/build-015-4-runtime-registry.md | 360 ++ .../builds/build-015-5-runtime-core.md | 275 ++ .../build-015-6-runtime-dependencies.md | 265 ++ .../builds/build-015-7-runtime-validation.md | 280 ++ .../builds/build-015-8-runtime-service.md | 226 ++ ...-016-1-common-models-coordinator-result.md | 192 + .../build-016-2-coordinator-protocol.md | 189 + .../build-016-3-coordinator-exceptions.md | 168 + .../build-016-4-coordinator-validation.md | 212 + .../builds/build-016-5-coordinator-rules.md | 199 + .../builds/build-016-6-coordinator-service.md | 252 ++ ...uild-016-coordinator-layer-architecture.md | 363 ++ .../builds/build-017-1-trading-models.md | 187 + .../builds/build-017-2-trading-protocol.md | 191 + .../builds/build-017-3-trading-exceptions.md | 180 + .../builds/build-017-4-trading-validation.md | 224 ++ .../builds/build-017-5-trading-rules.md | 194 + .../builds/build-017-6-trading-service.md | 253 ++ ...build-017-7-trading-boundary-correction.md | 230 ++ .../build-017-trading-layer-architecture.md | 325 ++ docs/market_intelligence/common/README.md | 0 docs/market_intelligence/decisions/README.md | 130 + .../decision-001-architecture-first.md | 157 + .../decisions/decision-002-build-lifecycle.md | 209 + .../decisions/decision-003-domain-review.md | 181 + ...cision-004-no-existing-code-assumptions.md | 171 + .../decision-005-human-readable-comments.md | 198 + .../decision-006-immutable-engine-contract.md | 157 + .../decision-007-documentation-is-code.md | 179 + ...decision-008-engine-metadata-separation.md | 257 ++ .../development/build_task_standard.md | 0 .../development/how_to_work_with_chatgpt.md | 0 .../development_process.md | 3060 ++++++++++++++ docs/market_intelligence/diagrams/README.md | 0 docs/market_intelligence/engines/README.md | 0 .../engines/architecture.md | 171 + docs/market_intelligence/glossary/README.md | 0 .../market_intelligence/information/README.md | 0 .../dzengi_market_data_inventory.md | 3499 +++++++++++++++++ ...market_intelligence_information_mapping.md | 246 ++ .../information/dzengi_openapi.json | 1 + .../information/dzengi_rest_api.md | 0 .../information/dzengi_websocket_api.md | 0 .../information/external_sources.md | 0 .../information/market_information_backlog.md | 0 .../market_information_catalogue.md | 295 ++ ...ormation_catalogue_architecture_charter.md | 434 ++ .../knowledge/auction_context.md | 0 .../knowledge/continuation.md | 0 .../knowledge/entry_quality.md | 0 .../knowledge/instrument_profile.md | 0 .../knowledge/liquidity.md | 0 .../knowledge/market_cycle.md | 0 .../knowledge/market_regime.md | 0 .../knowledge/market_structure.md | 0 .../market_intelligence/knowledge/momentum.md | 0 .../knowledge/position_context.md | 0 .../market_intelligence/knowledge/reversal.md | 0 docs/market_intelligence/knowledge/trend.md | 0 .../knowledge/volatility.md | 0 docs/market_intelligence/knowledge/wave.md | 0 .../reference_model/engine_map.md | 0 .../reference_model/knowledge_hierarchy.md | 0 .../market_intelligence_reference_model.md | 0 ...intelligence_reference_model_v0.1_intro.md | 261 ++ .../reference_model/ontology.md | 0 .../reference_model/section_4_1_market.md | 77 + .../methodology/research_methodology.md | 585 +++ .../research/results/dzengi/README.md | 0 .../protocol_stream_marketData.subscribe.md | 341 ++ .../protocol_websocket_transport.md | 46 + .../reports/rest/rest_api_v1_aggTrades.md | 0 .../dzengi/reports/rest/rest_api_v1_depth.md | 0 .../reports/rest/rest_api_v1_exchangeInfo.md | 0 .../dzengi/reports/rest/rest_api_v1_klines.md | 0 .../reports/rest/rest_api_v1_ticker_24hr.md | 0 .../dzengi/reports/rest/rest_api_v1_time.md | 0 .../stream/stream_OHLCMarketData.subscribe.md | 0 .../stream_depthMarketData.subscribe.md | 0 .../stream/stream_marketData.subscribe.md | 269 ++ .../reports/stream/stream_trades.subscribe.md | 0 .../reports/websocket/ws_api_v1_aggTrades.md | 0 .../reports/websocket/ws_api_v1_depth.md | 0 .../websocket/ws_api_v1_exchangeInfo.md | 0 .../reports/websocket/ws_api_v1_klines.md | 0 .../websocket/ws_api_v1_ticker_24hr.md | 0 .../reports/websocket/ws_api_v1_time.md | 0 .../standards/research_standard_rest.md | 261 ++ .../standards/research_standard_stream.md | 334 ++ .../research_standard_websocket_request.md | 274 ++ .../research_standard_websocket_transport.md | 0 .../reviews/architecture_reviews.md | 0 .../reviews/compile_checks.md | 0 .../reviews/domain_reviews.md | 0 docs/market_intelligence/roadmap/README.md | 0 docs/market_intelligence/runtime/README.md | 140 + .../runtime/architecture.md | 588 +++ docs/market_intelligence/runtime_contract.md | 1992 ++++++++++ .../specifications/README.md | 0 docs/migrations/build_001.md | 325 ++ docs/migrations/build_002.md | 421 ++ docs/migrations/build_003.md | 850 ++++ docs/migrations/build_004.md | 775 ++++ docs/migrations/build_005.md | 1067 +++++ docs/migrations/build_006.md | 1071 +++++ docs/migrations/build_007.md | 999 +++++ docs/migrations/build_008.md | 1092 +++++ docs/migrations/build_009.md | 1275 ++++++ docs/migrations/build_010.md | 1411 +++++++ docs/migrations/build_011.md | 1602 ++++++++ docs/migrations/build_012.md | 1556 ++++++++ docs/migrations/build_013.md | 812 ++++ docs/migrations/build_014.md | 890 +++++ docs/migrations/build_015.md | 552 +++ docs/migrations/build_016.md | 544 +++ docs/migrations/build_017.md | 564 +++ docs/migrations/build_018.md | 589 +++ docs/migrations/build_019.md | 618 +++ docs/migrations/build_020.md | 863 ++++ docs/migrations/build_021.md | 814 ++++ docs/migrations/build_022.md | 1169 ++++++ docs/migrations/build_023.md | 335 ++ docs/migrations/build_024.md | 290 ++ docs/migrations/build_025.md | 599 +++ docs/migrations/build_026.md | 799 ++++ docs/migrations/build_027.md | 700 ++++ docs/migrations/build_028.md | 591 +++ docs/migrations/build_029.md | 717 ++++ docs/migrations/build_030.md | 630 +++ docs/migrations/build_031.md | 894 +++++ docs/migrations/build_032.md | 1033 +++++ docs/migrations/build_033.md | 864 ++++ docs/migrations/build_034.md | 594 +++ docs/migrations/build_035.md | 898 +++++ docs/migrations/build_036.md | 739 ++++ docs/migrations/build_037.md | 608 +++ docs/migrations/build_038.md | 567 +++ docs/migrations/greps.txt | 246 ++ .../instrument_reference_data_migration.md | 201 + .../Вывод grep по дополнительным полям.txt | 70 + .../knowledge_architecture_charter_v1.1.md | 395 ++ .../stage-08_2-architecture_build_log.md | 527 +++ scripts/create_market_common.sh | 46 + scripts/create_market_engine.sh | 34 + 443 files changed, 80452 insertions(+), 1335 deletions(-) create mode 100644 app/scripts/get_ticker_24hr.py create mode 100644 app/src/market_data/__init__.py create mode 100644 app/src/market_data/acquisition/__init__.py create mode 100644 app/src/market_data/acquisition/adapters/__init__.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/__init__.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/auth.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/mapper.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/models.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/parser.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/rest.py create mode 100644 app/src/market_data/acquisition/adapters/dzengi/websocket.py create mode 100644 app/src/market_data/acquisition/exceptions.py create mode 100644 app/src/market_data/acquisition/feeds/__init__.py create mode 100644 app/src/market_data/acquisition/feeds/candles_feed.py create mode 100644 app/src/market_data/acquisition/feeds/derivatives_feed.py create mode 100644 app/src/market_data/acquisition/feeds/index_feed.py create mode 100644 app/src/market_data/acquisition/feeds/instrument_feed.py create mode 100644 app/src/market_data/acquisition/feeds/orderbook_feed.py create mode 100644 app/src/market_data/acquisition/feeds/quotes_feed.py create mode 100644 app/src/market_data/acquisition/feeds/status_feed.py create mode 100644 app/src/market_data/acquisition/feeds/time_feed.py create mode 100644 app/src/market_data/acquisition/feeds/trades_feed.py create mode 100644 app/src/market_data/acquisition/handlers/__init__.py create mode 100644 app/src/market_data/acquisition/handlers/candles_handler.py create mode 100644 app/src/market_data/acquisition/handlers/derivatives_handler.py create mode 100644 app/src/market_data/acquisition/handlers/index_handler.py create mode 100644 app/src/market_data/acquisition/handlers/instrument_handler.py create mode 100644 app/src/market_data/acquisition/handlers/orderbook_handler.py create mode 100644 app/src/market_data/acquisition/handlers/quotes_handler.py create mode 100644 app/src/market_data/acquisition/handlers/status_handler.py create mode 100644 app/src/market_data/acquisition/handlers/time_handler.py create mode 100644 app/src/market_data/acquisition/handlers/trades_handler.py create mode 100644 app/src/market_data/acquisition/models/__init__.py create mode 100644 app/src/market_data/acquisition/models/candle.py create mode 100644 app/src/market_data/acquisition/models/derivative.py create mode 100644 app/src/market_data/acquisition/models/index.py create mode 100644 app/src/market_data/acquisition/models/instrument.py create mode 100644 app/src/market_data/acquisition/models/orderbook.py create mode 100644 app/src/market_data/acquisition/models/quote.py create mode 100644 app/src/market_data/acquisition/models/status.py create mode 100644 app/src/market_data/acquisition/models/time.py create mode 100644 app/src/market_data/acquisition/models/trade.py create mode 100644 app/src/market_data/acquisition/protocol.py create mode 100644 app/src/market_data/acquisition/registry.py create mode 100644 app/src/market_data/acquisition/runtime/__init__.py create mode 100644 app/src/market_data/acquisition/runtime/heartbeat.py create mode 100644 app/src/market_data/acquisition/runtime/reconnect.py create mode 100644 app/src/market_data/acquisition/runtime/scheduler.py create mode 100644 app/src/market_data/acquisition/runtime/supervisor.py create mode 100644 app/src/market_data/acquisition/service.py create mode 100644 app/src/market_data/acquisition/symbols.py create mode 100644 app/src/market_data/acquisition/validation/__init__.py create mode 100644 app/src/market_data/acquisition/validation/schema.py create mode 100644 app/src/market_data/acquisition/validation/sequence.py create mode 100644 app/src/market_data/acquisition/validation/values.py create mode 100644 app/src/storage/exceptions.py create mode 100644 app/src/storage/instrument_store.py create mode 100644 app/src/storage/quote_store.py delete mode 100644 app/src/telegram/handlers/market.py create mode 100644 app/src/trading/decision/__init__.py create mode 100644 app/src/trading/decision/exceptions.py create mode 100644 app/src/trading/decision/models.py create mode 100644 app/src/trading/decision/protocol.py create mode 100644 app/src/trading/decision/rules.py create mode 100644 app/src/trading/decision/service.py create mode 100644 app/src/trading/decision/validation.py create mode 100644 app/src/trading/market_intelligence/__init__.py create mode 100644 app/src/trading/market_intelligence/common/__init__.py create mode 100644 app/src/trading/market_intelligence/common/checks.py create mode 100644 app/src/trading/market_intelligence/common/constants.py create mode 100644 app/src/trading/market_intelligence/common/enums.py create mode 100644 app/src/trading/market_intelligence/common/events.py create mode 100644 app/src/trading/market_intelligence/common/models.py create mode 100644 app/src/trading/market_intelligence/common/payloads.py create mode 100644 app/src/trading/market_intelligence/common/reasons.py create mode 100644 app/src/trading/market_intelligence/common/scores.py create mode 100644 app/src/trading/market_intelligence/common/snapshots.py create mode 100644 app/src/trading/market_intelligence/common/timeframes.py create mode 100644 app/src/trading/market_intelligence/common/types.py create mode 100644 app/src/trading/market_intelligence/common/validation.py create mode 100644 app/src/trading/market_intelligence/coordinator/__init__.py create mode 100644 app/src/trading/market_intelligence/coordinator/exceptions.py create mode 100644 app/src/trading/market_intelligence/coordinator/protocol.py create mode 100644 app/src/trading/market_intelligence/coordinator/rules.py create mode 100644 app/src/trading/market_intelligence/coordinator/service.py create mode 100644 app/src/trading/market_intelligence/coordinator/validation.py create mode 100644 app/src/trading/market_intelligence/cycle/__init__.py create mode 100644 app/src/trading/market_intelligence/cycle/calculators.py create mode 100644 app/src/trading/market_intelligence/cycle/checks.py create mode 100644 app/src/trading/market_intelligence/cycle/constants.py create mode 100644 app/src/trading/market_intelligence/cycle/engine.py create mode 100644 app/src/trading/market_intelligence/cycle/evaluators.py create mode 100644 app/src/trading/market_intelligence/cycle/models.py create mode 100644 app/src/trading/market_intelligence/cycle/payloads.py create mode 100644 app/src/trading/market_intelligence/engine/__init__.py create mode 100644 app/src/trading/market_intelligence/engine/base.py create mode 100644 app/src/trading/market_intelligence/engine/exceptions.py create mode 100644 app/src/trading/market_intelligence/engine/protocol.py create mode 100644 app/src/trading/market_intelligence/liquidity/__init__.py create mode 100644 app/src/trading/market_intelligence/liquidity/calculators.py create mode 100644 app/src/trading/market_intelligence/liquidity/checks.py create mode 100644 app/src/trading/market_intelligence/liquidity/constants.py create mode 100644 app/src/trading/market_intelligence/liquidity/engine.py create mode 100644 app/src/trading/market_intelligence/liquidity/evaluators.py create mode 100644 app/src/trading/market_intelligence/liquidity/models.py create mode 100644 app/src/trading/market_intelligence/liquidity/payloads.py create mode 100644 app/src/trading/market_intelligence/momentum/__init__.py create mode 100644 app/src/trading/market_intelligence/momentum/calculators.py create mode 100644 app/src/trading/market_intelligence/momentum/checks.py create mode 100644 app/src/trading/market_intelligence/momentum/constants.py create mode 100644 app/src/trading/market_intelligence/momentum/engine.py create mode 100644 app/src/trading/market_intelligence/momentum/evaluators.py create mode 100644 app/src/trading/market_intelligence/momentum/models.py create mode 100644 app/src/trading/market_intelligence/momentum/payloads.py create mode 100644 app/src/trading/market_intelligence/regime/__init__.py create mode 100644 app/src/trading/market_intelligence/regime/calculators.py create mode 100644 app/src/trading/market_intelligence/regime/checks.py create mode 100644 app/src/trading/market_intelligence/regime/constants.py create mode 100644 app/src/trading/market_intelligence/regime/engine.py create mode 100644 app/src/trading/market_intelligence/regime/evaluators.py create mode 100644 app/src/trading/market_intelligence/regime/models.py create mode 100644 app/src/trading/market_intelligence/regime/payloads.py create mode 100644 app/src/trading/market_intelligence/runtime/__init__.py create mode 100644 app/src/trading/market_intelligence/runtime/dependencies.py create mode 100644 app/src/trading/market_intelligence/runtime/exceptions.py create mode 100644 app/src/trading/market_intelligence/runtime/protocol.py create mode 100644 app/src/trading/market_intelligence/runtime/registry.py create mode 100644 app/src/trading/market_intelligence/runtime/runner.py create mode 100644 app/src/trading/market_intelligence/runtime/service.py create mode 100644 app/src/trading/market_intelligence/runtime/validation.py create mode 100644 app/src/trading/market_intelligence/structure/__init__.py create mode 100644 app/src/trading/market_intelligence/structure/calculators.py create mode 100644 app/src/trading/market_intelligence/structure/checks.py create mode 100644 app/src/trading/market_intelligence/structure/constants.py create mode 100644 app/src/trading/market_intelligence/structure/engine.py create mode 100644 app/src/trading/market_intelligence/structure/evaluators.py create mode 100644 app/src/trading/market_intelligence/structure/models.py create mode 100644 app/src/trading/market_intelligence/structure/payloads.py create mode 100644 app/src/trading/market_intelligence/trend/__init__.py create mode 100644 app/src/trading/market_intelligence/trend/calculators.py create mode 100644 app/src/trading/market_intelligence/trend/checks.py create mode 100644 app/src/trading/market_intelligence/trend/constants.py create mode 100644 app/src/trading/market_intelligence/trend/engine.py create mode 100644 app/src/trading/market_intelligence/trend/evaluators.py create mode 100644 app/src/trading/market_intelligence/trend/models.py create mode 100644 app/src/trading/market_intelligence/trend/payloads.py create mode 100644 app/src/trading/market_intelligence/volatility/__init__.py create mode 100644 app/src/trading/market_intelligence/volatility/calculators.py create mode 100644 app/src/trading/market_intelligence/volatility/checks.py create mode 100644 app/src/trading/market_intelligence/volatility/constants.py create mode 100644 app/src/trading/market_intelligence/volatility/engine.py create mode 100644 app/src/trading/market_intelligence/volatility/evaluators.py create mode 100644 app/src/trading/market_intelligence/volatility/models.py create mode 100644 app/src/trading/market_intelligence/volatility/payloads.py create mode 100644 app/src/trading/market_intelligence/wave/__init__.py create mode 100644 app/src/trading/market_intelligence/wave/calculators.py create mode 100644 app/src/trading/market_intelligence/wave/checks.py create mode 100644 app/src/trading/market_intelligence/wave/constants.py create mode 100644 app/src/trading/market_intelligence/wave/engine.py create mode 100644 app/src/trading/market_intelligence/wave/evaluators.py create mode 100644 app/src/trading/market_intelligence/wave/models.py create mode 100644 app/src/trading/market_intelligence/wave/payloads.py create mode 100644 app/tests/unit/integrations/exchange/test_market_cache.py create mode 100644 app/tests/unit/integrations/exchange/test_market_data_runner.py create mode 100644 app/tests/unit/integrations/exchange/test_market_stream.py create mode 100644 app/tests/unit/integrations/exchange/test_service_execution_quote.py create mode 100644 app/tests/unit/integrations/exchange/test_service_instruments.py create mode 100644 app/tests/unit/integrations/exchange/test_service_quote.py create mode 100644 app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py create mode 100644 app/tests/unit/integrations/exchange/test_service_validate_symbol.py create mode 100644 app/tests/unit/integrations/exchange/test_status.py create mode 100644 app/tests/unit/integrations/exchange/test_symbol_utils.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py create mode 100644 app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py create mode 100644 app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py create mode 100644 app/tests/unit/market_data/acquisition/feeds/test_quotes_feed.py create mode 100644 app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py create mode 100644 app/tests/unit/market_data/acquisition/handlers/test_quotes_handler.py create mode 100644 app/tests/unit/market_data/acquisition/models/test_instrument.py create mode 100644 app/tests/unit/market_data/acquisition/models/test_instrument_status.py create mode 100644 app/tests/unit/market_data/acquisition/test_protocol.py create mode 100644 app/tests/unit/market_data/acquisition/test_registry.py create mode 100644 app/tests/unit/market_data/acquisition/test_service.py create mode 100644 app/tests/unit/market_data/acquisition/test_symbols.py create mode 100644 app/tests/unit/market_data/acquisition/validation/test_quote_schema.py create mode 100644 app/tests/unit/market_data/acquisition/validation/test_quote_values.py create mode 100644 app/tests/unit/market_data/acquisition/validation/test_schema.py create mode 100644 app/tests/unit/market_data/acquisition/validation/test_values.py create mode 100644 app/tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py create mode 100644 app/tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py create mode 100644 app/tests/unit/storage/test_instrument_store.py create mode 100644 app/tests/unit/storage/test_quote_store.py create mode 100644 app/tests/unit/telegram/__init__.py create mode 100644 app/tests/unit/telegram/ui/__init__.py create mode 100644 app/tests/unit/telegram/ui/test_currency_ui.py create mode 100644 app/tests/unit/trading/auto/test_execution_quality.py create mode 100644 app/tests/unit/trading/auto/test_signal_runtime_quote.py create mode 100644 app/tests/unit/trading/debug/test_execution.py create mode 100644 app/tests/unit/trading/strategies/test_scalp_quote.py create mode 100644 app/tests/unit/trading/strategies/test_trend_quote.py create mode 100644 app/ticker_24hr_response.json create mode 100644 app/tools/dzengi_probe/README.md create mode 100644 app/tools/dzengi_probe/__init__.py create mode 100644 app/tools/dzengi_probe/ask_price_equivalence_probe.py create mode 100644 app/tools/dzengi_probe/config.py create mode 100644 app/tools/dzengi_probe/connection_probe.py create mode 100644 app/tools/dzengi_probe/probe.py create mode 100644 app/tools/dzengi_probe/reports/README.md create mode 100644 app/tools/dzengi_probe/response_store.py create mode 100644 app/tools/dzengi_probe/rest_probe.py create mode 100644 app/tools/dzengi_probe/runtime_samples/README.md create mode 100644 app/tools/dzengi_probe/runtime_samples/reports/ask_price_equivalence_BTC_USD_LEVERAGE.csv create mode 100644 app/tools/dzengi_probe/runtime_samples/reports/ticker24hr_update_BTC_USD_LEVERAGE.csv create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD_LEVERAGE.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/depth/BTC_USD_LEVERAGE.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_1m.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_LEVERAGE_1m.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/BTC_USD_LEVERAGE.json create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/README.md create mode 100644 app/tools/dzengi_probe/runtime_samples/rest/time/response.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/depth/BTC_USD_LEVERAGE.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/depthMarketData.subscribe/BTC_USD.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.jsonl create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.request.json create mode 100644 app/tools/dzengi_probe/runtime_samples/websocket/trades.subscribe/BTC_USD.jsonl create mode 100644 app/tools/dzengi_probe/stream_depth_market_data_probe.py create mode 100644 app/tools/dzengi_probe/stream_market_data_probe.py create mode 100644 app/tools/dzengi_probe/stream_ohlc_market_data_probe.py create mode 100644 app/tools/dzengi_probe/stream_trades_probe.py create mode 100644 app/tools/dzengi_probe/ticker24hr_update_probe.py create mode 100644 app/tools/dzengi_probe/websocket_probe.py create mode 100644 docs/market_intelligence/README.md create mode 100644 docs/market_intelligence/architecture_principles.md create mode 100644 docs/market_intelligence/build_history.md create mode 100644 docs/market_intelligence/builds/build-001-common-enums.md create mode 100644 docs/market_intelligence/builds/build-002-common-types.md create mode 100644 docs/market_intelligence/builds/build-003-common-constants.md create mode 100644 docs/market_intelligence/builds/build-004-common-reasons.md create mode 100644 docs/market_intelligence/builds/build-005-common-scores.md create mode 100644 docs/market_intelligence/builds/build-006-1-common-models-engine-metadata.md create mode 100644 docs/market_intelligence/builds/build-006-common-models.md create mode 100644 docs/market_intelligence/builds/build-007-common-validation.md create mode 100644 docs/market_intelligence/builds/build-008-common-checks.md create mode 100644 docs/market_intelligence/builds/build-009-common-payloads.md create mode 100644 docs/market_intelligence/builds/build-010-common-snapshots.md create mode 100644 docs/market_intelligence/builds/build-011-common-events.md create mode 100644 docs/market_intelligence/builds/build-012-common-timeframes.md create mode 100644 docs/market_intelligence/builds/build-013-runtime-architecture.md create mode 100644 docs/market_intelligence/builds/build-014-1-engine-protocol.md create mode 100644 docs/market_intelligence/builds/build-014-2-engine-base.md create mode 100644 docs/market_intelligence/builds/build-014-3-engine-exceptions.md create mode 100644 docs/market_intelligence/builds/build-015-1-common-models-runtime-result.md create mode 100644 docs/market_intelligence/builds/build-015-2-runtime-protocol.md create mode 100644 docs/market_intelligence/builds/build-015-3-common-models-engine-registration.md create mode 100644 docs/market_intelligence/builds/build-015-4-runtime-registry.md create mode 100644 docs/market_intelligence/builds/build-015-5-runtime-core.md create mode 100644 docs/market_intelligence/builds/build-015-6-runtime-dependencies.md create mode 100644 docs/market_intelligence/builds/build-015-7-runtime-validation.md create mode 100644 docs/market_intelligence/builds/build-015-8-runtime-service.md create mode 100644 docs/market_intelligence/builds/build-016-1-common-models-coordinator-result.md create mode 100644 docs/market_intelligence/builds/build-016-2-coordinator-protocol.md create mode 100644 docs/market_intelligence/builds/build-016-3-coordinator-exceptions.md create mode 100644 docs/market_intelligence/builds/build-016-4-coordinator-validation.md create mode 100644 docs/market_intelligence/builds/build-016-5-coordinator-rules.md create mode 100644 docs/market_intelligence/builds/build-016-6-coordinator-service.md create mode 100644 docs/market_intelligence/builds/build-016-coordinator-layer-architecture.md create mode 100644 docs/market_intelligence/builds/build-017-1-trading-models.md create mode 100644 docs/market_intelligence/builds/build-017-2-trading-protocol.md create mode 100644 docs/market_intelligence/builds/build-017-3-trading-exceptions.md create mode 100644 docs/market_intelligence/builds/build-017-4-trading-validation.md create mode 100644 docs/market_intelligence/builds/build-017-5-trading-rules.md create mode 100644 docs/market_intelligence/builds/build-017-6-trading-service.md create mode 100644 docs/market_intelligence/builds/build-017-7-trading-boundary-correction.md create mode 100644 docs/market_intelligence/builds/build-017-trading-layer-architecture.md create mode 100644 docs/market_intelligence/common/README.md create mode 100644 docs/market_intelligence/decisions/README.md create mode 100644 docs/market_intelligence/decisions/decision-001-architecture-first.md create mode 100644 docs/market_intelligence/decisions/decision-002-build-lifecycle.md create mode 100644 docs/market_intelligence/decisions/decision-003-domain-review.md create mode 100644 docs/market_intelligence/decisions/decision-004-no-existing-code-assumptions.md create mode 100644 docs/market_intelligence/decisions/decision-005-human-readable-comments.md create mode 100644 docs/market_intelligence/decisions/decision-006-immutable-engine-contract.md create mode 100644 docs/market_intelligence/decisions/decision-007-documentation-is-code.md create mode 100644 docs/market_intelligence/decisions/decision-008-engine-metadata-separation.md create mode 100644 docs/market_intelligence/development/build_task_standard.md create mode 100644 docs/market_intelligence/development/how_to_work_with_chatgpt.md create mode 100644 docs/market_intelligence/development_process.md create mode 100644 docs/market_intelligence/diagrams/README.md create mode 100644 docs/market_intelligence/engines/README.md create mode 100644 docs/market_intelligence/engines/architecture.md create mode 100644 docs/market_intelligence/glossary/README.md create mode 100644 docs/market_intelligence/information/README.md create mode 100644 docs/market_intelligence/information/dzengi_market_data_inventory.md create mode 100644 docs/market_intelligence/information/dzengi_market_intelligence_information_mapping.md create mode 100644 docs/market_intelligence/information/dzengi_openapi.json create mode 100644 docs/market_intelligence/information/dzengi_rest_api.md create mode 100644 docs/market_intelligence/information/dzengi_websocket_api.md create mode 100644 docs/market_intelligence/information/external_sources.md create mode 100644 docs/market_intelligence/information/market_information_backlog.md create mode 100644 docs/market_intelligence/information/market_information_catalogue.md create mode 100644 docs/market_intelligence/information/market_information_catalogue_architecture_charter.md create mode 100644 docs/market_intelligence/knowledge/auction_context.md create mode 100644 docs/market_intelligence/knowledge/continuation.md create mode 100644 docs/market_intelligence/knowledge/entry_quality.md create mode 100644 docs/market_intelligence/knowledge/instrument_profile.md create mode 100644 docs/market_intelligence/knowledge/liquidity.md create mode 100644 docs/market_intelligence/knowledge/market_cycle.md create mode 100644 docs/market_intelligence/knowledge/market_regime.md create mode 100644 docs/market_intelligence/knowledge/market_structure.md create mode 100644 docs/market_intelligence/knowledge/momentum.md create mode 100644 docs/market_intelligence/knowledge/position_context.md create mode 100644 docs/market_intelligence/knowledge/reversal.md create mode 100644 docs/market_intelligence/knowledge/trend.md create mode 100644 docs/market_intelligence/knowledge/volatility.md create mode 100644 docs/market_intelligence/knowledge/wave.md create mode 100644 docs/market_intelligence/reference_model/engine_map.md create mode 100644 docs/market_intelligence/reference_model/knowledge_hierarchy.md create mode 100644 docs/market_intelligence/reference_model/market_intelligence_reference_model.md create mode 100644 docs/market_intelligence/reference_model/market_intelligence_reference_model_v0.1_intro.md create mode 100644 docs/market_intelligence/reference_model/ontology.md create mode 100644 docs/market_intelligence/reference_model/section_4_1_market.md create mode 100644 docs/market_intelligence/research/methodology/research_methodology.md create mode 100644 docs/market_intelligence/research/results/dzengi/README.md create mode 100644 docs/market_intelligence/research/results/dzengi/protocols/stream/protocol_stream_marketData.subscribe.md create mode 100644 docs/market_intelligence/research/results/dzengi/protocols/websocket_transport/protocol_websocket_transport.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_aggTrades.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_depth.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_exchangeInfo.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_klines.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_ticker_24hr.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_time.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/stream/stream_OHLCMarketData.subscribe.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/stream/stream_depthMarketData.subscribe.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/stream/stream_marketData.subscribe.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/stream/stream_trades.subscribe.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_aggTrades.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_depth.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_exchangeInfo.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_klines.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_ticker_24hr.md create mode 100644 docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_time.md create mode 100644 docs/market_intelligence/research/standards/research_standard_rest.md create mode 100644 docs/market_intelligence/research/standards/research_standard_stream.md create mode 100644 docs/market_intelligence/research/standards/research_standard_websocket_request.md create mode 100644 docs/market_intelligence/research/standards/research_standard_websocket_transport.md create mode 100644 docs/market_intelligence/reviews/architecture_reviews.md create mode 100644 docs/market_intelligence/reviews/compile_checks.md create mode 100644 docs/market_intelligence/reviews/domain_reviews.md create mode 100644 docs/market_intelligence/roadmap/README.md create mode 100644 docs/market_intelligence/runtime/README.md create mode 100644 docs/market_intelligence/runtime/architecture.md create mode 100644 docs/market_intelligence/runtime_contract.md create mode 100644 docs/market_intelligence/specifications/README.md create mode 100644 docs/migrations/build_001.md create mode 100644 docs/migrations/build_002.md create mode 100644 docs/migrations/build_003.md create mode 100644 docs/migrations/build_004.md create mode 100644 docs/migrations/build_005.md create mode 100644 docs/migrations/build_006.md create mode 100644 docs/migrations/build_007.md create mode 100644 docs/migrations/build_008.md create mode 100644 docs/migrations/build_009.md create mode 100644 docs/migrations/build_010.md create mode 100644 docs/migrations/build_011.md create mode 100644 docs/migrations/build_012.md create mode 100644 docs/migrations/build_013.md create mode 100644 docs/migrations/build_014.md create mode 100644 docs/migrations/build_015.md create mode 100644 docs/migrations/build_016.md create mode 100644 docs/migrations/build_017.md create mode 100644 docs/migrations/build_018.md create mode 100644 docs/migrations/build_019.md create mode 100644 docs/migrations/build_020.md create mode 100644 docs/migrations/build_021.md create mode 100644 docs/migrations/build_022.md create mode 100644 docs/migrations/build_023.md create mode 100644 docs/migrations/build_024.md create mode 100644 docs/migrations/build_025.md create mode 100644 docs/migrations/build_026.md create mode 100644 docs/migrations/build_027.md create mode 100644 docs/migrations/build_028.md create mode 100644 docs/migrations/build_029.md create mode 100644 docs/migrations/build_030.md create mode 100644 docs/migrations/build_031.md create mode 100644 docs/migrations/build_032.md create mode 100644 docs/migrations/build_033.md create mode 100644 docs/migrations/build_034.md create mode 100644 docs/migrations/build_035.md create mode 100644 docs/migrations/build_036.md create mode 100644 docs/migrations/build_037.md create mode 100644 docs/migrations/build_038.md create mode 100644 docs/migrations/greps.txt create mode 100644 docs/migrations/instrument_reference_data_migration.md create mode 100644 docs/migrations/Вывод grep по дополнительным полям.txt create mode 100644 docs/reference_model/knowledge_architecture_charter_v1.1.md create mode 100644 docs/stages/stage-08_2-architecture_build_log.md create mode 100755 scripts/create_market_common.sh create mode 100755 scripts/create_market_engine.sh diff --git a/app/scripts/get_ticker_24hr.py b/app/scripts/get_ticker_24hr.py new file mode 100644 index 0000000..9feffb4 --- /dev/null +++ b/app/scripts/get_ticker_24hr.py @@ -0,0 +1,71 @@ +# app/scripts/get_ticker_24hr.py + +from __future__ import annotations + +import argparse +import json +import sys + +from src.core.config import load_settings +from src.integrations.exchange.rest_client import ExchangeRestClient + + +def parse_args() -> argparse.Namespace: + settings = load_settings() + + parser = argparse.ArgumentParser( + description="Получить реальный ответ Dzengi ticker/24hr.", + ) + parser.add_argument( + "symbol", + nargs="?", + default=settings.default_symbol, + help=( + "Торговый символ. " + f"По умолчанию: {settings.default_symbol}" + ), + ) + + return parser.parse_args() + + +def main() -> int: + args = parse_args() + symbol = str(args.symbol).strip() + + if not symbol: + print( + "Торговый символ не должен быть пустым.", + file=sys.stderr, + ) + return 2 + + try: + payload = ExchangeRestClient().get_json( + "/api/v1/ticker/24hr", + params={ + "symbol": symbol, + }, + ) + except Exception as exc: + print( + f"Не удалось получить ticker/24hr для {symbol}: " + f"{type(exc).__name__}: {exc}", + file=sys.stderr, + ) + return 1 + + print( + json.dumps( + payload, + ensure_ascii=False, + indent=2, + sort_keys=True, + ) + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/app/src/integrations/exchange/market_cache.py b/app/src/integrations/exchange/market_cache.py index 2e61d88..daac1a8 100644 --- a/app/src/integrations/exchange/market_cache.py +++ b/app/src/integrations/exchange/market_cache.py @@ -2,85 +2,42 @@ from __future__ import annotations -import time -from dataclasses import dataclass -from datetime import datetime -from zoneinfo import ZoneInfo - -from src.core.config import load_settings +from src.market_data.acquisition.models.quote import Quote +from src.storage.quote_store import InMemoryQuoteStore, QuoteStoreProtocol -@dataclass(slots=True) -class MarketPriceSnapshot: - symbol: str - price: float - bid_price: float | None - ask_price: float | None - updated_at: str - source: str = "market-cache" - runtime_key: str = "default" - received_monotonic: float = 0.0 - - def age_seconds(self) -> float: - if self.received_monotonic <= 0: - return 999999.0 - - return max(0.0, time.monotonic() - self.received_monotonic) - - def has_bid_ask(self) -> bool: - return ( - self.bid_price is not None - and self.ask_price is not None - and self.bid_price > 0 - and self.ask_price > 0 - ) +_MARKET_PRICE_CACHE_SOURCE_NAME = "legacy-market-price-cache" class MarketPriceCache: - _prices: dict[tuple[str, str], MarketPriceSnapshot] = {} + # Временный compatibility facade над каноническим Quote Store. + _store: QuoteStoreProtocol = InMemoryQuoteStore() @classmethod - def _key(cls, *, symbol: str, runtime_key: str = "default") -> tuple[str, str]: - return runtime_key.strip().lower(), symbol.upper() - - @classmethod - def set_price( + def set_quote( cls, + quote: Quote, *, - symbol: str, - price: float, - bid_price: float | None = None, - ask_price: float | None = None, - updated_at: str | None = None, - source: str = "market-polling", runtime_key: str = "default", ) -> None: - settings = load_settings() - - if updated_at is None: - updated_at = datetime.now(ZoneInfo(settings.tz)).strftime("%d.%m.%Y %H:%M:%S") - - normalized_runtime_key = runtime_key.strip().lower() - - cls._prices[cls._key(symbol=symbol, runtime_key=normalized_runtime_key)] = MarketPriceSnapshot( - symbol=symbol.upper(), - price=float(price), - bid_price=float(bid_price) if bid_price is not None else None, - ask_price=float(ask_price) if ask_price is not None else None, - updated_at=updated_at, - source=source, - runtime_key=normalized_runtime_key, - received_monotonic=time.monotonic(), + cls._store.set( + _MARKET_PRICE_CACHE_SOURCE_NAME, + quote, + runtime_key=cls._normalize_runtime_key(runtime_key), ) @classmethod - def get_price( + def get_quote( cls, symbol: str, *, runtime_key: str = "default", - ) -> MarketPriceSnapshot | None: - return cls._prices.get(cls._key(symbol=symbol, runtime_key=runtime_key)) + ) -> Quote | None: + return cls._store.get( + _MARKET_PRICE_CACHE_SOURCE_NAME, + cls._normalize_symbol(symbol), + runtime_key=cls._normalize_runtime_key(runtime_key), + ) @classmethod def clear( @@ -89,23 +46,24 @@ class MarketPriceCache: *, runtime_key: str | None = None, ) -> None: - if symbol is None and runtime_key is None: - cls._prices.clear() - return + cls._store.clear( + source_name=_MARKET_PRICE_CACHE_SOURCE_NAME, + symbol=( + cls._normalize_symbol(symbol) + if symbol is not None + else None + ), + runtime_key=( + cls._normalize_runtime_key(runtime_key) + if runtime_key is not None + else None + ), + ) - if symbol is not None and runtime_key is not None: - cls._prices.pop(cls._key(symbol=symbol, runtime_key=runtime_key), None) - return + @staticmethod + def _normalize_symbol(symbol: str) -> str: + return str(symbol).strip().upper() - keys_to_delete = [] - - for key_runtime, key_symbol in cls._prices.keys(): - if runtime_key is not None and key_runtime == runtime_key.strip().lower(): - keys_to_delete.append((key_runtime, key_symbol)) - continue - - if symbol is not None and key_symbol == symbol.upper(): - keys_to_delete.append((key_runtime, key_symbol)) - - for key in keys_to_delete: - cls._prices.pop(key, None) \ No newline at end of file + @staticmethod + def _normalize_runtime_key(runtime_key: str) -> str: + return str(runtime_key).strip().lower() diff --git a/app/src/integrations/exchange/market_data_runner.py b/app/src/integrations/exchange/market_data_runner.py index d492842..683e1d0 100644 --- a/app/src/integrations/exchange/market_data_runner.py +++ b/app/src/integrations/exchange/market_data_runner.py @@ -13,6 +13,12 @@ from src.core.types import JsonDict, NumericLike from src.integrations.exchange.market_cache import MarketPriceCache from src.integrations.exchange.service import ExchangeService from src.integrations.exchange.ws_client import ExchangeWebSocketClient +from src.market_data.acquisition.adapters.dzengi.websocket import ( + DzengiWebSocketQuoteAdapter, +) +from src.market_data.acquisition.exceptions import ( + MarketDataAcquisitionError, +) from src.trading.journal.service import JournalService @@ -297,6 +303,7 @@ class MarketDataRunner: valid_payload_count = 0 invalid_payload_count = 0 + adapter = DzengiWebSocketQuoteAdapter() async for payload in ExchangeWebSocketClient().stream_depth( ws_symbol, @@ -306,20 +313,31 @@ class MarketDataRunner: if current_symbol and current_symbol != symbol: break - best_bid = cls._extract_best_price(payload, "bids") - best_ask = cls._extract_best_price(payload, "asks") - - if best_bid is None or best_ask is None: + try: + quote = adapter.map_message(payload) + except MarketDataAcquisitionError: invalid_payload_count += 1 if invalid_payload_count >= 5: raise RuntimeError( - "WebSocket depth stream does not contain valid bids/asks." + "WebSocket depth stream does not contain valid quotes." + ) + + continue + + if quote.symbol.strip().upper() != cache_symbol.strip().upper(): + invalid_payload_count += 1 + + if invalid_payload_count >= 5: + raise RuntimeError( + "WebSocket depth stream returned another symbol." ) continue invalid_payload_count = 0 + best_bid = float(quote.bid_price) + best_ask = float(quote.ask_price) if valid_payload_count == 0: should_log_connected = ( @@ -354,12 +372,8 @@ class MarketDataRunner: valid_payload_count += 1 - MarketPriceCache.set_price( - symbol=cache_symbol, - price=(best_bid + best_ask) / 2, - bid_price=best_bid, - ask_price=best_ask, - source=f"ws_depth:{context.runtime_key}", + MarketPriceCache.set_quote( + quote, runtime_key=context.runtime_key, ) diff --git a/app/src/integrations/exchange/market_stream.py b/app/src/integrations/exchange/market_stream.py index 4c7aeb3..2880001 100644 --- a/app/src/integrations/exchange/market_stream.py +++ b/app/src/integrations/exchange/market_stream.py @@ -12,6 +12,12 @@ from src.core.types import JsonDict, NumericLike from src.integrations.exchange.market_cache import MarketPriceCache from src.integrations.exchange.service import ExchangeService from src.integrations.exchange.ws_client import ExchangeWebSocketClient +from src.market_data.acquisition.adapters.dzengi.websocket import ( + DzengiWebSocketQuoteAdapter, +) +from src.market_data.acquisition.exceptions import ( + MarketDataAcquisitionError, +) from src.trading.journal.service import JournalService @@ -145,6 +151,7 @@ async def start_market_stream() -> None: symbol = validation.normalized_symbol client = ExchangeWebSocketClient() + adapter = DzengiWebSocketQuoteAdapter() journal.log_info( "market_ws_started", @@ -153,29 +160,16 @@ async def start_market_stream() -> None: ) async for message in client.stream_depth(symbol): - event = _extract_market_event(message) - - if event is None: + try: + quote = adapter.map_message(message) + except MarketDataAcquisitionError: continue - price = safe_float(event.get("price")) - bid_price = safe_float(event.get("bid_price")) - ask_price = safe_float(event.get("ask_price")) - - if price is None or bid_price is None or ask_price is None: + if quote.symbol.strip().upper() != symbol.strip().upper(): continue - MarketPriceCache.set_price( - symbol=symbol, - price=price, - bid_price=bid_price, - ask_price=ask_price, - updated_at=( - str(event.get("updated_at")) - if event.get("updated_at") is not None - else None - ), - source="ws_market_stream", + MarketPriceCache.set_quote( + quote, runtime_key="default", ) diff --git a/app/src/integrations/exchange/mock_data.py b/app/src/integrations/exchange/mock_data.py index 36ee4c9..a38a677 100644 --- a/app/src/integrations/exchange/mock_data.py +++ b/app/src/integrations/exchange/mock_data.py @@ -1,8 +1,12 @@ +# app/src/integrations/exchange/mock_data.py + from __future__ import annotations from datetime import datetime, timezone +from decimal import Decimal -from src.integrations.exchange.models import BalanceSummary, ExchangeHealth, TickerPrice +from src.integrations.exchange.models import BalanceSummary, ExchangeHealth +from src.market_data.acquisition.models.quote import Quote def mock_exchange_health() -> ExchangeHealth: @@ -13,20 +17,23 @@ def mock_exchange_health() -> ExchangeHealth: ) -def mock_ticker_price(symbol: str) -> TickerPrice: - symbol = symbol.upper().strip() +def mock_quote(symbol: str) -> Quote: + normalized_symbol = symbol.upper().strip() fake_prices = { - "BTCUSDT": 68425.10, - "ETHUSDT": 3521.44, - "BNBUSDT": 612.33, + "BTCUSDT": Decimal("68425.10"), + "ETHUSDT": Decimal("3521.44"), + "BNBUSDT": Decimal("612.33"), } - price = fake_prices.get(symbol, 100.00) - updated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - return TickerPrice( - symbol=symbol, - price=price, + price = fake_prices.get(normalized_symbol, Decimal("100.00")) + + return Quote( + symbol=normalized_symbol, + last_price=price, + bid_price=price, + ask_price=price, + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), source="mock", - updated_at=updated_at, ) diff --git a/app/src/integrations/exchange/models.py b/app/src/integrations/exchange/models.py index f025665..0e7a46c 100644 --- a/app/src/integrations/exchange/models.py +++ b/app/src/integrations/exchange/models.py @@ -3,6 +3,11 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from src.market_data.acquisition.models.instrument import Instrument # Состояние публичного API биржи. @@ -25,13 +30,6 @@ class TimeSyncStatus: message: str -# Текущая рыночная цена инструмента. -@dataclass(slots=True) -class TickerPrice: - symbol: str - price: float - source: str - updated_at: str # Snapshot цен для execution layer. @@ -62,26 +60,7 @@ class BalanceSummary: source: str -# Информация о торговом инструменте биржи. -@dataclass(slots=True) -class ExchangeSymbol: - symbol: str - name: str - status: str - - base_asset: str - quote_asset: str - - market_modes: list[str] - market_type: str - - tick_size: float | None - step_size: float | None - min_qty: float | None - min_notional: float | None - - -# Результат проверки символа. +# Результат проверки торгового символа по каноническому справочнику Instrument. @dataclass(slots=True) class SymbolValidationResult: requested_symbol: str @@ -90,7 +69,7 @@ class SymbolValidationResult: is_valid: bool message: str - symbol_info: ExchangeSymbol | None + symbol_info: Instrument | None # Состояние приватного API аккаунта. @@ -134,6 +113,7 @@ class KlineBatch: candles: list[Kline] source: str + # Информация о торговой комиссии для инструмента. @dataclass(slots=True) class TradingFee: diff --git a/app/src/integrations/exchange/service.py b/app/src/integrations/exchange/service.py index d2cbf12..388d7cc 100644 --- a/app/src/integrations/exchange/service.py +++ b/app/src/integrations/exchange/service.py @@ -4,7 +4,7 @@ from __future__ import annotations import time import socket -from datetime import datetime +from datetime import datetime, timezone from zoneinfo import ZoneInfo from src.core.config import load_settings @@ -16,18 +16,16 @@ from src.integrations.exchange.market_cache import MarketPriceCache from src.integrations.exchange.mock_data import ( mock_balance_summary, mock_exchange_health, - mock_ticker_price, + mock_quote, ) from src.integrations.exchange.models import ( BalanceSummary, ExchangeHealth, - ExchangeSymbol, ExecutionPriceSnapshot, Kline, KlineBatch, PrivateAuthHealth, SymbolValidationResult, - TickerPrice, TimeSyncStatus, TradingFee, ) @@ -43,12 +41,46 @@ from src.integrations.exchange.status import ( build_mock_exchange_status, classify_exchange_error, ) -from src.integrations.exchange.symbol_utils import normalize_symbol, symbol_candidates +from src.market_data.acquisition.adapters.dzengi.rest import ( + DzengiInstrumentDocumentSource, + DzengiQuoteDocumentSource, +) +from src.market_data.acquisition.feeds.instrument_feed import InstrumentFeed +from src.market_data.acquisition.feeds.quotes_feed import QuotesFeed +from src.market_data.acquisition.handlers.instrument_handler import ( + DzengiInstrumentDocumentHandler, +) +from src.market_data.acquisition.handlers.quotes_handler import ( + DzengiQuoteDocumentHandler, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.registry import ( + InstrumentFeedRegistry, + QuoteFeedRegistry, +) +from src.market_data.acquisition.service import ( + InstrumentAcquisitionService, + QuoteAcquisitionService, +) +from src.market_data.acquisition.symbols import ( + normalize_symbol, + resolve_symbol_index, +) +from src.storage.instrument_store import ( + InMemoryInstrumentStore, + InstrumentStoreProtocol, +) from src.trading.journal.service import JournalService +_INSTRUMENT_REFERENCE_SOURCE_NAME = "dzengi" +_QUOTE_SOURCE_NAME = "dzengi" + + class ExchangeService: - _exchange_symbols_cache: list[ExchangeSymbol] | None = None + _instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore() + _execution_cache_max_age_seconds = 2.0 _default_runtime_key = "auto" @@ -108,17 +140,28 @@ class ExchangeService: return status try: - snapshot = self.get_fresh_market_snapshot(validation.normalized_symbol) + quote = self._get_fresh_quote( + validation.normalized_symbol, + ) except Exception: return status - age_seconds = safe_float(snapshot.get("age_seconds")) + exchange_timestamp_ms = ( + int(quote.exchange_timestamp.timestamp() * 1000) + if quote.exchange_timestamp is not None + else None + ) + age_seconds = self._exchange_timestamp_age_seconds( + exchange_timestamp_ms + ) if age_seconds is not None and age_seconds > 60: return build_market_stale_status( symbol=validation.normalized_symbol, age_seconds=age_seconds, - updated_at=str(snapshot.get("updated_at") or ""), + updated_at=self._format_exchange_time( + exchange_timestamp_ms + ), ) return status @@ -668,7 +711,9 @@ class ExchangeService: ) try: - ticker = self._get_real_price(str(status.symbol or self.settings.default_symbol)) + quote = self._get_fresh_quote( + str(status.symbol or self.settings.default_symbol) + ) except ExchangeError as exc: return ExchangeHealth( ok=False, @@ -679,7 +724,10 @@ class ExchangeService: return ExchangeHealth( ok=True, mode="real_public_api", - message=f"Public API OK. Цена {ticker.symbol}: {ticker.price:.2f}", + message=( + f"Public API OK. Цена {quote.symbol}: " + f"{float(quote.last_price):.2f}" + ), ) # Проверить доступность приватного API и валидность ключей аккаунта. @@ -722,149 +770,41 @@ class ExchangeService: message=f"Private API OK. Балансов получено: {len(balances)}", ) - # Обновить price cache и вернуть TickerPrice. - def refresh_price_cache( + # Получить каноническую текущую котировку из Store или REST Quotes Feed. + def get_quote( self, symbol: str | None = None, *, runtime_key: str | None = None, - ) -> TickerPrice: - snapshot = self.refresh_market_snapshot_cache( - symbol, - runtime_key=runtime_key, - ) - - price = safe_float(snapshot.get("last_price")) - - if price is None: - raise ExchangeError("Field 'last_price' is missing in market snapshot.") - - return TickerPrice( - symbol=str(snapshot["symbol"]), - price=price, - source=str(snapshot.get("source") or self._source_name()), - updated_at=str(snapshot["updated_at"]), - ) - - # Обновить market snapshot cache через свежий REST-запрос. - def refresh_market_snapshot_cache( - self, - symbol: str | None = None, - *, - runtime_key: str | None = None, - ) -> dict[str, object]: - normalized_runtime_key = self._runtime_key(runtime_key) - snapshot = self.get_fresh_market_snapshot(symbol) - - last_price = safe_float(snapshot.get("last_price")) - bid_price = safe_float(snapshot.get("bid_price")) - ask_price = safe_float(snapshot.get("ask_price")) - - if last_price is None or bid_price is None or ask_price is None: - raise ExchangeError("Market snapshot contains invalid price fields.") - - MarketPriceCache.set_price( - symbol=str(snapshot["symbol"]), - price=last_price, - bid_price=bid_price, - ask_price=ask_price, - updated_at=str(snapshot["updated_at"]), - source=str(snapshot.get("source") or "rest_polling"), - runtime_key=normalized_runtime_key, - ) - - return snapshot - - # Получить последнюю цену инструмента из cache или REST API. - def get_price( - self, - symbol: str | None = None, - *, - runtime_key: str | None = None, - ) -> TickerPrice: + ) -> Quote: symbol_to_use = symbol or self.settings.default_symbol normalized_runtime_key = self._runtime_key(runtime_key) if not self.settings.exchange_enabled: - return mock_ticker_price(symbol_to_use) + return mock_quote(symbol_to_use) validation = self.validate_symbol(symbol_to_use) if not validation.is_valid: raise ExchangeError(validation.message) - cached_price = MarketPriceCache.get_price( + cached_quote = MarketPriceCache.get_quote( validation.normalized_symbol, runtime_key=normalized_runtime_key, ) - if cached_price is not None: - return TickerPrice( - symbol=cached_price.symbol, - price=cached_price.price, - source=cached_price.source, - updated_at=cached_price.updated_at, - ) + if ( + cached_quote is not None + and self._quote_age_seconds(cached_quote) + <= self._execution_cache_max_age_seconds + ): + return cached_quote - return self._get_real_price(validation.normalized_symbol) - - # Получить market snapshot: last/bid/ask/source/age/freshness. - def get_market_snapshot( - self, - symbol: str | None = None, - *, - runtime_key: str | None = None, - ) -> dict[str, object]: - symbol_to_use = symbol or self.settings.default_symbol - normalized_runtime_key = self._runtime_key(runtime_key) - - if not self.settings.exchange_enabled: - ticker = mock_ticker_price(symbol_to_use) - return { - "symbol": ticker.symbol, - "last_price": ticker.price, - "bid_price": ticker.price, - "ask_price": ticker.price, - "updated_at": ticker.updated_at, - "source": ticker.source, - "runtime_key": normalized_runtime_key, - "age_seconds": 0.0, - "is_fresh": True, - } - - validation = self.validate_symbol(symbol_to_use) - if not validation.is_valid: - raise ExchangeError(validation.message) - - cached_price = MarketPriceCache.get_price( - validation.normalized_symbol, + quote = self._get_fresh_quote(validation.normalized_symbol) + MarketPriceCache.set_quote( + quote, runtime_key=normalized_runtime_key, ) - - if cached_price is not None: - age = cached_price.age_seconds() - - if age <= self._execution_cache_max_age_seconds: - return { - "symbol": cached_price.symbol, - "last_price": cached_price.price, - "bid_price": cached_price.bid_price or cached_price.price, - "ask_price": cached_price.ask_price or cached_price.price, - "updated_at": cached_price.updated_at, - "source": cached_price.source, - "runtime_key": cached_price.runtime_key, - "age_seconds": round(age, 3), - "is_fresh": True, - } - - snapshot = self.refresh_market_snapshot_cache( - validation.normalized_symbol, - runtime_key=normalized_runtime_key, - ) - snapshot["runtime_key"] = normalized_runtime_key - snapshot["age_seconds"] = 0.0 - snapshot["is_fresh"] = True - - return snapshot + return quote # Получить snapshot, пригодный для execution layer. def get_execution_snapshot( @@ -877,15 +817,10 @@ class ExchangeService: normalized_runtime_key = self._runtime_key(runtime_key) if not self.settings.exchange_enabled: - ticker = mock_ticker_price(symbol_to_use) - return ExecutionPriceSnapshot( - symbol=ticker.symbol, - last_price=ticker.price, - bid_price=ticker.price, - ask_price=ticker.price, - updated_at=ticker.updated_at, - source=ticker.source, - is_fresh=True, + quote = mock_quote(symbol_to_use) + return self._execution_snapshot_from_quote( + quote, + source=quote.source, age_seconds=0.0, ) @@ -893,125 +828,96 @@ class ExchangeService: if not validation.is_valid: raise ExchangeError(validation.message) - cached_price = MarketPriceCache.get_price( + quote = MarketPriceCache.get_quote( validation.normalized_symbol, runtime_key=normalized_runtime_key, ) - if cached_price is not None: - age = cached_price.age_seconds() + if quote is not None: + age_seconds = self._quote_age_seconds(quote) - if ( - age <= self._execution_cache_max_age_seconds - and cached_price.has_bid_ask() - ): - bid_price = safe_float(cached_price.bid_price) - ask_price = safe_float(cached_price.ask_price) - last_price = safe_float(cached_price.price) + if age_seconds <= self._execution_cache_max_age_seconds: + return self._execution_snapshot_from_quote( + quote, + source=f"{quote.source}:fresh_cache", + age_seconds=round(age_seconds, 3), + ) - if ( - last_price is not None - and bid_price is not None - and ask_price is not None - ): - return ExecutionPriceSnapshot( - symbol=cached_price.symbol, - last_price=last_price, - bid_price=bid_price, - ask_price=ask_price, - updated_at=cached_price.updated_at, - source=f"{cached_price.source}:fresh_cache", - is_fresh=True, - age_seconds=round(age, 3), - ) + quote = self._get_fresh_quote( + validation.normalized_symbol + ) + MarketPriceCache.set_quote( + quote, + runtime_key=normalized_runtime_key, + ) - snapshot = self.get_fresh_market_snapshot(validation.normalized_symbol) + return self._execution_snapshot_from_quote( + quote, + source="rest_fallback", + age_seconds=round( + self._quote_age_seconds(quote), + 3, + ), + ) - last_price = safe_float(snapshot.get("last_price")) - bid_price = safe_float(snapshot.get("bid_price")) - ask_price = safe_float(snapshot.get("ask_price")) + def _execution_snapshot_from_quote( + self, + quote: Quote, + *, + source: str, + age_seconds: float, + ) -> ExecutionPriceSnapshot: + timestamp = ( + quote.exchange_timestamp + if quote.exchange_timestamp is not None + else quote.received_at + ) - if last_price is None or bid_price is None or ask_price is None: - raise ExchangeError("Market snapshot contains invalid execution prices.") + if timestamp.tzinfo is None: + timestamp = timestamp.replace(tzinfo=timezone.utc) - age_seconds = safe_float(snapshot.get("age_seconds")) + updated_at = timestamp.astimezone( + ZoneInfo(self.settings.tz) + ).strftime("%d.%m.%Y %H:%M:%S") return ExecutionPriceSnapshot( - symbol=str(snapshot["symbol"]), - last_price=last_price, - bid_price=bid_price, - ask_price=ask_price, - updated_at=str(snapshot["updated_at"]), - source="rest_fallback", - is_fresh=bool(snapshot.get("is_fresh")), + symbol=quote.symbol, + last_price=float(quote.last_price), + bid_price=float(quote.bid_price), + ask_price=float(quote.ask_price), + updated_at=updated_at, + source=source, + is_fresh=( + age_seconds + <= self._execution_cache_max_age_seconds + ), age_seconds=age_seconds, ) - # Получить свежий snapshot напрямую из REST API. - def get_fresh_market_snapshot(self, symbol: str | None = None) -> dict[str, object]: - symbol_to_use = symbol or self.settings.default_symbol + def _quote_age_seconds(self, quote: Quote) -> float: + received_at = quote.received_at + if received_at.tzinfo is None: + received_at = received_at.replace(tzinfo=timezone.utc) - if not self.settings.exchange_enabled: - ticker = mock_ticker_price(symbol_to_use) - return { - "symbol": ticker.symbol, - "last_price": ticker.price, - "bid_price": ticker.price, - "ask_price": ticker.price, - "updated_at": ticker.updated_at, - "source": "mock", - "age_seconds": 0.0, - "is_fresh": True, - } - - validation = self.validate_symbol(symbol_to_use) - if not validation.is_valid: - raise ExchangeError(validation.message) - - client = ExchangeRestClient() + return max( + 0.0, + ( + datetime.now(timezone.utc) + - received_at.astimezone(timezone.utc) + ).total_seconds(), + ) + def _get_fresh_quote(self, normalized_symbol: str) -> Quote: try: - payload = client.get_json( - "/api/v1/ticker/24hr", - params={"symbol": validation.normalized_symbol}, - ) + return self._load_quote_via_acquisition(normalized_symbol) except Exception as exc: self._log_exchange_error( endpoint="ticker/24hr", exc=exc, - symbol=validation.normalized_symbol, + symbol=normalized_symbol, ) raise ExchangeError(str(exc)) from exc - last_price = safe_float(payload.get("lastPrice")) - - if last_price is None: - exc = ExchangeError("Field 'lastPrice' is missing in ticker response.") - self._log_exchange_error( - endpoint="ticker/24hr", - exc=exc, - symbol=validation.normalized_symbol, - ) - raise exc - - bid_price = safe_float(payload.get("bidPrice")) or last_price - ask_price = safe_float(payload.get("askPrice")) or last_price - close_time = payload.get("closeTime") or payload.get("eventTime") - - age_seconds = self._exchange_timestamp_age_seconds(close_time) - is_fresh = age_seconds is not None and age_seconds <= 60 - - return { - "symbol": validation.normalized_symbol, - "last_price": last_price, - "bid_price": bid_price, - "ask_price": ask_price, - "updated_at": self._format_exchange_time(close_time), - "source": "fresh_rest", - "age_seconds": age_seconds, - "is_fresh": is_fresh, - } - # Получить live-балансы аккаунта. def get_balance_summary(self) -> list[BalanceSummary]: if not self.settings.exchange_enabled: @@ -1056,20 +962,22 @@ class ExchangeService: return balances - # Получить и распарсить список инструментов биржи. - def get_exchange_symbols(self) -> list[ExchangeSymbol]: + # Получить канонический справочник инструментов через Instrument Store. + def get_instruments(self) -> tuple[Instrument, ...]: if not self.settings.exchange_enabled: - return [] + return () - cached_symbols = type(self)._exchange_symbols_cache + instrument_store = type(self)._instrument_store - if cached_symbols is not None: - return cached_symbols + instruments = instrument_store.get( + _INSTRUMENT_REFERENCE_SOURCE_NAME + ) - client = ExchangeRestClient() + if instruments is not None: + return instruments try: - payload = client.get_json("/api/v1/exchangeInfo") + instruments = self._load_instruments_via_acquisition() except Exception as exc: self._log_exchange_error( endpoint="exchangeInfo", @@ -1077,98 +985,65 @@ class ExchangeService: ) raise ExchangeError(str(exc)) from exc - symbols_raw = self._extract_exchange_symbols_raw(payload) - items: list[ExchangeSymbol] = [] - - for item in symbols_raw: - if not isinstance(item, dict): - continue - - symbol = self._parse_exchange_symbol(item) - - if symbol.symbol: - items.append(symbol) - - type(self)._exchange_symbols_cache = items - - return items - - # Извлечь сырой список symbols из exchangeInfo. - def _extract_exchange_symbols_raw( - self, - payload: dict[str, object], - ) -> list[object]: - symbols = payload.get("symbols") - - if isinstance(symbols, list): - return symbols - - inner = payload.get("payload") - - if isinstance(inner, dict): - nested_symbols = inner.get("symbols") - - if isinstance(nested_symbols, list): - return nested_symbols - - exc = ExchangeError("Field 'symbols' is missing in exchangeInfo response.") - self._log_exchange_error( - endpoint="exchangeInfo", - exc=exc, + instrument_store.set( + _INSTRUMENT_REFERENCE_SOURCE_NAME, + instruments, ) - raise exc - # Преобразовать один сырой symbol item в ExchangeSymbol. - def _parse_exchange_symbol( + return instruments + + # Собрать Quotes acquisition pipeline и вернуть каноническую модель Quote. + def _load_quote_via_acquisition( self, - item: dict[object, object], - ) -> ExchangeSymbol: - filters = item.get("filters") + symbol: str, + ) -> Quote: + source = DzengiQuoteDocumentSource() + handler = DzengiQuoteDocumentHandler() - tick_size = safe_float(item.get("tickSize")) - if tick_size is None: - tick_size = self._extract_filter_value( - filters, - filter_names=["PRICE_FILTER"], - keys=["tickSize"], - ) + feed = QuotesFeed( + source=source, + handler=handler, + ) - step_size = safe_float(item.get("stepSize")) - if step_size is None: - step_size = self._extract_filter_value( - filters, - filter_names=["LOT_SIZE", "MARKET_LOT_SIZE"], - keys=["stepSize"], - ) + registry = QuoteFeedRegistry() + registry.register( + _QUOTE_SOURCE_NAME, + feed, + ) - min_qty = safe_float(item.get("minQty")) - if min_qty is None: - min_qty = self._extract_filter_value( - filters, - filter_names=["LOT_SIZE", "MARKET_LOT_SIZE"], - keys=["minQty"], - ) + acquisition_service = QuoteAcquisitionService( + registry=registry, + ) - min_notional = safe_float(item.get("minNotional")) - if min_notional is None: - min_notional = self._extract_filter_value( - filters, - filter_names=["MIN_NOTIONAL", "NOTIONAL"], - keys=["minNotional", "notional"], - ) + return acquisition_service.load_quote( + _QUOTE_SOURCE_NAME, + symbol, + ) - return ExchangeSymbol( - symbol=self._safe_str(item.get("symbol")), - name=self._safe_str(item.get("name")), - status=self._parse_exchange_symbol_status(item), - base_asset=self._safe_str(item.get("baseAsset")), - quote_asset=self._safe_str(item.get("quoteAsset")), - market_modes=self._parse_market_modes(item.get("marketModes")), - market_type=self._safe_str(item.get("marketType"), "unknown"), - tick_size=tick_size, - step_size=step_size, - min_qty=min_qty, - min_notional=min_notional, + # Собрать acquisition pipeline и вернуть канонические модели Instrument. + def _load_instruments_via_acquisition( + self, + ) -> tuple[Instrument, ...]: + source = DzengiInstrumentDocumentSource() + handler = DzengiInstrumentDocumentHandler() + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + registry = InstrumentFeedRegistry() + registry.register( + _INSTRUMENT_REFERENCE_SOURCE_NAME, + feed, + ) + + acquisition_service = InstrumentAcquisitionService( + registry=registry, + ) + + return acquisition_service.load_instruments( + _INSTRUMENT_REFERENCE_SOURCE_NAME ) # Безопасно привести значение к строке. @@ -1178,91 +1053,6 @@ class ExchangeService: return str(value).strip() - def _parse_exchange_symbol_status(self, item: dict[object, object]) -> str: - status = self._safe_str(item.get("status"), "unknown") - - false_flags = { - "isTradingAllowed", - "tradingAllowed", - "availableForTrading", - "isTradable", - "tradable", - "isMarketOpen", - "marketOpen", - "isOpen", - "enabled", - } - - for key in false_flags: - if key not in item: - continue - - value = item.get(key) - - if isinstance(value, bool) and not value: - return "NOT_TRADABLE" - - if str(value).strip().lower() in {"false", "0", "no", "disabled"}: - return "NOT_TRADABLE" - - for key in ("tradingMode", "tradeMode", "mode", "state"): - value = str(item.get(key) or "").strip().upper() - - if value in { - "NOT_TRADABLE", - "TRADING_DISABLED", - "MARKET_DISABLED", - "UNAVAILABLE_FOR_TRADING", - "CLOSE_ONLY", - "REDUCE_ONLY", - "VIEW_ONLY", - }: - return value - - return status - - # Привести marketModes к list[str]. - def _parse_market_modes(self, value: object) -> list[str]: - if isinstance(value, list): - return [ - str(item).strip() - for item in value - if str(item).strip() - ] - - if isinstance(value, str) and value.strip(): - return [value.strip()] - - return [] - - # Извлечь числовое значение из filters exchangeInfo. - def _extract_filter_value( - self, - filters: object, - *, - filter_names: list[str], - keys: list[str], - ) -> float | None: - if not isinstance(filters, list): - return None - - normalized_filter_names = {name.upper() for name in filter_names} - - for entry in filters: - if not isinstance(entry, dict): - continue - - filter_type = str(entry.get("filterType", "")).strip().upper() - if filter_type not in normalized_filter_names: - continue - - for key in keys: - value = safe_float(entry.get(key)) - if value is not None: - return value - - return None - # Проверить, существует ли инструмент на бирже. def validate_symbol(self, raw_symbol: str) -> SymbolValidationResult: requested = normalize_symbol(raw_symbol) @@ -1285,43 +1075,40 @@ class ExchangeService: symbol_info=None, ) - symbols = self.get_exchange_symbols() - candidates = symbol_candidates(requested) + instruments = self.get_instruments() - for candidate in candidates: - for symbol_info in symbols: - if normalize_symbol(symbol_info.symbol) == candidate: - return SymbolValidationResult( - requested_symbol=requested, - normalized_symbol=normalize_symbol(symbol_info.symbol), - is_valid=True, - message="Символ найден в exchangeInfo.", - symbol_info=symbol_info, - ) + matched_index = resolve_symbol_index( + requested, + [ + instrument.symbol + for instrument in instruments + ], + ) + + if matched_index is not None: + instrument = instruments[matched_index] + + return SymbolValidationResult( + requested_symbol=requested, + normalized_symbol=normalize_symbol( + instrument.symbol + ), + is_valid=True, + message="Символ найден в exchangeInfo.", + symbol_info=instrument, + ) return SymbolValidationResult( requested_symbol=requested, normalized_symbol=requested, is_valid=False, - message=f"Символ '{requested}' не найден в exchangeInfo.", + message=( + f"Символ '{requested}' " + "не найден в exchangeInfo." + ), symbol_info=None, ) - # Получить реальную цену инструмента через свежий REST snapshot. - def _get_real_price(self, symbol: str) -> TickerPrice: - snapshot = self.get_fresh_market_snapshot(symbol) - price = safe_float(snapshot.get("last_price")) - - if price is None: - raise ExchangeError("Field 'last_price' is missing in market snapshot.") - - return TickerPrice( - symbol=str(snapshot["symbol"]), - price=price, - source=self._source_name(), - updated_at=str(snapshot["updated_at"]), - ) - def get_exchange_server_time_ms(self) -> int: payload = ExchangeRestClient().get_json("/api/v1/time") diff --git a/app/src/integrations/exchange/status.py b/app/src/integrations/exchange/status.py index 741ba0c..3f5e5fb 100644 --- a/app/src/integrations/exchange/status.py +++ b/app/src/integrations/exchange/status.py @@ -9,6 +9,10 @@ from src.integrations.exchange.exceptions import ( ExchangeConnectionError, ExchangeResponseError, ) +from src.market_data.acquisition.models.status import ( + InstrumentTradingState, + classify_instrument_status, +) class ExchangeStatusCode(StrEnum): @@ -35,7 +39,7 @@ class ExchangeRuntimeStatus: raw_status: str | None = None raw_error: str | None = None - # вернуть статус в dict для старого UI-кода на время миграции + # Вернуть статус в dict для старого UI-кода на время миграции. def as_dict(self) -> dict[str, object]: return { "code": self.code.value, @@ -79,7 +83,7 @@ def build_market_stale_status( ) -# собрать статус mock-режима +# Собрать статус mock-режима. def build_mock_exchange_status(*, symbol: str) -> ExchangeRuntimeStatus: return ExchangeRuntimeStatus( code=ExchangeStatusCode.OPEN, @@ -95,48 +99,21 @@ def build_mock_exchange_status(*, symbol: str) -> ExchangeRuntimeStatus: ) -# собрать статус ошибки авторизации аккаунта +# Собрать статус ошибки авторизации аккаунта. def build_account_auth_status(exc: Exception) -> ExchangeRuntimeStatus: return build_exchange_error_status(exc) -OPEN_STATUSES = { - "TRADING", - "OPEN", - "ACTIVE", - "ENABLED", - "ONLINE", -} - -BREAK_STATUSES = { - "BREAK", - "CLOSED", - "HALT", - "HALTED", - "PAUSED", - "SUSPENDED", - "DISABLED", - "SETTLING", - "POST_ONLY", - "NOT_TRADABLE", - "TRADING_DISABLED", - "MARKET_DISABLED", - "UNAVAILABLE_FOR_TRADING", - "CLOSE_ONLY", - "REDUCE_ONLY", - "VIEW_ONLY", -} - - -# определить единый runtime-статус по статусу инструмента биржи +# Собрать legacy runtime-статус по канонической классификации инструмента. def build_market_status_from_symbol_status( *, raw_status: str | None, symbol: str, ) -> ExchangeRuntimeStatus: - normalized_status = str(raw_status or "").strip().upper() + classification = classify_instrument_status(raw_status) + normalized_status = classification.normalized_status - if normalized_status in OPEN_STATUSES: + if classification.state == InstrumentTradingState.OPEN: return ExchangeRuntimeStatus( code=ExchangeStatusCode.OPEN, is_open=True, @@ -150,15 +127,7 @@ def build_market_status_from_symbol_status( symbol=symbol, ) - if normalized_status in { - "NOT_TRADABLE", - "TRADING_DISABLED", - "MARKET_DISABLED", - "UNAVAILABLE_FOR_TRADING", - "CLOSE_ONLY", - "REDUCE_ONLY", - "VIEW_ONLY", - }: + if classification.state == InstrumentTradingState.NOT_TRADABLE: return ExchangeRuntimeStatus( code=ExchangeStatusCode.BREAK, is_open=False, @@ -171,8 +140,8 @@ def build_market_status_from_symbol_status( raw_status=normalized_status, symbol=symbol, ) - - if normalized_status in BREAK_STATUSES: + + if classification.state == InstrumentTradingState.BREAK: return ExchangeRuntimeStatus( code=ExchangeStatusCode.BREAK, is_open=False, @@ -198,12 +167,12 @@ def build_market_status_from_symbol_status( ), ui_line="⚠️ Статус торгов неизвестен", reason="market_status_unknown", - raw_status=normalized_status or None, + raw_status=normalized_status, symbol=symbol, ) -# собрать единый статус для неверного торгового инструмента +# Собрать единый статус для неверного торгового инструмента. def build_invalid_symbol_status( *, symbol: str, @@ -223,7 +192,7 @@ def build_invalid_symbol_status( ) -# собрать единый статус по ошибке exchange/API +# Собрать единый статус по ошибке exchange/API. def build_exchange_error_status(exc: Exception) -> ExchangeRuntimeStatus: error_type = classify_exchange_error(exc) raw_error = str(exc) @@ -270,7 +239,7 @@ def build_exchange_error_status(exc: Exception) -> ExchangeRuntimeStatus: ) -# классифицировать ошибку биржи для единого UI и логов +# Классифицировать ошибку биржи для единого UI и логов. def classify_exchange_error(exc: Exception) -> str: text = str(exc).lower() @@ -326,7 +295,7 @@ def classify_exchange_error(exc: Exception) -> str: return "generic" -# проверить, относится ли reason к unified exchange status layer +# Проверить, относится ли reason к unified exchange status layer. def is_exchange_status_reason(reason: str | None) -> bool: if not reason: return False diff --git a/app/src/integrations/exchange/symbol_utils.py b/app/src/integrations/exchange/symbol_utils.py index cbb1d6f..044561c 100644 --- a/app/src/integrations/exchange/symbol_utils.py +++ b/app/src/integrations/exchange/symbol_utils.py @@ -2,24 +2,13 @@ from __future__ import annotations - -def normalize_symbol(raw_symbol: str) -> str: - return (raw_symbol or "").strip().upper() +from src.market_data.acquisition.symbols import ( + normalize_symbol, + symbol_candidates, +) -def symbol_candidates(raw_symbol: str) -> list[str]: - value = normalize_symbol(raw_symbol) - if not value: - return [] - - candidates = [value] - - compact = value.replace("%2F", "/") - if compact not in candidates: - candidates.append(compact) - - no_spaces = compact.replace(" ", "") - if no_spaces not in candidates: - candidates.append(no_spaces) - - return candidates +__all__ = [ + "normalize_symbol", + "symbol_candidates", +] \ No newline at end of file diff --git a/app/src/market_data/__init__.py b/app/src/market_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/__init__.py b/app/src/market_data/acquisition/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/adapters/__init__.py b/app/src/market_data/acquisition/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/adapters/dzengi/__init__.py b/app/src/market_data/acquisition/adapters/dzengi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/adapters/dzengi/auth.py b/app/src/market_data/acquisition/adapters/dzengi/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/adapters/dzengi/mapper.py b/app/src/market_data/acquisition/adapters/dzengi/mapper.py new file mode 100644 index 0000000..90f7345 --- /dev/null +++ b/app/src/market_data/acquisition/adapters/dzengi/mapper.py @@ -0,0 +1,316 @@ +# app/src/market_data/acquisition/adapters/dzengi/mapper.py + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal, InvalidOperation + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiExchangeInfoResponse, + DzengiExchangeInfoSymbol, + DzengiInstrumentFilter, + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiRawNumeric, + DzengiTicker24hrResponse, + DzengiWebSocketQuoteResponse, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceMappingError, + QuoteMappingError, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.models.quote import Quote + + +_DZENGI_SOURCE_NAME = "dzengi" + + +def map_dzengi_symbol_to_instrument( + symbol: DzengiExchangeInfoSymbol, +) -> Instrument: + """ + Преобразовать проверенную raw-модель инструмента Dzengi + во внутреннюю source-independent модель Instrument. + + Функция предполагает, что до mapper уже были выполнены: + schema validation, parsing и value validation. + """ + + lot_size = _find_single_filter( + symbol.filters, + DzengiLotSizeFilter, + filter_name="LOT_SIZE", + symbol=symbol.symbol, + ) + min_notional = _find_single_filter( + symbol.filters, + DzengiMinNotionalFilter, + filter_name="MIN_NOTIONAL", + symbol=symbol.symbol, + ) + + return Instrument( + symbol=symbol.symbol, + name=symbol.name, + status=symbol.status, + base_asset=symbol.base_asset, + quote_asset=symbol.quote_asset, + asset_type=_optional_text(symbol.asset_type), + market_type=symbol.market_type, + market_modes=symbol.market_modes, + order_types=symbol.order_types, + base_asset_precision=symbol.base_asset_precision, + quote_asset_precision=symbol.quote_precision, + tick_size=_optional_decimal( + symbol.tick_size, + field_name="tickSize", + symbol=symbol.symbol, + ), + tick_value=_optional_decimal( + symbol.tick_value, + field_name="tickValue", + symbol=symbol.symbol, + ), + step_size=_optional_decimal( + lot_size.step_size if lot_size is not None else None, + field_name="stepSize", + symbol=symbol.symbol, + ), + min_qty=_optional_decimal( + lot_size.min_qty if lot_size is not None else None, + field_name="minQty", + symbol=symbol.symbol, + ), + max_qty=_optional_decimal( + lot_size.max_qty if lot_size is not None else None, + field_name="maxQty", + symbol=symbol.symbol, + ), + min_notional=_optional_decimal( + min_notional.min_notional + if min_notional is not None + else None, + field_name="minNotional", + symbol=symbol.symbol, + ), + country=_optional_text(symbol.country), + sector=_optional_text(symbol.sector), + industry=_optional_text(symbol.industry), + trading_hours=_optional_text(symbol.trading_hours), + ) + + +def map_dzengi_exchange_info_to_instruments( + response: DzengiExchangeInfoResponse, +) -> tuple[Instrument, ...]: + """ + Преобразовать все инструменты exchangeInfo + во внутренние модели Instrument. + """ + + return tuple( + map_dzengi_symbol_to_instrument(symbol) + for symbol in response.payload.symbols + ) + + +def map_dzengi_ticker_to_quote( + response: DzengiTicker24hrResponse, + *, + received_at: datetime, +) -> Quote: + """ + Преобразовать проверенную raw-модель Dzengi ticker/24hr + во внутреннюю source-independent модель Quote. + + Функция предполагает, что до mapper уже были выполнены: + schema validation, parsing и value validation. + """ + + normalized_received_at = _require_aware_datetime( + received_at, + field_name="received_at", + ) + + return Quote( + symbol=response.symbol.strip(), + last_price=_required_quote_decimal( + response.last_price, + field_name="lastPrice", + ), + bid_price=_required_quote_decimal( + response.bid_price, + field_name="bidPrice", + ), + ask_price=_required_quote_decimal( + response.ask_price, + field_name="askPrice", + ), + exchange_timestamp=_timestamp_ms_to_utc_datetime( + response.close_time, + ), + received_at=normalized_received_at, + source=_DZENGI_SOURCE_NAME, + ) + + +def _timestamp_ms_to_utc_datetime(value: int) -> datetime: + try: + return datetime.fromtimestamp( + value / 1000, + tz=timezone.utc, + ) + except (OverflowError, OSError, ValueError) as exc: + raise QuoteMappingError( + "Поле closeTime невозможно преобразовать " + "в UTC datetime." + ) from exc + + +def _required_quote_decimal( + value: DzengiRawNumeric, + *, + field_name: str, +) -> Decimal: + try: + result = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise QuoteMappingError( + f"Поле {field_name} котировки невозможно " + "преобразовать в Decimal." + ) from exc + + if not result.is_finite(): + raise QuoteMappingError( + f"Поле {field_name} котировки должно быть " + "конечным числом." + ) + + return result + + +def _require_aware_datetime( + value: datetime, + *, + field_name: str, +) -> datetime: + if value.tzinfo is None or value.utcoffset() is None: + raise QuoteMappingError( + f"Поле {field_name} должно содержать timezone-aware datetime." + ) + + return value + + +def _find_single_filter[ + FilterT: DzengiInstrumentFilter +]( + filters: tuple[DzengiInstrumentFilter, ...], + filter_type: type[FilterT], + *, + filter_name: str, + symbol: str, +) -> FilterT | None: + matches = tuple( + instrument_filter + for instrument_filter in filters + if isinstance(instrument_filter, filter_type) + ) + + if len(matches) > 1: + raise InstrumentReferenceMappingError( + f"Инструмент '{symbol}' содержит несколько " + f"фильтров {filter_name}." + ) + + if not matches: + return None + + return matches[0] + + +def _optional_decimal( + value: DzengiRawNumeric | None, + *, + field_name: str, + symbol: str, +) -> Decimal | None: + if value is None: + return None + + try: + result = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise InstrumentReferenceMappingError( + f"Поле {field_name} инструмента '{symbol}' " + f"невозможно преобразовать в Decimal." + ) from exc + + if not result.is_finite(): + raise InstrumentReferenceMappingError( + f"Поле {field_name} инструмента '{symbol}' " + f"должно быть конечным числом." + ) + + return result + + +def _optional_text(value: str | None) -> str | None: + if value is None: + return None + + normalized = value.strip() + + if not normalized: + return None + + return normalized + + +def map_dzengi_websocket_quote_to_quote( + response: DzengiWebSocketQuoteResponse, + *, + received_at: datetime, +) -> Quote: + """ + Преобразовать проверенную WebSocket-модель Dzengi в канонический Quote. + + Depth-сообщение не содержит цену последней сделки, поэтому временно + используется midpoint best bid / best ask — так же, как в legacy runtime. + """ + + normalized_received_at = _require_aware_datetime( + received_at, + field_name="received_at", + ) + bid_price = _required_quote_decimal( + response.bid_price, + field_name="bidPrice", + ) + ask_price = _required_quote_decimal( + response.ask_price, + field_name="askPrice", + ) + + exchange_timestamp = None + if response.timestamp is not None: + try: + exchange_timestamp = datetime.fromtimestamp( + response.timestamp / 1000, + tz=timezone.utc, + ) + except (OverflowError, OSError, ValueError) as exc: + raise QuoteMappingError( + "Поле timestamp невозможно преобразовать в UTC datetime." + ) from exc + + return Quote( + symbol=response.symbol.strip(), + last_price=(bid_price + ask_price) / Decimal("2"), + bid_price=bid_price, + ask_price=ask_price, + exchange_timestamp=exchange_timestamp, + received_at=normalized_received_at, + source=_DZENGI_SOURCE_NAME, + ) diff --git a/app/src/market_data/acquisition/adapters/dzengi/models.py b/app/src/market_data/acquisition/adapters/dzengi/models.py new file mode 100644 index 0000000..0ff6631 --- /dev/null +++ b/app/src/market_data/acquisition/adapters/dzengi/models.py @@ -0,0 +1,133 @@ +# app/src/market_data/acquisition/adapters/dzengi/models.py + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TypeAlias + + +# Число в исходном JSON-ответе Dzengi без предметного преобразования. +DzengiJsonNumber: TypeAlias = int | float + +# Числовое значение, которое Dzengi может передать числом или строкой. +DzengiRawNumeric: TypeAlias = str | int | float + +# Скалярное значение неизвестного поля транспортного ответа. +DzengiJsonScalar: TypeAlias = str | int | float | bool | None + + +# Лимит запросов из exchangeInfo. +@dataclass(frozen=True, slots=True) +class DzengiRateLimit: + interval: str + interval_num: int + limit: int + rate_limit_type: str + + +# Базовый контракт фильтра инструмента Dzengi. +@dataclass(frozen=True, slots=True) +class DzengiInstrumentFilter: + filter_type: str + + +# Ограничения размера заявки. +@dataclass(frozen=True, slots=True) +class DzengiLotSizeFilter(DzengiInstrumentFilter): + min_qty: DzengiRawNumeric | None + max_qty: DzengiRawNumeric | None + step_size: DzengiRawNumeric | None + + +# Ограничение минимальной стоимости заявки. +@dataclass(frozen=True, slots=True) +class DzengiMinNotionalFilter(DzengiInstrumentFilter): + min_notional: DzengiRawNumeric | None + + +# Неизвестный тип фильтра, который ещё не поддерживается адаптером. +@dataclass(frozen=True, slots=True) +class DzengiUnknownFilter(DzengiInstrumentFilter): + fields: tuple[tuple[str, DzengiJsonScalar], ...] + + +# Один инструмент из ответа Dzengi exchangeInfo. +@dataclass(frozen=True, slots=True) +class DzengiExchangeInfoSymbol: + symbol: str + name: str + status: str + + asset_type: str | None + + base_asset: str + base_asset_precision: int | None + + quote_asset: str + quote_asset_id: str | None + quote_precision: int | None + + order_types: tuple[str, ...] + filters: tuple[DzengiInstrumentFilter, ...] + + market_modes: tuple[str, ...] + market_type: str + + country: str | None + sector: str | None + industry: str | None + trading_hours: str | None + + tick_size: DzengiJsonNumber | None + tick_value: DzengiJsonNumber | None + + trading_fee: DzengiJsonNumber | None + exchange_fee: DzengiJsonNumber | None + + long_rate: DzengiJsonNumber | None + short_rate: DzengiJsonNumber | None + swap_charge_interval: int | None + + min_sl_gap: DzengiJsonNumber | None + max_sl_gap: DzengiJsonNumber | None + min_tp_gap: DzengiJsonNumber | None + max_tp_gap: DzengiJsonNumber | None + + +# Содержимое exchangeInfo независимо от внешней оболочки API. +@dataclass(frozen=True, slots=True) +class DzengiExchangeInfoPayload: + timezone: str | None + server_time: int | None + rate_limits: tuple[DzengiRateLimit, ...] + exchange_filters: tuple[DzengiUnknownFilter, ...] + symbols: tuple[DzengiExchangeInfoSymbol, ...] + + +# Нормализованное транспортное представление ответа exchangeInfo. +@dataclass(frozen=True, slots=True) +class DzengiExchangeInfoResponse: + payload: DzengiExchangeInfoPayload + + # Поля присутствуют в wrapped-формате ответа и отсутствуют + # в фактическом unwrapped-ответе публичного REST endpoint. + status: str | None = None + correlation_id: str | None = None + +# Транспортное представление ответа Dzengi GET /api/v1/ticker/24hr. +@dataclass(frozen=True, slots=True) +class DzengiTicker24hrResponse: + symbol: str + last_price: DzengiRawNumeric + bid_price: DzengiRawNumeric + ask_price: DzengiRawNumeric + close_time: int + + +# Нормализованное транспортное представление котировки из Dzengi WebSocket. +@dataclass(frozen=True, slots=True) +class DzengiWebSocketQuoteResponse: + symbol: str + bid_price: DzengiRawNumeric + ask_price: DzengiRawNumeric + timestamp: int | None diff --git a/app/src/market_data/acquisition/adapters/dzengi/parser.py b/app/src/market_data/acquisition/adapters/dzengi/parser.py new file mode 100644 index 0000000..d811d90 --- /dev/null +++ b/app/src/market_data/acquisition/adapters/dzengi/parser.py @@ -0,0 +1,716 @@ +# app/src/market_data/acquisition/adapters/dzengi/parser.py + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiExchangeInfoPayload, + DzengiExchangeInfoResponse, + DzengiExchangeInfoSymbol, + DzengiInstrumentFilter, + DzengiJsonNumber, + DzengiJsonScalar, + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiRateLimit, + DzengiRawNumeric, + DzengiUnknownFilter, + DzengiTicker24hrResponse, + DzengiWebSocketQuoteResponse, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceParseError, + QuoteParseError, +) +from src.market_data.acquisition.validation.schema import ( + ValidatedExchangeInfoDocument, + ValidatedQuoteDocument, + ValidatedWebSocketQuoteDocument, +) + + +def parse_exchange_info( + document: ValidatedExchangeInfoDocument, +) -> DzengiExchangeInfoResponse: + """ + Преобразовать структурно проверенный exchangeInfo в raw-модели Dzengi. + + Функция не выполняет schema validation, предметную валидацию, + нормализацию символов или преобразование в Instrument. + """ + + payload = document.payload + + return DzengiExchangeInfoResponse( + status=_optional_string( + document.status, + path="$.status", + ), + correlation_id=_optional_string( + document.correlation_id, + path="$.correlationId", + ), + payload=DzengiExchangeInfoPayload( + timezone=_optional_string( + payload.get("timezone"), + path="$.payload.timezone", + ), + server_time=_optional_int( + payload.get("serverTime"), + path="$.payload.serverTime", + ), + rate_limits=_parse_rate_limits( + payload.get("rateLimits"), + path="$.payload.rateLimits", + ), + exchange_filters=_parse_exchange_filters( + payload.get("exchangeFilters"), + path="$.payload.exchangeFilters", + ), + symbols=_parse_symbols( + payload["symbols"], + path="$.payload.symbols", + ), + ), + ) + + +def _parse_symbols( + value: object, + *, + path: str, +) -> tuple[DzengiExchangeInfoSymbol, ...]: + items = _require_sequence(value, path=path) + + symbols: list[DzengiExchangeInfoSymbol] = [] + + for index, item in enumerate(items): + item_path = f"{path}[{index}]" + mapping = _require_mapping(item, path=item_path) + symbols.append(_parse_symbol(mapping, path=item_path)) + + return tuple(symbols) + + +def _parse_symbol( + item: Mapping[str, object], + *, + path: str, +) -> DzengiExchangeInfoSymbol: + return DzengiExchangeInfoSymbol( + symbol=_required_string( + item.get("symbol"), + path=f"{path}.symbol", + ), + name=_required_string( + item.get("name"), + path=f"{path}.name", + ), + status=_required_string( + item.get("status"), + path=f"{path}.status", + ), + asset_type=_optional_string( + item.get("assetType"), + path=f"{path}.assetType", + ), + base_asset=_required_string( + item.get("baseAsset"), + path=f"{path}.baseAsset", + ), + base_asset_precision=_optional_int( + item.get("baseAssetPrecision"), + path=f"{path}.baseAssetPrecision", + ), + quote_asset=_required_string( + item.get("quoteAsset"), + path=f"{path}.quoteAsset", + ), + quote_asset_id=_optional_string( + item.get("quoteAssetId"), + path=f"{path}.quoteAssetId", + ), + quote_precision=_optional_int( + item.get("quotePrecision"), + path=f"{path}.quotePrecision", + ), + order_types=_optional_string_tuple( + item.get("orderTypes"), + path=f"{path}.orderTypes", + ), + filters=_parse_instrument_filters( + item.get("filters"), + path=f"{path}.filters", + ), + market_modes=_optional_string_tuple( + item.get("marketModes"), + path=f"{path}.marketModes", + ), + market_type=_required_string( + item.get("marketType"), + path=f"{path}.marketType", + ), + country=_optional_string( + item.get("country"), + path=f"{path}.country", + ), + sector=_optional_string( + item.get("sector"), + path=f"{path}.sector", + ), + industry=_optional_string( + item.get("industry"), + path=f"{path}.industry", + ), + trading_hours=_optional_string( + item.get("tradingHours"), + path=f"{path}.tradingHours", + ), + tick_size=_optional_json_number( + item.get("tickSize"), + path=f"{path}.tickSize", + ), + tick_value=_optional_json_number( + item.get("tickValue"), + path=f"{path}.tickValue", + ), + trading_fee=_optional_json_number( + item.get("tradingFee"), + path=f"{path}.tradingFee", + ), + exchange_fee=_optional_json_number( + item.get("exchangeFee"), + path=f"{path}.exchangeFee", + ), + long_rate=_optional_json_number( + item.get("longRate"), + path=f"{path}.longRate", + ), + short_rate=_optional_json_number( + item.get("shortRate"), + path=f"{path}.shortRate", + ), + swap_charge_interval=_optional_int( + item.get("swapChargeInterval"), + path=f"{path}.swapChargeInterval", + ), + min_sl_gap=_optional_json_number( + item.get("minSLGap"), + path=f"{path}.minSLGap", + ), + max_sl_gap=_optional_json_number( + item.get("maxSLGap"), + path=f"{path}.maxSLGap", + ), + min_tp_gap=_optional_json_number( + item.get("minTPGap"), + path=f"{path}.minTPGap", + ), + max_tp_gap=_optional_json_number( + item.get("maxTPGap"), + path=f"{path}.maxTPGap", + ), + ) + + +def _parse_rate_limits( + value: object, + *, + path: str, +) -> tuple[DzengiRateLimit, ...]: + if value is None: + return () + + items = _require_sequence(value, path=path) + rate_limits: list[DzengiRateLimit] = [] + + for index, item in enumerate(items): + item_path = f"{path}[{index}]" + mapping = _require_mapping(item, path=item_path) + + rate_limits.append( + DzengiRateLimit( + interval=_required_string( + mapping.get("interval"), + path=f"{item_path}.interval", + ), + interval_num=_required_int( + mapping.get("intervalNum"), + path=f"{item_path}.intervalNum", + ), + limit=_required_int( + mapping.get("limit"), + path=f"{item_path}.limit", + ), + rate_limit_type=_required_string( + mapping.get("rateLimitType"), + path=f"{item_path}.rateLimitType", + ), + ) + ) + + return tuple(rate_limits) + + +def _parse_exchange_filters( + value: object, + *, + path: str, +) -> tuple[DzengiUnknownFilter, ...]: + if value is None: + return () + + items = _require_sequence(value, path=path) + filters: list[DzengiUnknownFilter] = [] + + for index, item in enumerate(items): + item_path = f"{path}[{index}]" + mapping = _require_mapping(item, path=item_path) + + filters.append( + _parse_unknown_filter( + mapping, + path=item_path, + filter_type_required=False, + ) + ) + + return tuple(filters) + + +def _parse_instrument_filters( + value: object, + *, + path: str, +) -> tuple[DzengiInstrumentFilter, ...]: + if value is None: + return () + + items = _require_sequence(value, path=path) + filters: list[DzengiInstrumentFilter] = [] + + for index, item in enumerate(items): + item_path = f"{path}[{index}]" + mapping = _require_mapping(item, path=item_path) + + filter_type = _required_string( + mapping.get("filterType"), + path=f"{item_path}.filterType", + ) + + if filter_type == "LOT_SIZE": + filters.append( + DzengiLotSizeFilter( + filter_type=filter_type, + min_qty=_optional_raw_numeric( + mapping.get("minQty"), + path=f"{item_path}.minQty", + ), + max_qty=_optional_raw_numeric( + mapping.get("maxQty"), + path=f"{item_path}.maxQty", + ), + step_size=_optional_raw_numeric( + mapping.get("stepSize"), + path=f"{item_path}.stepSize", + ), + ) + ) + continue + + if filter_type == "MIN_NOTIONAL": + filters.append( + DzengiMinNotionalFilter( + filter_type=filter_type, + min_notional=_optional_raw_numeric( + mapping.get("minNotional"), + path=f"{item_path}.minNotional", + ), + ) + ) + continue + + filters.append( + _parse_unknown_filter( + mapping, + path=item_path, + filter_type_required=True, + ) + ) + + return tuple(filters) + + +def _parse_unknown_filter( + mapping: Mapping[str, object], + *, + path: str, + filter_type_required: bool, +) -> DzengiUnknownFilter: + if filter_type_required: + filter_type = _required_string( + mapping.get("filterType"), + path=f"{path}.filterType", + ) + else: + filter_type = _optional_string( + mapping.get("filterType"), + path=f"{path}.filterType", + ) or "" + + fields: list[tuple[str, DzengiJsonScalar]] = [] + + for key, value in mapping.items(): + if key == "filterType": + continue + + fields.append( + ( + key, + _require_json_scalar( + value, + path=f"{path}.{key}", + ), + ) + ) + + return DzengiUnknownFilter( + filter_type=filter_type, + fields=tuple(fields), + ) + + +def _optional_string_tuple( + value: object, + *, + path: str, +) -> tuple[str, ...]: + if value is None: + return () + + items = _require_sequence(value, path=path) + result: list[str] = [] + + for index, item in enumerate(items): + result.append( + _required_string( + item, + path=f"{path}[{index}]", + ) + ) + + return tuple(result) + + +def _required_string( + value: object, + *, + path: str, +) -> str: + if not isinstance(value, str): + raise InstrumentReferenceParseError( + f"{path} должен быть строкой, " + f"получен {type(value).__name__}." + ) + + return value + + +def _optional_string( + value: object, + *, + path: str, +) -> str | None: + if value is None: + return None + + return _required_string(value, path=path) + + +def _required_int( + value: object, + *, + path: str, +) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise InstrumentReferenceParseError( + f"{path} должен быть целым числом, " + f"получен {type(value).__name__}." + ) + + return value + + +def _optional_int( + value: object, + *, + path: str, +) -> int | None: + if value is None: + return None + + return _required_int(value, path=path) + + +def _optional_json_number( + value: object, + *, + path: str, +) -> DzengiJsonNumber | None: + if value is None: + return None + + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise InstrumentReferenceParseError( + f"{path} должен быть JSON-числом, " + f"получен {type(value).__name__}." + ) + + return value + + +def _optional_raw_numeric( + value: object, + *, + path: str, +) -> DzengiRawNumeric | None: + if value is None: + return None + + if isinstance(value, bool) or not isinstance(value, (str, int, float)): + raise InstrumentReferenceParseError( + f"{path} должен быть строкой или JSON-числом, " + f"получен {type(value).__name__}." + ) + + return value + + +def _require_json_scalar( + value: object, + *, + path: str, +) -> DzengiJsonScalar: + if value is None or isinstance(value, (str, bool)): + return value + + if isinstance(value, (int, float)): + return value + + raise InstrumentReferenceParseError( + f"{path} должен быть скалярным JSON-значением, " + f"получен {type(value).__name__}." + ) + + +def _require_mapping( + value: object, + *, + path: str, +) -> Mapping[str, object]: + if not isinstance(value, Mapping): + raise InstrumentReferenceParseError( + f"{path} должен быть отображением, " + f"получен {type(value).__name__}." + ) + + for key in value: + if not isinstance(key, str): + raise InstrumentReferenceParseError( + f"{path} содержит нестроковый ключ " + f"типа {type(key).__name__}." + ) + + return value + + +def _require_sequence( + value: object, + *, + path: str, +) -> Sequence[object]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise InstrumentReferenceParseError( + f"{path} должен быть последовательностью, " + f"получен {type(value).__name__}." + ) + + return value + +def parse_quote( + document: ValidatedQuoteDocument, +) -> DzengiTicker24hrResponse: + """ + Преобразовать структурно проверенный ticker/24hr в raw-модель Dzengi. + + Функция не выполняет schema validation, предметную валидацию + или mapping во внутреннюю модель Quote. + """ + + payload = document.payload + + return DzengiTicker24hrResponse( + symbol=_quote_required_string( + payload.get("symbol"), + path="$.payload.symbol", + ), + last_price=_quote_required_raw_numeric( + payload.get("lastPrice"), + path="$.payload.lastPrice", + ), + bid_price=_quote_required_raw_numeric( + payload.get("bidPrice"), + path="$.payload.bidPrice", + ), + ask_price=_quote_required_raw_numeric( + payload.get("askPrice"), + path="$.payload.askPrice", + ), + close_time=_quote_required_int( + payload.get("closeTime"), + path="$.payload.closeTime", + ), + ) + + +def _quote_required_string( + value: object, + *, + path: str, +) -> str: + if not isinstance(value, str): + raise QuoteParseError( + f"{path} должен быть строкой, " + f"получен {type(value).__name__}." + ) + + return value + + +def _quote_required_raw_numeric( + value: object, + *, + path: str, +) -> DzengiRawNumeric: + if isinstance(value, bool) or not isinstance(value, (str, int, float)): + raise QuoteParseError( + f"{path} должен быть строкой или JSON-числом, " + f"получен {type(value).__name__}." + ) + + return value + + +def _quote_required_int( + value: object, + *, + path: str, +) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise QuoteParseError( + f"{path} должен быть целым числом, " + f"получен {type(value).__name__}." + ) + + return value + + +def parse_dzengi_websocket_quote( + document: ValidatedWebSocketQuoteDocument, +) -> DzengiWebSocketQuoteResponse: + """Преобразовать проверенное WebSocket-сообщение в raw-модель Dzengi.""" + + payload = document.payload + symbol_value = ( + payload.get("symbolName") + or payload.get("symbol") + or document.root_symbol + ) + + symbol = _quote_required_string( + symbol_value, + path="$.payload.symbol", + ) + + if "bid" in payload: + bid_price = _quote_required_raw_numeric( + payload.get("bid"), + path="$.payload.bid", + ) + ask_key = "ofr" if "ofr" in payload else "ask" + ask_price = _quote_required_raw_numeric( + payload.get(ask_key), + path=f"$.payload.{ask_key}", + ) + else: + bid_price = _websocket_depth_price( + payload.get("bids"), + side="bids", + ) + ask_price = _websocket_depth_price( + payload.get("asks"), + side="asks", + ) + + timestamp = _websocket_optional_timestamp( + payload.get("timestamp"), + path="$.payload.timestamp", + ) + + return DzengiWebSocketQuoteResponse( + symbol=symbol, + bid_price=bid_price, + ask_price=ask_price, + timestamp=timestamp, + ) + + +def _websocket_depth_price( + value: object, + *, + side: str, +) -> DzengiRawNumeric: + if not isinstance(value, list) or not value: + raise QuoteParseError( + f"$.payload.{side} должен быть непустым списком." + ) + + first = value[0] + + if isinstance(first, list): + if not first: + raise QuoteParseError( + f"$.payload.{side}[0] не должен быть пустым." + ) + return _quote_required_raw_numeric( + first[0], + path=f"$.payload.{side}[0][0]", + ) + + if isinstance(first, Mapping): + for key in ("price", "p", "bidPrice", "askPrice"): + if key in first: + return _quote_required_raw_numeric( + first.get(key), + path=f"$.payload.{side}[0].{key}", + ) + + raise QuoteParseError( + f"$.payload.{side}[0] не содержит поле цены." + ) + + raise QuoteParseError( + f"$.payload.{side}[0] должен быть JSON-массивом или объектом." + ) + + +def _websocket_optional_timestamp( + value: object, + *, + path: str, +) -> int | None: + if value is None: + return None + + return _quote_required_int(value, path=path) diff --git a/app/src/market_data/acquisition/adapters/dzengi/rest.py b/app/src/market_data/acquisition/adapters/dzengi/rest.py new file mode 100644 index 0000000..36f9fac --- /dev/null +++ b/app/src/market_data/acquisition/adapters/dzengi/rest.py @@ -0,0 +1,106 @@ +# app/src/market_data/acquisition/adapters/dzengi/rest.py + +from __future__ import annotations + +from typing import Protocol + +from src.integrations.exchange.rest_client import ExchangeRestClient +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceTransportError, + QuoteTransportError, +) + + +_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo" +_TICKER_24HR_PATH = "/api/v1/ticker/24hr" + + +# Минимальный транспортный контракт, необходимый Dzengi REST adapter. +class _PayloadRestClient(Protocol): + def get_payload( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> object: + ... + + +class DzengiInstrumentDocumentSource: + """ + Источник сырого документа Instrument Reference Data через Dzengi REST API. + + На переходном этапе использует legacy ExchangeRestClient. + Зависимость должна быть удалена после появления общего transport-клиента + или после полного вывода integrations/exchange из эксплуатации. + """ + + def __init__( + self, + client: _PayloadRestClient | None = None, + ) -> None: + self._client = client + + def fetch_instrument_document(self) -> object: + """ + Получить декодированный ответ Dzengi exchangeInfo без его обработки. + + Метод не выполняет schema validation, parsing, value validation, + mapping или кэширование. + """ + + try: + client: _PayloadRestClient = ( + self._client + if self._client is not None + else ExchangeRestClient() + ) + + return client.get_payload(_EXCHANGE_INFO_PATH) + + except Exception as exc: + raise InstrumentReferenceTransportError( + "Не удалось получить Instrument Reference Data " + f"от Dzengi: {exc}" + ) from exc + + +class DzengiQuoteDocumentSource: + """Источник сырого документа текущей котировки через Dzengi REST API.""" + + def __init__( + self, + client: _PayloadRestClient | None = None, + ) -> None: + self._client = client + + def fetch_quote_document( + self, + symbol: str, + ) -> object: + """ + Получить декодированный ответ Dzengi ticker/24hr без его обработки. + + Метод не выполняет нормализацию symbol, schema validation, parsing, + value validation, mapping, retry или кэширование. + """ + + try: + client: _PayloadRestClient = ( + self._client + if self._client is not None + else ExchangeRestClient() + ) + + return client.get_payload( + _TICKER_24HR_PATH, + params={ + "symbol": symbol, + }, + ) + + except Exception as exc: + raise QuoteTransportError( + "Не удалось получить текущую котировку " + f"от Dzengi для символа '{symbol}': {exc}" + ) from exc diff --git a/app/src/market_data/acquisition/adapters/dzengi/websocket.py b/app/src/market_data/acquisition/adapters/dzengi/websocket.py new file mode 100644 index 0000000..0f728af --- /dev/null +++ b/app/src/market_data/acquisition/adapters/dzengi/websocket.py @@ -0,0 +1,37 @@ +# app/src/market_data/acquisition/adapters/dzengi/websocket.py + +from __future__ import annotations + +from datetime import datetime, timezone + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_websocket_quote_to_quote, +) +from src.market_data.acquisition.adapters.dzengi.parser import ( + parse_dzengi_websocket_quote, +) +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.validation.schema import ( + validate_dzengi_websocket_quote_schema, +) +from src.market_data.acquisition.validation.values import ( + validate_dzengi_websocket_quote_values, +) + + +# Преобразует одно декодированное сообщение Dzengi WebSocket в Quote. +class DzengiWebSocketQuoteAdapter: + def map_message( + self, + document: object, + *, + received_at: datetime | None = None, + ) -> Quote: + validated = validate_dzengi_websocket_quote_schema(document) + response = parse_dzengi_websocket_quote(validated) + validate_dzengi_websocket_quote_values(response) + + return map_dzengi_websocket_quote_to_quote( + response, + received_at=received_at or datetime.now(timezone.utc), + ) diff --git a/app/src/market_data/acquisition/exceptions.py b/app/src/market_data/acquisition/exceptions.py new file mode 100644 index 0000000..3ede317 --- /dev/null +++ b/app/src/market_data/acquisition/exceptions.py @@ -0,0 +1,67 @@ +# app/src/market_data/acquisition/exceptions.py + +from __future__ import annotations + + +# Базовая ошибка подсистемы получения рыночных данных. +class MarketDataAcquisitionError(Exception): + pass + + +# Ошибка получения Instrument Reference Data от внешнего источника. +class InstrumentReferenceTransportError(MarketDataAcquisitionError): + pass + + +# Ошибка структуры документа Instrument Reference Data. +class InstrumentReferenceSchemaError(MarketDataAcquisitionError): + pass + + +# Ошибка преобразования проверенного документа в raw-модели адаптера. +class InstrumentReferenceParseError(MarketDataAcquisitionError): + pass + + +# Ошибка допустимости значений Instrument Reference Data. +class InstrumentReferenceValueError(MarketDataAcquisitionError): + pass + + +# Ошибка преобразования raw-модели источника во внутреннюю модель Instrument. +class InstrumentReferenceMappingError(MarketDataAcquisitionError): + pass + + +# Ошибка регистрации или получения Instrument Feed. +class InstrumentFeedRegistryError(MarketDataAcquisitionError): + pass + +# Ошибка получения Quotes Feed от внешнего источника. +class QuoteTransportError(MarketDataAcquisitionError): + pass + + +# Ошибка структуры документа Quotes Feed. +class QuoteSchemaError(MarketDataAcquisitionError): + pass + + +# Ошибка преобразования проверенного документа в raw-модель котировки. +class QuoteParseError(MarketDataAcquisitionError): + pass + + +# Ошибка допустимости значений Quotes Feed. +class QuoteValueError(MarketDataAcquisitionError): + pass + + +# Ошибка преобразования raw-модели источника во внутреннюю модель Quote. +class QuoteMappingError(MarketDataAcquisitionError): + pass + + +# Ошибка регистрации или получения Quotes Feed. +class QuoteFeedRegistryError(MarketDataAcquisitionError): + pass diff --git a/app/src/market_data/acquisition/feeds/__init__.py b/app/src/market_data/acquisition/feeds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/feeds/candles_feed.py b/app/src/market_data/acquisition/feeds/candles_feed.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/feeds/derivatives_feed.py b/app/src/market_data/acquisition/feeds/derivatives_feed.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/feeds/index_feed.py b/app/src/market_data/acquisition/feeds/index_feed.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/feeds/instrument_feed.py b/app/src/market_data/acquisition/feeds/instrument_feed.py new file mode 100644 index 0000000..878d74c --- /dev/null +++ b/app/src/market_data/acquisition/feeds/instrument_feed.py @@ -0,0 +1,33 @@ +# app/src/market_data/acquisition/feeds/instrument_feed.py + +from __future__ import annotations + +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.protocol import ( + InstrumentDocumentHandler, + InstrumentDocumentSource, +) + + +# Feed справочника инструментов: получает документ и передаёт его обработчику. +class InstrumentFeed: + def __init__( + self, + *, + source: InstrumentDocumentSource, + handler: InstrumentDocumentHandler, + ) -> None: + self._source = source + self._handler = handler + + def load_instruments(self) -> tuple[Instrument, ...]: + """ + Получить документ от источника и преобразовать его в модели Instrument. + + Feed не выполняет transport, parsing, validation, mapping, + кэширование или обработку ошибок самостоятельно. + """ + + document = self._source.fetch_instrument_document() + + return self._handler.handle_instrument_document(document) \ No newline at end of file diff --git a/app/src/market_data/acquisition/feeds/orderbook_feed.py b/app/src/market_data/acquisition/feeds/orderbook_feed.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/feeds/quotes_feed.py b/app/src/market_data/acquisition/feeds/quotes_feed.py new file mode 100644 index 0000000..712f394 --- /dev/null +++ b/app/src/market_data/acquisition/feeds/quotes_feed.py @@ -0,0 +1,36 @@ +# app/src/market_data/acquisition/feeds/quotes_feed.py + +from __future__ import annotations + +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.protocol import ( + QuoteDocumentHandler, + QuoteDocumentSource, +) + + +# Feed текущих котировок: получает документ и передаёт его обработчику. +class QuotesFeed: + def __init__( + self, + *, + source: QuoteDocumentSource, + handler: QuoteDocumentHandler, + ) -> None: + self._source = source + self._handler = handler + + def load_quote( + self, + symbol: str, + ) -> Quote: + """ + Получить документ котировки и преобразовать его в модель Quote. + + Feed не выполняет transport, parsing, validation, mapping, + нормализацию symbol, retry, кэширование или обработку ошибок. + """ + + document = self._source.fetch_quote_document(symbol) + + return self._handler.handle_quote_document(document) diff --git a/app/src/market_data/acquisition/feeds/status_feed.py b/app/src/market_data/acquisition/feeds/status_feed.py new file mode 100644 index 0000000..c69abb5 --- /dev/null +++ b/app/src/market_data/acquisition/feeds/status_feed.py @@ -0,0 +1,2 @@ +# app/src/market_data/acquisition/feeds/status_feed.py + diff --git a/app/src/market_data/acquisition/feeds/time_feed.py b/app/src/market_data/acquisition/feeds/time_feed.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/feeds/trades_feed.py b/app/src/market_data/acquisition/feeds/trades_feed.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/__init__.py b/app/src/market_data/acquisition/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/candles_handler.py b/app/src/market_data/acquisition/handlers/candles_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/derivatives_handler.py b/app/src/market_data/acquisition/handlers/derivatives_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/index_handler.py b/app/src/market_data/acquisition/handlers/index_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/instrument_handler.py b/app/src/market_data/acquisition/handlers/instrument_handler.py new file mode 100644 index 0000000..f1e3a5c --- /dev/null +++ b/app/src/market_data/acquisition/handlers/instrument_handler.py @@ -0,0 +1,32 @@ +# app/src/market_data/acquisition/handlers/instrument_handler.py + +from __future__ import annotations + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_exchange_info_to_instruments, +) +from src.market_data.acquisition.adapters.dzengi.parser import ( + parse_exchange_info, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.validation.schema import ( + validate_exchange_info_schema, +) +from src.market_data.acquisition.validation.values import ( + validate_exchange_info_values, +) + + +# Обработчик документа Instrument Reference Data формата Dzengi exchangeInfo. +class DzengiInstrumentDocumentHandler: + def handle_instrument_document( + self, + document: object, + ) -> tuple[Instrument, ...]: + validated_document = validate_exchange_info_schema(document) + + response = parse_exchange_info(validated_document) + + validate_exchange_info_values(response) + + return map_dzengi_exchange_info_to_instruments(response) \ No newline at end of file diff --git a/app/src/market_data/acquisition/handlers/orderbook_handler.py b/app/src/market_data/acquisition/handlers/orderbook_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/quotes_handler.py b/app/src/market_data/acquisition/handlers/quotes_handler.py new file mode 100644 index 0000000..bf47010 --- /dev/null +++ b/app/src/market_data/acquisition/handlers/quotes_handler.py @@ -0,0 +1,35 @@ +# app/src/market_data/acquisition/handlers/quotes_handler.py + +from __future__ import annotations + +from datetime import datetime, timezone + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_ticker_to_quote, +) +from src.market_data.acquisition.adapters.dzengi.parser import parse_quote +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.validation.schema import ( + validate_quote_schema, +) +from src.market_data.acquisition.validation.values import ( + validate_quote_values, +) + + +# Обработчик документа Quotes Feed формата Dzengi ticker/24hr. +class DzengiQuoteDocumentHandler: + def handle_quote_document( + self, + document: object, + ) -> Quote: + validated_document = validate_quote_schema(document) + + response = parse_quote(validated_document) + + validate_quote_values(response) + + return map_dzengi_ticker_to_quote( + response, + received_at=datetime.now(timezone.utc), + ) diff --git a/app/src/market_data/acquisition/handlers/status_handler.py b/app/src/market_data/acquisition/handlers/status_handler.py new file mode 100644 index 0000000..873c7ee --- /dev/null +++ b/app/src/market_data/acquisition/handlers/status_handler.py @@ -0,0 +1,2 @@ +# app/src/market_data/acquisition/handlers/status_handler.py + diff --git a/app/src/market_data/acquisition/handlers/time_handler.py b/app/src/market_data/acquisition/handlers/time_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/handlers/trades_handler.py b/app/src/market_data/acquisition/handlers/trades_handler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/models/__init__.py b/app/src/market_data/acquisition/models/__init__.py new file mode 100644 index 0000000..eb0281b --- /dev/null +++ b/app/src/market_data/acquisition/models/__init__.py @@ -0,0 +1 @@ +# app/src/market_data/acquisition/models/__init__.py \ No newline at end of file diff --git a/app/src/market_data/acquisition/models/candle.py b/app/src/market_data/acquisition/models/candle.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/models/derivative.py b/app/src/market_data/acquisition/models/derivative.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/models/index.py b/app/src/market_data/acquisition/models/index.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/models/instrument.py b/app/src/market_data/acquisition/models/instrument.py new file mode 100644 index 0000000..4b2c623 --- /dev/null +++ b/app/src/market_data/acquisition/models/instrument.py @@ -0,0 +1,38 @@ +# app/src/market_data/acquisition/models/instrument.py + +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal + + +# Независимое от источника справочное описание торгового инструмента. +@dataclass(frozen=True, slots=True) +class Instrument: + symbol: str + name: str + status: str + + base_asset: str + quote_asset: str + asset_type: str | None + + market_type: str + market_modes: tuple[str, ...] + order_types: tuple[str, ...] + + base_asset_precision: int | None + quote_asset_precision: int | None + + tick_size: Decimal | None + tick_value: Decimal | None + + step_size: Decimal | None + min_qty: Decimal | None + max_qty: Decimal | None + min_notional: Decimal | None + + country: str | None + sector: str | None + industry: str | None + trading_hours: str | None \ No newline at end of file diff --git a/app/src/market_data/acquisition/models/orderbook.py b/app/src/market_data/acquisition/models/orderbook.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/models/quote.py b/app/src/market_data/acquisition/models/quote.py new file mode 100644 index 0000000..d7c5ba3 --- /dev/null +++ b/app/src/market_data/acquisition/models/quote.py @@ -0,0 +1,22 @@ +# app/src/market_data/acquisition/models/quote.py + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal + + +# Независимый от источника снимок текущей рыночной котировки инструмента. +@dataclass(frozen=True, slots=True) +class Quote: + symbol: str + + last_price: Decimal + bid_price: Decimal + ask_price: Decimal + + exchange_timestamp: datetime | None + received_at: datetime + + source: str \ No newline at end of file diff --git a/app/src/market_data/acquisition/models/status.py b/app/src/market_data/acquisition/models/status.py new file mode 100644 index 0000000..698ed06 --- /dev/null +++ b/app/src/market_data/acquisition/models/status.py @@ -0,0 +1,88 @@ +# app/src/market_data/acquisition/models/status.py + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + + +# Каноническое состояние торговой доступности инструмента. +class InstrumentTradingState(StrEnum): + OPEN = "OPEN" + BREAK = "BREAK" + NOT_TRADABLE = "NOT_TRADABLE" + UNKNOWN = "UNKNOWN" + + +# Результат классификации сырого статуса инструмента. +@dataclass(frozen=True, slots=True) +class InstrumentStatusClassification: + state: InstrumentTradingState + normalized_status: str | None + + +_OPEN_STATUSES = frozenset( + { + "TRADING", + "OPEN", + "ACTIVE", + "ENABLED", + "ONLINE", + } +) + +_NOT_TRADABLE_STATUSES = frozenset( + { + "NOT_TRADABLE", + "TRADING_DISABLED", + "MARKET_DISABLED", + "UNAVAILABLE_FOR_TRADING", + "CLOSE_ONLY", + "REDUCE_ONLY", + "VIEW_ONLY", + } +) + +_BREAK_STATUSES = frozenset( + { + "BREAK", + "CLOSED", + "HALT", + "HALTED", + "PAUSED", + "SUSPENDED", + "DISABLED", + "SETTLING", + "POST_ONLY", + } +) + + +# Классифицировать сырой статус торгового инструмента. +def classify_instrument_status( + raw_status: str | None, +) -> InstrumentStatusClassification: + normalized_status = str(raw_status or "").strip().upper() + + if normalized_status in _OPEN_STATUSES: + return InstrumentStatusClassification( + state=InstrumentTradingState.OPEN, + normalized_status=normalized_status, + ) + + if normalized_status in _NOT_TRADABLE_STATUSES: + return InstrumentStatusClassification( + state=InstrumentTradingState.NOT_TRADABLE, + normalized_status=normalized_status, + ) + + if normalized_status in _BREAK_STATUSES: + return InstrumentStatusClassification( + state=InstrumentTradingState.BREAK, + normalized_status=normalized_status, + ) + + return InstrumentStatusClassification( + state=InstrumentTradingState.UNKNOWN, + normalized_status=normalized_status or None, + ) \ No newline at end of file diff --git a/app/src/market_data/acquisition/models/time.py b/app/src/market_data/acquisition/models/time.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/models/trade.py b/app/src/market_data/acquisition/models/trade.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/protocol.py b/app/src/market_data/acquisition/protocol.py new file mode 100644 index 0000000..48d575d --- /dev/null +++ b/app/src/market_data/acquisition/protocol.py @@ -0,0 +1,86 @@ +# app/src/market_data/acquisition/protocol.py + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.models.quote import Quote + + +# Источник сырого документа Instrument Reference Data. +@runtime_checkable +class InstrumentDocumentSource(Protocol): + def fetch_instrument_document(self) -> object: + """ + Получить декодированный транспортный документ Instrument Reference Data. + + Источник не выполняет schema validation, parsing, value validation + или mapping во внутреннюю модель Instrument. + """ + ... + + +# Обработчик сырого документа Instrument Reference Data. +@runtime_checkable +class InstrumentDocumentHandler(Protocol): + def handle_instrument_document( + self, + document: object, + ) -> tuple[Instrument, ...]: + """ + Преобразовать сырой документ в проверенные внутренние модели Instrument. + """ + ... + + +# Источник готового справочника инструментов для Acquisition Service. +@runtime_checkable +class InstrumentFeedProtocol(Protocol): + def load_instruments(self) -> tuple[Instrument, ...]: + """ + Получить полный immutable-набор внутренних моделей Instrument. + """ + ... + + +# Источник сырого документа Quotes Feed. +@runtime_checkable +class QuoteDocumentSource(Protocol): + def fetch_quote_document( + self, + symbol: str, + ) -> object: + """ + Получить декодированный транспортный документ текущей котировки. + + Источник не выполняет schema validation, parsing, value validation + или mapping во внутреннюю модель Quote. + """ + ... + + +# Обработчик сырого документа Quotes Feed. +@runtime_checkable +class QuoteDocumentHandler(Protocol): + def handle_quote_document( + self, + document: object, + ) -> Quote: + """ + Преобразовать сырой документ в проверенную внутреннюю модель Quote. + """ + ... + + +# Источник готовой текущей котировки для Acquisition Service. +@runtime_checkable +class QuoteFeedProtocol(Protocol): + def load_quote( + self, + symbol: str, + ) -> Quote: + """ + Получить внутреннюю модель текущей котировки инструмента. + """ + ... \ No newline at end of file diff --git a/app/src/market_data/acquisition/registry.py b/app/src/market_data/acquisition/registry.py new file mode 100644 index 0000000..abf13c8 --- /dev/null +++ b/app/src/market_data/acquisition/registry.py @@ -0,0 +1,142 @@ +# app/src/market_data/acquisition/registry.py + +from __future__ import annotations + +from src.market_data.acquisition.exceptions import ( + InstrumentFeedRegistryError, + QuoteFeedRegistryError, +) +from src.market_data.acquisition.protocol import ( + InstrumentFeedProtocol, + QuoteFeedProtocol, +) + + +# Реестр доступных Feed справочника инструментов. +class InstrumentFeedRegistry: + def __init__(self) -> None: + self._feeds: dict[str, InstrumentFeedProtocol] = {} + + def register( + self, + source_name: str, + feed: InstrumentFeedProtocol, + ) -> None: + """ + Зарегистрировать Instrument Feed для указанного источника. + + Повторная регистрация того же имени запрещена, чтобы исключить + неявную замену production-зависимости. + """ + + normalized_source_name = self._normalize_source_name(source_name) + + if not isinstance(feed, InstrumentFeedProtocol): + raise InstrumentFeedRegistryError( + f"Объект для источника '{normalized_source_name}' " + "не соответствует InstrumentFeedProtocol." + ) + + if normalized_source_name in self._feeds: + raise InstrumentFeedRegistryError( + f"Instrument Feed для источника " + f"'{normalized_source_name}' уже зарегистрирован." + ) + + self._feeds[normalized_source_name] = feed + + def get( + self, + source_name: str, + ) -> InstrumentFeedProtocol: + """Вернуть зарегистрированный Instrument Feed по имени источника.""" + + normalized_source_name = self._normalize_source_name(source_name) + + feed = self._feeds.get(normalized_source_name) + + if feed is None: + raise InstrumentFeedRegistryError( + f"Instrument Feed для источника " + f"'{normalized_source_name}' не зарегистрирован." + ) + + return feed + + def _normalize_source_name( + self, + source_name: str, + ) -> str: + normalized_source_name = source_name.strip() + + if not normalized_source_name: + raise InstrumentFeedRegistryError( + "Имя источника Instrument Feed не должно быть пустым." + ) + + return normalized_source_name + + +# Реестр доступных потоков текущих котировок. +class QuoteFeedRegistry: + def __init__(self) -> None: + self._feeds: dict[str, QuoteFeedProtocol] = {} + + def register( + self, + source_name: str, + feed: QuoteFeedProtocol, + ) -> None: + """ + Зарегистрировать Quotes Feed для указанного источника. + + Повторная регистрация того же имени запрещена, чтобы исключить + неявную замену production-зависимости. + """ + + normalized_source_name = self._normalize_source_name(source_name) + + if not isinstance(feed, QuoteFeedProtocol): + raise QuoteFeedRegistryError( + f"Объект для источника '{normalized_source_name}' " + "не соответствует QuoteFeedProtocol." + ) + + if normalized_source_name in self._feeds: + raise QuoteFeedRegistryError( + f"Quotes Feed для источника " + f"'{normalized_source_name}' уже зарегистрирован." + ) + + self._feeds[normalized_source_name] = feed + + def get( + self, + source_name: str, + ) -> QuoteFeedProtocol: + """Вернуть зарегистрированный Quotes Feed по имени источника.""" + + normalized_source_name = self._normalize_source_name(source_name) + + feed = self._feeds.get(normalized_source_name) + + if feed is None: + raise QuoteFeedRegistryError( + f"Quotes Feed для источника " + f"'{normalized_source_name}' не зарегистрирован." + ) + + return feed + + def _normalize_source_name( + self, + source_name: str, + ) -> str: + normalized_source_name = source_name.strip() + + if not normalized_source_name: + raise QuoteFeedRegistryError( + "Имя источника Quotes Feed не должно быть пустым." + ) + + return normalized_source_name diff --git a/app/src/market_data/acquisition/runtime/__init__.py b/app/src/market_data/acquisition/runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/runtime/heartbeat.py b/app/src/market_data/acquisition/runtime/heartbeat.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/runtime/reconnect.py b/app/src/market_data/acquisition/runtime/reconnect.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/runtime/scheduler.py b/app/src/market_data/acquisition/runtime/scheduler.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/runtime/supervisor.py b/app/src/market_data/acquisition/runtime/supervisor.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/service.py b/app/src/market_data/acquisition/service.py new file mode 100644 index 0000000..b14cb39 --- /dev/null +++ b/app/src/market_data/acquisition/service.py @@ -0,0 +1,62 @@ +# app/src/market_data/acquisition/service.py + +from __future__ import annotations + +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.registry import ( + InstrumentFeedRegistry, + QuoteFeedRegistry, +) + + +# Application-level сервис получения справочника инструментов. +class InstrumentAcquisitionService: + def __init__( + self, + *, + registry: InstrumentFeedRegistry, + ) -> None: + self._registry = registry + + def load_instruments( + self, + source_name: str, + ) -> tuple[Instrument, ...]: + """ + Получить Instrument Feed из Registry и загрузить справочник инструментов. + + Service не создаёт Feed, не выполняет transport, parsing, validation, + mapping, retry, кэширование или преобразование результата. + """ + + feed = self._registry.get(source_name) + + return feed.load_instruments() + + +# Application-level сервис получения текущих котировок. +class QuoteAcquisitionService: + def __init__( + self, + *, + registry: QuoteFeedRegistry, + ) -> None: + self._registry = registry + + def load_quote( + self, + source_name: str, + symbol: str, + ) -> Quote: + """ + Получить Quotes Feed из Registry и загрузить текущую котировку. + + Service не создаёт Feed, не выполняет transport, parsing, validation, + mapping, нормализацию symbol, retry, кэширование или преобразование + результата. + """ + + feed = self._registry.get(source_name) + + return feed.load_quote(symbol) diff --git a/app/src/market_data/acquisition/symbols.py b/app/src/market_data/acquisition/symbols.py new file mode 100644 index 0000000..4c58e51 --- /dev/null +++ b/app/src/market_data/acquisition/symbols.py @@ -0,0 +1,45 @@ +# app/src/market_data/acquisition/symbols.py + +from __future__ import annotations + +from collections.abc import Sequence + + +# Привести идентификатор торгового инструмента к базовой канонической форме. +def normalize_symbol(raw_symbol: str) -> str: + return (raw_symbol or "").strip().upper() + + +# Сформировать упорядоченные варианты идентификатора инструмента. +def symbol_candidates(raw_symbol: str) -> list[str]: + value = normalize_symbol(raw_symbol) + + if not value: + return [] + + candidates = [value] + + compact = value.replace("%2F", "/") + + if compact not in candidates: + candidates.append(compact) + + no_spaces = compact.replace(" ", "") + + if no_spaces not in candidates: + candidates.append(no_spaces) + + return candidates + + +# Найти индекс первого доступного символа с учётом порядка кандидатов. +def resolve_symbol_index( + raw_symbol: str, + available_symbols: Sequence[str], +) -> int | None: + for candidate in symbol_candidates(raw_symbol): + for index, available_symbol in enumerate(available_symbols): + if normalize_symbol(available_symbol) == candidate: + return index + + return None \ No newline at end of file diff --git a/app/src/market_data/acquisition/validation/__init__.py b/app/src/market_data/acquisition/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/market_data/acquisition/validation/schema.py b/app/src/market_data/acquisition/validation/schema.py new file mode 100644 index 0000000..38f2aa1 --- /dev/null +++ b/app/src/market_data/acquisition/validation/schema.py @@ -0,0 +1,400 @@ +# app/src/market_data/acquisition/validation/schema.py + +from __future__ import annotations + +from dataclasses import dataclass +from types import MappingProxyType +from typing import Mapping + +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceSchemaError, + QuoteSchemaError, +) + + +# Проверенное структурное представление ответа exchangeInfo. +@dataclass(frozen=True, slots=True) +class ValidatedExchangeInfoDocument: + payload: Mapping[str, object] + is_wrapped: bool + status: object | None + correlation_id: object | None + + +def validate_exchange_info_schema( + document: object, +) -> ValidatedExchangeInfoDocument: + """ + Проверить структуру ответа exchangeInfo без разбора предметных значений. + + Поддерживаются: + + 1. Unwrapped-формат: + + { + "symbols": [...] + } + + 2. Wrapped-формат: + + { + "status": "OK", + "correlationId": "2", + "payload": { + "symbols": [...] + } + } + """ + + root = _require_mapping( + document, + path="$", + ) + + is_wrapped = "payload" in root + + if is_wrapped: + payload = _require_mapping( + root.get("payload"), + path="$.payload", + ) + else: + payload = root + + _validate_exchange_info_payload(payload) + + return ValidatedExchangeInfoDocument( + payload=MappingProxyType(dict(payload)), + is_wrapped=is_wrapped, + status=root.get("status") if is_wrapped else None, + correlation_id=( + root.get("correlationId") + if is_wrapped + else None + ), + ) + + +def _validate_exchange_info_payload( + payload: Mapping[str, object], +) -> None: + symbols = _require_list( + payload.get("symbols"), + path="$.payload.symbols", + ) + + for index, symbol in enumerate(symbols): + symbol_path = f"$.payload.symbols[{index}]" + + symbol_mapping = _require_mapping( + symbol, + path=symbol_path, + ) + + _validate_optional_mapping_list( + symbol_mapping, + key="filters", + path=f"{symbol_path}.filters", + ) + + _validate_optional_string_list( + symbol_mapping, + key="marketModes", + path=f"{symbol_path}.marketModes", + ) + + _validate_optional_string_list( + symbol_mapping, + key="orderTypes", + path=f"{symbol_path}.orderTypes", + ) + + _validate_optional_mapping_list( + payload, + key="rateLimits", + path="$.payload.rateLimits", + ) + + _validate_optional_mapping_list( + payload, + key="exchangeFilters", + path="$.payload.exchangeFilters", + ) + + +def _validate_optional_mapping_list( + mapping: Mapping[str, object], + *, + key: str, + path: str, +) -> None: + if key not in mapping: + return + + items = _require_list( + mapping.get(key), + path=path, + ) + + for index, item in enumerate(items): + _require_mapping( + item, + path=f"{path}[{index}]", + ) + + +def _validate_optional_string_list( + mapping: Mapping[str, object], + *, + key: str, + path: str, +) -> None: + if key not in mapping: + return + + items = _require_list( + mapping.get(key), + path=path, + ) + + for index, item in enumerate(items): + if not isinstance(item, str): + raise InstrumentReferenceSchemaError( + f"{path}[{index}] должен быть строкой, " + f"получен {type(item).__name__}." + ) + + +def _require_mapping( + value: object, + *, + path: str, +) -> Mapping[str, object]: + if not isinstance(value, dict): + raise InstrumentReferenceSchemaError( + f"{path} должен быть JSON-объектом, " + f"получен {type(value).__name__}." + ) + + for key in value: + if not isinstance(key, str): + raise InstrumentReferenceSchemaError( + f"{path} содержит нестроковый ключ " + f"типа {type(key).__name__}." + ) + + return value + + +def _require_list( + value: object, + *, + path: str, +) -> list[object]: + if not isinstance(value, list): + raise InstrumentReferenceSchemaError( + f"{path} должен быть JSON-массивом, " + f"получен {type(value).__name__}." + ) + + return value + +# Структурно проверенное представление ответа ticker/24hr. +@dataclass(frozen=True, slots=True) +class ValidatedQuoteDocument: + payload: Mapping[str, object] + is_wrapped: bool + status: object | None + correlation_id: object | None + + +def validate_quote_schema( + document: object, +) -> ValidatedQuoteDocument: + """ + Проверить структуру ответа Dzengi ticker/24hr без проверки значений. + + Поддерживаются прямой JSON-объект котировки и wrapped-формат + с объектом котировки в поле payload. + """ + + root = _require_quote_mapping( + document, + path="$", + ) + + is_wrapped = "payload" in root + + if is_wrapped: + payload = _require_quote_mapping( + root.get("payload"), + path="$.payload", + ) + else: + payload = root + + _validate_quote_payload(payload) + + return ValidatedQuoteDocument( + payload=MappingProxyType(dict(payload)), + is_wrapped=is_wrapped, + status=root.get("status") if is_wrapped else None, + correlation_id=( + root.get("correlationId") + if is_wrapped + else None + ), + ) + + +def _validate_quote_payload( + payload: Mapping[str, object], +) -> None: + _require_quote_key(payload, key="symbol", path="$.payload.symbol") + _require_quote_key(payload, key="lastPrice", path="$.payload.lastPrice") + _require_quote_key(payload, key="bidPrice", path="$.payload.bidPrice") + _require_quote_key(payload, key="askPrice", path="$.payload.askPrice") + _require_quote_key(payload, key="closeTime", path="$.payload.closeTime") + + +def _require_quote_key( + mapping: Mapping[str, object], + *, + key: str, + path: str, +) -> None: + if key not in mapping: + raise QuoteSchemaError( + f"{path} отсутствует в документе ticker/24hr." + ) + + +def _require_quote_mapping( + value: object, + *, + path: str, +) -> Mapping[str, object]: + if not isinstance(value, dict): + raise QuoteSchemaError( + f"{path} должен быть JSON-объектом, " + f"получен {type(value).__name__}." + ) + + for key in value: + if not isinstance(key, str): + raise QuoteSchemaError( + f"{path} содержит нестроковый ключ " + f"типа {type(key).__name__}." + ) + + return value + + +# Структурно проверенное представление сообщения котировки Dzengi WebSocket. +@dataclass(frozen=True, slots=True) +class ValidatedWebSocketQuoteDocument: + payload: Mapping[str, object] + root_symbol: object | None + + +def validate_dzengi_websocket_quote_schema( + document: object, +) -> ValidatedWebSocketQuoteDocument: + """ + Проверить структуру одного декодированного сообщения Dzengi WebSocket. + + Поддерживаются сообщения без оболочки и до двух известных оболочек + ``payload`` / ``Payload``. Проверка не преобразует цены и не выполняет + предметную валидацию. + """ + + root = _require_quote_mapping(document, path="$") + root_symbol = root.get("symbol") + payload = _unwrap_websocket_quote_payload(root) + + _validate_websocket_quote_payload( + payload, + root_symbol=root_symbol, + ) + + return ValidatedWebSocketQuoteDocument( + payload=MappingProxyType(dict(payload)), + root_symbol=root_symbol, + ) + + +def _unwrap_websocket_quote_payload( + root: Mapping[str, object], +) -> Mapping[str, object]: + payload = root + + for level in range(2): + nested: object | None = None + nested_path = "$.payload" if level == 0 else "$.payload.payload" + + for key in ("payload", "Payload"): + candidate = payload.get(key) + if candidate is not None: + nested = candidate + break + + if nested is None: + break + + payload = _require_quote_mapping( + nested, + path=nested_path, + ) + + return payload + + +def _validate_websocket_quote_payload( + payload: Mapping[str, object], + *, + root_symbol: object | None, +) -> None: + if ( + "symbolName" not in payload + and "symbol" not in payload + and root_symbol is None + ): + raise QuoteSchemaError( + "$.payload не содержит symbolName или symbol." + ) + + has_direct_bid = "bid" in payload + has_direct_ask = "ask" in payload or "ofr" in payload + has_depth_bid = "bids" in payload + has_depth_ask = "asks" in payload + + if has_direct_bid or has_direct_ask: + if not has_direct_bid or not has_direct_ask: + raise QuoteSchemaError( + "WebSocket quote должна содержать полный набор bid и ask/ofr." + ) + return + + if has_depth_bid or has_depth_ask: + if not has_depth_bid or not has_depth_ask: + raise QuoteSchemaError( + "WebSocket depth quote должна содержать bids и asks." + ) + + bids = payload.get("bids") + asks = payload.get("asks") + + if not isinstance(bids, list) or not bids: + raise QuoteSchemaError( + "$.payload.bids должен быть непустым JSON-массивом." + ) + + if not isinstance(asks, list) or not asks: + raise QuoteSchemaError( + "$.payload.asks должен быть непустым JSON-массивом." + ) + return + + raise QuoteSchemaError( + "WebSocket quote не содержит bid/ask либо bids/asks." + ) diff --git a/app/src/market_data/acquisition/validation/sequence.py b/app/src/market_data/acquisition/validation/sequence.py new file mode 100644 index 0000000..efbb61b --- /dev/null +++ b/app/src/market_data/acquisition/validation/sequence.py @@ -0,0 +1 @@ +# app/src/market_data/acquisition/validation/sequence.py \ No newline at end of file diff --git a/app/src/market_data/acquisition/validation/values.py b/app/src/market_data/acquisition/validation/values.py new file mode 100644 index 0000000..5793e1d --- /dev/null +++ b/app/src/market_data/acquisition/validation/values.py @@ -0,0 +1,551 @@ +# app/src/market_data/acquisition/validation/values.py + +from __future__ import annotations + +from decimal import Decimal, InvalidOperation + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiExchangeInfoResponse, + DzengiExchangeInfoSymbol, + DzengiInstrumentFilter, + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiRateLimit, + DzengiRawNumeric, + DzengiUnknownFilter, + DzengiTicker24hrResponse, + DzengiWebSocketQuoteResponse, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceValueError, + QuoteValueError, +) + + +def validate_exchange_info_values( + response: DzengiExchangeInfoResponse, +) -> None: + """ + Проверить допустимость значений в raw-моделях Dzengi exchangeInfo. + + Функция не изменяет модели, не выполняет mapping в Instrument + и не повторяет schema validation или parsing. + """ + + _validate_optional_non_empty_string( + response.status, + path="$.status", + ) + _validate_optional_non_empty_string( + response.correlation_id, + path="$.correlationId", + ) + + payload = response.payload + + _validate_optional_non_empty_string( + payload.timezone, + path="$.payload.timezone", + ) + + for index, rate_limit in enumerate(payload.rate_limits): + _validate_rate_limit( + rate_limit, + path=f"$.payload.rateLimits[{index}]", + ) + + for index, exchange_filter in enumerate(payload.exchange_filters): + _validate_unknown_filter( + exchange_filter, + path=f"$.payload.exchangeFilters[{index}]", + allow_empty_filter_type=True, + ) + + for index, symbol in enumerate(payload.symbols): + _validate_symbol( + symbol, + path=f"$.payload.symbols[{index}]", + ) + + +def _validate_symbol( + symbol: DzengiExchangeInfoSymbol, + *, + path: str, +) -> None: + _validate_required_non_empty_string( + symbol.symbol, + path=f"{path}.symbol", + ) + _validate_required_non_empty_string( + symbol.name, + path=f"{path}.name", + ) + _validate_required_non_empty_string( + symbol.status, + path=f"{path}.status", + ) + _validate_required_non_empty_string( + symbol.base_asset, + path=f"{path}.baseAsset", + ) + _validate_required_non_empty_string( + symbol.quote_asset, + path=f"{path}.quoteAsset", + ) + _validate_required_non_empty_string( + symbol.market_type, + path=f"{path}.marketType", + ) + + _validate_optional_non_empty_string( + symbol.asset_type, + path=f"{path}.assetType", + ) + _validate_optional_non_empty_string( + symbol.quote_asset_id, + path=f"{path}.quoteAssetId", + ) + _validate_optional_non_empty_string( + symbol.trading_hours, + path=f"{path}.tradingHours", + ) + + # Dzengi может возвращать пустые строки для country, sector и industry. + # Эти значения сохраняются как часть raw-контракта и не считаются ошибкой. + + _validate_non_empty_string_tuple( + symbol.order_types, + path=f"{path}.orderTypes", + ) + _validate_non_empty_string_tuple( + symbol.market_modes, + path=f"{path}.marketModes", + ) + + _validate_optional_non_negative_int( + symbol.base_asset_precision, + path=f"{path}.baseAssetPrecision", + ) + _validate_optional_non_negative_int( + symbol.quote_precision, + path=f"{path}.quotePrecision", + ) + _validate_optional_non_negative_int( + symbol.swap_charge_interval, + path=f"{path}.swapChargeInterval", + ) + + _validate_optional_positive_number( + symbol.tick_size, + path=f"{path}.tickSize", + ) + + _validate_optional_finite_number( + symbol.tick_value, + path=f"{path}.tickValue", + ) + _validate_optional_finite_number( + symbol.trading_fee, + path=f"{path}.tradingFee", + ) + _validate_optional_finite_number( + symbol.exchange_fee, + path=f"{path}.exchangeFee", + ) + _validate_optional_finite_number( + symbol.long_rate, + path=f"{path}.longRate", + ) + _validate_optional_finite_number( + symbol.short_rate, + path=f"{path}.shortRate", + ) + _validate_optional_finite_number( + symbol.min_sl_gap, + path=f"{path}.minSLGap", + ) + _validate_optional_finite_number( + symbol.max_sl_gap, + path=f"{path}.maxSLGap", + ) + _validate_optional_finite_number( + symbol.min_tp_gap, + path=f"{path}.minTPGap", + ) + _validate_optional_finite_number( + symbol.max_tp_gap, + path=f"{path}.maxTPGap", + ) + + for index, instrument_filter in enumerate(symbol.filters): + _validate_instrument_filter( + instrument_filter, + path=f"{path}.filters[{index}]", + ) + + +def _validate_rate_limit( + rate_limit: DzengiRateLimit, + *, + path: str, +) -> None: + _validate_required_non_empty_string( + rate_limit.interval, + path=f"{path}.interval", + ) + _validate_required_non_empty_string( + rate_limit.rate_limit_type, + path=f"{path}.rateLimitType", + ) + _validate_positive_int( + rate_limit.interval_num, + path=f"{path}.intervalNum", + ) + _validate_positive_int( + rate_limit.limit, + path=f"{path}.limit", + ) + + +def _validate_instrument_filter( + instrument_filter: DzengiInstrumentFilter, + *, + path: str, +) -> None: + _validate_required_non_empty_string( + instrument_filter.filter_type, + path=f"{path}.filterType", + ) + + if isinstance(instrument_filter, DzengiLotSizeFilter): + _validate_lot_size_filter( + instrument_filter, + path=path, + ) + return + + if isinstance(instrument_filter, DzengiMinNotionalFilter): + _validate_min_notional_filter( + instrument_filter, + path=path, + ) + return + + if isinstance(instrument_filter, DzengiUnknownFilter): + _validate_unknown_filter( + instrument_filter, + path=path, + allow_empty_filter_type=False, + ) + + +def _validate_lot_size_filter( + lot_size: DzengiLotSizeFilter, + *, + path: str, +) -> None: + min_qty = _validate_optional_positive_raw_numeric( + lot_size.min_qty, + path=f"{path}.minQty", + ) + max_qty = _validate_optional_positive_raw_numeric( + lot_size.max_qty, + path=f"{path}.maxQty", + ) + _validate_optional_positive_raw_numeric( + lot_size.step_size, + path=f"{path}.stepSize", + ) + + if ( + min_qty is not None + and max_qty is not None + and min_qty > max_qty + ): + raise InstrumentReferenceValueError( + f"{path}.minQty не должно превышать {path}.maxQty." + ) + + +def _validate_min_notional_filter( + min_notional: DzengiMinNotionalFilter, + *, + path: str, +) -> None: + _validate_optional_non_negative_raw_numeric( + min_notional.min_notional, + path=f"{path}.minNotional", + ) + + +def _validate_unknown_filter( + unknown_filter: DzengiUnknownFilter, + *, + path: str, + allow_empty_filter_type: bool, +) -> None: + if allow_empty_filter_type: + if unknown_filter.filter_type and not unknown_filter.filter_type.strip(): + raise InstrumentReferenceValueError( + f"{path}.filterType не должен состоять только из пробелов." + ) + return + + _validate_required_non_empty_string( + unknown_filter.filter_type, + path=f"{path}.filterType", + ) + + +def _validate_required_non_empty_string( + value: str, + *, + path: str, +) -> None: + if not value.strip(): + raise InstrumentReferenceValueError( + f"{path} не должен быть пустым." + ) + + +def _validate_optional_non_empty_string( + value: str | None, + *, + path: str, +) -> None: + if value is None: + return + + if not value.strip(): + raise InstrumentReferenceValueError( + f"{path} не должен быть пустым." + ) + + +def _validate_non_empty_string_tuple( + values: tuple[str, ...], + *, + path: str, +) -> None: + for index, value in enumerate(values): + if not value.strip(): + raise InstrumentReferenceValueError( + f"{path}[{index}] не должен быть пустым." + ) + + +def _validate_optional_non_negative_int( + value: int | None, + *, + path: str, +) -> None: + if value is None: + return + + if value < 0: + raise InstrumentReferenceValueError( + f"{path} должно быть больше или равно нулю." + ) + + +def _validate_positive_int( + value: int, + *, + path: str, +) -> None: + if value <= 0: + raise InstrumentReferenceValueError( + f"{path} должно быть больше нуля." + ) + + +def _validate_optional_positive_number( + value: int | float | None, + *, + path: str, +) -> None: + if value is None: + return + + decimal_value = _to_finite_decimal( + value, + path=path, + ) + + if decimal_value <= 0: + raise InstrumentReferenceValueError( + f"{path} должно быть больше нуля." + ) + + +def _validate_optional_finite_number( + value: int | float | None, + *, + path: str, +) -> None: + if value is None: + return + + _to_finite_decimal( + value, + path=path, + ) + + +def _validate_optional_positive_raw_numeric( + value: DzengiRawNumeric | None, + *, + path: str, +) -> Decimal | None: + if value is None: + return None + + decimal_value = _to_finite_decimal( + value, + path=path, + ) + + if decimal_value <= 0: + raise InstrumentReferenceValueError( + f"{path} должно быть больше нуля." + ) + + return decimal_value + + +def _validate_optional_non_negative_raw_numeric( + value: DzengiRawNumeric | None, + *, + path: str, +) -> Decimal | None: + if value is None: + return None + + decimal_value = _to_finite_decimal( + value, + path=path, + ) + + if decimal_value < 0: + raise InstrumentReferenceValueError( + f"{path} должно быть больше или равно нулю." + ) + + return decimal_value + + +def _to_finite_decimal( + value: str | int | float, + *, + path: str, +) -> Decimal: + try: + decimal_value = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise InstrumentReferenceValueError( + f"{path} должно быть корректным числом." + ) from exc + + if not decimal_value.is_finite(): + raise InstrumentReferenceValueError( + f"{path} должно быть конечным числом." + ) + + return decimal_value + +def validate_quote_values( + response: DzengiTicker24hrResponse, +) -> None: + """ + Проверить допустимость значений raw-модели Dzengi ticker/24hr. + + Функция не изменяет модель и не выполняет mapping в Quote. + """ + + if not response.symbol.strip(): + raise QuoteValueError( + "$.payload.symbol не должен быть пустым." + ) + + last_price = _quote_positive_decimal( + response.last_price, + path="$.payload.lastPrice", + ) + bid_price = _quote_positive_decimal( + response.bid_price, + path="$.payload.bidPrice", + ) + ask_price = _quote_positive_decimal( + response.ask_price, + path="$.payload.askPrice", + ) + + if response.close_time <= 0: + raise QuoteValueError( + "$.payload.closeTime должно быть больше нуля." + ) + + if bid_price > ask_price: + raise QuoteValueError( + "$.payload.bidPrice не должно превышать $.payload.askPrice." + ) + + # Явное чтение сохраняет проверку обязательности lastPrice + # как самостоятельного положительного рыночного значения. + del last_price + + +def _quote_positive_decimal( + value: DzengiRawNumeric, + *, + path: str, +) -> Decimal: + try: + decimal_value = Decimal(str(value)) + except (InvalidOperation, ValueError) as exc: + raise QuoteValueError( + f"{path} должно быть корректным числом." + ) from exc + + if not decimal_value.is_finite(): + raise QuoteValueError( + f"{path} должно быть конечным числом." + ) + + if decimal_value <= 0: + raise QuoteValueError( + f"{path} должно быть больше нуля." + ) + + return decimal_value + + +def validate_dzengi_websocket_quote_values( + response: DzengiWebSocketQuoteResponse, +) -> None: + """Проверить значения raw-модели котировки Dzengi WebSocket.""" + + if not response.symbol.strip(): + raise QuoteValueError( + "$.payload.symbol не должен быть пустым." + ) + + bid_price = _quote_positive_decimal( + response.bid_price, + path="$.payload.bidPrice", + ) + ask_price = _quote_positive_decimal( + response.ask_price, + path="$.payload.askPrice", + ) + + if bid_price > ask_price: + raise QuoteValueError( + "$.payload.bidPrice не должно превышать $.payload.askPrice." + ) + + if response.timestamp is not None and response.timestamp <= 0: + raise QuoteValueError( + "$.payload.timestamp должно быть больше нуля." + ) diff --git a/app/src/storage/exceptions.py b/app/src/storage/exceptions.py new file mode 100644 index 0000000..38b5e78 --- /dev/null +++ b/app/src/storage/exceptions.py @@ -0,0 +1,18 @@ +# app/src/storage/exceptions.py + +from __future__ import annotations + + +# Базовая ошибка storage-слоя. +class StorageError(Exception): + """Base storage layer error.""" + + +# Ошибка хранилища справочника инструментов. +class InstrumentStoreError(StorageError): + """Instrument store contract or operation error.""" + + +# Ошибка хранилища канонических котировок. +class QuoteStoreError(StorageError): + """Quote store contract or operation error.""" diff --git a/app/src/storage/instrument_store.py b/app/src/storage/instrument_store.py new file mode 100644 index 0000000..db57c24 --- /dev/null +++ b/app/src/storage/instrument_store.py @@ -0,0 +1,110 @@ +# app/src/storage/instrument_store.py + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from src.market_data.acquisition.models.instrument import Instrument +from src.storage.exceptions import InstrumentStoreError + + +# Контракт runtime-хранилища канонического справочника инструментов. +@runtime_checkable +class InstrumentStoreProtocol(Protocol): + def get( + self, + source_name: str, + ) -> tuple[Instrument, ...] | None: + """ + Вернуть сохранённый набор инструментов для источника. + + None означает cache miss: данные для источника ещё не сохранялись. + Пустой tuple означает успешное сохранение пустого справочника. + """ + + def set( + self, + source_name: str, + instruments: tuple[Instrument, ...], + ) -> None: + """Сохранить полный immutable-набор инструментов источника.""" + + def clear( + self, + source_name: str | None = None, + ) -> None: + """ + Очистить данные одного источника или всё хранилище. + + source_name=None очищает все сохранённые источники. + """ + + +# In-memory реализация runtime-хранилища справочника инструментов. +class InMemoryInstrumentStore: + def __init__(self) -> None: + self._items: dict[str, tuple[Instrument, ...]] = {} + + def get( + self, + source_name: str, + ) -> tuple[Instrument, ...] | None: + normalized_source_name = self._normalize_source_name( + source_name + ) + + return self._items.get(normalized_source_name) + + def set( + self, + source_name: str, + instruments: tuple[Instrument, ...], + ) -> None: + normalized_source_name = self._normalize_source_name( + source_name + ) + + if not isinstance(instruments, tuple): + raise InstrumentStoreError( + "Справочник инструментов должен быть передан как tuple." + ) + + if not all( + isinstance(instrument, Instrument) + for instrument in instruments + ): + raise InstrumentStoreError( + "Справочник содержит объект, не являющийся Instrument." + ) + + self._items[normalized_source_name] = instruments + + def clear( + self, + source_name: str | None = None, + ) -> None: + if source_name is None: + self._items.clear() + return + + normalized_source_name = self._normalize_source_name( + source_name + ) + + self._items.pop( + normalized_source_name, + None, + ) + + def _normalize_source_name( + self, + source_name: str, + ) -> str: + normalized_source_name = str(source_name or "").strip() + + if not normalized_source_name: + raise InstrumentStoreError( + "Имя источника Instrument Store не должно быть пустым." + ) + + return normalized_source_name \ No newline at end of file diff --git a/app/src/storage/quote_store.py b/app/src/storage/quote_store.py new file mode 100644 index 0000000..caca39b --- /dev/null +++ b/app/src/storage/quote_store.py @@ -0,0 +1,215 @@ +# app/src/storage/quote_store.py + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from src.market_data.acquisition.models.quote import Quote +from src.storage.exceptions import QuoteStoreError + + +# Контракт runtime-хранилища канонических котировок. +@runtime_checkable +class QuoteStoreProtocol(Protocol): + def get( + self, + source_name: str, + symbol: str, + *, + runtime_key: str = "default", + ) -> Quote | None: + """Вернуть котировку или None, если запись отсутствует.""" + + def set( + self, + source_name: str, + quote: Quote, + *, + runtime_key: str = "default", + ) -> None: + """Сохранить каноническую котировку без копирования модели.""" + + def clear( + self, + source_name: str | None = None, + symbol: str | None = None, + *, + runtime_key: str | None = None, + ) -> None: + """Удалить записи, соответствующие переданным фильтрам.""" + + +# In-memory реализация runtime-хранилища канонических котировок. +class InMemoryQuoteStore: + def __init__(self) -> None: + self._items: dict[tuple[str, str, str], Quote] = {} + + def get( + self, + source_name: str, + symbol: str, + *, + runtime_key: str = "default", + ) -> Quote | None: + return self._items.get( + self._key( + source_name=source_name, + symbol=symbol, + runtime_key=runtime_key, + ) + ) + + def set( + self, + source_name: str, + quote: Quote, + *, + runtime_key: str = "default", + ) -> None: + normalized_source_name = self._normalize_source_name( + source_name + ) + normalized_runtime_key = self._normalize_runtime_key( + runtime_key + ) + + if not isinstance(quote, Quote): + raise QuoteStoreError( + "Quote Store принимает только объект Quote." + ) + + normalized_symbol = self._normalize_symbol( + quote.symbol + ) + + self._items[ + ( + normalized_source_name, + normalized_runtime_key, + normalized_symbol, + ) + ] = quote + + def clear( + self, + source_name: str | None = None, + symbol: str | None = None, + *, + runtime_key: str | None = None, + ) -> None: + if ( + source_name is None + and symbol is None + and runtime_key is None + ): + self._items.clear() + return + + normalized_source_name = ( + self._normalize_source_name(source_name) + if source_name is not None + else None + ) + normalized_symbol = ( + self._normalize_symbol(symbol) + if symbol is not None + else None + ) + normalized_runtime_key = ( + self._normalize_runtime_key(runtime_key) + if runtime_key is not None + else None + ) + + keys_to_delete = [ + key + for key in self._items + if self._matches_filters( + key, + source_name=normalized_source_name, + symbol=normalized_symbol, + runtime_key=normalized_runtime_key, + ) + ] + + for key in keys_to_delete: + self._items.pop(key, None) + + def _key( + self, + *, + source_name: str, + symbol: str, + runtime_key: str, + ) -> tuple[str, str, str]: + return ( + self._normalize_source_name(source_name), + self._normalize_runtime_key(runtime_key), + self._normalize_symbol(symbol), + ) + + def _matches_filters( + self, + key: tuple[str, str, str], + *, + source_name: str | None, + symbol: str | None, + runtime_key: str | None, + ) -> bool: + key_source_name, key_runtime_key, key_symbol = key + + if ( + source_name is not None + and key_source_name != source_name + ): + return False + + if ( + runtime_key is not None + and key_runtime_key != runtime_key + ): + return False + + if symbol is not None and key_symbol != symbol: + return False + + return True + + def _normalize_source_name( + self, + source_name: str, + ) -> str: + normalized_source_name = str(source_name or "").strip() + + if not normalized_source_name: + raise QuoteStoreError( + "Имя источника Quote Store не должно быть пустым." + ) + + return normalized_source_name + + def _normalize_runtime_key( + self, + runtime_key: str, + ) -> str: + normalized_runtime_key = str(runtime_key or "").strip().lower() + + if not normalized_runtime_key: + raise QuoteStoreError( + "Runtime key Quote Store не должен быть пустым." + ) + + return normalized_runtime_key + + def _normalize_symbol( + self, + symbol: str, + ) -> str: + normalized_symbol = str(symbol or "").strip().upper() + + if not normalized_symbol: + raise QuoteStoreError( + "Символ Quote Store не должен быть пустым." + ) + + return normalized_symbol diff --git a/app/src/storage/repositories/balance_snapshots.py b/app/src/storage/repositories/balance_snapshots.py index 33d01a5..4b1bd00 100644 --- a/app/src/storage/repositories/balance_snapshots.py +++ b/app/src/storage/repositories/balance_snapshots.py @@ -1,3 +1,5 @@ +# app/src/storage/repositories/balance_snapshots.py + from __future__ import annotations import json @@ -57,4 +59,4 @@ class BalanceSnapshotRepository: } ) - return items + return items \ No newline at end of file diff --git a/app/src/storage/session.py b/app/src/storage/session.py index d2413a2..67d2fcc 100644 --- a/app/src/storage/session.py +++ b/app/src/storage/session.py @@ -41,4 +41,4 @@ def check_database_health() -> tuple[bool, str]: except Exception as exc: return False, f"PostgreSQL error: {exc}" - return True, version + return True, version \ No newline at end of file diff --git a/app/src/telegram/handlers/__init__.py b/app/src/telegram/handlers/__init__.py index d8df7b8..ca2dae4 100644 --- a/app/src/telegram/handlers/__init__.py +++ b/app/src/telegram/handlers/__init__.py @@ -1 +1,3 @@ -"""Package marker.""" +# app/src/telegram/handlers/__init__.py + +"""Package marker.""" \ No newline at end of file diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py index d1ddb1c..e40ac1e 100644 --- a/app/src/telegram/handlers/auto/ui.py +++ b/app/src/telegram/handlers/auto/ui.py @@ -10,6 +10,7 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.quote import Quote from src.integrations.exchange.runtime_ui import build_runtime_exchange_alert_lines from src.telegram.ui.common import mode_line from src.trading.auto.service import AutoTradeService @@ -40,10 +41,10 @@ def build_auto_notification_text() -> str: def _build_signal_notification_text(state, signal: str) -> str: - snapshot = _market_snapshot(getattr(state, "symbol", None)) + quote = _market_quote(getattr(state, "symbol", None)) - bid_price = _price_from_snapshot(snapshot, "bid_price") - ask_price = _price_from_snapshot(snapshot, "ask_price") + bid_price = _price_from_quote(quote, "bid_price") + ask_price = _price_from_quote(quote, "ask_price") side = "Long" if signal == "BUY" else "Short" side_icon = _signal_icon(signal) @@ -85,28 +86,28 @@ def _build_signal_notification_text(state, signal: str) -> str: return "\n".join(lines) -def _price_from_snapshot( - snapshot: dict[str, object] | None, +def _price_from_quote( + quote: Quote | None, key: str, ) -> float | None: - if snapshot is None: + if quote is None: return None - return safe_float(snapshot.get(key)) + return safe_float(getattr(quote, key, None)) def _position_current_price(state) -> float | None: - snapshot = _market_snapshot(getattr(state, "symbol", None)) + quote = _market_quote(getattr(state, "symbol", None)) - if snapshot is not None: + if quote is not None: side = str(getattr(state, "position_side", "") or "").upper() if side == "LONG": - price = snapshot.get("bid_price") or snapshot.get("last_price") + price = quote.bid_price or quote.last_price elif side == "SHORT": - price = snapshot.get("ask_price") or snapshot.get("last_price") + price = quote.ask_price or quote.last_price else: - price = snapshot.get("last_price") + price = quote.last_price parsed = safe_float(price) if parsed is not None: @@ -720,12 +721,15 @@ def _max_reserved_line(state, price: float | None = None) -> str: return f"Маржа · {_format_usd_compact(own_funds_usd)}" -def _market_snapshot(symbol: str | None) -> dict[str, object] | None: +def _market_quote(symbol: str | None) -> Quote | None: if not symbol: return None try: - return ExchangeService().get_market_snapshot(symbol, runtime_key="auto") + return ExchangeService().get_quote( + symbol, + runtime_key="auto", + ) except Exception: return None @@ -907,10 +911,10 @@ def _commission_lines_for_position( def _current_price(symbol: str | None) -> float | None: - snapshot = _market_snapshot(symbol) + quote = _market_quote(symbol) - if snapshot is not None: - price = snapshot.get("last_price") + if quote is not None: + price = quote.last_price if price is not None: try: parsed = safe_float(price) @@ -922,25 +926,25 @@ def _current_price(symbol: str | None) -> float | None: return None try: - return float(ExchangeService().get_price(symbol).price) + return float(ExchangeService().get_quote(symbol).last_price) except Exception: return None def _signal_entry_price(state) -> float | None: - snapshot = _market_snapshot(state.symbol) + quote = _market_quote(state.symbol) - if snapshot is None: + if quote is None: return _current_price(state.symbol) signal = (state.last_signal or "HOLD").upper() if signal == "BUY": - price = snapshot.get("ask_price") + price = quote.ask_price elif signal == "SELL": - price = snapshot.get("bid_price") + price = quote.bid_price else: - price = snapshot.get("last_price") + price = quote.last_price if price is None: return None diff --git a/app/src/telegram/handlers/debug_auto/ui.py b/app/src/telegram/handlers/debug_auto/ui.py index d4e181d..d217eca 100644 --- a/app/src/telegram/handlers/debug_auto/ui.py +++ b/app/src/telegram/handlers/debug_auto/ui.py @@ -3,10 +3,15 @@ from __future__ import annotations import time +from datetime import datetime, timezone +from decimal import Decimal +from zoneinfo import ZoneInfo from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder +from src.core.config import load_settings +from src.core.types import NumericLike from src.integrations.exchange.service import ExchangeService from src.trading.debug.service import DebugTradeService @@ -113,6 +118,23 @@ def _format_updated_at(value: object) -> str: if not value: return "—" + if isinstance(value, datetime): + current = value + + if current.tzinfo is None: + current = current.replace(tzinfo=timezone.utc) + + try: + settings = load_settings() + + current = current.astimezone( + ZoneInfo(settings.tz), + ) + except Exception: + current = current.astimezone() + + return current.strftime("%H:%M:%S") + text = str(value) if " " in text: @@ -121,6 +143,23 @@ def _format_updated_at(value: object) -> str: return text +def _quote_age_seconds(quote: object) -> float | None: + received_at = getattr(quote, "received_at", None) + if not isinstance(received_at, datetime): + return None + + if received_at.tzinfo is None: + received_at = received_at.replace(tzinfo=timezone.utc) + + return max( + 0.0, + ( + datetime.now(timezone.utc) + - received_at.astimezone(timezone.utc) + ).total_seconds(), + ) + + def _market_snapshot_lines(symbol: str | None) -> list[str]: if not symbol: return [ @@ -141,7 +180,7 @@ def _market_snapshot_lines(symbol: str | None) -> list[str]: error = None try: - market = ExchangeService().get_market_snapshot( + market = ExchangeService().get_quote( symbol, runtime_key="debug_auto", ) @@ -167,11 +206,11 @@ def _market_snapshot_lines(symbol: str | None) -> list[str]: f"Error · {error or 'unknown'}", ] - last_price = market.get("last_price") if market else getattr(execution, "last_price", None) - bid_price = market.get("bid_price") if market else getattr(execution, "bid_price", None) - ask_price = market.get("ask_price") if market else getattr(execution, "ask_price", None) - market_source = market.get("source") if market else "—" - market_age = market.get("age_seconds") if market else None + last_price = market.last_price if market else getattr(execution, "last_price", None) + bid_price = market.bid_price if market else getattr(execution, "bid_price", None) + ask_price = market.ask_price if market else getattr(execution, "ask_price", None) + market_source = market.source if market else "—" + market_age = _quote_age_seconds(market) if market else None execution_source = getattr(execution, "source", "—") if execution else "—" execution_age = getattr(execution, "age_seconds", None) if execution else None @@ -184,7 +223,7 @@ def _market_snapshot_lines(symbol: str | None) -> list[str]: f"Ask · {_format_usd_or_dash(ask_price)}", f"Source · {market_source or '—'}", f"Quote age · {_format_age(market_age)}", - f"Exchange time · {_format_updated_at(market.get('updated_at') if market else None)}", + f"Exchange time · {_format_updated_at(market.exchange_timestamp if market else None)}", "", "Execution", f"Source · {execution_source or '—'}", @@ -274,7 +313,9 @@ def _format_crypto_size(value: float | int | None) -> str: return f"{float(value):.5f}".rstrip("0").rstrip(".") -def _format_money_compact(value: float | int | None) -> str: +def _format_money_compact( + value: float | int | Decimal | None, +) -> str: if value is None: return "—" @@ -286,21 +327,25 @@ def _format_money_compact(value: float | int | None) -> str: return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".") -def _format_usd_or_dash(value: float | int | None) -> str: +def _format_usd_or_dash( + value: float | int | Decimal | None, +) -> str: if value is None: return "—" return f"$ {_format_money_compact(value)}" -def _format_usd_or_off(value: float | int | None) -> str: +def _format_usd_or_off( + value: float | int | Decimal | None, +) -> str: if value is None: - return "off" + return "Выкл." return f"$ {_format_money_compact(value)}" -def _format_signed_usd(value: float | int | None) -> str: +def _format_signed_usd(value: float | int | Decimal | None) -> str: if value is None: return "—" @@ -315,7 +360,7 @@ def _format_signed_usd(value: float | int | None) -> str: return "$ 0" -def _format_age(value: object) -> str: +def _format_age(value: NumericLike | None) -> str: if value is None: return "—" diff --git a/app/src/telegram/handlers/market.py b/app/src/telegram/handlers/market.py deleted file mode 100644 index 88818e7..0000000 --- a/app/src/telegram/handlers/market.py +++ /dev/null @@ -1,505 +0,0 @@ -# app/src/telegram/handlers/market.py - -from __future__ import annotations - -from aiogram import F, Router -from aiogram.fsm.context import FSMContext -from aiogram.types import ( - CallbackQuery, - InaccessibleMessage, - InlineKeyboardMarkup, - Message, -) -from aiogram.utils.keyboard import InlineKeyboardBuilder - -from src.core.numbers import safe_float -from src.core.types import NumericLike -from src.integrations.exchange.exceptions import ExchangeError -from src.integrations.exchange.service import ExchangeService -from src.integrations.exchange.status import ( - ExchangeRuntimeStatus, - ExchangeStatusCode, - build_exchange_error_status, - classify_exchange_error, -) -from src.telegram.live.active_screen import ActiveScreenManager -from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry -from src.telegram.ui.common import mode_line, now_line -from src.telegram.ui.currency_ui import format_usd_amount -from src.telegram.ui.exchange_error import ( - show_callback_exchange_error, - show_message_exchange_error, -) -from src.trading.journal.service import JournalService - - -router = Router(name="market") - -_last_market_prices: dict[str, float] = {} -_last_market_directions: dict[str, str] = {} - - -def _require_message(callback: CallbackQuery) -> Message | None: - message = callback.message - - if message is None or isinstance(message, InaccessibleMessage): - return None - - return message - - -def _market_keyboard() -> InlineKeyboardMarkup: - builder = InlineKeyboardBuilder() - builder.button(text="📊 К мониторингу", callback_data="monitoring:home") - builder.adjust(1) - return builder.as_markup() - - -# собрать текст, когда рынок/биржа недоступны через unified status layer -def _build_market_status_text(status: ExchangeRuntimeStatus) -> str: - icon = "⏸️" if status.code == ExchangeStatusCode.BREAK else "⛔️" - - return ( - "📈 Рынок\n" - f"{mode_line()}" - f"{icon} {status.title}\n\n" - f"{status.message}\n\n" - f"{now_line()}" - ) - - -def _build_market_text( - *, - ticker_price: NumericLike, - name: str, - market_type: str, - base_asset: str, - quote_asset: str, -) -> str: - price = safe_float(ticker_price) - - if price is None: - price = 0.0 - - previous_price = _last_market_prices.get(name) - price_direction = _last_market_directions.get(name, "▲") - - if previous_price is not None: - if price > previous_price: - price_direction = "🔺" - elif price < previous_price: - price_direction = "🔻" - - _last_market_prices[name] = price - _last_market_directions[name] = price_direction - - type_map = { - "LEVERAGE": "leverage", - "SPOT": "spot", - } - market_type_ru = type_map.get(market_type.upper(), market_type.lower()) - - return ( - "📈 Рынок\n" - f"{mode_line()}" - "\n" - f"{base_asset} / {quote_asset} ({market_type_ru})\n\n" - f"$ {format_usd_amount(price)} {price_direction}\n\n" - f"{now_line()}" - ) - - -# live-render должен сам уметь показать ошибку, иначе runner просто потеряет экран -def _build_market_live_text() -> str: - service = ExchangeService() - requested_symbol = service.settings.default_symbol - - try: - runtime_status = service.get_symbol_runtime_status(requested_symbol) - except Exception as exc: - return _build_market_status_text(build_exchange_error_status(exc)) - - if runtime_status.code != ExchangeStatusCode.OPEN: - return _build_market_status_text(runtime_status) - - symbol = runtime_status.symbol or requested_symbol - - validation = service.validate_symbol(symbol) - - if not validation.is_valid: - return _build_market_status_text( - service.get_symbol_runtime_status(requested_symbol) - ) - - ticker = service.get_price(validation.normalized_symbol) - - symbol_info = validation.symbol_info - market_type = symbol_info.market_type if symbol_info else "n/a" - base_asset = ( - symbol_info.base_asset - if symbol_info and symbol_info.base_asset - else "n/a" - ) - quote_asset = ( - symbol_info.quote_asset - if symbol_info and symbol_info.quote_asset - else "n/a" - ) - name = ( - symbol_info.name - if symbol_info and symbol_info.name - else ticker.symbol - ) - - return _build_market_text( - ticker_price=ticker.price, - name=name, - market_type=market_type, - base_asset=base_asset, - quote_asset=quote_asset, - ) - - -def _register_market_live_screen(message: Message) -> None: - bot = message.bot - - if bot is None: - return - - LiveScreenRunner.unregister_message( - chat_id=message.chat.id, - message_id=message.message_id, - ) - - ScreenRegistry.unregister_message( - chat_id=message.chat.id, - message_id=message.message_id, - ) - - LiveScreenRunner.register_screen( - LiveScreen( - screen="market", - bot=bot, - chat_id=message.chat.id, - message_id=message.message_id, - render_text=_build_market_live_text, - render_markup=_market_keyboard, - interval_seconds=5, - ) - ) - - LiveScreenRunner.start("market") - - -async def _prepare_market_from_message(message: Message) -> bool: - bot = message.bot - - if bot is None: - return False - - await ActiveScreenManager.prepare_new_screen( - screen="market", - bot=bot, - chat_id=message.chat.id, - ) - - return True - - -async def _prepare_market_from_callback(callback: CallbackQuery) -> bool: - message = _require_message(callback) - - if message is None: - await callback.answer("Сообщение недоступно", show_alert=True) - return False - - bot = message.bot - - if bot is None: - await callback.answer("Bot недоступен", show_alert=True) - return False - - await ActiveScreenManager.prepare_new_screen( - screen="market", - bot=bot, - chat_id=message.chat.id, - keep_message_id=message.message_id, - ) - - return True - - -async def _send_or_edit_market_screen( - target_message: Message, - *, - text: str, - edit_mode: bool, -) -> None: - if edit_mode: - await target_message.edit_text(text, reply_markup=_market_keyboard()) - _register_market_live_screen(target_message) - ActiveScreenManager.register(screen="market", message=target_message) - return - - sent_message = await target_message.answer( - text, - reply_markup=_market_keyboard(), - ) - _register_market_live_screen(sent_message) - ActiveScreenManager.register(screen="market", message=sent_message) - - -async def _render_market_screen( - target_message: Message, - *, - user_id: int | None, - chat_id: int | None, - edit_mode: bool, - action: str, -) -> None: - service = ExchangeService() - journal = JournalService() - requested_symbol = service.settings.default_symbol - - journal.log_ui_info( - event_type="market_open_requested", - message="Запрошено открытие экрана рынка.", - screen="market", - action=action, - user_id=user_id, - chat_id=chat_id, - payload={"symbol": requested_symbol}, - ) - - runtime_status = service.get_symbol_runtime_status(requested_symbol) - - if runtime_status.code != ExchangeStatusCode.OPEN: - journal.log_ui_warning( - event_type="market_status_blocked", - message=runtime_status.message, - screen="market", - action=action, - user_id=user_id, - chat_id=chat_id, - payload=runtime_status.as_dict(), - ) - - await _send_or_edit_market_screen( - target_message, - text=_build_market_status_text(runtime_status), - edit_mode=edit_mode, - ) - return - - symbol = runtime_status.symbol or requested_symbol - validation = service.validate_symbol(symbol) - - if not validation.is_valid: - invalid_status = service.get_symbol_runtime_status(requested_symbol) - - journal.log_ui_warning( - event_type="market_symbol_invalid", - message=invalid_status.message, - screen="market", - action=action, - user_id=user_id, - chat_id=chat_id, - payload=invalid_status.as_dict(), - ) - - await _send_or_edit_market_screen( - target_message, - text=_build_market_status_text(invalid_status), - edit_mode=edit_mode, - ) - return - - ticker = service.get_price(validation.normalized_symbol) - - symbol_info = validation.symbol_info - market_type = symbol_info.market_type if symbol_info else "n/a" - base_asset = ( - symbol_info.base_asset - if symbol_info and symbol_info.base_asset - else "n/a" - ) - quote_asset = ( - symbol_info.quote_asset - if symbol_info and symbol_info.quote_asset - else "n/a" - ) - name = ( - symbol_info.name - if symbol_info and symbol_info.name - else ticker.symbol - ) - - text = _build_market_text( - ticker_price=ticker.price, - name=name, - market_type=market_type, - base_asset=base_asset, - quote_asset=quote_asset, - ) - - journal.log_ui_info( - event_type="market_open_success", - message="Экран рынка загружен.", - screen="market", - action=action, - user_id=user_id, - chat_id=chat_id, - payload={ - "symbol": ticker.symbol, - "price": safe_float(ticker.price), - "runtime_status": runtime_status.as_dict(), - }, - ) - - await _send_or_edit_market_screen( - target_message, - text=text, - edit_mode=edit_mode, - ) - - -@router.message(F.text == "📈 Рынок") -async def open_market(message: Message, state: FSMContext) -> None: - await state.clear() - - if not await _prepare_market_from_message(message): - return - - user_id = message.from_user.id if message.from_user else None - chat_id = message.chat.id if message.chat else None - - try: - await _render_market_screen( - message, - user_id=user_id, - chat_id=chat_id, - edit_mode=False, - action="open", - ) - except ExchangeError as exc: - JournalService().log_ui_error( - event_type="market_open_error", - message="Не удалось загрузить экран рынка.", - screen="market", - action="open", - user_id=user_id, - chat_id=chat_id, - error_type=classify_exchange_error(exc), - raw_error=str(exc), - ) - - await show_message_exchange_error( - message, - title="📈 Рынок", - exc=exc, - network_details="Рыночные данные недоступны.\nОбнови экран.", - auth_details="Не удалось получить рыночные данные.\nПроверь API ключи.", - retry_callback_data="market:retry", - ) - - -@router.callback_query(F.data == "monitoring:market") -async def open_market_from_monitoring( - callback: CallbackQuery, - state: FSMContext, -) -> None: - await state.clear() - - if not await _prepare_market_from_callback(callback): - return - - message = _require_message(callback) - - if message is None: - await callback.answer("Сообщение недоступно", show_alert=True) - return - - user_id = callback.from_user.id if callback.from_user else None - chat_id = message.chat.id - - try: - await _render_market_screen( - message, - user_id=user_id, - chat_id=chat_id, - edit_mode=True, - action="open_from_monitoring", - ) - await callback.answer() - - except ExchangeError as exc: - JournalService().log_ui_error( - event_type="market_open_error", - message="Не удалось загрузить экран рынка из мониторинга.", - screen="market", - action="open_from_monitoring", - user_id=user_id, - chat_id=chat_id, - error_type=classify_exchange_error(exc), - raw_error=str(exc), - ) - - await show_callback_exchange_error( - callback, - title="📈 Рынок", - exc=exc, - network_details="Рыночные данные недоступны.\nОбнови экран.", - auth_details="Не удалось получить рыночные данные.\nПроверь API ключи.", - retry_callback_data="market:retry", - ) - - -@router.callback_query(F.data == "market:retry") -async def retry_market( - callback: CallbackQuery, - state: FSMContext, -) -> None: - await state.clear() - - if not await _prepare_market_from_callback(callback): - return - - message = _require_message(callback) - - if message is None: - await callback.answer("Сообщение недоступно", show_alert=True) - return - - user_id = callback.from_user.id if callback.from_user else None - chat_id = message.chat.id - - try: - await _render_market_screen( - message, - user_id=user_id, - chat_id=chat_id, - edit_mode=True, - action="retry", - ) - await callback.answer() - - except ExchangeError as exc: - JournalService().log_ui_error( - event_type="market_retry_error", - message="Не удалось обновить экран рынка.", - screen="market", - action="retry", - user_id=user_id, - chat_id=chat_id, - error_type=classify_exchange_error(exc), - raw_error=str(exc), - ) - - await show_callback_exchange_error( - callback, - title="📈 Рынок", - exc=exc, - network_details="Рыночные данные недоступны.\nОбнови экран.", - auth_details="Не удалось получить рыночные данные.\nПроверь API ключи.", - retry_callback_data="market:retry", - ) \ No newline at end of file diff --git a/app/src/telegram/ui/currency_ui.py b/app/src/telegram/ui/currency_ui.py index 81bb168..29c0dac 100644 --- a/app/src/telegram/ui/currency_ui.py +++ b/app/src/telegram/ui/currency_ui.py @@ -3,8 +3,9 @@ from __future__ import annotations from src.integrations.exchange.exceptions import ExchangeError -from src.integrations.exchange.models import BalanceSummary, ExchangeSymbol +from src.integrations.exchange.models import BalanceSummary from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.instrument import Instrument FIAT_CURRENCIES = {"USD", "USDT", "EUR", "RUB", "BYN"} @@ -31,7 +32,10 @@ def is_fiat_currency(currency: str) -> bool: def get_currency_icon(currency: str) -> str: - return CURRENCY_ICONS.get(currency.upper(), currency.upper()) + return CURRENCY_ICONS.get( + currency.upper(), + currency.upper(), + ) def get_currency_label(currency: str) -> str: @@ -45,6 +49,7 @@ def render_currency_title(currency: str) -> str: def format_amount(currency: str, value: float) -> str: if is_fiat_currency(currency): return f"{value:,.2f}".replace(",", " ") + return f"{value:,.8f}".replace(",", " ") @@ -52,7 +57,9 @@ def format_usd_amount(value: float) -> str: return f"{value:,.2f}".replace(",", " ") -def format_usd_price(value: float | int | str | None) -> str: +def format_usd_price( + value: float | int | str | None, +) -> str: if value is None: return "—" @@ -62,7 +69,9 @@ def format_usd_price(value: float | int | str | None) -> str: return "—" -def format_usd_pnl(value: float | int | str | None) -> str: +def format_usd_pnl( + value: float | int | str | None, +) -> str: if value is None: return "—" @@ -87,7 +96,10 @@ def render_currency_line( show_code: bool = True, ) -> str: icon = get_currency_icon(currency) - amount = format_amount(currency, value) + amount = format_amount( + currency, + value, + ) if show_code: return f"{icon} {currency.upper()} · {amount}" @@ -100,61 +112,79 @@ def balance_total(item: BalanceSummary) -> float: def is_zero_balance(item: BalanceSummary) -> bool: - return abs(item.available) < 1e-12 and abs(item.locked) < 1e-12 + return ( + abs(item.available) < 1e-12 + and abs(item.locked) < 1e-12 + ) def _quote_priority(quote_asset: str) -> int: value = (quote_asset or "").upper() + if value == "USD": return 3 + if value == "USDT": return 2 + return 0 def _status_priority(status: str) -> int: value = (status or "").upper() + if value == "TRADING": return 2 + if value in {"HALT", "BREAK"}: return 0 + return 1 def _market_type_priority(market_type: str) -> int: value = (market_type or "").upper() + if value == "SPOT": return 3 + if value == "LEVERAGE": return 2 + return 1 -def _symbol_priority(symbol_info: ExchangeSymbol) -> tuple[int, int, int, str]: +def _instrument_priority( + instrument: Instrument, +) -> tuple[int, int, int, str]: return ( - _quote_priority(symbol_info.quote_asset), - _status_priority(symbol_info.status), - _market_type_priority(symbol_info.market_type), - symbol_info.symbol.upper(), + _quote_priority(instrument.quote_asset), + _status_priority(instrument.status), + _market_type_priority(instrument.market_type), + instrument.symbol.upper(), ) -def _resolve_asset_quote_symbol( +def _resolve_asset_quote_instrument( exchange_service: ExchangeService, asset: str, -) -> ExchangeSymbol | None: +) -> Instrument | None: asset_upper = asset.upper() try: - symbols = exchange_service.get_exchange_symbols() + instruments = exchange_service.get_instruments() except ExchangeError: return None - candidates: list[ExchangeSymbol] = [] + candidates: list[Instrument] = [] - for symbol_info in symbols: - base_asset = (symbol_info.base_asset or "").upper() - quote_asset = (symbol_info.quote_asset or "").upper() + for instrument in instruments: + base_asset = ( + instrument.base_asset or "" + ).upper() + quote_asset = ( + instrument.quote_asset or "" + ).upper() if base_asset != asset_upper: continue @@ -162,12 +192,16 @@ def _resolve_asset_quote_symbol( if quote_asset not in {"USD", "USDT"}: continue - candidates.append(symbol_info) + candidates.append(instrument) if not candidates: return None - candidates.sort(key=_symbol_priority, reverse=True) + candidates.sort( + key=_instrument_priority, + reverse=True, + ) + return candidates[0] @@ -184,18 +218,26 @@ def get_asset_usd_rate( if asset in price_cache: return price_cache[asset] - symbol_info = _resolve_asset_quote_symbol(exchange_service, asset) - if symbol_info is None: + instrument = _resolve_asset_quote_instrument( + exchange_service, + asset, + ) + + if instrument is None: price_cache[asset] = None return None try: - ticker = exchange_service.get_price(symbol_info.symbol) - rate = float(ticker.price) + quote = exchange_service.get_quote( + instrument.symbol + ) + rate = float(quote.last_price) - # Пока считаем USDT ~= USD + # Пока считаем USDT ~= USD. price_cache[asset] = rate + return rate + except ExchangeError: price_cache[asset] = None return None @@ -207,10 +249,16 @@ def estimate_balance_usd( price_cache: dict[str, float | None], ) -> float | None: total = balance_total(item) + if total <= 0: return None - rate = get_asset_usd_rate(exchange_service, item.currency, price_cache) + rate = get_asset_usd_rate( + exchange_service, + item.currency, + price_cache, + ) + if rate is None: return None diff --git a/app/src/trading/auto/execution_quality.py b/app/src/trading/auto/execution_quality.py index 5191bd4..d767b25 100644 --- a/app/src/trading/auto/execution_quality.py +++ b/app/src/trading/auto/execution_quality.py @@ -6,6 +6,7 @@ import time from src.core.numbers import safe_float from src.core.types import NumericLike +from src.integrations.exchange.models import ExecutionPriceSnapshot from src.integrations.exchange.service import ExchangeService from src.integrations.exchange.status import ( ExchangeRuntimeStatus, @@ -253,34 +254,20 @@ class AutoExecutionQualityMixin: return try: - snapshot = ExchangeService().get_market_snapshot( + snapshot = ExchangeService().get_execution_snapshot( state.symbol, runtime_key="auto", ) - age_seconds = safe_float(snapshot.get("age_seconds")) - - if ( - age_seconds is not None - and age_seconds > self._warning_snapshot_age_seconds - ): - try: - snapshot = ExchangeService().refresh_market_snapshot_cache( - state.symbol, - runtime_key="auto", - ) - except Exception: - pass - except Exception as exc: fallback_price = None try: fallback_price = safe_float( - ExchangeService().get_price( + ExchangeService().get_quote( state.symbol, runtime_key="auto", - ).price + ).last_price ) except Exception: pass @@ -319,12 +306,12 @@ class AutoExecutionQualityMixin: ) return - bid_price = safe_float(snapshot.get("bid_price")) - ask_price = safe_float(snapshot.get("ask_price")) - last_price = safe_float(snapshot.get("last_price")) - age_seconds = safe_float(snapshot.get("age_seconds")) - is_fresh = bool(snapshot.get("is_fresh", False)) - source = str(snapshot.get("source") or "") + bid_price = safe_float(snapshot.bid_price) + ask_price = safe_float(snapshot.ask_price) + last_price = safe_float(snapshot.last_price) + age_seconds = safe_float(snapshot.age_seconds) + is_fresh = snapshot.is_fresh + source = snapshot.source self._sync_execution_pricing_state( state, @@ -432,15 +419,15 @@ class AutoExecutionQualityMixin: def _sync_execution_pricing_state( self, state: AutoTradeState, - snapshot: dict[str, object], + snapshot: ExecutionPriceSnapshot, ) -> None: - age_seconds = safe_float(snapshot.get("age_seconds")) + age_seconds = safe_float(snapshot.age_seconds) - state.execution_price_source = str(snapshot.get("source") or "") + state.execution_price_source = snapshot.source state.execution_price_age_seconds = age_seconds - state.execution_bid_price = safe_float(snapshot.get("bid_price")) - state.execution_ask_price = safe_float(snapshot.get("ask_price")) - state.execution_last_price = safe_float(snapshot.get("last_price")) + state.execution_bid_price = safe_float(snapshot.bid_price) + state.execution_ask_price = safe_float(snapshot.ask_price) + state.execution_last_price = safe_float(snapshot.last_price) if age_seconds is None: state.execution_price_freshness = "UNKNOWN" diff --git a/app/src/trading/auto/signal_runtime.py b/app/src/trading/auto/signal_runtime.py index b9687a5..63dc908 100644 --- a/app/src/trading/auto/signal_runtime.py +++ b/app/src/trading/auto/signal_runtime.py @@ -9,6 +9,7 @@ from src.core.event_bus import EventBus from src.core.numbers import safe_float from src.core.types import JsonDict, NumericLike from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.quote import Quote from src.trading.auto.state import AutoTradeState from src.trading.auto.state_reset import ( reset_after_market_runtime_expired, @@ -716,7 +717,7 @@ class AutoSignalRuntimeMixin: self, *, state: AutoTradeState, - snapshot: JsonDict, + quote: Quote | None, signal: str, signal_intent: str, confidence: float, @@ -787,9 +788,9 @@ class AutoSignalRuntimeMixin: "snapshot_age_seconds": state.snapshot_age_seconds, # ---------- Live Snapshot ---------- - "bid_price": snapshot.get("bid_price"), - "ask_price": snapshot.get("ask_price"), - "last_price": snapshot.get("last_price"), + "bid_price": safe_float(quote.bid_price) if quote is not None else None, + "ask_price": safe_float(quote.ask_price) if quote is not None else None, + "last_price": safe_float(quote.last_price) if quote is not None else None, # ---------- Market Score ---------- "market_score": state.market_score, @@ -875,12 +876,12 @@ class AutoSignalRuntimeMixin: return try: - snapshot = ExchangeService().get_market_snapshot( + quote = ExchangeService().get_quote( state.symbol, runtime_key="auto", ) except Exception: - snapshot = {} + quote = None try: JournalService().log_ui_info( @@ -892,7 +893,7 @@ class AutoSignalRuntimeMixin: action="signal_ready", payload=self._build_ready_signal_payload( state=state, - snapshot=snapshot, + quote=quote, signal=normalized_signal, signal_intent=signal_intent, confidence=confidence, diff --git a/app/src/trading/debug/execution.py b/app/src/trading/debug/execution.py index 9b46229..4aa6574 100644 --- a/app/src/trading/debug/execution.py +++ b/app/src/trading/debug/execution.py @@ -5,6 +5,7 @@ from __future__ import annotations import math from datetime import datetime +from src.core.types import NumericLike from src.integrations.exchange.service import ExchangeService from src.trading.debug.state import DebugPositionState, DebugTradeState from src.trading.execution.models import ExecutionDecision @@ -389,49 +390,88 @@ class DebugExecutionEngine: return self._market_last_price(state.symbol) def _entry_price_for_side(self, symbol: str, side: str) -> float: - snapshot = ExchangeService().get_fresh_market_snapshot(symbol) + snapshot = ExchangeService().get_execution_snapshot( + symbol, + runtime_key="debug_auto", + ) if side == "LONG": - return self._snapshot_price(snapshot, "ask_price", "last_price") + return self._execution_price( + snapshot.ask_price, + snapshot.last_price, + price_name="ask_price", + ) if side == "SHORT": - return self._snapshot_price(snapshot, "bid_price", "last_price") + return self._execution_price( + snapshot.bid_price, + snapshot.last_price, + price_name="bid_price", + ) - return self._snapshot_price(snapshot, "last_price") + return self._execution_price( + snapshot.last_price, + price_name="last_price", + ) def _exit_price_for_side(self, symbol: str, side: str) -> float: - snapshot = ExchangeService().get_fresh_market_snapshot(symbol) + snapshot = ExchangeService().get_execution_snapshot( + symbol, + runtime_key="debug_auto", + ) if side == "LONG": - return self._snapshot_price(snapshot, "bid_price", "last_price") + return self._execution_price( + snapshot.bid_price, + snapshot.last_price, + price_name="bid_price", + ) if side == "SHORT": - return self._snapshot_price(snapshot, "ask_price", "last_price") + return self._execution_price( + snapshot.ask_price, + snapshot.last_price, + price_name="ask_price", + ) - return self._snapshot_price(snapshot, "last_price") + return self._execution_price( + snapshot.last_price, + price_name="last_price", + ) def _market_last_price(self, symbol: str) -> float: - snapshot = ExchangeService().get_fresh_market_snapshot(symbol) - return self._snapshot_price(snapshot, "last_price") + snapshot = ExchangeService().get_execution_snapshot( + symbol, + runtime_key="debug_auto", + ) + return self._execution_price( + snapshot.last_price, + price_name="last_price", + ) - def _snapshot_price( + def _execution_price( self, - snapshot: dict[str, object], - primary_key: str, - fallback_key: str | None = None, + raw_price: NumericLike | None, + fallback_price: NumericLike | None = None, + *, + price_name: str, ) -> float: - raw_price = snapshot.get(primary_key) + value = raw_price - if raw_price is None and fallback_key is not None: - raw_price = snapshot.get(fallback_key) + if value is None: + value = fallback_price - if raw_price is None: - raise ValueError(f"Market snapshot price '{primary_key}' is missing.") + if value is None: + raise ValueError( + f"Execution price '{price_name}' is missing." + ) - price = float(raw_price) + price = float(value) if price <= 0: - raise ValueError(f"Market snapshot price '{primary_key}' is invalid: {price}") + raise ValueError( + f"Execution price '{price_name}' is invalid: {price}" + ) return price diff --git a/app/src/trading/decision/__init__.py b/app/src/trading/decision/__init__.py new file mode 100644 index 0000000..854fb37 --- /dev/null +++ b/app/src/trading/decision/__init__.py @@ -0,0 +1 @@ +# app/src/trading/decision/__init__.py \ No newline at end of file diff --git a/app/src/trading/decision/exceptions.py b/app/src/trading/decision/exceptions.py new file mode 100644 index 0000000..dc5fd33 --- /dev/null +++ b/app/src/trading/decision/exceptions.py @@ -0,0 +1,19 @@ +# app/src/trading/decision/exceptions.py + +from __future__ import annotations + + +class TradingError(Exception): + """Базовая ошибка Trading Layer.""" + + +class InvalidTradingDecisionError(TradingError): + """Trading Layer сформировал некорректное торговое решение.""" + + +class TradingValidationError(TradingError): + """Ошибка проверки входных данных Trading Layer.""" + + +class TradingExecutionError(TradingError): + """Ошибка выполнения Trading Layer.""" \ No newline at end of file diff --git a/app/src/trading/decision/models.py b/app/src/trading/decision/models.py new file mode 100644 index 0000000..588f64b --- /dev/null +++ b/app/src/trading/decision/models.py @@ -0,0 +1,67 @@ +# app/src/trading/decision/models.py + +from __future__ import annotations + +from dataclasses import dataclass, field + +from src.trading.market_intelligence.common.enums import EngineStatus +from src.trading.market_intelligence.common.models import CoordinatorResult +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.scores import ( + EngineConfidence, + EngineScore, +) +from src.trading.market_intelligence.common.types import ( + ContextDict, + DiagnosticMessages, + DurationMs, + PayloadDict, +) + + +@dataclass(frozen=True, slots=True) +class TradingDiagnostics: + reason: ReasonCode = ReasonCode.UNKNOWN + details: ContextDict = field(default_factory=dict) + warnings: DiagnosticMessages = field(default_factory=list) + errors: DiagnosticMessages = field(default_factory=list) + + @property + def has_warnings(self) -> bool: + return bool(self.warnings) + + @property + def has_errors(self) -> bool: + return bool(self.errors) + + +@dataclass(frozen=True, slots=True) +class TradingEvaluationMeta: + trading_version: str + calculated_at: float | None = None + duration_ms: DurationMs | None = None + + +@dataclass(frozen=True, slots=True) +class TradingDecision: + coordinator_result: CoordinatorResult + diagnostics: TradingDiagnostics = field(default_factory=TradingDiagnostics) + meta: TradingEvaluationMeta | None = None + payload: PayloadDict = field(default_factory=dict) + + status: EngineStatus = EngineStatus.UNKNOWN + score: EngineScore = field(default_factory=EngineScore) + confidence: EngineConfidence = field(default_factory=EngineConfidence) + reason: ReasonCode = ReasonCode.UNKNOWN + + @property + def is_usable(self) -> bool: + return self.status in { + EngineStatus.OK, + EngineStatus.PARTIAL, + EngineStatus.STALE, + } + + @property + def has_errors(self) -> bool: + return self.diagnostics.has_errors \ No newline at end of file diff --git a/app/src/trading/decision/protocol.py b/app/src/trading/decision/protocol.py new file mode 100644 index 0000000..85fb6bf --- /dev/null +++ b/app/src/trading/decision/protocol.py @@ -0,0 +1,19 @@ +# app/src/trading/decision/protocol.py + +from __future__ import annotations + +from typing import Protocol + +from src.trading.market_intelligence.common.models import CoordinatorResult +from src.trading.decision.models import TradingDecision + + +class TradingProtocol(Protocol): + """Контракт Trading Layer.""" + + async def decide( + self, + coordinator_result: CoordinatorResult, + ) -> TradingDecision: + """Принять торговое решение на основе CoordinatorResult.""" + ... \ No newline at end of file diff --git a/app/src/trading/decision/rules.py b/app/src/trading/decision/rules.py new file mode 100644 index 0000000..6fe19a3 --- /dev/null +++ b/app/src/trading/decision/rules.py @@ -0,0 +1,65 @@ +# app/src/trading/decision/rules.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.enums import EngineStatus +from src.trading.market_intelligence.common.models import CoordinatorResult +from src.trading.decision.models import ( + TradingDecision, + TradingDiagnostics, + TradingEvaluationMeta, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.scores import ( + EngineConfidence, + EngineScore, +) + + +class TradingRules: + """Правила формирования торгового решения.""" + + def decide( + self, + coordinator_result: CoordinatorResult, + ) -> TradingDecision: + """Сформировать TradingDecision.""" + return TradingDecision( + coordinator_result=coordinator_result, + diagnostics=TradingDiagnostics(), + meta=TradingEvaluationMeta( + trading_version="1.0", + ), + status=self._resolve_status(coordinator_result), + score=self._resolve_score(coordinator_result), + confidence=self._resolve_confidence(coordinator_result), + reason=self._resolve_reason(coordinator_result), + ) + + def _resolve_status( + self, + coordinator_result: CoordinatorResult, + ) -> EngineStatus: + """Определить итоговый статус Trading.""" + return coordinator_result.status + + def _resolve_score( + self, + coordinator_result: CoordinatorResult, + ) -> EngineScore: + """Вычислить итоговый Score.""" + return EngineScore() + + def _resolve_confidence( + self, + coordinator_result: CoordinatorResult, + ) -> EngineConfidence: + """Вычислить итоговый Confidence.""" + return EngineConfidence() + + def _resolve_reason( + self, + coordinator_result: CoordinatorResult, + ) -> ReasonCode: + """Определить причину принятого решения.""" + return ReasonCode.UNKNOWN \ No newline at end of file diff --git a/app/src/trading/decision/service.py b/app/src/trading/decision/service.py new file mode 100644 index 0000000..217f1e7 --- /dev/null +++ b/app/src/trading/decision/service.py @@ -0,0 +1,29 @@ +# app/src/trading/decision/service.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import CoordinatorResult +from src.trading.decision.models import TradingDecision +from src.trading.decision.protocol import TradingProtocol +from src.trading.decision.rules import TradingRules +from src.trading.decision.validation import ( + TradingValidation, +) + + +class TradingService(TradingProtocol): + """Единая публичная точка входа Trading Layer.""" + + def __init__(self) -> None: + """Создать Trading Service.""" + self._validation = TradingValidation() + self._rules = TradingRules() + + async def decide( + self, + coordinator_result: CoordinatorResult, + ) -> TradingDecision: + """Сформировать торговое решение.""" + self._validation.validate(coordinator_result) + + return self._rules.decide(coordinator_result) \ No newline at end of file diff --git a/app/src/trading/decision/validation.py b/app/src/trading/decision/validation.py new file mode 100644 index 0000000..5f94521 --- /dev/null +++ b/app/src/trading/decision/validation.py @@ -0,0 +1,51 @@ +# app/src/trading/decision/validation.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import CoordinatorResult +from src.trading.decision.exceptions import ( + TradingValidationError, +) + + +class TradingValidation: + """Проверка входного CoordinatorResult для Trading Layer.""" + + def validate( + self, + coordinator_result: CoordinatorResult, + ) -> None: + """Проверить CoordinatorResult перед принятием торгового решения.""" + self._validate_result_exists(coordinator_result) + self._validate_result_usable(coordinator_result) + self._validate_result_has_no_errors(coordinator_result) + + def _validate_result_exists( + self, + coordinator_result: CoordinatorResult, + ) -> None: + """Проверить, что CoordinatorResult передан.""" + if coordinator_result is None: + raise TradingValidationError( + "CoordinatorResult is required for Trading." + ) + + def _validate_result_usable( + self, + coordinator_result: CoordinatorResult, + ) -> None: + """Проверить пригодность CoordinatorResult для принятия решения.""" + if not coordinator_result.is_usable: + raise TradingValidationError( + "CoordinatorResult is not usable." + ) + + def _validate_result_has_no_errors( + self, + coordinator_result: CoordinatorResult, + ) -> None: + """Проверить отсутствие критических ошибок Coordinator.""" + if coordinator_result.has_errors: + raise TradingValidationError( + "CoordinatorResult contains errors." + ) \ No newline at end of file diff --git a/app/src/trading/diagnostics/snapshot.py b/app/src/trading/diagnostics/snapshot.py index be48414..bc3c0de 100644 --- a/app/src/trading/diagnostics/snapshot.py +++ b/app/src/trading/diagnostics/snapshot.py @@ -259,20 +259,20 @@ class SemanticDiagnosticSnapshotBuilder: try: from src.integrations.exchange.service import ExchangeService - snapshot = ExchangeService().get_market_snapshot( + quote = ExchangeService().get_quote( state.symbol, runtime_key="auto", ) side = str(state.position_side or "").upper() - price = snapshot.get("last_price") + price = quote.last_price if side == "LONG": - price = snapshot.get("bid_price") or price + price = quote.bid_price or price elif side == "SHORT": - price = snapshot.get("ask_price") or price + price = quote.ask_price or price return safe_float(price) diff --git a/app/src/trading/market_intelligence/__init__.py b/app/src/trading/market_intelligence/__init__.py new file mode 100644 index 0000000..de5f548 --- /dev/null +++ b/app/src/trading/market_intelligence/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/__init__.py diff --git a/app/src/trading/market_intelligence/common/__init__.py b/app/src/trading/market_intelligence/common/__init__.py new file mode 100644 index 0000000..7401f14 --- /dev/null +++ b/app/src/trading/market_intelligence/common/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/common/__init__.py diff --git a/app/src/trading/market_intelligence/common/checks.py b/app/src/trading/market_intelligence/common/checks.py new file mode 100644 index 0000000..eedb0fa --- /dev/null +++ b/app/src/trading/market_intelligence/common/checks.py @@ -0,0 +1,130 @@ +# app/src/trading/market_intelligence/common/checks.py + +from __future__ import annotations + +from dataclasses import dataclass, field + +from src.trading.market_intelligence.common.enums import ( + CheckStatus, + ProcessingStage, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.types import ( + ContextDict, + ReasonText, +) + + +@dataclass(frozen=True, slots=True) +class EngineCheck: + # Результат одной архитектурной проверки. + # + # Проверка относится не ко всему Engine, а к одному этапу обработки. + # Например: + # + # INPUT + # CALCULATION + # VALIDATION + # RESULT + # + # Engine может выполнить несколько независимых проверок, + # после чего они объединяются в общий отчёт. + stage: ProcessingStage + status: CheckStatus + reason: ReasonCode + message: ReasonText + details: ContextDict = field(default_factory=dict) + + @property + def is_ok(self) -> bool: + return self.status == CheckStatus.OK + + @property + def is_warning(self) -> bool: + return self.status == CheckStatus.WARNING + + @property + def is_error(self) -> bool: + return self.status == CheckStatus.ERROR + + @property + def is_skipped(self) -> bool: + return self.status == CheckStatus.SKIPPED + + +@dataclass(frozen=True, slots=True) +class EngineCheckReport: + # Общий результат внутренних проверок Engine. + # + # Отчёт не содержит торговой логики. + # Его задача — показать, какие этапы обработки были + # успешно выполнены, а какие завершились предупреждением, + # ошибкой или были пропущены. + checks: tuple[EngineCheck, ...] = () + + @property + def has_errors(self) -> bool: + return any(check.is_error for check in self.checks) + + @property + def has_warnings(self) -> bool: + return any(check.is_warning for check in self.checks) + + @property + def has_skipped(self) -> bool: + return any(check.is_skipped for check in self.checks) + + @property + def overall_status(self) -> CheckStatus: + # Общий статус определяется по наиболее серьёзному результату. + if self.has_errors: + return CheckStatus.ERROR + + if self.has_warnings: + return CheckStatus.WARNING + + if self.has_skipped: + return CheckStatus.SKIPPED + + return CheckStatus.OK + + @property + def completed_checks(self) -> int: + return sum( + check.status != CheckStatus.SKIPPED + for check in self.checks + ) + + @property + def total_checks(self) -> int: + return len(self.checks) + + +def build_check( + *, + stage: ProcessingStage, + status: CheckStatus, + reason: ReasonCode, + message: ReasonText, + details: ContextDict | None = None, +) -> EngineCheck: + # Создаёт одну архитектурную проверку. + # + # Используется всеми Engine для формирования единого + # формата внутренних проверок. + return EngineCheck( + stage=stage, + status=status, + reason=reason, + message=message, + details=details or {}, + ) + + +def build_check_report( + *checks: EngineCheck, +) -> EngineCheckReport: + # Собирает общий отчёт из набора отдельных проверок. + # + # Порядок проверок сохраняется. + return EngineCheckReport(checks=checks) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/constants.py b/app/src/trading/market_intelligence/common/constants.py new file mode 100644 index 0000000..d284f41 --- /dev/null +++ b/app/src/trading/market_intelligence/common/constants.py @@ -0,0 +1,125 @@ +# app/src/trading/market_intelligence/common/constants.py + +from __future__ import annotations + + +# Минимальная и максимальная оценка движка. +# Все аналитические оценки в Market Intelligence должны быть в диапазоне 0...100. +MIN_SCORE = 0.0 +MAX_SCORE = 100.0 + + +# Минимальная и максимальная уверенность движка. +# Уверенность показывает не силу сигнала, а насколько движок доверяет своему выводу. +MIN_CONFIDENCE = 0.0 +MAX_CONFIDENCE = 1.0 + + +# Минимальная и максимальная вероятность. +# Вероятность используется, например, для оценки продолжения движения или изменения направления. +MIN_PROBABILITY = 0.0 +MAX_PROBABILITY = 100.0 + + +# Минимальный и максимальный вес показателя. +# Вес показывает, насколько сильно отдельный показатель влияет на итоговую оценку. +MIN_WEIGHT = 0.0 +MAX_WEIGHT = 1.0 + + +# Значения по умолчанию. +# Они используются, когда данных недостаточно или движок безопасно возвращает пустой результат. +DEFAULT_SCORE = 0.0 +DEFAULT_CONFIDENCE = 0.0 +DEFAULT_PROBABILITY = 0.0 +DEFAULT_WEIGHT = 1.0 + + +# Границы для словесной оценки качества score. +# Эти значения не принимают торговых решений, а только помогают читать диагностику. +WEAK_SCORE_THRESHOLD = 30.0 +NORMAL_SCORE_THRESHOLD = 50.0 +GOOD_SCORE_THRESHOLD = 70.0 +EXCELLENT_SCORE_THRESHOLD = 85.0 + + +# Границы для словесной оценки confidence. +# Число confidence остаётся основным значением, а эти границы помогают +# объяснять его человеку в журнале и диагностике. +VERY_LOW_CONFIDENCE_THRESHOLD = 0.15 +LOW_CONFIDENCE_THRESHOLD = 0.30 +NORMAL_CONFIDENCE_THRESHOLD = 0.50 +HIGH_CONFIDENCE_THRESHOLD = 0.70 +VERY_HIGH_CONFIDENCE_THRESHOLD = 0.85 + + +# Возраст данных по умолчанию. +# Если данные старше этого значения, результат можно считать устаревшим. +DEFAULT_STALE_AFTER_SECONDS = 180.0 + + +# Время жизни аналитического сигнала по умолчанию. +# Старый сигнал постепенно теряет значение для анализа. +DEFAULT_SIGNAL_TTL_SECONDS = 180.0 + + +# Ограничение на количество зависимостей одного движка. +# Это защитный архитектурный предел, чтобы движки не превращались +# в большие модули, зависящие от всей платформы сразу. +MAX_ENGINE_DEPENDENCIES = 8 + + +# Ограничение на количество метрик в одном результате. +# Если метрик становится слишком много, значит движок, возможно, +# начинает выполнять чужую работу и его нужно разделить. +MAX_ENGINE_METRICS = 32 + + +# Ограничение на количество предупреждений в диагностике. +# Это защищает журнал от слишком шумных записей. +MAX_DIAGNOSTIC_WARNINGS = 16 + + +# Ограничение на количество ошибок в диагностике. +# Даже если ошибок много, в результате нужно сохранять только полезную часть. +MAX_DIAGNOSTIC_ERRORS = 16 + + +# Базовый набор таймфреймов для первого этапа Market Intelligence. +# Движки не должны жёстко проверять эти значения внутри своей логики. +DEFAULT_TIMEFRAMES = ( + "1m", + "5m", + "15m", + "1h", +) + + +# Таймфреймы, которые архитектура должна поддерживать в будущем. +# Они указаны здесь как допустимое расширение, но не обязаны использоваться +# на первом этапе реализации. +FUTURE_TIMEFRAMES = ( + "4h", + "1d", + "1w", +) + + +# Поля, которые запрещены в результатах Market Intelligence. +# Аналитический слой не должен принимать торговые решения или создавать заявки. +FORBIDDEN_TRADING_FIELDS = frozenset( + { + "should_buy", + "should_sell", + "should_enter", + "should_exit", + "should_close", + "should_flip", + "open_position", + "close_position", + "place_order", + "cancel_order", + "leverage", + "order_id", + } +) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/enums.py b/app/src/trading/market_intelligence/common/enums.py new file mode 100644 index 0000000..90e9118 --- /dev/null +++ b/app/src/trading/market_intelligence/common/enums.py @@ -0,0 +1,152 @@ +# app/src/trading/market_intelligence/common/enums.py + +from __future__ import annotations + +from enum import StrEnum + + +class MarketDirection(StrEnum): + # Направление движения цены. + # Это не торговое решение, а только описание того, + # куда в основном двигалась цена в анализируемом участке. + UNKNOWN = "unknown" + UP = "up" + DOWN = "down" + SIDEWAYS = "sideways" + MIXED = "mixed" + + +class MarketBias(StrEnum): + # Общий перекос рынка. + # Например, рынок может быть больше в пользу роста, + # больше в пользу снижения или противоречивым. + UNKNOWN = "unknown" + BULLISH = "bullish" + BEARISH = "bearish" + NEUTRAL = "neutral" + CONFLICTED = "conflicted" + + +class MarketPhase(StrEnum): + # Фаза рынка простыми словами: + # рынок может ускоряться, откатываться, восстанавливаться, + # расширять движение, выдыхаться или разворачиваться. + UNKNOWN = "unknown" + IMPULSE = "impulse" + PULLBACK = "pullback" + RECOVERY = "recovery" + EXPANSION = "expansion" + EXHAUSTION = "exhaustion" + REVERSAL = "reversal" + CONSOLIDATION = "consolidation" + + +class MarketRegime(StrEnum): + # Режим рынка описывает общий характер поведения цены. + # Это помогает понять, рынок сейчас движется направленно, + # стоит в диапазоне, резко меняется или ведёт себя нестабильно. + UNKNOWN = "unknown" + TRENDING = "trending" + RANGE = "range" + BREAKOUT = "breakout" + MEAN_REVERSION = "mean_reversion" + HIGH_VOLATILITY = "high_volatility" + LOW_VOLATILITY = "low_volatility" + PANIC = "panic" + EUPHORIA = "euphoria" + ACCUMULATION = "accumulation" + DISTRIBUTION = "distribution" + + +class MarketQuality(StrEnum): + # Качество рынка показывает, насколько рынок понятен для анализа. + # Плохое качество означает много шума и мало надёжных признаков. + UNKNOWN = "unknown" + POOR = "poor" + WEAK = "weak" + NORMAL = "normal" + GOOD = "good" + EXCELLENT = "excellent" + + +class EngineStatus(StrEnum): + # Статус показывает, насколько успешно движок выполнил свою работу. + # Даже при ошибке движок должен вернуть понятный статус, + # чтобы вся система могла продолжить работу безопасно. + UNKNOWN = "unknown" + OK = "ok" + PARTIAL = "partial" + STALE = "stale" + INSUFFICIENT_DATA = "insufficient_data" + ERROR = "error" + DISABLED = "disabled" + + +class ConfidenceLevel(StrEnum): + # Словесный уровень уверенности. + # Число confidence удобно для расчётов, но человеку проще читать + # понятный уровень: низкая, нормальная или высокая уверенность. + UNKNOWN = "unknown" + VERY_LOW = "very_low" + LOW = "low" + NORMAL = "normal" + HIGH = "high" + VERY_HIGH = "very_high" + + +class SignalFreshness(StrEnum): + # Свежесть сигнала показывает его возраст. + # Старый сигнал постепенно теряет значение для анализа. + UNKNOWN = "unknown" + NEW = "new" + ACTIVE = "active" + AGING = "aging" + EXPIRED = "expired" + + +class RiskLevel(StrEnum): + # Общий уровень риска рыночной ситуации. + # Это не решение закрыть или открыть сделку, + # а только оценка сложности текущего рынка. + UNKNOWN = "unknown" + LOW = "low" + NORMAL = "normal" + ELEVATED = "elevated" + HIGH = "high" + CRITICAL = "critical" + + +class TimeframeRole(StrEnum): + # Роль временного интервала в общем анализе. + # Например, младший интервал показывает детали, + # а старший помогает понять общий фон. + UNKNOWN = "unknown" + LOWER = "lower" + PRIMARY = "primary" + HIGHER = "higher" + CONFIRMATION = "confirmation" + + +class CheckStatus(StrEnum): + # Статус внутренней проверки блока. + # Используется, чтобы проверять движки по частям, + # а не ждать завершения всего движка целиком. + OK = "ok" + WARNING = "warning" + ERROR = "error" + SKIPPED = "skipped" + + +class ProcessingStage(StrEnum): + # Этап обработки внутри движка или общего блока. + # Это не название файла, а смысловая часть работы: + # входные данные, расчёт, оценка, проверка, payload или результат. + UNKNOWN = "unknown" + INPUT = "input" + CALCULATION = "calculation" + EVALUATION = "evaluation" + VALIDATION = "validation" + PAYLOAD = "payload" + SNAPSHOT = "snapshot" + RESULT = "result" + EVENT = "event" \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/events.py b/app/src/trading/market_intelligence/common/events.py new file mode 100644 index 0000000..01c3420 --- /dev/null +++ b/app/src/trading/market_intelligence/common/events.py @@ -0,0 +1,110 @@ +# app/src/trading/market_intelligence/common/events.py + +from __future__ import annotations + +from dataclasses import dataclass, field +from time import time + +from src.trading.market_intelligence.common.models import EngineResult +from src.trading.market_intelligence.common.payloads import ( + engine_result_to_payload, +) +from src.trading.market_intelligence.common.snapshots import ( + EngineSnapshot, + build_engine_snapshot, + engine_snapshot_to_payload, +) +from src.trading.market_intelligence.common.types import ( + EngineName, + PayloadDict, + SymbolName, + TimeframeName, +) + + +@dataclass(frozen=True, slots=True) +class MarketIntelligenceEvent: + # Событие Market Intelligence описывает факт завершения + # аналитического действия. + # + # Важно: этот объект сам ничего не публикует. + # Он только задаёт единый формат события, который позже сможет + # использовать EventBus, журнал или Runtime. + event_type: str + engine_name: EngineName + symbol: SymbolName + timeframe: TimeframeName + created_at: float + payload: PayloadDict = field(default_factory=dict) + + +def build_engine_result_event( + result: EngineResult, + *, + event_type: str = "market_intelligence.engine_result", +) -> MarketIntelligenceEvent: + # Формирует событие напрямую из EngineResult. + # + # Используется, когда нужно зафиксировать сам факт получения + # результата Engine без отдельного Snapshot. + return MarketIntelligenceEvent( + event_type=event_type, + engine_name=result.engine_name, + symbol=result.symbol, + timeframe=result.timeframe, + created_at=time(), + payload=engine_result_to_payload(result), + ) + + +def build_engine_snapshot_event( + snapshot: EngineSnapshot, + *, + event_type: str = "market_intelligence.engine_snapshot", +) -> MarketIntelligenceEvent: + # Формирует событие из уже созданного Snapshot. + # + # Такой вариант нужен, когда сначала фиксируется состояние результата, + # а уже затем это состояние передаётся во внешний слой событий. + return MarketIntelligenceEvent( + event_type=event_type, + engine_name=snapshot.engine_name, + symbol=snapshot.symbol, + timeframe=snapshot.timeframe, + created_at=time(), + payload=engine_snapshot_to_payload(snapshot), + ) + + +def build_result_snapshot_event( + result: EngineResult, + *, + event_type: str = "market_intelligence.engine_snapshot", +) -> MarketIntelligenceEvent: + # Удобный безопасный путь: + # EngineResult -> EngineSnapshot -> Event. + # + # Это сохраняет единый порядок фиксации результата анализа. + snapshot = build_engine_snapshot(result) + + return build_engine_snapshot_event( + snapshot, + event_type=event_type, + ) + + +def market_intelligence_event_to_payload( + event: MarketIntelligenceEvent, +) -> PayloadDict: + # Преобразует событие в обычный словарь. + # + # Функция нужна для будущей передачи события в журнал, + # EventBus или внешний Runtime-слой. + return { + "event_type": event.event_type, + "engine_name": event.engine_name, + "symbol": event.symbol, + "timeframe": event.timeframe, + "created_at": event.created_at, + "payload": event.payload, + } \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/models.py b/app/src/trading/market_intelligence/common/models.py new file mode 100644 index 0000000..d2a8c66 --- /dev/null +++ b/app/src/trading/market_intelligence/common/models.py @@ -0,0 +1,333 @@ +# app/src/trading/market_intelligence/common/models.py + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Protocol + +from src.trading.market_intelligence.common.enums import ( + EngineStatus, + MarketBias, + MarketDirection, + MarketPhase, + MarketRegime, + RiskLevel, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.scores import ( + EngineConfidence, + EngineScore, +) +from src.trading.market_intelligence.common.types import ( + ContextDict, + DiagnosticMessages, + DiagnosticValue, + DurationMs, + EngineName, + EngineVersion, + MarketData, + MetricsDict, + PayloadDict, + SymbolName, + TimeframeName, +) + + +@dataclass(frozen=True, slots=True) +class EngineMetadata: + # Описание Engine как компонента платформы. + # Metadata не содержит аналитической логики и не является результатом анализа. + # Она нужна Runtime, Registry, Coordinator и документации. + name: EngineName + version: EngineVersion + description: str = "" + supported_timeframes: tuple[TimeframeName, ...] = () + required_dependencies: tuple[EngineName, ...] = () + optional_dependencies: tuple[EngineName, ...] = () + enabled_by_default: bool = True + + +class EngineTypeProtocol(Protocol): + # Минимальный контракт класса Engine для хранения в Common Layer. + # Common не должен импортировать EngineProtocol из Engine Layer. + @classmethod + def get_metadata(cls) -> EngineMetadata: + # Вернуть metadata Engine без создания экземпляра. + ... + + async def analyze( + self, + context: EngineContext, + ) -> EngineResult: + # Выполнить анализ рыночного контекста. + ... + + +@dataclass(frozen=True, slots=True) +class EngineRegistration: + # Атомарная запись регистрации Engine. + # Не дублирует данные из EngineMetadata. + engine_type: type[EngineTypeProtocol] + metadata: EngineMetadata + + +@dataclass(frozen=True, slots=True) +class EngineMetric: + # Одна измеримая величина внутри движка. + # Например: сила движения, ширина спреда, возраст данных или качество структуры. + # Метрика не является решением, она только объясняет часть расчёта. + name: str + value: DiagnosticValue + unit: str | None = None + description: str | None = None + + +@dataclass(frozen=True, slots=True) +class EngineDiagnostics: + # Диагностика объясняет, что произошло во время работы движка. + # Она нужна для журнала, отладки и будущих проверок качества. + reason: ReasonCode = ReasonCode.UNKNOWN + details: ContextDict = field(default_factory=dict) + warnings: DiagnosticMessages = field(default_factory=list) + errors: DiagnosticMessages = field(default_factory=list) + + @property + def has_warnings(self) -> bool: + # Отдельный признак помогает быстро понять, + # были ли у движка некритичные проблемы. + return bool(self.warnings) + + @property + def has_errors(self) -> bool: + # Ошибки не должны ломать всю платформу. + # Но результат должен честно сообщать, что они были. + return bool(self.errors) + + +@dataclass(frozen=True, slots=True) +class EngineEvaluationMeta: + # Служебная информация о расчёте. + # Она помогает понять, какой движок, какой версии и когда сформировал результат. + engine_name: EngineName + engine_version: EngineVersion + calculated_at: float | None = None + duration_ms: DurationMs | None = None + input_age_seconds: float | None = None + + +@dataclass(frozen=True, slots=True) +class EngineDependencyResult: + # Краткое описание результата другого движка. + # Это позволяет использовать зависимости без прямого вызова соседних Engine. + engine_name: EngineName + status: EngineStatus = EngineStatus.UNKNOWN + score: EngineScore = field(default_factory=EngineScore) + confidence: EngineConfidence = field(default_factory=EngineConfidence) + reason: ReasonCode = ReasonCode.UNKNOWN + updated_at: float | None = None + + +@dataclass(frozen=True, slots=True) +class EngineContext: + # Единый входной объект для любого Engine. + # Важно: здесь нет позиции, плеча, баланса, ордеров или Telegram. + # Market Intelligence получает только данные для анализа рынка. + symbol: SymbolName + timeframe: TimeframeName + market_data: MarketData = field(default_factory=dict) + previous_snapshot: Any | None = None + dependency_results: tuple[EngineDependencyResult, ...] = () + settings: ContextDict = field(default_factory=dict) + metadata: ContextDict = field(default_factory=dict) + created_at: float | None = None + + +@dataclass(frozen=True, slots=True) +class EngineResult: + # Единый результат любого аналитического движка. + # Это мнение о рынке, а не торговое действие. + engine_name: EngineName + engine_version: EngineVersion + symbol: SymbolName + timeframe: TimeframeName + + status: EngineStatus = EngineStatus.UNKNOWN + score: EngineScore = field(default_factory=EngineScore) + confidence: EngineConfidence = field(default_factory=EngineConfidence) + reason: ReasonCode = ReasonCode.UNKNOWN + + metrics: tuple[EngineMetric, ...] = () + diagnostics: EngineDiagnostics = field(default_factory=EngineDiagnostics) + dependencies: tuple[EngineDependencyResult, ...] = () + meta: EngineEvaluationMeta | None = None + payload: PayloadDict = field(default_factory=dict) + + direction: MarketDirection = MarketDirection.UNKNOWN + bias: MarketBias = MarketBias.UNKNOWN + phase: MarketPhase = MarketPhase.UNKNOWN + regime: MarketRegime = MarketRegime.UNKNOWN + risk_level: RiskLevel = RiskLevel.UNKNOWN + + @property + def is_ok(self) -> bool: + # Удобный признак для Coordinator. + # Он показывает, что движок отработал без критической ошибки. + return self.status == EngineStatus.OK + + @property + def is_usable(self) -> bool: + # Результат может быть полезен даже если он частичный. + # Например, часть данных отсутствовала, но базовая оценка всё равно рассчитана. + return self.status in { + EngineStatus.OK, + EngineStatus.PARTIAL, + EngineStatus.STALE, + } + + @property + def has_errors(self) -> bool: + # Быстрый доступ к признаку ошибок внутри диагностики. + return self.diagnostics.has_errors + + +@dataclass(frozen=True, slots=True) +class RuntimeResult: + # Единый результат выполнения Runtime. + # RuntimeResult агрегирует результаты нескольких Engine, + # но не содержит аналитики и не принимает торговых решений. + engine_results: tuple[EngineResult, ...] = () + diagnostics: ContextDict = field(default_factory=dict) + started_at: float | None = None + finished_at: float | None = None + duration_ms: DurationMs | None = None + metadata: ContextDict = field(default_factory=dict) + + @property + def successful_results(self) -> tuple[EngineResult, ...]: + # Результаты Engine, которые успешно завершили анализ. + return tuple( + result + for result in self.engine_results + if result.status == EngineStatus.OK + ) + + @property + def failed_results(self) -> tuple[EngineResult, ...]: + # Результаты Engine, которые завершились ошибкой. + return tuple( + result + for result in self.engine_results + if result.status == EngineStatus.ERROR + ) + + @property + def partial_results(self) -> tuple[EngineResult, ...]: + # Частичные, но потенциально пригодные результаты Engine. + return tuple( + result + for result in self.engine_results + if result.status == EngineStatus.PARTIAL + ) + + @property + def stale_results(self) -> tuple[EngineResult, ...]: + # Результаты Engine, построенные на устаревших данных. + return tuple( + result + for result in self.engine_results + if result.status == EngineStatus.STALE + ) + + @property + def executed_engines(self) -> tuple[EngineName, ...]: + # Имена Engine, которые вернули результат. + return tuple(result.engine_name for result in self.engine_results) + + @property + def failed_engines(self) -> tuple[EngineName, ...]: + # Имена Engine, которые завершились ошибкой. + return tuple(result.engine_name for result in self.failed_results) + + @property + def successful_engines(self) -> tuple[EngineName, ...]: + # Имена Engine, которые завершились успешно. + return tuple(result.engine_name for result in self.successful_results) + + @property + def has_errors(self) -> bool: + # Быстрый признак наличия ошибок Runtime-выполнения. + return bool(self.failed_results) + + @property + def is_successful(self) -> bool: + # Runtime считается успешным, если ни один Engine не завершился ERROR. + return not self.has_errors + + @property + def total_engines(self) -> int: + # Количество Engine, вернувших результат. + return len(self.engine_results) + + @property + def successful_count(self) -> int: + # Количество успешно завершённых Engine. + return len(self.successful_results) + + +@dataclass(frozen=True, slots=True) +class CoordinatorDiagnostics: + # Диагностика Coordinator. + reason: ReasonCode = ReasonCode.UNKNOWN + details: ContextDict = field(default_factory=dict) + warnings: DiagnosticMessages = field(default_factory=list) + errors: DiagnosticMessages = field(default_factory=list) + + @property + def has_warnings(self) -> bool: + return bool(self.warnings) + + @property + def has_errors(self) -> bool: + return bool(self.errors) + + +@dataclass(frozen=True, slots=True) +class CoordinatorEvaluationMeta: + # Служебная информация о расчёте Coordinator. + coordinator_version: str + calculated_at: float | None = None + duration_ms: DurationMs | None = None + + +@dataclass(frozen=True, slots=True) +class CoordinatorResult: + # Единый результат Coordinator Layer. + # Это итоговая аналитическая интерпретация RuntimeResult, + # но ещё не торговое решение. + runtime_result: RuntimeResult + diagnostics: CoordinatorDiagnostics = field(default_factory=CoordinatorDiagnostics) + meta: CoordinatorEvaluationMeta | None = None + payload: PayloadDict = field(default_factory=dict) + + status: EngineStatus = EngineStatus.UNKNOWN + score: EngineScore = field(default_factory=EngineScore) + confidence: EngineConfidence = field(default_factory=EngineConfidence) + reason: ReasonCode = ReasonCode.UNKNOWN + + direction: MarketDirection = MarketDirection.UNKNOWN + bias: MarketBias = MarketBias.UNKNOWN + phase: MarketPhase = MarketPhase.UNKNOWN + regime: MarketRegime = MarketRegime.UNKNOWN + risk_level: RiskLevel = RiskLevel.UNKNOWN + + @property + def is_usable(self) -> bool: + return self.status in { + EngineStatus.OK, + EngineStatus.PARTIAL, + EngineStatus.STALE, + } + + @property + def has_errors(self) -> bool: + return self.diagnostics.has_errors \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/payloads.py b/app/src/trading/market_intelligence/common/payloads.py new file mode 100644 index 0000000..59d9127 --- /dev/null +++ b/app/src/trading/market_intelligence/common/payloads.py @@ -0,0 +1,173 @@ +# app/src/trading/market_intelligence/common/payloads.py + +from __future__ import annotations + +from dataclasses import asdict, is_dataclass +from enum import Enum +from typing import Any, Mapping, cast + +from src.trading.market_intelligence.common.models import ( + EngineDependencyResult, + EngineDiagnostics, + EngineMetric, + EngineResult, +) +from src.trading.market_intelligence.common.scores import ( + EngineConfidence, + EngineScore, +) +from src.trading.market_intelligence.common.types import PayloadDict +from src.trading.market_intelligence.common.validation import ( + validate_payload_has_no_trading_fields, +) + + +def value_to_payload(value: Any) -> Any: + # Приводит значение к виду, который безопасно положить в payload. + # Payload должен состоять из простых структур: dict, list, str, int, + # float, bool или None. Так его можно сохранить в журнал, событие + # или будущий snapshot. + if isinstance(value, Enum): + return value.value + + if is_dataclass(value): + # is_dataclass() возвращает True и для экземпляров dataclass, + # и для самих классов dataclass. asdict() работает только + # с экземпляром, поэтому класс пропускаем как обычное значение. + if not isinstance(value, type): + return mapping_to_payload(asdict(value)) + + if isinstance(value, Mapping): + return mapping_to_payload(value) + + if isinstance(value, (tuple, list)): + return [value_to_payload(item) for item in value] + + return value + + +def mapping_to_payload(mapping: Mapping[str, Any]) -> PayloadDict: + # Преобразует словарь в безопасный payload. + # Все вложенные enum, dataclass, list и tuple приводятся + # к простым значениям. + return { + str(key): value_to_payload(value) + for key, value in mapping.items() + } + + +def engine_score_to_payload(score: EngineScore) -> PayloadDict: + # Сериализует оценку Engine. + # Оценка не является торговым решением, + # это только числовое качество анализа. + return { + "value": value_to_payload(score.value), + "quality": value_to_payload(score.quality), + "reason": value_to_payload(score.reason), + } + + +def engine_confidence_to_payload(confidence: EngineConfidence) -> PayloadDict: + # Сериализует уверенность Engine. + # Уверенность показывает надёжность результата, + # а не торговую рекомендацию. + return { + "value": value_to_payload(confidence.value), + "level": value_to_payload(confidence.level), + "reason": value_to_payload(confidence.reason), + "is_reliable": confidence.is_reliable, + } + + +def engine_metric_to_payload(metric: EngineMetric) -> PayloadDict: + # Сериализует одну измеримую метрику Engine. + return { + "name": metric.name, + "value": value_to_payload(metric.value), + "unit": metric.unit, + "description": metric.description, + } + + +def engine_diagnostics_to_payload( + diagnostics: EngineDiagnostics, +) -> PayloadDict: + # Сериализует диагностику Engine. + # Диагностика нужна для журнала и отладки, + # но не является пользовательским UI-текстом. + return { + "reason": value_to_payload(diagnostics.reason), + "details": mapping_to_payload(diagnostics.details), + "warnings": value_to_payload(diagnostics.warnings), + "errors": value_to_payload(diagnostics.errors), + "has_warnings": diagnostics.has_warnings, + "has_errors": diagnostics.has_errors, + } + + +def engine_dependency_to_payload( + dependency: EngineDependencyResult, +) -> PayloadDict: + # Сериализует краткий результат зависимого Engine. + # Это позволяет передавать результат зависимости + # без прямого импорта самого Engine. + return { + "engine_name": dependency.engine_name, + "status": value_to_payload(dependency.status), + "score": engine_score_to_payload(dependency.score), + "confidence": engine_confidence_to_payload(dependency.confidence), + "reason": value_to_payload(dependency.reason), + "updated_at": dependency.updated_at, + } + + +def engine_result_to_payload(result: EngineResult) -> PayloadDict: + # Преобразует EngineResult в единый payload. + # Функция не меняет результат Engine и не добавляет торговые поля. + payload: PayloadDict = { + "engine_name": result.engine_name, + "engine_version": result.engine_version, + "symbol": result.symbol, + "timeframe": result.timeframe, + "status": value_to_payload(result.status), + "reason": value_to_payload(result.reason), + "score": engine_score_to_payload(result.score), + "confidence": engine_confidence_to_payload(result.confidence), + "metrics": [ + engine_metric_to_payload(metric) + for metric in result.metrics + ], + "diagnostics": engine_diagnostics_to_payload(result.diagnostics), + "dependencies": [ + engine_dependency_to_payload(dependency) + for dependency in result.dependencies + ], + "meta": value_to_payload(result.meta), + "payload": mapping_to_payload(result.payload), + "direction": value_to_payload(result.direction), + "bias": value_to_payload(result.bias), + "phase": value_to_payload(result.phase), + "regime": value_to_payload(result.regime), + "risk_level": value_to_payload(result.risk_level), + "is_ok": result.is_ok, + "is_usable": result.is_usable, + "has_errors": result.has_errors, + } + + validation = validate_payload_has_no_trading_fields(payload) + + if validation.is_ok: + return payload + + # Если итоговый payload нарушил границы Market Intelligence, + # мы не скрываем проблему, а добавляем диагностический блок. + # Это не исправляет payload автоматически, чтобы нарушение + # было видно в журнале и при Engineering Review. + payload["payload_validation"] = { + "status": value_to_payload(validation.status), + "reason": value_to_payload(validation.reason), + "issues": value_to_payload(validation.issues), + "details": mapping_to_payload(validation.details), + } + + return cast(PayloadDict, payload) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/reasons.py b/app/src/trading/market_intelligence/common/reasons.py new file mode 100644 index 0000000..9295bab --- /dev/null +++ b/app/src/trading/market_intelligence/common/reasons.py @@ -0,0 +1,153 @@ +# app/src/trading/market_intelligence/common/reasons.py + +from __future__ import annotations + +from enum import StrEnum + + +class ReasonCode(StrEnum): + # Универсальный код причины. + # Движки используют эти значения, чтобы объяснить результат + # в машинно-читаемом виде. Человекочитаемый текст будет строиться + # отдельным слоем диагностики, а не внутри движка. + + # ---------- Common ---------- + UNKNOWN = "unknown" + OK = "ok" + NOT_APPLICABLE = "not_applicable" + SKIPPED = "skipped" + + # ---------- Data ---------- + INSUFFICIENT_DATA = "insufficient_data" + EMPTY_MARKET_DATA = "empty_market_data" + STALE_MARKET_DATA = "stale_market_data" + INVALID_MARKET_DATA = "invalid_market_data" + MISSING_PRICE_DATA = "missing_price_data" + MISSING_VOLUME_DATA = "missing_volume_data" + MISSING_TIMEFRAME_DATA = "missing_timeframe_data" + + # ---------- Engine Runtime ---------- + ENGINE_DISABLED = "engine_disabled" + ENGINE_ERROR = "engine_error" + ENGINE_PARTIAL_RESULT = "engine_partial_result" + ENGINE_DEPENDENCY_MISSING = "engine_dependency_missing" + ENGINE_DEPENDENCY_STALE = "engine_dependency_stale" + ENGINE_DEPENDENCY_ERROR = "engine_dependency_error" + + # ---------- Validation ---------- + VALIDATION_PASSED = "validation_passed" + VALIDATION_FAILED = "validation_failed" + SCORE_OUT_OF_RANGE = "score_out_of_range" + CONFIDENCE_OUT_OF_RANGE = "confidence_out_of_range" + PROBABILITY_OUT_OF_RANGE = "probability_out_of_range" + FORBIDDEN_TRADING_FIELD_FOUND = "forbidden_trading_field_found" + REQUIRED_FIELD_MISSING = "required_field_missing" + + # ---------- Market State ---------- + MARKET_UNDEFINED = "market_undefined" + MARKET_CLEAR = "market_clear" + MARKET_NOISY = "market_noisy" + MARKET_CONFLICTED = "market_conflicted" + MARKET_QUALITY_LOW = "market_quality_low" + MARKET_QUALITY_NORMAL = "market_quality_normal" + MARKET_QUALITY_HIGH = "market_quality_high" + + # ---------- Structure ---------- + STRUCTURE_UNDEFINED = "structure_undefined" + STRUCTURE_UP = "structure_up" + STRUCTURE_DOWN = "structure_down" + STRUCTURE_SIDEWAYS = "structure_sideways" + STRUCTURE_CHANGED = "structure_changed" + STRUCTURE_BROKEN = "structure_broken" + + # ---------- Trend ---------- + TREND_UNDEFINED = "trend_undefined" + TREND_UP = "trend_up" + TREND_DOWN = "trend_down" + TREND_SIDEWAYS = "trend_sideways" + TREND_STRONG = "trend_strong" + TREND_NORMAL = "trend_normal" + TREND_WEAK = "trend_weak" + TREND_CLEAN = "trend_clean" + TREND_NOISY = "trend_noisy" + TREND_OVEREXTENDED = "trend_overextended" + + # ---------- Momentum ---------- + MOMENTUM_UNDEFINED = "momentum_undefined" + MOMENTUM_UP = "momentum_up" + MOMENTUM_DOWN = "momentum_down" + MOMENTUM_ACCELERATING = "momentum_accelerating" + MOMENTUM_DECELERATING = "momentum_decelerating" + MOMENTUM_EXHAUSTED = "momentum_exhausted" + MOMENTUM_ABSENT = "momentum_absent" + + # ---------- Volatility ---------- + VOLATILITY_UNDEFINED = "volatility_undefined" + VOLATILITY_LOW = "volatility_low" + VOLATILITY_NORMAL = "volatility_normal" + VOLATILITY_HIGH = "volatility_high" + VOLATILITY_EXPANDING = "volatility_expanding" + VOLATILITY_COMPRESSING = "volatility_compressing" + VOLATILITY_UNSTABLE = "volatility_unstable" + + # ---------- Wave ---------- + WAVE_UNDEFINED = "wave_undefined" + WAVE_IMPULSE = "wave_impulse" + WAVE_PULLBACK = "wave_pullback" + WAVE_RECOVERY = "wave_recovery" + WAVE_MATURE = "wave_mature" + WAVE_COMPLETED = "wave_completed" + WAVE_TOO_SHORT = "wave_too_short" + + # ---------- Cycle ---------- + CYCLE_UNDEFINED = "cycle_undefined" + CYCLE_IMPULSE = "cycle_impulse" + CYCLE_PULLBACK = "cycle_pullback" + CYCLE_RECOVERY = "cycle_recovery" + CYCLE_EXPANSION = "cycle_expansion" + CYCLE_EXHAUSTION = "cycle_exhaustion" + CYCLE_REVERSAL = "cycle_reversal" + + # ---------- Liquidity ---------- + LIQUIDITY_UNDEFINED = "liquidity_undefined" + LIQUIDITY_GOOD = "liquidity_good" + LIQUIDITY_NORMAL = "liquidity_normal" + LIQUIDITY_POOR = "liquidity_poor" + SPREAD_NORMAL = "spread_normal" + SPREAD_WIDE = "spread_wide" + DEPTH_NORMAL = "depth_normal" + DEPTH_THIN = "depth_thin" + EXECUTION_QUALITY_LOW = "execution_quality_low" + + # ---------- Regime ---------- + REGIME_UNDEFINED = "regime_undefined" + REGIME_TRENDING = "regime_trending" + REGIME_RANGE = "regime_range" + REGIME_BREAKOUT = "regime_breakout" + REGIME_MEAN_REVERSION = "regime_mean_reversion" + REGIME_HIGH_VOLATILITY = "regime_high_volatility" + REGIME_LOW_VOLATILITY = "regime_low_volatility" + REGIME_PANIC = "regime_panic" + REGIME_EUPHORIA = "regime_euphoria" + REGIME_ACCUMULATION = "regime_accumulation" + REGIME_DISTRIBUTION = "regime_distribution" + + # ---------- Confidence ---------- + CONFIDENCE_UNDEFINED = "confidence_undefined" + CONFIDENCE_LOW = "confidence_low" + CONFIDENCE_NORMAL = "confidence_normal" + CONFIDENCE_HIGH = "confidence_high" + CONFIDENCE_UNSTABLE = "confidence_unstable" + CONFIDENCE_STABLE = "confidence_stable" + + # ---------- Signal Aging ---------- + SIGNAL_NEW = "signal_new" + SIGNAL_ACTIVE = "signal_active" + SIGNAL_AGING = "signal_aging" + SIGNAL_EXPIRED = "signal_expired" + + # ---------- Timeframe ---------- + TIMEFRAME_UNDEFINED = "timeframe_undefined" + TIMEFRAME_ALIGNED = "timeframe_aligned" + TIMEFRAME_CONFLICTED = "timeframe_conflicted" + TIMEFRAME_PRIMARY_MISSING = "timeframe_primary_missing" \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/scores.py b/app/src/trading/market_intelligence/common/scores.py new file mode 100644 index 0000000..9ab9f8d --- /dev/null +++ b/app/src/trading/market_intelligence/common/scores.py @@ -0,0 +1,263 @@ +# app/src/trading/market_intelligence/common/scores.py + +from __future__ import annotations + +from dataclasses import dataclass, field + +from src.trading.market_intelligence.common.constants import ( + DEFAULT_CONFIDENCE, + DEFAULT_PROBABILITY, + DEFAULT_SCORE, + DEFAULT_WEIGHT, + EXCELLENT_SCORE_THRESHOLD, + GOOD_SCORE_THRESHOLD, + HIGH_CONFIDENCE_THRESHOLD, + LOW_CONFIDENCE_THRESHOLD, + MAX_CONFIDENCE, + MAX_PROBABILITY, + MAX_SCORE, + MAX_WEIGHT, + MIN_CONFIDENCE, + MIN_PROBABILITY, + MIN_SCORE, + MIN_WEIGHT, + NORMAL_CONFIDENCE_THRESHOLD, + NORMAL_SCORE_THRESHOLD, + VERY_HIGH_CONFIDENCE_THRESHOLD, + VERY_LOW_CONFIDENCE_THRESHOLD, + WEAK_SCORE_THRESHOLD, +) +from src.trading.market_intelligence.common.enums import ( + ConfidenceLevel, + MarketQuality, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.types import ( + ConfidenceValue, + ProbabilityValue, + ReasonCode as ReasonCodeType, + ScoreValue, + WeightValue, +) + + +def clamp_score(value: float | int | None) -> ScoreValue: + # Приводим оценку к безопасному диапазону 0...100. + # Это защищает все будущие движки от случайного выхода за границы шкалы. + if value is None: + return DEFAULT_SCORE + + return max(MIN_SCORE, min(MAX_SCORE, float(value))) + + +def clamp_confidence(value: float | int | None) -> ConfidenceValue: + # Приводим уверенность к безопасному диапазону 0...1. + # Уверенность показывает качество вывода, а не силу движения рынка. + if value is None: + return DEFAULT_CONFIDENCE + + return max(MIN_CONFIDENCE, min(MAX_CONFIDENCE, float(value))) + + +def clamp_probability(value: float | int | None) -> ProbabilityValue: + # Приводим вероятность к безопасному диапазону 0...100. + # Это единая шкала для будущих движков вероятности. + if value is None: + return DEFAULT_PROBABILITY + + return max(MIN_PROBABILITY, min(MAX_PROBABILITY, float(value))) + + +def clamp_weight(value: float | int | None) -> WeightValue: + # Приводим вес показателя к безопасному диапазону 0...1. + # Вес показывает, насколько сильно показатель влияет на итоговую оценку. + if value is None: + return DEFAULT_WEIGHT + + return max(MIN_WEIGHT, min(MAX_WEIGHT, float(value))) + + +def classify_score_quality(value: float | int | None) -> MarketQuality: + # Переводим числовую оценку в понятное качество. + # Это нужно для журнала и диагностики, чтобы не читать только сухие числа. + score = clamp_score(value) + + if score >= EXCELLENT_SCORE_THRESHOLD: + return MarketQuality.EXCELLENT + + if score >= GOOD_SCORE_THRESHOLD: + return MarketQuality.GOOD + + if score >= NORMAL_SCORE_THRESHOLD: + return MarketQuality.NORMAL + + if score >= WEAK_SCORE_THRESHOLD: + return MarketQuality.WEAK + + return MarketQuality.POOR + + +def classify_confidence_level(value: float | int | None) -> ConfidenceLevel: + # Переводим числовую уверенность в понятный уровень. + # Это помогает человеку быстро понять, насколько надёжен вывод движка. + confidence = clamp_confidence(value) + + if confidence >= VERY_HIGH_CONFIDENCE_THRESHOLD: + return ConfidenceLevel.VERY_HIGH + + if confidence >= HIGH_CONFIDENCE_THRESHOLD: + return ConfidenceLevel.HIGH + + if confidence >= NORMAL_CONFIDENCE_THRESHOLD: + return ConfidenceLevel.NORMAL + + if confidence >= LOW_CONFIDENCE_THRESHOLD: + return ConfidenceLevel.LOW + + if confidence >= VERY_LOW_CONFIDENCE_THRESHOLD: + return ConfidenceLevel.VERY_LOW + + return ConfidenceLevel.VERY_LOW + + +@dataclass(frozen=True, slots=True) +class EngineScore: + # Единая модель оценки движка. + # Оценка всегда хранится в диапазоне 0...100 и дополнительно имеет + # человекочитаемое качество для диагностики. + value: ScoreValue = DEFAULT_SCORE + quality: MarketQuality = MarketQuality.POOR + reason: ReasonCode = ReasonCode.UNKNOWN + + @classmethod + def create( + cls, + value: float | int | None, + *, + reason: ReasonCode = ReasonCode.UNKNOWN, + ) -> EngineScore: + safe_value = clamp_score(value) + + return cls( + value=safe_value, + quality=classify_score_quality(safe_value), + reason=reason, + ) + + +@dataclass(frozen=True, slots=True) +class EngineConfidence: + # Единая модель уверенности движка. + # Уверенность показывает, насколько результат можно считать надёжным. + value: ConfidenceValue = DEFAULT_CONFIDENCE + level: ConfidenceLevel = ConfidenceLevel.VERY_LOW + reason: ReasonCode = ReasonCode.UNKNOWN + + @property + def is_reliable(self) -> bool: + # Считаем результат достаточно надёжным, если уверенность + # не ниже нормального уровня. + return self.value >= NORMAL_CONFIDENCE_THRESHOLD + + @classmethod + def create( + cls, + value: float | int | None, + *, + reason: ReasonCode = ReasonCode.UNKNOWN, + ) -> EngineConfidence: + safe_value = clamp_confidence(value) + + return cls( + value=safe_value, + level=classify_confidence_level(safe_value), + reason=reason, + ) + + +@dataclass(frozen=True, slots=True) +class ProbabilityScore: + # Единая модель вероятности. + # Используется для будущих движков продолжения и изменения направления. + value: ProbabilityValue = DEFAULT_PROBABILITY + confidence: EngineConfidence = field(default_factory=EngineConfidence) + reason: ReasonCode = ReasonCode.UNKNOWN + + @classmethod + def create( + cls, + value: float | int | None, + *, + confidence: EngineConfidence | None = None, + reason: ReasonCode = ReasonCode.UNKNOWN, + ) -> ProbabilityScore: + return cls( + value=clamp_probability(value), + confidence=confidence or EngineConfidence(), + reason=reason, + ) + + +@dataclass(frozen=True, slots=True) +class WeightedScore: + # Одна часть итоговой оценки. + # Например, общий результат может состоять из оценки тренда, + # оценки изменчивости рынка и оценки ликвидности. + name: str + value: ScoreValue + weight: WeightValue = DEFAULT_WEIGHT + reason: ReasonCodeType = ReasonCode.UNKNOWN + + @property + def weighted_value(self) -> float: + # Возвращает вклад этой части в итоговую оценку. + return self.value * self.weight + + @classmethod + def create( + cls, + *, + name: str, + value: float | int | None, + weight: float | int | None = DEFAULT_WEIGHT, + reason: ReasonCode = ReasonCode.UNKNOWN, + ) -> WeightedScore: + return cls( + name=name, + value=clamp_score(value), + weight=clamp_weight(weight), + reason=reason, + ) + + +@dataclass(frozen=True, slots=True) +class ScoreBreakdown: + # Подробная структура итоговой оценки. + # Нужна для диагностики: система должна объяснять, из каких частей + # получилась итоговая оценка. + items: tuple[WeightedScore, ...] = () + + @property + def total_weight(self) -> float: + return sum(item.weight for item in self.items) + + @property + def total_score(self) -> float: + return sum(item.weighted_value for item in self.items) + + @property + def final_score(self) -> ScoreValue: + # Если веса отсутствуют, безопасно возвращаем нулевую оценку. + # Это лучше, чем делить на ноль или создавать ложный результат. + if self.total_weight <= 0: + return DEFAULT_SCORE + + return clamp_score(self.total_score / self.total_weight) + + @property + def quality(self) -> MarketQuality: + return classify_score_quality(self.final_score) + + @classmethod + def create(cls, items: tuple[WeightedScore, ...]) -> ScoreBreakdown: + return cls(items=items) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/snapshots.py b/app/src/trading/market_intelligence/common/snapshots.py new file mode 100644 index 0000000..eb2c3e3 --- /dev/null +++ b/app/src/trading/market_intelligence/common/snapshots.py @@ -0,0 +1,79 @@ +# app/src/trading/market_intelligence/common/snapshots.py + +from __future__ import annotations + +from dataclasses import dataclass, field +from time import time + +from src.trading.market_intelligence.common.models import EngineResult +from src.trading.market_intelligence.common.payloads import ( + engine_result_to_payload, +) +from src.trading.market_intelligence.common.types import ( + EngineName, + EngineVersion, + PayloadDict, + SymbolName, + TimeframeName, +) + + +@dataclass(frozen=True, slots=True) +class EngineSnapshot: + # Snapshot представляет собой неизменяемый снимок результата работы + # аналитического движка в определённый момент времени. + # + # Snapshot используется для: + # + # • журналирования; + # • Runtime Diagnostics; + # • последующего сравнения состояний; + # • формирования Event; + # • хранения истории анализа. + # + # Snapshot не является Runtime-состоянием движка. + # После создания его содержимое больше не изменяется. + + engine_name: EngineName + engine_version: EngineVersion + + symbol: SymbolName + timeframe: TimeframeName + + created_at: float + + payload: PayloadDict = field(default_factory=dict) + + +def build_engine_snapshot(result: EngineResult) -> EngineSnapshot: + # Формирует неизменяемый Snapshot из результата Engine. + # + # Snapshot всегда строится через общий Payload Layer, + # чтобы все движки сохраняли результаты в едином формате. + + return EngineSnapshot( + engine_name=result.engine_name, + engine_version=result.engine_version, + symbol=result.symbol, + timeframe=result.timeframe, + created_at=time(), + payload=engine_result_to_payload(result), + ) + + +def engine_snapshot_to_payload( + snapshot: EngineSnapshot, +) -> PayloadDict: + # Преобразует Snapshot в сериализуемый Payload. + # + # Отдельная функция позволяет в будущем расширять Snapshot, + # не изменяя код остальных компонентов платформы. + + return { + "engine_name": snapshot.engine_name, + "engine_version": snapshot.engine_version, + "symbol": snapshot.symbol, + "timeframe": snapshot.timeframe, + "created_at": snapshot.created_at, + "payload": snapshot.payload, + } \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/timeframes.py b/app/src/trading/market_intelligence/common/timeframes.py new file mode 100644 index 0000000..e4e2b00 --- /dev/null +++ b/app/src/trading/market_intelligence/common/timeframes.py @@ -0,0 +1,169 @@ +# app/src/trading/market_intelligence/common/timeframes.py + +from __future__ import annotations + +from dataclasses import dataclass + +from src.trading.market_intelligence.common.enums import TimeframeRole +from src.trading.market_intelligence.common.types import ( + TimeframeName, +) + + +@dataclass(frozen=True, slots=True) +class Timeframe: + # Единое описание временного интервала. + # + # Timeframe не знает ничего о бирже, стратегии или торговом решении. + # Он только описывает длительность интервала и его базовую роль + # внутри многоуровневого анализа рынка. + name: TimeframeName + duration_minutes: int + role: TimeframeRole = TimeframeRole.UNKNOWN + description: str | None = None + + +M1 = Timeframe( + name="1m", + duration_minutes=1, + role=TimeframeRole.LOWER, + description="Минутный интервал для детального краткосрочного анализа.", +) + +M5 = Timeframe( + name="5m", + duration_minutes=5, + role=TimeframeRole.PRIMARY, + description="Основной рабочий интервал первого этапа Market Intelligence.", +) + +M15 = Timeframe( + name="15m", + duration_minutes=15, + role=TimeframeRole.CONFIRMATION, + description="Интервал подтверждения между локальным и старшим анализом.", +) + +H1 = Timeframe( + name="1h", + duration_minutes=60, + role=TimeframeRole.HIGHER, + description="Старший интервал, совместимый с текущим HTF-анализом.", +) + +H4 = Timeframe( + name="4h", + duration_minutes=240, + role=TimeframeRole.HIGHER, + description="Старший интервал для будущего более глубокого анализа.", +) + +D1 = Timeframe( + name="1d", + duration_minutes=1440, + role=TimeframeRole.HIGHER, + description="Дневной интервал для будущего долгосрочного контекста.", +) + +W1 = Timeframe( + name="1w", + duration_minutes=10080, + role=TimeframeRole.HIGHER, + description="Недельный интервал для будущего стратегического контекста.", +) + + +SUPPORTED_TIMEFRAMES: tuple[Timeframe, ...] = ( + M1, + M5, + M15, + H1, + H4, + D1, + W1, +) + + +SUPPORTED_TIMEFRAME_NAMES: tuple[TimeframeName, ...] = tuple( + timeframe.name + for timeframe in SUPPORTED_TIMEFRAMES +) + + +# Базовая карта старшего таймфрейма. +# Она сохраняет совместимость с текущим MarketAnalysisService, +# где для рабочего 5m используется старший 1h. +HIGHER_TIMEFRAME_BY_NAME: dict[TimeframeName, TimeframeName] = { + "1m": "5m", + "5m": "1h", + "15m": "1h", + "1h": "4h", + "4h": "1d", + "1d": "1w", +} + + +def get_timeframe(name: TimeframeName) -> Timeframe | None: + # Возвращает описание таймфрейма по его имени. + # Если интервал пока не поддерживается архитектурой, + # возвращается None вместо исключения. + normalized_name = str(name).strip().lower() + + for timeframe in SUPPORTED_TIMEFRAMES: + if timeframe.name == normalized_name: + return timeframe + + return None + + +def require_timeframe(name: TimeframeName) -> Timeframe: + # Возвращает Timeframe или явно сообщает о неподдерживаемом интервале. + # Эту функцию следует использовать там, где отсутствие таймфрейма + # является ошибкой конфигурации, а не обычным Runtime-состоянием. + timeframe = get_timeframe(name) + + if timeframe is None: + raise ValueError(f"Unsupported timeframe: {name}") + + return timeframe + + +def is_supported_timeframe(name: TimeframeName) -> bool: + # Проверяет, известен ли интервал архитектуре Market Intelligence. + return get_timeframe(name) is not None + + +def get_higher_timeframe(name: TimeframeName) -> Timeframe | None: + # Возвращает старший таймфрейм для указанного интервала. + # Если для интервала нет старшего уровня, возвращается None. + normalized_name = str(name).strip().lower() + higher_name = HIGHER_TIMEFRAME_BY_NAME.get(normalized_name) + + if higher_name is None: + return None + + return get_timeframe(higher_name) + + +def get_timeframes_by_role( + role: TimeframeRole, +) -> tuple[Timeframe, ...]: + # Возвращает все интервалы с указанной архитектурной ролью. + # Это пригодится Multi-Timeframe Engine без жёсткой привязки + # к конкретным строковым значениям. + return tuple( + timeframe + for timeframe in SUPPORTED_TIMEFRAMES + if timeframe.role == role + ) + + +def timeframe_to_payload(timeframe: Timeframe) -> dict[str, object]: + # Преобразует описание таймфрейма в простой словарь. + # Это нужно для диагностики, payload и будущих snapshot. + return { + "name": timeframe.name, + "duration_minutes": timeframe.duration_minutes, + "role": timeframe.role.value, + "description": timeframe.description, + } \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/types.py b/app/src/trading/market_intelligence/common/types.py new file mode 100644 index 0000000..cb9d912 --- /dev/null +++ b/app/src/trading/market_intelligence/common/types.py @@ -0,0 +1,112 @@ +# app/src/trading/market_intelligence/common/types.py + +from __future__ import annotations + +from typing import Any, TypeAlias + +from src.core.types import JsonDict, JsonList + + +# Название торгового инструмента. +# Например: BTC/USD, ETH/USD. +SymbolName: TypeAlias = str + + +# Название временного интервала. +# Например: 1m, 5m, 15m, 1h. +TimeframeName: TypeAlias = str + + +# Название движка Market Intelligence. +# Например: trend, momentum, volatility. +EngineName: TypeAlias = str + + +# Версия движка. +# Нужна для журнала и диагностики, чтобы понимать, +# какая версия логики рассчитала конкретный результат. +EngineVersion: TypeAlias = str + + +# Код причины. +# Это короткое машинное имя причины, которое удобно хранить в журнале, +# payload и внутренних проверках. +ReasonCode: TypeAlias = str + + +# Человекочитаемое описание причины. +# Оно должно быть понятно без глубоких знаний трейдинга. +ReasonText: TypeAlias = str + + +# Оценка от 0 до 100. +# 0 означает очень слабое качество или отсутствие признака. +# 100 означает максимально сильное качество или признак. +ScoreValue: TypeAlias = float + + +# Уверенность от 0 до 1. +# 0 означает, что движок не уверен в своём выводе. +# 1 означает, что данных достаточно и вывод считается надёжным. +ConfidenceValue: TypeAlias = float + + +# Вероятность от 0 до 100. +# Используется для оценки вероятности события. +# Например, продолжения движения или изменения направления. +ProbabilityValue: TypeAlias = float + + +# Вес показателя при сборе общей оценки. +# Чем больше вес, тем сильнее показатель влияет на итоговую оценку. +WeightValue: TypeAlias = float + + +# Возраст данных или сигнала в секундах. +# Нужен, чтобы понимать, насколько свежий результат использует система. +AgeSeconds: TypeAlias = float + + +# Длительность выполнения в миллисекундах. +# Нужна для диагностики производительности движков. +DurationMs: TypeAlias = float + + +# Метрики движка. +# Здесь хранятся измеримые значения: наклон, расстояние, скорость, +# качество движения, возраст сигнала и другие расчётные данные. +MetricsDict: TypeAlias = JsonDict + + +# Payload движка. +# Это единый диагностический словарь для журнала, событий и отладки. +PayloadDict: TypeAlias = JsonDict + + +# Дополнительный контекст. +# Используется для редких служебных данных, которые не стоит делать +# отдельными полями базовой модели. +ContextDict: TypeAlias = JsonDict + + +# Сырые рыночные данные. +# Формат может отличаться в зависимости от источника данных, +# поэтому пока оставляем его универсальным. +MarketData: TypeAlias = JsonDict + + +# Результаты зависимых движков. +# Ключ — имя движка, значение — его результат или диагностический снимок. +# Пока используется Any, поскольку общий результат Engine +# будет определён позже в models.py. +DependencyResults: TypeAlias = dict[EngineName, Any] + + +# Список диагностических сообщений. +# Используется там, где один блок может вернуть несколько предупреждений. +DiagnosticMessages: TypeAlias = JsonList + + +# Универсальное значение для диагностических деталей. +# Например: число, строка, список, словарь или None. +DiagnosticValue: TypeAlias = Any \ No newline at end of file diff --git a/app/src/trading/market_intelligence/common/validation.py b/app/src/trading/market_intelligence/common/validation.py new file mode 100644 index 0000000..9452b89 --- /dev/null +++ b/app/src/trading/market_intelligence/common/validation.py @@ -0,0 +1,258 @@ +# app/src/trading/market_intelligence/common/validation.py + +from __future__ import annotations + +from dataclasses import dataclass, field + +from src.trading.market_intelligence.common.constants import ( + FORBIDDEN_TRADING_FIELDS, + MAX_ENGINE_DEPENDENCIES, + MAX_ENGINE_METRICS, +) +from src.trading.market_intelligence.common.enums import CheckStatus, EngineStatus +from src.trading.market_intelligence.common.models import ( + EngineContext, + EngineResult, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.types import ( + ContextDict, + PayloadDict, + ReasonText, +) + + +@dataclass(frozen=True, slots=True) +class ValidationIssue: + # Одна проблема, найденная во время проверки. + # Это не ошибка Python, а понятное описание нарушения контракта. + status: CheckStatus + reason: ReasonCode + message: ReasonText + field_name: str | None = None + + +@dataclass(frozen=True, slots=True) +class ValidationResult: + # Итог проверки одного объекта. + # Validation Layer не выбрасывает исключения, а возвращает результат, + # который можно безопасно записать в журнал или диагностику. + status: CheckStatus = CheckStatus.OK + reason: ReasonCode = ReasonCode.VALIDATION_PASSED + issues: tuple[ValidationIssue, ...] = () + details: ContextDict = field(default_factory=dict) + + @property + def is_ok(self) -> bool: + # Проверка считается успешной только если нет ошибок. + return self.status == CheckStatus.OK + + @property + def has_errors(self) -> bool: + return any(issue.status == CheckStatus.ERROR for issue in self.issues) + + @property + def has_warnings(self) -> bool: + return any(issue.status == CheckStatus.WARNING for issue in self.issues) + + +def _validation_result_from_issues( + issues: tuple[ValidationIssue, ...], + *, + details: ContextDict | None = None, +) -> ValidationResult: + # Собираем общий статус из списка найденных проблем. + # Ошибка важнее предупреждения, предупреждение важнее OK. + if any(issue.status == CheckStatus.ERROR for issue in issues): + return ValidationResult( + status=CheckStatus.ERROR, + reason=ReasonCode.VALIDATION_FAILED, + issues=issues, + details=details or {}, + ) + + if any(issue.status == CheckStatus.WARNING for issue in issues): + return ValidationResult( + status=CheckStatus.WARNING, + reason=ReasonCode.VALIDATION_PASSED, + issues=issues, + details=details or {}, + ) + + return ValidationResult( + status=CheckStatus.OK, + reason=ReasonCode.VALIDATION_PASSED, + issues=issues, + details=details or {}, + ) + + +def validate_payload_has_no_trading_fields( + payload: PayloadDict, +) -> ValidationResult: + # Market Intelligence не должен отдавать торговые команды. + # Поэтому payload проверяется на поля, похожие на действия торговли. + issues: list[ValidationIssue] = [] + + for field_name in sorted(FORBIDDEN_TRADING_FIELDS): + if field_name in payload: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.FORBIDDEN_TRADING_FIELD_FOUND, + message=( + "Payload содержит поле, запрещённое для " + "аналитического слоя Market Intelligence." + ), + field_name=field_name, + ) + ) + + return _validation_result_from_issues( + tuple(issues), + details={"checked_fields": sorted(FORBIDDEN_TRADING_FIELDS)}, + ) + + +def validate_engine_context(context: EngineContext) -> ValidationResult: + # Проверяем только базовый входной контракт Engine. + # Глубокая проверка рыночных данных будет задачей отдельных Engine, + # потому что разные движки могут требовать разные данные. + issues: list[ValidationIssue] = [] + + if not context.symbol: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.REQUIRED_FIELD_MISSING, + message="В EngineContext не указан торговый инструмент.", + field_name="symbol", + ) + ) + + if not context.timeframe: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.REQUIRED_FIELD_MISSING, + message="В EngineContext не указан таймфрейм анализа.", + field_name="timeframe", + ) + ) + + if not context.market_data: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.EMPTY_MARKET_DATA, + message="В EngineContext отсутствуют рыночные данные.", + field_name="market_data", + ) + ) + + if len(context.dependency_results) > MAX_ENGINE_DEPENDENCIES: + issues.append( + ValidationIssue( + status=CheckStatus.WARNING, + reason=ReasonCode.ENGINE_DEPENDENCY_ERROR, + message=( + "Количество зависимостей Engine превышает " + "архитектурный лимит Common Layer." + ), + field_name="dependency_results", + ) + ) + + return _validation_result_from_issues( + tuple(issues), + details={ + "symbol": context.symbol, + "timeframe": context.timeframe, + "dependency_count": len(context.dependency_results), + }, + ) + + +def validate_engine_result(result: EngineResult) -> ValidationResult: + # Проверяем единый результат Engine. + # Эта проверка не оценивает качество анализа рынка, + # а только подтверждает соблюдение Runtime Contract. + issues: list[ValidationIssue] = [] + + if not result.engine_name: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.REQUIRED_FIELD_MISSING, + message="В EngineResult не указано имя движка.", + field_name="engine_name", + ) + ) + + if not result.engine_version: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.REQUIRED_FIELD_MISSING, + message="В EngineResult не указана версия движка.", + field_name="engine_version", + ) + ) + + if not result.symbol: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.REQUIRED_FIELD_MISSING, + message="В EngineResult не указан торговый инструмент.", + field_name="symbol", + ) + ) + + if not result.timeframe: + issues.append( + ValidationIssue( + status=CheckStatus.ERROR, + reason=ReasonCode.REQUIRED_FIELD_MISSING, + message="В EngineResult не указан таймфрейм анализа.", + field_name="timeframe", + ) + ) + + if result.status == EngineStatus.UNKNOWN: + issues.append( + ValidationIssue( + status=CheckStatus.WARNING, + reason=ReasonCode.VALIDATION_FAILED, + message="EngineResult вернул неопределённый статус выполнения.", + field_name="status", + ) + ) + + if len(result.metrics) > MAX_ENGINE_METRICS: + issues.append( + ValidationIssue( + status=CheckStatus.WARNING, + reason=ReasonCode.VALIDATION_FAILED, + message=( + "Количество метрик Engine превышает архитектурный лимит. " + "Возможно, движок выполняет слишком много задач." + ), + field_name="metrics", + ) + ) + + payload_validation = validate_payload_has_no_trading_fields(result.payload) + issues.extend(payload_validation.issues) + + return _validation_result_from_issues( + tuple(issues), + details={ + "engine_name": result.engine_name, + "engine_version": result.engine_version, + "symbol": result.symbol, + "timeframe": result.timeframe, + "status": result.status.value, + "metrics_count": len(result.metrics), + }, + ) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/coordinator/__init__.py b/app/src/trading/market_intelligence/coordinator/__init__.py new file mode 100644 index 0000000..9054527 --- /dev/null +++ b/app/src/trading/market_intelligence/coordinator/__init__.py @@ -0,0 +1,2 @@ +# app/src/trading/market_intelligence/coordinator/__init__.py + diff --git a/app/src/trading/market_intelligence/coordinator/exceptions.py b/app/src/trading/market_intelligence/coordinator/exceptions.py new file mode 100644 index 0000000..03dae52 --- /dev/null +++ b/app/src/trading/market_intelligence/coordinator/exceptions.py @@ -0,0 +1,19 @@ +# app/src/trading/market_intelligence/coordinator/exceptions.py + +from __future__ import annotations + + +class CoordinatorError(Exception): + """Базовая ошибка Coordinator Layer.""" + + +class InvalidCoordinatorResultError(CoordinatorError): + """Coordinator сформировал некорректный результат.""" + + +class CoordinatorValidationError(CoordinatorError): + """Ошибка проверки входных данных Coordinator.""" + + +class CoordinatorExecutionError(CoordinatorError): + """Ошибка выполнения Coordinator.""" \ No newline at end of file diff --git a/app/src/trading/market_intelligence/coordinator/protocol.py b/app/src/trading/market_intelligence/coordinator/protocol.py new file mode 100644 index 0000000..a61b069 --- /dev/null +++ b/app/src/trading/market_intelligence/coordinator/protocol.py @@ -0,0 +1,21 @@ +# app/src/trading/market_intelligence/coordinator/protocol.py + +from __future__ import annotations + +from typing import Protocol + +from src.trading.market_intelligence.common.models import ( + CoordinatorResult, + RuntimeResult, +) + + +class CoordinatorProtocol(Protocol): + """Контракт Coordinator Layer.""" + + async def coordinate( + self, + runtime_result: RuntimeResult, + ) -> CoordinatorResult: + """Согласовать результаты Runtime и вернуть итоговый результат Coordinator.""" + ... \ No newline at end of file diff --git a/app/src/trading/market_intelligence/coordinator/rules.py b/app/src/trading/market_intelligence/coordinator/rules.py new file mode 100644 index 0000000..e7b3143 --- /dev/null +++ b/app/src/trading/market_intelligence/coordinator/rules.py @@ -0,0 +1,108 @@ +# app/src/trading/market_intelligence/coordinator/rules.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.enums import ( + EngineStatus, + MarketBias, + MarketDirection, + MarketPhase, + MarketRegime, + RiskLevel, +) +from src.trading.market_intelligence.common.models import ( + CoordinatorDiagnostics, + CoordinatorEvaluationMeta, + CoordinatorResult, + RuntimeResult, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.common.scores import ( + EngineConfidence, + EngineScore, +) + + +class CoordinatorRules: + """Правила согласования результатов Engine.""" + + def coordinate( + self, + runtime_result: RuntimeResult, + ) -> CoordinatorResult: + """Согласовать результаты Runtime.""" + return CoordinatorResult( + runtime_result=runtime_result, + diagnostics=CoordinatorDiagnostics(), + meta=CoordinatorEvaluationMeta( + coordinator_version="1.0", + ), + status=self._resolve_status(runtime_result), + score=self._resolve_score(runtime_result), + confidence=self._resolve_confidence(runtime_result), + reason=ReasonCode.UNKNOWN, + direction=self._resolve_direction(runtime_result), + bias=self._resolve_bias(runtime_result), + phase=self._resolve_phase(runtime_result), + regime=self._resolve_regime(runtime_result), + risk_level=self._resolve_risk_level(runtime_result), + ) + + def _resolve_status( + self, + runtime_result: RuntimeResult, + ) -> EngineStatus: + """Определить итоговый статус Coordinator.""" + if runtime_result.is_successful: + return EngineStatus.OK + + return EngineStatus.ERROR + + def _resolve_score( + self, + runtime_result: RuntimeResult, + ) -> EngineScore: + """Вычислить итоговый Score.""" + return EngineScore() + + def _resolve_confidence( + self, + runtime_result: RuntimeResult, + ) -> EngineConfidence: + """Вычислить итоговый Confidence.""" + return EngineConfidence() + + def _resolve_direction( + self, + runtime_result: RuntimeResult, + ) -> MarketDirection: + """Определить итоговое направление рынка.""" + return MarketDirection.UNKNOWN + + def _resolve_bias( + self, + runtime_result: RuntimeResult, + ) -> MarketBias: + """Определить итоговый Bias.""" + return MarketBias.UNKNOWN + + def _resolve_phase( + self, + runtime_result: RuntimeResult, + ) -> MarketPhase: + """Определить итоговую фазу рынка.""" + return MarketPhase.UNKNOWN + + def _resolve_regime( + self, + runtime_result: RuntimeResult, + ) -> MarketRegime: + """Определить итоговый режим рынка.""" + return MarketRegime.UNKNOWN + + def _resolve_risk_level( + self, + runtime_result: RuntimeResult, + ) -> RiskLevel: + """Определить итоговый уровень риска.""" + return RiskLevel.UNKNOWN \ No newline at end of file diff --git a/app/src/trading/market_intelligence/coordinator/service.py b/app/src/trading/market_intelligence/coordinator/service.py new file mode 100644 index 0000000..ec4231f --- /dev/null +++ b/app/src/trading/market_intelligence/coordinator/service.py @@ -0,0 +1,31 @@ +# app/src/trading/market_intelligence/coordinator/service.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import ( + CoordinatorResult, + RuntimeResult, +) +from src.trading.market_intelligence.coordinator.protocol import CoordinatorProtocol +from src.trading.market_intelligence.coordinator.rules import CoordinatorRules +from src.trading.market_intelligence.coordinator.validation import ( + CoordinatorValidation, +) + + +class CoordinatorService(CoordinatorProtocol): + """Единая публичная точка входа Coordinator Layer.""" + + def __init__(self) -> None: + """Создать Coordinator Service.""" + self._validation = CoordinatorValidation() + self._rules = CoordinatorRules() + + async def coordinate( + self, + runtime_result: RuntimeResult, + ) -> CoordinatorResult: + """Согласовать результаты Runtime.""" + self._validation.validate(runtime_result) + + return self._rules.coordinate(runtime_result) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/coordinator/validation.py b/app/src/trading/market_intelligence/coordinator/validation.py new file mode 100644 index 0000000..83383a4 --- /dev/null +++ b/app/src/trading/market_intelligence/coordinator/validation.py @@ -0,0 +1,51 @@ +# app/src/trading/market_intelligence/coordinator/validation.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import RuntimeResult +from src.trading.market_intelligence.coordinator.exceptions import ( + CoordinatorValidationError, +) + + +class CoordinatorValidation: + """Проверка входного RuntimeResult для Coordinator.""" + + def validate( + self, + runtime_result: RuntimeResult, + ) -> None: + """Проверить RuntimeResult перед координацией.""" + self._validate_runtime_result_exists(runtime_result) + self._validate_runtime_has_results(runtime_result) + self._validate_runtime_successful(runtime_result) + + def _validate_runtime_result_exists( + self, + runtime_result: RuntimeResult, + ) -> None: + """Проверить, что RuntimeResult передан.""" + if runtime_result is None: + raise CoordinatorValidationError( + "RuntimeResult is required for Coordinator." + ) + + def _validate_runtime_has_results( + self, + runtime_result: RuntimeResult, + ) -> None: + """Проверить наличие результатов Engine.""" + if runtime_result.total_engines <= 0: + raise CoordinatorValidationError( + "RuntimeResult does not contain Engine results." + ) + + def _validate_runtime_successful( + self, + runtime_result: RuntimeResult, + ) -> None: + """Проверить отсутствие критических ошибок Runtime.""" + if not runtime_result.is_successful: + raise CoordinatorValidationError( + "RuntimeResult contains failed Engine results." + ) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/cycle/__init__.py b/app/src/trading/market_intelligence/cycle/__init__.py new file mode 100644 index 0000000..667d674 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/__init__.py diff --git a/app/src/trading/market_intelligence/cycle/calculators.py b/app/src/trading/market_intelligence/cycle/calculators.py new file mode 100644 index 0000000..4a65c64 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/calculators.py diff --git a/app/src/trading/market_intelligence/cycle/checks.py b/app/src/trading/market_intelligence/cycle/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/cycle/constants.py b/app/src/trading/market_intelligence/cycle/constants.py new file mode 100644 index 0000000..9a656a7 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/constants.py diff --git a/app/src/trading/market_intelligence/cycle/engine.py b/app/src/trading/market_intelligence/cycle/engine.py new file mode 100644 index 0000000..97a9848 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/engine.py diff --git a/app/src/trading/market_intelligence/cycle/evaluators.py b/app/src/trading/market_intelligence/cycle/evaluators.py new file mode 100644 index 0000000..c92c5e9 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/evaluators.py diff --git a/app/src/trading/market_intelligence/cycle/models.py b/app/src/trading/market_intelligence/cycle/models.py new file mode 100644 index 0000000..26e9ee1 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/models.py diff --git a/app/src/trading/market_intelligence/cycle/payloads.py b/app/src/trading/market_intelligence/cycle/payloads.py new file mode 100644 index 0000000..539c106 --- /dev/null +++ b/app/src/trading/market_intelligence/cycle/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/cycle/payloads.py diff --git a/app/src/trading/market_intelligence/engine/__init__.py b/app/src/trading/market_intelligence/engine/__init__.py new file mode 100644 index 0000000..a949981 --- /dev/null +++ b/app/src/trading/market_intelligence/engine/__init__.py @@ -0,0 +1,2 @@ +# app/src/trading/market_intelligence/engine/__init__.py + diff --git a/app/src/trading/market_intelligence/engine/base.py b/app/src/trading/market_intelligence/engine/base.py new file mode 100644 index 0000000..46ef06c --- /dev/null +++ b/app/src/trading/market_intelligence/engine/base.py @@ -0,0 +1,130 @@ +# app/src/trading/market_intelligence/engine/base.py + +from __future__ import annotations + +import time +from abc import ABC, abstractmethod +from dataclasses import replace +from typing import ClassVar + +from src.trading.market_intelligence.common.enums import EngineStatus +from src.trading.market_intelligence.common.models import ( + EngineContext, + EngineDiagnostics, + EngineEvaluationMeta, + EngineMetadata, + EngineResult, +) +from src.trading.market_intelligence.common.reasons import ReasonCode +from src.trading.market_intelligence.engine.protocol import EngineProtocol +from src.trading.market_intelligence.engine.exceptions import ( + EngineConfigurationError, + EngineContextError, + EngineContractError, +) + + +class BaseEngine(EngineProtocol, ABC): + """Базовая инфраструктура аналитического Engine.""" + + ENGINE_METADATA: ClassVar[EngineMetadata | None] = None + + @classmethod + def get_metadata(cls) -> EngineMetadata: + """Вернуть metadata Engine без создания экземпляра.""" + if cls.ENGINE_METADATA is None: + raise EngineConfigurationError("ENGINE_METADATA must be defined") + + return cls.ENGINE_METADATA + + async def analyze( + self, + context: EngineContext, + ) -> EngineResult: + """Выполнить единый жизненный цикл анализа.""" + started_at = time.perf_counter() + + try: + self._validate_context(context) + + result = await self._analyze_impl(context) + + duration_ms = (time.perf_counter() - started_at) * 1000.0 + return self._normalize_result(result, context, duration_ms) + + except Exception as error: + duration_ms = (time.perf_counter() - started_at) * 1000.0 + return self._build_error_result(context, error, duration_ms) + + def _validate_context( + self, + context: EngineContext, + ) -> None: + """Проверить минимальную корректность входного контекста.""" + if not context.symbol: + raise EngineContextError("EngineContext.symbol is required") + + if not context.timeframe: + raise EngineContextError("EngineContext.timeframe is required") + + @abstractmethod + async def _analyze_impl( + self, + context: EngineContext, + ) -> EngineResult: + """Выполнить предметный анализ в конкретном Engine.""" + raise NotImplementedError + + def _normalize_result( + self, + result: EngineResult, + context: EngineContext, + duration_ms: float, + ) -> EngineResult: + """Дополнить результат служебной metadata выполнения.""" + metadata = self.get_metadata() + + if not isinstance(result, EngineResult): + raise EngineContractError("_analyze_impl() must return EngineResult") + + return replace( + result, + engine_name=metadata.name, + engine_version=metadata.version, + symbol=context.symbol, + timeframe=context.timeframe, + meta=EngineEvaluationMeta( + engine_name=metadata.name, + engine_version=metadata.version, + calculated_at=time.time(), + duration_ms=duration_ms, + ), + ) + + def _build_error_result( + self, + context: EngineContext, + error: Exception, + duration_ms: float, + ) -> EngineResult: + """Сформировать безопасный результат при ошибке Engine.""" + metadata = self.get_metadata() + + return EngineResult( + engine_name=metadata.name, + engine_version=metadata.version, + symbol=context.symbol, + timeframe=context.timeframe, + status=EngineStatus.ERROR, + reason=ReasonCode.UNKNOWN, + diagnostics=EngineDiagnostics( + reason=ReasonCode.UNKNOWN, + errors=[str(error)], + ), + meta=EngineEvaluationMeta( + engine_name=metadata.name, + engine_version=metadata.version, + calculated_at=time.time(), + duration_ms=duration_ms, + ), + ) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/engine/exceptions.py b/app/src/trading/market_intelligence/engine/exceptions.py new file mode 100644 index 0000000..a440ef8 --- /dev/null +++ b/app/src/trading/market_intelligence/engine/exceptions.py @@ -0,0 +1,23 @@ +# app/src/trading/market_intelligence/engine/exceptions.py + +from __future__ import annotations + + +class EngineError(Exception): + """Базовая ошибка Engine Layer.""" + + +class EngineConfigurationError(EngineError): + """Ошибка конфигурации Engine.""" + + +class EngineContractError(EngineError): + """Ошибка нарушения контракта Engine.""" + + +class EngineContextError(EngineError): + """Ошибка входного контекста Engine.""" + + +class EngineExecutionError(EngineError): + """Ошибка выполнения Engine.""" \ No newline at end of file diff --git a/app/src/trading/market_intelligence/engine/protocol.py b/app/src/trading/market_intelligence/engine/protocol.py new file mode 100644 index 0000000..a5437a1 --- /dev/null +++ b/app/src/trading/market_intelligence/engine/protocol.py @@ -0,0 +1,27 @@ +# app/src/trading/market_intelligence/engine/protocol.py + +from __future__ import annotations + +from typing import Protocol + +from src.trading.market_intelligence.common.models import ( + EngineContext, + EngineMetadata, + EngineResult, +) + + +class EngineProtocol(Protocol): + """Контракт аналитического Engine.""" + + @classmethod + def get_metadata(cls) -> EngineMetadata: + """Вернуть metadata Engine без создания экземпляра.""" + ... + + async def analyze( + self, + context: EngineContext, + ) -> EngineResult: + """Выполнить анализ рыночного контекста.""" + ... \ No newline at end of file diff --git a/app/src/trading/market_intelligence/liquidity/__init__.py b/app/src/trading/market_intelligence/liquidity/__init__.py new file mode 100644 index 0000000..fe2efbd --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/__init__.py diff --git a/app/src/trading/market_intelligence/liquidity/calculators.py b/app/src/trading/market_intelligence/liquidity/calculators.py new file mode 100644 index 0000000..bdb2b4a --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/calculators.py diff --git a/app/src/trading/market_intelligence/liquidity/checks.py b/app/src/trading/market_intelligence/liquidity/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/liquidity/constants.py b/app/src/trading/market_intelligence/liquidity/constants.py new file mode 100644 index 0000000..78f43bb --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/constants.py diff --git a/app/src/trading/market_intelligence/liquidity/engine.py b/app/src/trading/market_intelligence/liquidity/engine.py new file mode 100644 index 0000000..a5d7505 --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/engine.py diff --git a/app/src/trading/market_intelligence/liquidity/evaluators.py b/app/src/trading/market_intelligence/liquidity/evaluators.py new file mode 100644 index 0000000..bdca5a3 --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/evaluators.py diff --git a/app/src/trading/market_intelligence/liquidity/models.py b/app/src/trading/market_intelligence/liquidity/models.py new file mode 100644 index 0000000..b6db0c6 --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/models.py diff --git a/app/src/trading/market_intelligence/liquidity/payloads.py b/app/src/trading/market_intelligence/liquidity/payloads.py new file mode 100644 index 0000000..a5a3406 --- /dev/null +++ b/app/src/trading/market_intelligence/liquidity/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/liquidity/payloads.py diff --git a/app/src/trading/market_intelligence/momentum/__init__.py b/app/src/trading/market_intelligence/momentum/__init__.py new file mode 100644 index 0000000..f6232e3 --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/__init__.py diff --git a/app/src/trading/market_intelligence/momentum/calculators.py b/app/src/trading/market_intelligence/momentum/calculators.py new file mode 100644 index 0000000..1a5f300 --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/calculators.py diff --git a/app/src/trading/market_intelligence/momentum/checks.py b/app/src/trading/market_intelligence/momentum/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/momentum/constants.py b/app/src/trading/market_intelligence/momentum/constants.py new file mode 100644 index 0000000..85b7c0b --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/constants.py diff --git a/app/src/trading/market_intelligence/momentum/engine.py b/app/src/trading/market_intelligence/momentum/engine.py new file mode 100644 index 0000000..0750186 --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/engine.py diff --git a/app/src/trading/market_intelligence/momentum/evaluators.py b/app/src/trading/market_intelligence/momentum/evaluators.py new file mode 100644 index 0000000..781e69e --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/evaluators.py diff --git a/app/src/trading/market_intelligence/momentum/models.py b/app/src/trading/market_intelligence/momentum/models.py new file mode 100644 index 0000000..1233f6a --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/models.py diff --git a/app/src/trading/market_intelligence/momentum/payloads.py b/app/src/trading/market_intelligence/momentum/payloads.py new file mode 100644 index 0000000..89eef1f --- /dev/null +++ b/app/src/trading/market_intelligence/momentum/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/momentum/payloads.py diff --git a/app/src/trading/market_intelligence/regime/__init__.py b/app/src/trading/market_intelligence/regime/__init__.py new file mode 100644 index 0000000..e3e4f93 --- /dev/null +++ b/app/src/trading/market_intelligence/regime/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/__init__.py diff --git a/app/src/trading/market_intelligence/regime/calculators.py b/app/src/trading/market_intelligence/regime/calculators.py new file mode 100644 index 0000000..92f9cc7 --- /dev/null +++ b/app/src/trading/market_intelligence/regime/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/calculators.py diff --git a/app/src/trading/market_intelligence/regime/checks.py b/app/src/trading/market_intelligence/regime/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/regime/constants.py b/app/src/trading/market_intelligence/regime/constants.py new file mode 100644 index 0000000..ba5fdc5 --- /dev/null +++ b/app/src/trading/market_intelligence/regime/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/constants.py diff --git a/app/src/trading/market_intelligence/regime/engine.py b/app/src/trading/market_intelligence/regime/engine.py new file mode 100644 index 0000000..3edcdac --- /dev/null +++ b/app/src/trading/market_intelligence/regime/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/engine.py diff --git a/app/src/trading/market_intelligence/regime/evaluators.py b/app/src/trading/market_intelligence/regime/evaluators.py new file mode 100644 index 0000000..26221ba --- /dev/null +++ b/app/src/trading/market_intelligence/regime/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/evaluators.py diff --git a/app/src/trading/market_intelligence/regime/models.py b/app/src/trading/market_intelligence/regime/models.py new file mode 100644 index 0000000..1a3e2c2 --- /dev/null +++ b/app/src/trading/market_intelligence/regime/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/models.py diff --git a/app/src/trading/market_intelligence/regime/payloads.py b/app/src/trading/market_intelligence/regime/payloads.py new file mode 100644 index 0000000..395b0f8 --- /dev/null +++ b/app/src/trading/market_intelligence/regime/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/regime/payloads.py diff --git a/app/src/trading/market_intelligence/runtime/__init__.py b/app/src/trading/market_intelligence/runtime/__init__.py new file mode 100644 index 0000000..38ff32e --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/runtime/__init__.py \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/dependencies.py b/app/src/trading/market_intelligence/runtime/dependencies.py new file mode 100644 index 0000000..15d469e --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/dependencies.py @@ -0,0 +1,116 @@ +# app/src/trading/market_intelligence/runtime/dependencies.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import EngineRegistration +from src.trading.market_intelligence.common.types import EngineName +from src.trading.market_intelligence.runtime.exceptions import ( + CircularEngineDependencyError, + MissingEngineDependencyError, +) + + +class RuntimeDependencies: + """Разрешение зависимостей между Engine.""" + + def resolve( + self, + registrations: tuple[EngineRegistration, ...], + ) -> tuple[EngineRegistration, ...]: + """Вернуть Engine в порядке выполнения.""" + registration_by_name = self._build_registration_map(registrations) + graph = self._build_graph(registrations) + + self._validate_dependencies(graph, registration_by_name) + + return self._topological_sort(graph, registration_by_name) + + def _build_registration_map( + self, + registrations: tuple[EngineRegistration, ...], + ) -> dict[EngineName, EngineRegistration]: + """Построить быстрый доступ к регистрациям по имени Engine.""" + return { + registration.metadata.name: registration + for registration in registrations + } + + def _build_graph( + self, + registrations: tuple[EngineRegistration, ...], + ) -> dict[EngineName, tuple[EngineName, ...]]: + """Построить граф обязательных зависимостей Engine.""" + return { + registration.metadata.name: registration.metadata.required_dependencies + for registration in registrations + } + + def _validate_dependencies( + self, + graph: dict[EngineName, tuple[EngineName, ...]], + registration_by_name: dict[EngineName, EngineRegistration], + ) -> None: + """Проверить наличие всех обязательных зависимостей.""" + for engine_name, dependencies in graph.items(): + for dependency_name in dependencies: + if dependency_name not in registration_by_name: + raise MissingEngineDependencyError( + f"Engine '{engine_name}' requires missing dependency " + f"'{dependency_name}'" + ) + + def _topological_sort( + self, + graph: dict[EngineName, tuple[EngineName, ...]], + registration_by_name: dict[EngineName, EngineRegistration], + ) -> tuple[EngineRegistration, ...]: + """Выполнить топологическую сортировку Engine.""" + ordered: list[EngineRegistration] = [] + visiting: set[EngineName] = set() + visited: set[EngineName] = set() + + for engine_name in graph: + self._visit( + engine_name=engine_name, + graph=graph, + registration_by_name=registration_by_name, + visiting=visiting, + visited=visited, + ordered=ordered, + ) + + return tuple(ordered) + + def _visit( + self, + engine_name: EngineName, + graph: dict[EngineName, tuple[EngineName, ...]], + registration_by_name: dict[EngineName, EngineRegistration], + visiting: set[EngineName], + visited: set[EngineName], + ordered: list[EngineRegistration], + ) -> None: + """Посетить Engine и его зависимости.""" + if engine_name in visited: + return + + if engine_name in visiting: + raise CircularEngineDependencyError( + f"Circular Engine dependency detected for '{engine_name}'" + ) + + visiting.add(engine_name) + + for dependency_name in graph[engine_name]: + self._visit( + engine_name=dependency_name, + graph=graph, + registration_by_name=registration_by_name, + visiting=visiting, + visited=visited, + ordered=ordered, + ) + + visiting.remove(engine_name) + visited.add(engine_name) + ordered.append(registration_by_name[engine_name]) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/exceptions.py b/app/src/trading/market_intelligence/runtime/exceptions.py new file mode 100644 index 0000000..8d93f11 --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/exceptions.py @@ -0,0 +1,43 @@ +# app/src/trading/market_intelligence/runtime/exceptions.py + +from __future__ import annotations + + +class RuntimeError(Exception): + """Базовая ошибка Runtime Layer.""" + + +class EngineNotRegisteredError(RuntimeError): + """Engine не зарегистрирован в Runtime.""" + + +class InvalidRuntimeContextError(RuntimeError): + """Некорректный Runtime Context.""" + + +class EngineExecutionError(RuntimeError): + """Ошибка выполнения Engine внутри Runtime.""" + + +class MissingEngineDependencyError(RuntimeError): + """Обязательная зависимость Engine отсутствует.""" + + +class CircularEngineDependencyError(RuntimeError): + """Обнаружена циклическая зависимость Engine.""" + + +class RuntimeValidationError(RuntimeError): + """Базовая ошибка проверки Runtime.""" + + +class EmptyRuntimeRegistryError(RuntimeValidationError): + """Runtime Registry не содержит зарегистрированных Engine.""" + + +class DuplicateEngineRegistrationError(RuntimeValidationError): + """Обнаружена повторная регистрация Engine.""" + + +class InvalidEngineMetadataError(RuntimeValidationError): + """Некорректная metadata зарегистрированного Engine.""" \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/protocol.py b/app/src/trading/market_intelligence/runtime/protocol.py new file mode 100644 index 0000000..e44984e --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/protocol.py @@ -0,0 +1,44 @@ +# app/src/trading/market_intelligence/runtime/protocol.py + +from __future__ import annotations + +from typing import Protocol + +from src.trading.market_intelligence.common.models import ( + EngineContext, + RuntimeResult, +) +from src.trading.market_intelligence.common.types import EngineName +from src.trading.market_intelligence.engine.protocol import EngineProtocol + + +class RuntimeProtocol(Protocol): + """Контракт Runtime Layer.""" + + def register_engine( + self, + engine_type: type[EngineProtocol], + ) -> None: + """Зарегистрировать класс Engine в Runtime.""" + ... + + def unregister_engine( + self, + engine_name: EngineName, + ) -> None: + """Удалить Engine из Runtime по имени.""" + ... + + def get_engine( + self, + engine_name: EngineName, + ) -> type[EngineProtocol] | None: + """Вернуть зарегистрированный класс Engine по имени.""" + ... + + async def analyze( + self, + context: EngineContext, + ) -> RuntimeResult: + """Выполнить анализ через зарегистрированные Engine.""" + ... \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/registry.py b/app/src/trading/market_intelligence/runtime/registry.py new file mode 100644 index 0000000..052f434 --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/registry.py @@ -0,0 +1,56 @@ +# app/src/trading/market_intelligence/runtime/registry.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import EngineRegistration +from src.trading.market_intelligence.common.types import EngineName +from src.trading.market_intelligence.engine.protocol import EngineProtocol + + +class RuntimeRegistry: + """Каталог зарегистрированных Engine.""" + + def __init__(self) -> None: + """Создать пустой каталог Engine.""" + self._engines: dict[EngineName, EngineRegistration] = {} + + def register( + self, + engine_type: type[EngineProtocol], + ) -> None: + """Зарегистрировать класс Engine.""" + metadata = engine_type.get_metadata() + + self._engines[metadata.name] = EngineRegistration( + engine_type=engine_type, + metadata=metadata, + ) + + def unregister( + self, + engine_name: EngineName, + ) -> None: + """Удалить Engine из каталога.""" + self._engines.pop(engine_name, None) + + def get( + self, + engine_name: EngineName, + ) -> EngineRegistration | None: + """Вернуть регистрацию Engine по имени.""" + return self._engines.get(engine_name) + + def contains( + self, + engine_name: EngineName, + ) -> bool: + """Проверить наличие Engine в каталоге.""" + return engine_name in self._engines + + def list(self) -> tuple[EngineRegistration, ...]: + """Вернуть все зарегистрированные Engine.""" + return tuple(self._engines.values()) + + def clear(self) -> None: + """Очистить каталог Engine.""" + self._engines.clear() \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/runner.py b/app/src/trading/market_intelligence/runtime/runner.py new file mode 100644 index 0000000..3988835 --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/runner.py @@ -0,0 +1,23 @@ +# app/src/trading/market_intelligence/runtime/runner.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import ( + EngineContext, + EngineRegistration, + EngineResult, +) + + +class RuntimeRunner: + """Исполнитель одного зарегистрированного Engine.""" + + async def run( + self, + registration: EngineRegistration, + context: EngineContext, + ) -> EngineResult: + """Выполнить один зарегистрированный Engine.""" + engine = registration.engine_type() + + return await engine.analyze(context) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/service.py b/app/src/trading/market_intelligence/runtime/service.py new file mode 100644 index 0000000..3c9d0e6 --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/service.py @@ -0,0 +1,82 @@ +# app/src/trading/market_intelligence/runtime/service.py + +from __future__ import annotations + +import time + +from src.trading.market_intelligence.common.models import ( + EngineContext, + EngineRegistration, + RuntimeResult, +) +from src.trading.market_intelligence.common.types import EngineName +from src.trading.market_intelligence.engine.protocol import EngineProtocol +from src.trading.market_intelligence.runtime.dependencies import RuntimeDependencies +from src.trading.market_intelligence.runtime.registry import RuntimeRegistry +from src.trading.market_intelligence.runtime.runner import RuntimeRunner +from src.trading.market_intelligence.runtime.validation import RuntimeValidation + + +class RuntimeService: + """Единая публичная точка входа Runtime Layer.""" + + def __init__(self) -> None: + """Создать Runtime Service.""" + self._registry = RuntimeRegistry() + self._validation = RuntimeValidation() + self._dependencies = RuntimeDependencies() + self._runner = RuntimeRunner() + + def register_engine( + self, + engine_type: type[EngineProtocol], + ) -> None: + """Зарегистрировать класс Engine.""" + self._registry.register(engine_type) + + def unregister_engine( + self, + engine_name: EngineName, + ) -> None: + """Удалить Engine из Runtime.""" + self._registry.unregister(engine_name) + + def get_engine( + self, + engine_name: EngineName, + ) -> EngineRegistration | None: + """Вернуть регистрацию Engine по имени.""" + return self._registry.get(engine_name) + + async def analyze( + self, + context: EngineContext, + ) -> RuntimeResult: + """Выполнить анализ через зарегистрированные Engine.""" + started_at = time.time() + + self._validation.validate(self._registry) + + registrations = self._registry.list() + ordered_registrations = self._dependencies.resolve(registrations) + + engine_results = [] + + for registration in ordered_registrations: + result = await self._runner.run( + registration=registration, + context=context, + ) + engine_results.append(result) + + finished_at = time.time() + + return RuntimeResult( + engine_results=tuple(engine_results), + started_at=started_at, + finished_at=finished_at, + duration_ms=(finished_at - started_at) * 1000, + metadata={ + "runtime_component": "RuntimeService", + }, + ) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/runtime/validation.py b/app/src/trading/market_intelligence/runtime/validation.py new file mode 100644 index 0000000..852baa5 --- /dev/null +++ b/app/src/trading/market_intelligence/runtime/validation.py @@ -0,0 +1,79 @@ +# app/src/trading/market_intelligence/runtime/validation.py + +from __future__ import annotations + +from src.trading.market_intelligence.common.models import EngineRegistration +from src.trading.market_intelligence.common.types import EngineName +from src.trading.market_intelligence.runtime.dependencies import RuntimeDependencies +from src.trading.market_intelligence.runtime.exceptions import ( + DuplicateEngineRegistrationError, + EmptyRuntimeRegistryError, + InvalidEngineMetadataError, +) +from src.trading.market_intelligence.runtime.registry import RuntimeRegistry + + +class RuntimeValidation: + """Проверка корректности Runtime перед запуском.""" + + def __init__(self) -> None: + """Создать Runtime Validation.""" + self._dependencies = RuntimeDependencies() + + def validate( + self, + registry: RuntimeRegistry, + ) -> None: + """Проверить готовность Runtime.""" + registrations = registry.list() + + self._validate_registry(registrations) + self._validate_metadata(registrations) + self._validate_duplicates(registrations) + + self._dependencies.resolve(registrations) + + def _validate_registry( + self, + registrations: tuple[EngineRegistration, ...], + ) -> None: + """Проверить, что Registry не пуст.""" + if not registrations: + raise EmptyRuntimeRegistryError( + "Runtime Registry does not contain registered Engine." + ) + + def _validate_metadata( + self, + registrations: tuple[EngineRegistration, ...], + ) -> None: + """Проверить корректность metadata Engine.""" + for registration in registrations: + metadata = registration.metadata + + if not metadata.name: + raise InvalidEngineMetadataError( + "Engine name is not specified." + ) + + if not metadata.version: + raise InvalidEngineMetadataError( + f"Engine '{metadata.name}' has no version." + ) + + def _validate_duplicates( + self, + registrations: tuple[EngineRegistration, ...], + ) -> None: + """Проверить отсутствие повторной регистрации Engine.""" + names: set[EngineName] = set() + + for registration in registrations: + engine_name = registration.metadata.name + + if engine_name in names: + raise DuplicateEngineRegistrationError( + f"Duplicate Engine registration: '{engine_name}'." + ) + + names.add(engine_name) \ No newline at end of file diff --git a/app/src/trading/market_intelligence/structure/__init__.py b/app/src/trading/market_intelligence/structure/__init__.py new file mode 100644 index 0000000..2f0e84a --- /dev/null +++ b/app/src/trading/market_intelligence/structure/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/__init__.py diff --git a/app/src/trading/market_intelligence/structure/calculators.py b/app/src/trading/market_intelligence/structure/calculators.py new file mode 100644 index 0000000..32e295a --- /dev/null +++ b/app/src/trading/market_intelligence/structure/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/calculators.py diff --git a/app/src/trading/market_intelligence/structure/checks.py b/app/src/trading/market_intelligence/structure/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/structure/constants.py b/app/src/trading/market_intelligence/structure/constants.py new file mode 100644 index 0000000..abd6771 --- /dev/null +++ b/app/src/trading/market_intelligence/structure/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/constants.py diff --git a/app/src/trading/market_intelligence/structure/engine.py b/app/src/trading/market_intelligence/structure/engine.py new file mode 100644 index 0000000..d4887a2 --- /dev/null +++ b/app/src/trading/market_intelligence/structure/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/engine.py diff --git a/app/src/trading/market_intelligence/structure/evaluators.py b/app/src/trading/market_intelligence/structure/evaluators.py new file mode 100644 index 0000000..9c6a3ad --- /dev/null +++ b/app/src/trading/market_intelligence/structure/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/evaluators.py diff --git a/app/src/trading/market_intelligence/structure/models.py b/app/src/trading/market_intelligence/structure/models.py new file mode 100644 index 0000000..5e674eb --- /dev/null +++ b/app/src/trading/market_intelligence/structure/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/models.py diff --git a/app/src/trading/market_intelligence/structure/payloads.py b/app/src/trading/market_intelligence/structure/payloads.py new file mode 100644 index 0000000..7bdb1e7 --- /dev/null +++ b/app/src/trading/market_intelligence/structure/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/structure/payloads.py diff --git a/app/src/trading/market_intelligence/trend/__init__.py b/app/src/trading/market_intelligence/trend/__init__.py new file mode 100644 index 0000000..9ba78c0 --- /dev/null +++ b/app/src/trading/market_intelligence/trend/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/__init__.py diff --git a/app/src/trading/market_intelligence/trend/calculators.py b/app/src/trading/market_intelligence/trend/calculators.py new file mode 100644 index 0000000..3f331ea --- /dev/null +++ b/app/src/trading/market_intelligence/trend/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/calculators.py diff --git a/app/src/trading/market_intelligence/trend/checks.py b/app/src/trading/market_intelligence/trend/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/trend/constants.py b/app/src/trading/market_intelligence/trend/constants.py new file mode 100644 index 0000000..f8707c7 --- /dev/null +++ b/app/src/trading/market_intelligence/trend/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/constants.py diff --git a/app/src/trading/market_intelligence/trend/engine.py b/app/src/trading/market_intelligence/trend/engine.py new file mode 100644 index 0000000..43c7c51 --- /dev/null +++ b/app/src/trading/market_intelligence/trend/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/engine.py diff --git a/app/src/trading/market_intelligence/trend/evaluators.py b/app/src/trading/market_intelligence/trend/evaluators.py new file mode 100644 index 0000000..a09ba70 --- /dev/null +++ b/app/src/trading/market_intelligence/trend/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/evaluators.py diff --git a/app/src/trading/market_intelligence/trend/models.py b/app/src/trading/market_intelligence/trend/models.py new file mode 100644 index 0000000..75dcf1c --- /dev/null +++ b/app/src/trading/market_intelligence/trend/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/models.py diff --git a/app/src/trading/market_intelligence/trend/payloads.py b/app/src/trading/market_intelligence/trend/payloads.py new file mode 100644 index 0000000..09f1b53 --- /dev/null +++ b/app/src/trading/market_intelligence/trend/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/trend/payloads.py diff --git a/app/src/trading/market_intelligence/volatility/__init__.py b/app/src/trading/market_intelligence/volatility/__init__.py new file mode 100644 index 0000000..33a3035 --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/__init__.py diff --git a/app/src/trading/market_intelligence/volatility/calculators.py b/app/src/trading/market_intelligence/volatility/calculators.py new file mode 100644 index 0000000..d5f2a1f --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/calculators.py diff --git a/app/src/trading/market_intelligence/volatility/checks.py b/app/src/trading/market_intelligence/volatility/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/volatility/constants.py b/app/src/trading/market_intelligence/volatility/constants.py new file mode 100644 index 0000000..6ad6cd1 --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/constants.py diff --git a/app/src/trading/market_intelligence/volatility/engine.py b/app/src/trading/market_intelligence/volatility/engine.py new file mode 100644 index 0000000..77c07c6 --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/engine.py diff --git a/app/src/trading/market_intelligence/volatility/evaluators.py b/app/src/trading/market_intelligence/volatility/evaluators.py new file mode 100644 index 0000000..04642cf --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/evaluators.py diff --git a/app/src/trading/market_intelligence/volatility/models.py b/app/src/trading/market_intelligence/volatility/models.py new file mode 100644 index 0000000..4cdce16 --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/models.py diff --git a/app/src/trading/market_intelligence/volatility/payloads.py b/app/src/trading/market_intelligence/volatility/payloads.py new file mode 100644 index 0000000..2278610 --- /dev/null +++ b/app/src/trading/market_intelligence/volatility/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/volatility/payloads.py diff --git a/app/src/trading/market_intelligence/wave/__init__.py b/app/src/trading/market_intelligence/wave/__init__.py new file mode 100644 index 0000000..aec7afc --- /dev/null +++ b/app/src/trading/market_intelligence/wave/__init__.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/__init__.py diff --git a/app/src/trading/market_intelligence/wave/calculators.py b/app/src/trading/market_intelligence/wave/calculators.py new file mode 100644 index 0000000..03e0e17 --- /dev/null +++ b/app/src/trading/market_intelligence/wave/calculators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/calculators.py diff --git a/app/src/trading/market_intelligence/wave/checks.py b/app/src/trading/market_intelligence/wave/checks.py new file mode 100644 index 0000000..e69de29 diff --git a/app/src/trading/market_intelligence/wave/constants.py b/app/src/trading/market_intelligence/wave/constants.py new file mode 100644 index 0000000..ad82667 --- /dev/null +++ b/app/src/trading/market_intelligence/wave/constants.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/constants.py diff --git a/app/src/trading/market_intelligence/wave/engine.py b/app/src/trading/market_intelligence/wave/engine.py new file mode 100644 index 0000000..eccd0a2 --- /dev/null +++ b/app/src/trading/market_intelligence/wave/engine.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/engine.py diff --git a/app/src/trading/market_intelligence/wave/evaluators.py b/app/src/trading/market_intelligence/wave/evaluators.py new file mode 100644 index 0000000..7d460bd --- /dev/null +++ b/app/src/trading/market_intelligence/wave/evaluators.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/evaluators.py diff --git a/app/src/trading/market_intelligence/wave/models.py b/app/src/trading/market_intelligence/wave/models.py new file mode 100644 index 0000000..5139c85 --- /dev/null +++ b/app/src/trading/market_intelligence/wave/models.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/models.py diff --git a/app/src/trading/market_intelligence/wave/payloads.py b/app/src/trading/market_intelligence/wave/payloads.py new file mode 100644 index 0000000..806e2b6 --- /dev/null +++ b/app/src/trading/market_intelligence/wave/payloads.py @@ -0,0 +1 @@ +# app/src/trading/market_intelligence/wave/payloads.py diff --git a/app/src/trading/strategies/scalp.py b/app/src/trading/strategies/scalp.py index 44ebe28..7405f44 100644 --- a/app/src/trading/strategies/scalp.py +++ b/app/src/trading/strategies/scalp.py @@ -4,8 +4,11 @@ from __future__ import annotations import time from typing import Any +from decimal import Decimal +from src.core.types import NumericLike from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.quote import Quote from src.trading.market_analysis.models import ( MarketState, MarketStructure, @@ -56,7 +59,7 @@ class ScalpStrategy: ) try: - snapshot = ExchangeService().get_market_snapshot( + quote = ExchangeService().get_quote( context.symbol, runtime_key="auto", ) @@ -75,8 +78,8 @@ class ScalpStrategy: }, ) - symbol = str(snapshot.get("symbol") or context.symbol) - current_price = self._analysis_price(snapshot) + symbol = quote.symbol or context.symbol + current_price = self._analysis_price(quote) if current_price <= 0: return SignalResult( @@ -86,7 +89,7 @@ class ScalpStrategy: payload={ "strategy": self.name, "symbol": symbol, - "snapshot": snapshot, + "snapshot": self._quote_snapshot(quote), "market_analysis": market.payload, "entry_block_reason": "INVALID_MARKET_PRICE", "entry_block_message": "нет цены", @@ -113,7 +116,7 @@ class ScalpStrategy: base_payload = self._base_payload( symbol=symbol, current_price=current_price, - snapshot=snapshot, + quote=quote, market=market, prices=prices, ) @@ -378,7 +381,7 @@ class ScalpStrategy: *, symbol: str, current_price: float, - snapshot: dict[str, Any], + quote: Quote, market: Any, prices: list[float], ) -> dict[str, Any]: @@ -386,9 +389,9 @@ class ScalpStrategy: "strategy": self.name, "symbol": symbol, "analysis_price": current_price, - "last_price": snapshot.get("last_price"), - "bid_price": snapshot.get("bid_price"), - "ask_price": snapshot.get("ask_price"), + "last_price": self._safe_float(quote.last_price), + "bid_price": self._safe_float(quote.bid_price), + "ask_price": self._safe_float(quote.ask_price), "market_state": market.state.value, "market_trend": market.trend.value, "market_volatility": market.volatility.value, @@ -477,17 +480,26 @@ class ScalpStrategy: "runtime_window_size": len(prices), } + def _quote_snapshot(self, quote: Quote) -> dict[str, object]: + return { + "symbol": quote.symbol, + "last_price": self._safe_float(quote.last_price), + "bid_price": self._safe_float(quote.bid_price), + "ask_price": self._safe_float(quote.ask_price), + "source": quote.source, + } + def _analysis_price( self, - snapshot: dict[str, Any], + quote: Quote, ) -> float: - bid = self._safe_float(snapshot.get("bid_price")) - ask = self._safe_float(snapshot.get("ask_price")) + bid = self._safe_float(quote.bid_price) + ask = self._safe_float(quote.ask_price) if bid is not None and ask is not None and bid > 0 and ask > 0: return (bid + ask) / 2 - last = self._safe_float(snapshot.get("last_price")) + last = self._safe_float(quote.last_price) if last is not None and last > 0: return last @@ -496,7 +508,7 @@ class ScalpStrategy: def _safe_float( self, - value: float | int | str | None, + value: NumericLike | Decimal | None, ) -> float | None: if value is None: return None diff --git a/app/src/trading/strategies/trend.py b/app/src/trading/strategies/trend.py index e9b10cf..4a82c47 100644 --- a/app/src/trading/strategies/trend.py +++ b/app/src/trading/strategies/trend.py @@ -4,8 +4,11 @@ from __future__ import annotations import time from typing import Any +from decimal import Decimal +from src.core.types import NumericLike from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.quote import Quote from src.trading.market_analysis.models import ( EmaDistanceState, EntryTimingState, @@ -135,22 +138,22 @@ class TrendStrategy: limit=200, ) - snapshot_result = self._snapshot_or_hold( + quote_result = self._quote_or_hold( context=context, market=market, ) - if isinstance(snapshot_result, SignalResult): - return snapshot_result + if isinstance(quote_result, SignalResult): + return quote_result - snapshot = snapshot_result - symbol = str(snapshot.get("symbol") or context.symbol) - current_price = self._analysis_price(snapshot) + quote = quote_result + symbol = quote.symbol or context.symbol + current_price = self._analysis_price(quote) if current_price <= 0: return self._invalid_price_hold( symbol=symbol, - snapshot=snapshot, + quote=quote, market=market, ) @@ -162,7 +165,7 @@ class TrendStrategy: base_payload = self._base_payload( market=market, symbol=symbol, - snapshot=snapshot, + quote=quote, current_price=current_price, prices=prices, ) @@ -217,12 +220,12 @@ class TrendStrategy: ) # STEP 1. SNAPSHOT / PRICE - def _snapshot_or_hold( + def _quote_or_hold( self, *, context: StrategyContext, market: Any, - ) -> dict[str, Any] | SignalResult: + ) -> Quote | SignalResult: """ Получает live snapshot с биржи. @@ -232,11 +235,11 @@ class TrendStrategy: - проверки spread дальше по цепочке; - формирования payload для UI и журнала. - Если snapshot получить нельзя — стратегия возвращает HOLD. + Если quote получить нельзя — стратегия возвращает HOLD. Это безопаснее, чем строить сигнал на устаревших свечах. """ try: - return ExchangeService().get_market_snapshot( + return ExchangeService().get_quote( context.symbol, runtime_key="auto", ) @@ -260,11 +263,11 @@ class TrendStrategy: self, *, symbol: str, - snapshot: dict[str, Any], + quote: Quote, market: Any, ) -> SignalResult: """ - Возвращает HOLD, если цена из snapshot некорректная. + Возвращает HOLD, если цена из quote некорректная. Без валидной цены нельзя: - обновить live-окно; @@ -279,16 +282,25 @@ class TrendStrategy: **dict(market.payload or {}), "strategy": self.name, "symbol": symbol, - "snapshot": snapshot, + "snapshot": self._quote_snapshot(quote), "market_analysis": market.payload, "entry_block_reason": "INVALID_MARKET_PRICE", "entry_block_message": "нет цены", }, ) + def _quote_snapshot(self, quote: Quote) -> dict[str, object]: + return { + "symbol": quote.symbol, + "last_price": self._safe_float(quote.last_price), + "bid_price": self._safe_float(quote.bid_price), + "ask_price": self._safe_float(quote.ask_price), + "source": quote.source, + } + def _analysis_price( self, - snapshot: dict[str, Any], + quote: Quote, ) -> float: """ Выбирает цену для анализа live-импульса. @@ -301,13 +313,13 @@ class TrendStrategy: Midpoint лучше last_price, потому что меньше зависит от случайного последнего трейда. """ - bid = self._safe_float(snapshot.get("bid_price")) - ask = self._safe_float(snapshot.get("ask_price")) + bid = self._safe_float(quote.bid_price) + ask = self._safe_float(quote.ask_price) if bid is not None and ask is not None and bid > 0 and ask > 0: return (bid + ask) / 2 - last = self._safe_float(snapshot.get("last_price")) + last = self._safe_float(quote.last_price) if last is not None: return last @@ -356,7 +368,7 @@ class TrendStrategy: *, market: Any, symbol: str, - snapshot: dict[str, Any], + quote: Quote, current_price: float, prices: list[float], ) -> dict[str, Any]: @@ -386,9 +398,9 @@ class TrendStrategy: "strategy": self.name, "symbol": symbol, "analysis_price": current_price, - "last_price": snapshot.get("last_price"), - "bid_price": snapshot.get("bid_price"), - "ask_price": snapshot.get("ask_price"), + "last_price": self._safe_float(quote.last_price), + "bid_price": self._safe_float(quote.bid_price), + "ask_price": self._safe_float(quote.ask_price), "market_state": market.state.value, "market_trend": market.trend.value, "market_volatility": market.volatility.value, @@ -1156,16 +1168,17 @@ class TrendStrategy: def _safe_float( self, - value: float | int | str | None, + value: NumericLike | Decimal | None, ) -> float | None: """ - Безопасное приведение к float. + Безопасно привести числовое значение к float. - Нужно потому, что snapshot может вернуть числа: - - как float; - - как int; - - как строку; - - как None. + Поддерживаются: + - Decimal из канонической модели Quote; + - float; + - int; + - строковое представление числа; + - None. """ if value is None: return None diff --git a/app/tests/unit/integrations/exchange/test_market_cache.py b/app/tests/unit/integrations/exchange/test_market_cache.py new file mode 100644 index 0000000..665bb7d --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_market_cache.py @@ -0,0 +1,74 @@ +# app/tests/unit/integrations/exchange/test_market_cache.py + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal +from typing import Iterator + +import pytest + +from src.integrations.exchange.market_cache import MarketPriceCache +from src.market_data.acquisition.models.quote import Quote +from src.storage.quote_store import InMemoryQuoteStore, QuoteStoreProtocol + + +def _quote(symbol: str = "BTC/USD_LEVERAGE") -> Quote: + return Quote( + symbol=symbol, + last_price=Decimal("100"), + bid_price=Decimal("99"), + ask_price=Decimal("101"), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="test", + ) + + +@pytest.fixture(autouse=True) +def reset_market_price_cache() -> Iterator[None]: + original_store = MarketPriceCache._store + MarketPriceCache._store = InMemoryQuoteStore() + yield + MarketPriceCache._store = original_store + + +def test_market_price_cache_uses_quote_store_protocol() -> None: + assert isinstance(MarketPriceCache._store, QuoteStoreProtocol) + + +def test_set_and_get_quote_preserve_identity() -> None: + quote = _quote() + MarketPriceCache.set_quote(quote, runtime_key="auto") + assert MarketPriceCache.get_quote(quote.symbol, runtime_key="auto") is quote + + +def test_symbol_and_runtime_key_are_normalized() -> None: + quote = _quote("BTC/USD_LEVERAGE") + MarketPriceCache.set_quote(quote, runtime_key=" AUTO ") + assert MarketPriceCache.get_quote(" btc/usd_leverage ", runtime_key="auto") is quote + + +def test_runtime_keys_are_isolated() -> None: + auto = _quote() + debug = Quote( + symbol=auto.symbol, last_price=Decimal("200"), bid_price=Decimal("199"), + ask_price=Decimal("201"), exchange_timestamp=None, + received_at=datetime.now(timezone.utc), source="debug", + ) + MarketPriceCache.set_quote(auto, runtime_key="auto") + MarketPriceCache.set_quote(debug, runtime_key="debug_auto") + assert MarketPriceCache.get_quote(auto.symbol, runtime_key="auto") is auto + assert MarketPriceCache.get_quote(auto.symbol, runtime_key="debug_auto") is debug + + +def test_clear_targeted_and_all() -> None: + btc = _quote("BTC/USD_LEVERAGE") + eth = _quote("ETH/USD_LEVERAGE") + MarketPriceCache.set_quote(btc, runtime_key="auto") + MarketPriceCache.set_quote(eth, runtime_key="auto") + MarketPriceCache.clear(btc.symbol, runtime_key="auto") + assert MarketPriceCache.get_quote(btc.symbol, runtime_key="auto") is None + assert MarketPriceCache.get_quote(eth.symbol, runtime_key="auto") is eth + MarketPriceCache.clear() + assert MarketPriceCache.get_quote(eth.symbol, runtime_key="auto") is None diff --git a/app/tests/unit/integrations/exchange/test_market_data_runner.py b/app/tests/unit/integrations/exchange/test_market_data_runner.py new file mode 100644 index 0000000..2ff7bf3 --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_market_data_runner.py @@ -0,0 +1,327 @@ +# app/tests/unit/integrations/exchange/test_market_data_runner.py + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +import src.integrations.exchange.market_data_runner as runner_module +from src.integrations.exchange.market_data_runner import MarketDataRunner + + +class StubExchangeService: + def __init__( + self, + *, + validation: object | None = None, + error: Exception | None = None, + ) -> None: + self._validation = validation + self._error = error + self.requested_symbols: list[str] = [] + + def validate_symbol( + self, + symbol: str, + ) -> object: + self.requested_symbols.append(symbol) + + if self._error is not None: + raise self._error + + return self._validation + + +def test_cache_symbol_uses_normalized_symbol_from_validation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = SimpleNamespace( + is_valid=True, + normalized_symbol="BTC/USD_LEVERAGE", + ) + service = StubExchangeService( + validation=validation, + ) + + monkeypatch.setattr( + runner_module, + "ExchangeService", + lambda: service, + ) + + result = MarketDataRunner._cache_symbol( + " btc/usd_leverage " + ) + + assert result == "BTC/USD_LEVERAGE" + assert service.requested_symbols == [ + " btc/usd_leverage ", + ] + + +def test_cache_symbol_does_not_require_symbol_info( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = SimpleNamespace( + is_valid=True, + normalized_symbol="ETH/USD_LEVERAGE", + ) + service = StubExchangeService( + validation=validation, + ) + + monkeypatch.setattr( + runner_module, + "ExchangeService", + lambda: service, + ) + + result = MarketDataRunner._cache_symbol( + "ETH/USD_LEVERAGE" + ) + + assert result == "ETH/USD_LEVERAGE" + assert not hasattr( + validation, + "symbol_info", + ) + + +def test_cache_symbol_returns_original_symbol_when_validation_is_invalid( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = SimpleNamespace( + is_valid=False, + normalized_symbol="UNKNOWN/USD", + ) + service = StubExchangeService( + validation=validation, + ) + + monkeypatch.setattr( + runner_module, + "ExchangeService", + lambda: service, + ) + + result = MarketDataRunner._cache_symbol( + "unknown/usd" + ) + + assert result == "unknown/usd" + assert service.requested_symbols == [ + "unknown/usd", + ] + + +def test_cache_symbol_returns_original_symbol_when_validation_fails( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = StubExchangeService( + error=RuntimeError( + "Instrument reference data unavailable." + ), + ) + + monkeypatch.setattr( + runner_module, + "ExchangeService", + lambda: service, + ) + + result = MarketDataRunner._cache_symbol( + "BTC/USD" + ) + + assert result == "BTC/USD" + assert service.requested_symbols == [ + "BTC/USD", + ] + + +def test_ws_symbol_uses_cache_symbol( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[str] = [] + + def cache_symbol( + symbol: str, + ) -> str: + calls.append(symbol) + return "BTC/USD_LEVERAGE" + + monkeypatch.setattr( + MarketDataRunner, + "_cache_symbol", + cache_symbol, + ) + + result = MarketDataRunner._ws_symbol( + "btc/usd_leverage" + ) + + assert result == "BTC/USD_LEVERAGE" + assert calls == [ + "btc/usd_leverage", + ] + +def test_run_websocket_uses_canonical_quote_adapter( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import asyncio + from datetime import datetime, timezone + from decimal import Decimal + + from src.integrations.exchange.market_data_runner import MarketRuntimeContext + from src.market_data.acquisition.models.quote import Quote + + payload = {"payload": {"symbol": "BTC/USD_LEVERAGE"}} + quote = Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("100"), + bid_price=Decimal("99"), + ask_price=Decimal("101"), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + mapped_documents: list[object] = [] + stored_quotes: list[tuple[Quote, str]] = [] + + class Client: + async def stream_depth( + self, + symbol: str, + *, + interval_seconds: int, + ): + assert symbol == "BTC/USD_LEVERAGE" + assert interval_seconds == 1 + yield payload + + class Adapter: + def map_message(self, document: object) -> Quote: + mapped_documents.append(document) + return quote + + class Cache: + @classmethod + def set_quote( + cls, + value: Quote, + *, + runtime_key: str = "default", + ) -> None: + stored_quotes.append((value, runtime_key)) + + context = MarketRuntimeContext( + runtime_key="auto", + task=None, + interval_seconds=1, + symbol_provider=lambda: "BTC/USD_LEVERAGE", + screen=None, + action="market_data", + runtime_label=None, + ) + + monkeypatch.setattr( + MarketDataRunner, + "_cache_symbol", + lambda symbol: "BTC/USD_LEVERAGE", + ) + monkeypatch.setattr( + MarketDataRunner, + "_ws_symbol", + lambda symbol: "BTC/USD_LEVERAGE", + ) + monkeypatch.setattr( + MarketDataRunner, + "_can_log_runtime_event", + lambda event_key: False, + ) + monkeypatch.setattr( + MarketDataRunner, + "_log_ws_depth_debug", + lambda **kwargs: None, + ) + monkeypatch.setattr(runner_module, "ExchangeWebSocketClient", Client) + monkeypatch.setattr(runner_module, "DzengiWebSocketQuoteAdapter", Adapter) + monkeypatch.setattr(runner_module, "MarketPriceCache", Cache) + monkeypatch.setattr( + MarketDataRunner, + "_extract_best_price", + lambda *args, **kwargs: (_ for _ in ()).throw( + AssertionError("Legacy parser must not be called.") + ), + ) + + asyncio.run( + MarketDataRunner._run_websocket( + context, + "BTC/USD_LEVERAGE", + ) + ) + + assert mapped_documents == [payload] + assert stored_quotes == [(quote, "auto")] + + +def test_run_websocket_raises_after_five_invalid_quotes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import asyncio + + from src.integrations.exchange.market_data_runner import MarketRuntimeContext + from src.market_data.acquisition.exceptions import QuoteSchemaError + + class Client: + async def stream_depth( + self, + symbol: str, + *, + interval_seconds: int, + ): + del symbol, interval_seconds + + for index in range(5): + yield {"invalid": index} + + class Adapter: + def map_message(self, document: object) -> object: + del document + raise QuoteSchemaError("Invalid message.") + + context = MarketRuntimeContext( + runtime_key="auto", + task=None, + interval_seconds=1, + symbol_provider=lambda: "BTC/USD_LEVERAGE", + screen=None, + action="market_data", + runtime_label=None, + ) + + monkeypatch.setattr( + MarketDataRunner, + "_cache_symbol", + lambda symbol: "BTC/USD_LEVERAGE", + ) + monkeypatch.setattr( + MarketDataRunner, + "_ws_symbol", + lambda symbol: "BTC/USD_LEVERAGE", + ) + monkeypatch.setattr(runner_module, "ExchangeWebSocketClient", Client) + monkeypatch.setattr(runner_module, "DzengiWebSocketQuoteAdapter", Adapter) + + with pytest.raises( + RuntimeError, + match="does not contain valid quotes", + ): + asyncio.run( + MarketDataRunner._run_websocket( + context, + "BTC/USD_LEVERAGE", + ) + ) diff --git a/app/tests/unit/integrations/exchange/test_market_stream.py b/app/tests/unit/integrations/exchange/test_market_stream.py new file mode 100644 index 0000000..13a6763 --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_market_stream.py @@ -0,0 +1,450 @@ +# app/tests/unit/integrations/exchange/test_market_stream.py + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace + +import pytest + +import src.integrations.exchange.market_stream as stream_module +from src.integrations.exchange.market_stream import start_market_stream + + +class StopStream(Exception): + pass + + +class StubExchangeService: + def __init__( + self, + *, + validation: object, + ) -> None: + self._validation = validation + self.requested_symbols: list[str] = [] + + def validate_symbol( + self, + symbol: str, + ) -> object: + self.requested_symbols.append(symbol) + + return self._validation + + +class StubWebSocketClient: + def __init__( + self, + *, + requested_symbols: list[str], + ) -> None: + self._requested_symbols = requested_symbols + + async def stream_depth( + self, + symbol: str, + ): + self._requested_symbols.append(symbol) + + raise StopStream + + if False: + yield {} + + +async def _raise_stop_stream( + delay: float, +) -> None: + del delay + + raise StopStream + + +def test_start_market_stream_uses_normalized_symbol_from_validation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = SimpleNamespace( + is_valid=True, + normalized_symbol="BTC/USD_LEVERAGE", + ) + + service = StubExchangeService( + validation=validation, + ) + + websocket_symbols: list[str] = [] + + settings = SimpleNamespace( + exchange_enabled=True, + default_symbol=" btc/usd_leverage ", + ) + + monkeypatch.setattr( + stream_module, + "load_settings", + lambda: settings, + ) + + monkeypatch.setattr( + stream_module, + "JournalService", + lambda: SimpleNamespace( + log_info=lambda *args, **kwargs: None, + log_warning=lambda *args, **kwargs: None, + ), + ) + + monkeypatch.setattr( + stream_module, + "ExchangeService", + lambda: service, + ) + + monkeypatch.setattr( + stream_module, + "ExchangeWebSocketClient", + lambda: StubWebSocketClient( + requested_symbols=websocket_symbols, + ), + ) + + monkeypatch.setattr( + stream_module.asyncio, + "sleep", + _raise_stop_stream, + ) + + with pytest.raises(StopStream): + asyncio.run( + start_market_stream() + ) + + assert service.requested_symbols == [ + " btc/usd_leverage ", + ] + + assert websocket_symbols == [ + "BTC/USD_LEVERAGE", + ] + + +def test_start_market_stream_does_not_require_symbol_info( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = SimpleNamespace( + is_valid=True, + normalized_symbol="ETH/USD_LEVERAGE", + ) + + service = StubExchangeService( + validation=validation, + ) + + websocket_symbols: list[str] = [] + + settings = SimpleNamespace( + exchange_enabled=True, + default_symbol="ETH/USD_LEVERAGE", + ) + + monkeypatch.setattr( + stream_module, + "load_settings", + lambda: settings, + ) + + monkeypatch.setattr( + stream_module, + "JournalService", + lambda: SimpleNamespace( + log_info=lambda *args, **kwargs: None, + log_warning=lambda *args, **kwargs: None, + ), + ) + + monkeypatch.setattr( + stream_module, + "ExchangeService", + lambda: service, + ) + + monkeypatch.setattr( + stream_module, + "ExchangeWebSocketClient", + lambda: StubWebSocketClient( + requested_symbols=websocket_symbols, + ), + ) + + monkeypatch.setattr( + stream_module.asyncio, + "sleep", + _raise_stop_stream, + ) + + with pytest.raises(StopStream): + asyncio.run( + start_market_stream() + ) + + assert websocket_symbols == [ + "ETH/USD_LEVERAGE", + ] + + assert not hasattr( + validation, + "symbol_info", + ) + + +def test_start_market_stream_skips_websocket_for_invalid_symbol( + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = SimpleNamespace( + is_valid=False, + normalized_symbol="UNKNOWN/USD", + ) + + service = StubExchangeService( + validation=validation, + ) + + websocket_client_calls = 0 + + settings = SimpleNamespace( + exchange_enabled=True, + default_symbol="UNKNOWN/USD", + ) + + def create_websocket_client() -> object: + nonlocal websocket_client_calls + + websocket_client_calls += 1 + + raise AssertionError( + "WebSocket client must not be created " + "for an invalid symbol." + ) + + monkeypatch.setattr( + stream_module, + "load_settings", + lambda: settings, + ) + + monkeypatch.setattr( + stream_module, + "JournalService", + lambda: SimpleNamespace( + log_info=lambda *args, **kwargs: None, + log_warning=lambda *args, **kwargs: None, + ), + ) + + monkeypatch.setattr( + stream_module, + "ExchangeService", + lambda: service, + ) + + monkeypatch.setattr( + stream_module, + "ExchangeWebSocketClient", + create_websocket_client, + ) + + monkeypatch.setattr( + stream_module.asyncio, + "sleep", + _raise_stop_stream, + ) + + with pytest.raises(StopStream): + asyncio.run( + start_market_stream() + ) + + assert service.requested_symbols == [ + "UNKNOWN/USD", + ] + + assert websocket_client_calls == 0 + + +def test_start_market_stream_returns_when_exchange_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + settings = SimpleNamespace( + exchange_enabled=False, + default_symbol="BTC/USD", + ) + + service_calls = 0 + + def create_service() -> object: + nonlocal service_calls + + service_calls += 1 + + raise AssertionError( + "ExchangeService must not be created " + "when exchange is disabled." + ) + + monkeypatch.setattr( + stream_module, + "load_settings", + lambda: settings, + ) + + monkeypatch.setattr( + stream_module, + "JournalService", + lambda: SimpleNamespace(), + ) + + monkeypatch.setattr( + stream_module, + "ExchangeService", + create_service, + ) + + asyncio.run( + start_market_stream() + ) + + assert service_calls == 0 + +def test_start_market_stream_maps_message_to_quote( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from datetime import datetime, timezone + from decimal import Decimal + + from src.market_data.acquisition.models.quote import Quote + + validation = SimpleNamespace( + is_valid=True, + normalized_symbol="BTC/USD_LEVERAGE", + ) + service = StubExchangeService(validation=validation) + settings = SimpleNamespace( + exchange_enabled=True, + default_symbol="BTC/USD_LEVERAGE", + ) + payload = {"payload": {"symbol": "BTC/USD_LEVERAGE"}} + quote = Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("100"), + bid_price=Decimal("99"), + ask_price=Decimal("101"), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + mapped_documents: list[object] = [] + stored_quotes: list[tuple[Quote, str]] = [] + + class Client: + async def stream_depth(self, symbol: str): + assert symbol == "BTC/USD_LEVERAGE" + yield payload + raise StopStream + + class Adapter: + def map_message(self, document: object) -> Quote: + mapped_documents.append(document) + return quote + + class Cache: + @classmethod + def set_quote( + cls, + value: Quote, + *, + runtime_key: str = "default", + ) -> None: + stored_quotes.append((value, runtime_key)) + + monkeypatch.setattr(stream_module, "load_settings", lambda: settings) + monkeypatch.setattr( + stream_module, + "JournalService", + lambda: SimpleNamespace( + log_info=lambda *args, **kwargs: None, + log_warning=lambda *args, **kwargs: None, + ), + ) + monkeypatch.setattr(stream_module, "ExchangeService", lambda: service) + monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client) + monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter) + monkeypatch.setattr(stream_module, "MarketPriceCache", Cache) + monkeypatch.setattr( + stream_module, + "_extract_market_event", + lambda payload: (_ for _ in ()).throw( + AssertionError("Legacy parser must not be called.") + ), + ) + monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream) + + with pytest.raises(StopStream): + asyncio.run(start_market_stream()) + + assert mapped_documents == [payload] + assert stored_quotes == [(quote, "default")] + + +def test_start_market_stream_skips_invalid_adapter_message( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from src.market_data.acquisition.exceptions import QuoteSchemaError + + validation = SimpleNamespace( + is_valid=True, + normalized_symbol="BTC/USD_LEVERAGE", + ) + service = StubExchangeService(validation=validation) + settings = SimpleNamespace( + exchange_enabled=True, + default_symbol="BTC/USD_LEVERAGE", + ) + cache_calls = 0 + + class Client: + async def stream_depth(self, symbol: str): + del symbol + yield {"invalid": True} + raise StopStream + + class Adapter: + def map_message(self, document: object) -> object: + del document + raise QuoteSchemaError("Invalid message.") + + class Cache: + @classmethod + def set_quote(cls, *args: object, **kwargs: object) -> None: + nonlocal cache_calls + cache_calls += 1 + + monkeypatch.setattr(stream_module, "load_settings", lambda: settings) + monkeypatch.setattr( + stream_module, + "JournalService", + lambda: SimpleNamespace( + log_info=lambda *args, **kwargs: None, + log_warning=lambda *args, **kwargs: None, + ), + ) + monkeypatch.setattr(stream_module, "ExchangeService", lambda: service) + monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client) + monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter) + monkeypatch.setattr(stream_module, "MarketPriceCache", Cache) + monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream) + + with pytest.raises(StopStream): + asyncio.run(start_market_stream()) + + assert cache_calls == 0 diff --git a/app/tests/unit/integrations/exchange/test_service_execution_quote.py b/app/tests/unit/integrations/exchange/test_service_execution_quote.py new file mode 100644 index 0000000..f04edc6 --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_service_execution_quote.py @@ -0,0 +1,108 @@ +# app/tests/unit/integrations/exchange/test_service_execution_quote.py + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from types import SimpleNamespace +from typing import cast + +import pytest + +from src.core.config import Settings +from src.integrations.exchange.market_cache import MarketPriceCache +from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.quote import Quote + + +def _service() -> ExchangeService: + service = object.__new__(ExchangeService) + service.settings = cast( + Settings, + SimpleNamespace( + exchange_enabled=True, + default_symbol="BTC/USD_LEVERAGE", + tz="Europe/Minsk", + ), + ) + return service + + +def _quote(*, received_at: datetime | None = None) -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("100.5"), + bid_price=Decimal("100"), + ask_price=Decimal("101"), + exchange_timestamp=datetime(2026, 7, 13, 12, 0, tzinfo=timezone.utc), + received_at=received_at or datetime.now(timezone.utc), + source="dzengi", + ) + + +def _valid_validation() -> SimpleNamespace: + return SimpleNamespace( + is_valid=True, + normalized_symbol="BTC/USD_LEVERAGE", + message="ok", + ) + + +def test_execution_snapshot_reads_canonical_cached_quote( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + quote = _quote() + + monkeypatch.setattr(service, "validate_symbol", lambda _: _valid_validation()) + monkeypatch.setattr(MarketPriceCache, "get_quote", lambda *_, **__: quote) + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda _: (_ for _ in ()).throw( + AssertionError("REST fallback must not run for fresh quote") + ), + ) + + result = service.get_execution_snapshot( + "BTC/USD_LEVERAGE", + runtime_key="auto", + ) + + assert result.symbol == quote.symbol + assert result.last_price == 100.5 + assert result.bid_price == 100.0 + assert result.ask_price == 101.0 + assert result.source == "dzengi:fresh_cache" + assert result.is_fresh is True + assert result.age_seconds is not None + + +def test_execution_snapshot_uses_fresh_quote_for_stale_cache( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + stale = _quote( + received_at=datetime.now(timezone.utc) - timedelta(seconds=10), + ) + fresh = _quote() + stored: list[tuple[Quote, str]] = [] + + monkeypatch.setattr(service, "validate_symbol", lambda _: _valid_validation()) + monkeypatch.setattr(MarketPriceCache, "get_quote", lambda *_, **__: stale) + monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh) + monkeypatch.setattr( + MarketPriceCache, + "set_quote", + lambda quote, *, runtime_key: stored.append((quote, runtime_key)), + ) + + result = service.get_execution_snapshot( + "BTC/USD_LEVERAGE", + runtime_key="auto", + ) + + assert stored == [(fresh, "auto")] + assert result.source == "rest_fallback" + assert result.last_price == 100.5 + assert result.is_fresh is True diff --git a/app/tests/unit/integrations/exchange/test_service_instruments.py b/app/tests/unit/integrations/exchange/test_service_instruments.py new file mode 100644 index 0000000..d1ad697 --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_service_instruments.py @@ -0,0 +1,453 @@ +# app/tests/unit/integrations/exchange/test_service_instruments.py + +from __future__ import annotations + +from collections.abc import Iterator +from decimal import Decimal +from types import SimpleNamespace +from typing import cast + +import pytest + +from src.core.config import Settings +from src.integrations.exchange.exceptions import ExchangeError +from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceTransportError, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.storage.instrument_store import InMemoryInstrumentStore + + +_SOURCE_NAME = "dzengi" + + +def _settings( + *, + exchange_enabled: bool = True, +) -> Settings: + return cast( + Settings, + SimpleNamespace( + exchange_enabled=exchange_enabled, + exchange_name="dzengi", + ), + ) + + +def _service( + *, + exchange_enabled: bool = True, +) -> ExchangeService: + service = object.__new__(ExchangeService) + service.settings = _settings( + exchange_enabled=exchange_enabled, + ) + + return service + + +def _instrument( + *, + symbol: str = "BTC/USD_LEVERAGE", + name: str = "BTC/USD", + base_asset: str = "BTC", +) -> Instrument: + return Instrument( + symbol=symbol, + name=name, + status="TRADING", + base_asset=base_asset, + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET", "STOP"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +@pytest.fixture(autouse=True) +def reset_instrument_storage() -> Iterator[None]: + original_store = ExchangeService._instrument_store + + ExchangeService._instrument_store = InMemoryInstrumentStore() + + yield + + ExchangeService._instrument_store.clear() + ExchangeService._instrument_store = original_store + + +def test_get_instruments_returns_empty_tuple_when_exchange_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service( + exchange_enabled=False, + ) + + def fail_if_called() -> tuple[Instrument, ...]: + raise AssertionError( + "Acquisition must not run when exchange is disabled." + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + fail_if_called, + ) + + result = service.get_instruments() + + assert result == () + assert isinstance(result, tuple) + + +def test_exchange_disabled_does_not_read_existing_store( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service( + exchange_enabled=False, + ) + + instruments = ( + _instrument(), + ) + + ExchangeService._instrument_store.set( + _SOURCE_NAME, + instruments, + ) + + def fail_get( + source_name: str, + ) -> tuple[Instrument, ...] | None: + del source_name + + raise AssertionError( + "Instrument Store must not be read " + "when exchange is disabled." + ) + + monkeypatch.setattr( + ExchangeService._instrument_store, + "get", + fail_get, + ) + + assert service.get_instruments() == () + + +def test_store_hit_returns_same_tuple_object( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instruments = ( + _instrument(), + ) + + ExchangeService._instrument_store.set( + _SOURCE_NAME, + instruments, + ) + + def fail_if_called() -> tuple[Instrument, ...]: + raise AssertionError( + "Acquisition must not run on store hit." + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + fail_if_called, + ) + + result = service.get_instruments() + + assert result is instruments + + +def test_store_miss_runs_acquisition_once( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instruments = ( + _instrument(), + ) + + call_count = 0 + + def load_instruments() -> tuple[Instrument, ...]: + nonlocal call_count + + call_count += 1 + return instruments + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + load_instruments, + ) + + result = service.get_instruments() + + assert result is instruments + assert call_count == 1 + + +def test_loaded_instruments_are_saved_in_store( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instruments = ( + _instrument(), + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + lambda: instruments, + ) + + result = service.get_instruments() + + stored = ExchangeService._instrument_store.get( + _SOURCE_NAME + ) + + assert result is instruments + assert stored is instruments + + +def test_second_get_instruments_call_uses_store( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instruments = ( + _instrument(), + ) + + call_count = 0 + + def load_instruments() -> tuple[Instrument, ...]: + nonlocal call_count + + call_count += 1 + return instruments + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + load_instruments, + ) + + first = service.get_instruments() + second = service.get_instruments() + + assert first is instruments + assert second is instruments + assert call_count == 1 + + +def test_empty_tuple_in_store_is_valid_cache_hit( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + ExchangeService._instrument_store.set( + _SOURCE_NAME, + (), + ) + + def fail_if_called() -> tuple[Instrument, ...]: + raise AssertionError( + "Stored empty tuple must be treated as cache hit." + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + fail_if_called, + ) + + result = service.get_instruments() + + assert result == () + + +def test_multiple_service_instances_share_instrument_store( + monkeypatch: pytest.MonkeyPatch, +) -> None: + first_service = _service() + second_service = _service() + + instruments = ( + _instrument(), + ) + + call_count = 0 + + def load_instruments() -> tuple[Instrument, ...]: + nonlocal call_count + + call_count += 1 + return instruments + + monkeypatch.setattr( + first_service, + "_load_instruments_via_acquisition", + load_instruments, + ) + + monkeypatch.setattr( + second_service, + "_load_instruments_via_acquisition", + load_instruments, + ) + + first = first_service.get_instruments() + second = second_service.get_instruments() + + assert first is instruments + assert second is instruments + assert call_count == 1 + + +def test_acquisition_error_is_wrapped_in_exchange_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + original_error = InstrumentReferenceTransportError( + "Network error." + ) + + def raise_error() -> tuple[Instrument, ...]: + raise original_error + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + raise_error, + ) + + monkeypatch.setattr( + service, + "_log_exchange_error", + lambda **_: None, + ) + + with pytest.raises( + ExchangeError, + match=r"Network error", + ) as exc_info: + service.get_instruments() + + assert exc_info.value.__cause__ is original_error + + +def test_acquisition_error_is_logged_as_exchange_info( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + original_error = InstrumentReferenceTransportError( + "Network error." + ) + + logged_calls: list[dict[str, object]] = [] + + def raise_error() -> tuple[Instrument, ...]: + raise original_error + + def record_error( + *, + endpoint: str, + exc: Exception, + symbol: str | None = None, + extra_payload: dict[str, object] | None = None, + ) -> None: + logged_calls.append( + { + "endpoint": endpoint, + "exc": exc, + "symbol": symbol, + "extra_payload": extra_payload, + } + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + raise_error, + ) + + monkeypatch.setattr( + service, + "_log_exchange_error", + record_error, + ) + + with pytest.raises(ExchangeError): + service.get_instruments() + + assert logged_calls == [ + { + "endpoint": "exchangeInfo", + "exc": original_error, + "symbol": None, + "extra_payload": None, + } + ] + + +def test_acquisition_error_does_not_fill_store( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + def raise_error() -> tuple[Instrument, ...]: + raise InstrumentReferenceTransportError( + "Network error." + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + raise_error, + ) + + monkeypatch.setattr( + service, + "_log_exchange_error", + lambda **_: None, + ) + + with pytest.raises(ExchangeError): + service.get_instruments() + + assert ExchangeService._instrument_store.get( + _SOURCE_NAME + ) is None + + +def test_exchange_service_has_no_legacy_projection_cache() -> None: + assert not hasattr( + ExchangeService, + "_exchange_symbols_projection_cache", + ) \ No newline at end of file diff --git a/app/tests/unit/integrations/exchange/test_service_quote.py b/app/tests/unit/integrations/exchange/test_service_quote.py new file mode 100644 index 0000000..2f7be29 --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_service_quote.py @@ -0,0 +1,118 @@ +# app/tests/unit/integrations/exchange/test_service_quote.py + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from types import SimpleNamespace +from typing import cast + +import pytest + +from src.core.config import Settings +from src.integrations.exchange.exceptions import ExchangeError +from src.integrations.exchange.market_cache import MarketPriceCache +from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.quote import Quote +from src.storage.quote_store import InMemoryQuoteStore + + +def _service(*, exchange_enabled: bool = True) -> ExchangeService: + service = object.__new__(ExchangeService) + service.settings = cast( + Settings, + SimpleNamespace( + exchange_enabled=exchange_enabled, + default_symbol="BTC/USD_LEVERAGE", + ), + ) + return service + + +def _quote(*, age_seconds: float = 0.0) -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("100"), + bid_price=Decimal("99"), + ask_price=Decimal("101"), + exchange_timestamp=None, + received_at=( + datetime.now(timezone.utc) + - timedelta(seconds=age_seconds) + ), + source="test", + ) + + +@pytest.fixture(autouse=True) +def reset_store(): + original = MarketPriceCache._store + MarketPriceCache._store = InMemoryQuoteStore() + yield + MarketPriceCache._store = original + + +def test_get_quote_returns_fresh_cached_identity( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + quote = _quote() + MarketPriceCache.set_quote(quote, runtime_key="auto") + monkeypatch.setattr( + service, + "validate_symbol", + lambda _: SimpleNamespace( + is_valid=True, + normalized_symbol=quote.symbol, + message="", + ), + ) + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda _: (_ for _ in ()).throw(AssertionError("REST not expected")), + ) + + assert service.get_quote(quote.symbol, runtime_key="auto") is quote + + +def test_get_quote_refreshes_stale_quote( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + stale = _quote(age_seconds=10) + fresh = _quote() + MarketPriceCache.set_quote(stale, runtime_key="auto") + monkeypatch.setattr( + service, + "validate_symbol", + lambda _: SimpleNamespace( + is_valid=True, + normalized_symbol=fresh.symbol, + message="", + ), + ) + monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh) + + result = service.get_quote(fresh.symbol, runtime_key="auto") + + assert result is fresh + assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh + + +def test_get_quote_wraps_acquisition_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + original = RuntimeError("unavailable") + monkeypatch.setattr( + service, + "_load_quote_via_acquisition", + lambda _: (_ for _ in ()).throw(original), + ) + monkeypatch.setattr(service, "_log_exchange_error", lambda **_: None) + + with pytest.raises(ExchangeError) as exc_info: + service._get_fresh_quote("BTC/USD_LEVERAGE") + + assert exc_info.value.__cause__ is original diff --git a/app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py b/app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py new file mode 100644 index 0000000..c0140df --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py @@ -0,0 +1,646 @@ +# app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py + +from __future__ import annotations + +from collections.abc import Iterator +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from types import SimpleNamespace +from typing import cast + +import pytest + +import src.integrations.exchange.service as service_module +from src.core.config import Settings +from src.integrations.exchange.exceptions import ExchangeError +from src.integrations.exchange.models import SymbolValidationResult +from src.integrations.exchange.service import ExchangeService +from src.integrations.exchange.status import ( + ExchangeRuntimeStatus, + ExchangeStatusCode, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.models.quote import Quote + + +def _make_settings( + *, + exchange_enabled: bool = True, + default_symbol: str = "BTC/USD", +) -> Settings: + return cast( + Settings, + SimpleNamespace( + exchange_enabled=exchange_enabled, + default_symbol=default_symbol, + exchange_name="dzengi", + exchange_base_url="https://demo-api-adapter.dzengi.com", + tz="UTC", + ), + ) + + +def _quote_for_status( + symbol: str, + *, + age_seconds: float | None, +) -> Quote: + now = datetime.now(timezone.utc) + + exchange_timestamp = ( + now - timedelta(seconds=age_seconds) + if age_seconds is not None + else None + ) + + return Quote( + symbol=symbol, + last_price=Decimal("100.0"), + bid_price=Decimal("99.0"), + ask_price=Decimal("101.0"), + exchange_timestamp=exchange_timestamp, + received_at=now, + source="test", + ) + + +def _make_instrument( + *, + symbol: str = "BTC/USD", + status: str = "TRADING", +) -> Instrument: + return Instrument( + symbol=symbol, + name=symbol, + status=status, + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="unknown", + market_modes=(), + order_types=(), + base_asset_precision=None, + quote_asset_precision=None, + tick_size=None, + tick_value=None, + step_size=None, + min_qty=None, + max_qty=None, + min_notional=None, + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +def _make_valid_validation( + *, + symbol: str = "BTC/USD", + status: str = "TRADING", +) -> SymbolValidationResult: + symbol_info = _make_instrument( + symbol=symbol, + status=status, + ) + + return SymbolValidationResult( + requested_symbol=symbol, + normalized_symbol=symbol, + is_valid=True, + message="Символ найден в exchangeInfo.", + symbol_info=symbol_info, + ) + + +def _make_invalid_validation( + *, + symbol: str = "UNKNOWN/USD", + message: str | None = None, +) -> SymbolValidationResult: + return SymbolValidationResult( + requested_symbol=symbol, + normalized_symbol=symbol, + is_valid=False, + message=message or f"Символ '{symbol}' не найден в exchangeInfo.", + symbol_info=None, + ) + + +@pytest.fixture +def service( + monkeypatch: pytest.MonkeyPatch, +) -> Iterator[ExchangeService]: + monkeypatch.setattr( + service_module, + "load_settings", + lambda: _make_settings(), + ) + + monkeypatch.setattr( + service_module, + "JournalService", + lambda: SimpleNamespace(), + ) + + yield ExchangeService() + + +def test_get_symbol_runtime_status_returns_mock_status_when_exchange_disabled( + service: ExchangeService, +) -> None: + service.settings = _make_settings( + exchange_enabled=False, + default_symbol="ETH/USD", + ) + + result = service.get_symbol_runtime_status() + + assert isinstance(result, ExchangeRuntimeStatus) + assert result.code == ExchangeStatusCode.OPEN + assert result.is_open is True + assert result.is_available is True + assert result.is_auth_ok is True + assert result.reason == "mock_exchange" + assert result.symbol == "ETH/USD" + assert result.raw_status == "OPEN" + + +def test_get_symbol_runtime_status_uses_explicit_symbol( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured_symbols: list[str] = [] + + def fake_validate_symbol( + raw_symbol: str, + ) -> SymbolValidationResult: + captured_symbols.append(raw_symbol) + + return _make_invalid_validation( + symbol=raw_symbol, + ) + + monkeypatch.setattr( + service, + "validate_symbol", + fake_validate_symbol, + ) + + result = service.get_symbol_runtime_status("ETH/USD") + + assert captured_symbols == ["ETH/USD"] + assert result.code == ExchangeStatusCode.INVALID_SYMBOL + assert result.symbol == "ETH/USD" + + +def test_get_symbol_runtime_status_uses_default_symbol_when_symbol_is_none( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + service.settings = _make_settings( + default_symbol="ETH/USD", + ) + + captured_symbols: list[str] = [] + + def fake_validate_symbol( + raw_symbol: str, + ) -> SymbolValidationResult: + captured_symbols.append(raw_symbol) + + return _make_invalid_validation( + symbol=raw_symbol, + ) + + monkeypatch.setattr( + service, + "validate_symbol", + fake_validate_symbol, + ) + + result = service.get_symbol_runtime_status() + + assert captured_symbols == ["ETH/USD"] + assert result.symbol == "ETH/USD" + + +def test_get_symbol_runtime_status_returns_invalid_symbol_status( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = _make_invalid_validation( + symbol="UNKNOWN/USD", + message="Проверочное сообщение о недоступном инструменте.", + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + result = service.get_symbol_runtime_status("UNKNOWN/USD") + + assert result.code == ExchangeStatusCode.INVALID_SYMBOL + assert result.is_open is False + assert result.is_available is True + assert result.is_auth_ok is True + assert result.reason == "invalid_symbol" + assert result.symbol == "UNKNOWN/USD" + assert result.message == ( + "Проверочное сообщение о недоступном инструменте." + ) + + +def test_get_symbol_runtime_status_returns_exchange_error_when_validation_fails( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + error = ExchangeError("timeout while loading exchangeInfo") + + def fake_validate_symbol( + raw_symbol: str, + ) -> SymbolValidationResult: + raise error + + logged_errors: list[dict[str, object]] = [] + + monkeypatch.setattr( + service, + "validate_symbol", + fake_validate_symbol, + ) + + monkeypatch.setattr( + service, + "_log_exchange_error", + lambda **kwargs: logged_errors.append(kwargs), + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert result.code == ExchangeStatusCode.EXCHANGE_UNAVAILABLE + assert result.is_open is False + assert result.is_available is False + assert result.reason == "exchange_unavailable" + assert result.raw_error == str(error) + + assert logged_errors == [ + { + "endpoint": "symbol_market_status", + "exc": error, + "symbol": "BTC/USD", + } + ] + + +@pytest.mark.parametrize( + ("raw_status", "expected_code", "expected_reason"), + [ + ("TRADING", ExchangeStatusCode.OPEN, "market_open"), + ("OPEN", ExchangeStatusCode.OPEN, "market_open"), + ("ACTIVE", ExchangeStatusCode.OPEN, "market_open"), + ("ENABLED", ExchangeStatusCode.OPEN, "market_open"), + ("ONLINE", ExchangeStatusCode.OPEN, "market_open"), + ("BREAK", ExchangeStatusCode.BREAK, "market_break"), + ("CLOSED", ExchangeStatusCode.BREAK, "market_break"), + ("HALT", ExchangeStatusCode.BREAK, "market_break"), + ("HALTED", ExchangeStatusCode.BREAK, "market_break"), + ("PAUSED", ExchangeStatusCode.BREAK, "market_break"), + ("SUSPENDED", ExchangeStatusCode.BREAK, "market_break"), + ("DISABLED", ExchangeStatusCode.BREAK, "market_break"), + ("SETTLING", ExchangeStatusCode.BREAK, "market_break"), + ("POST_ONLY", ExchangeStatusCode.BREAK, "market_break"), + ( + "NOT_TRADABLE", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "TRADING_DISABLED", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "MARKET_DISABLED", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "UNAVAILABLE_FOR_TRADING", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "CLOSE_ONLY", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "REDUCE_ONLY", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "VIEW_ONLY", + ExchangeStatusCode.BREAK, + "market_not_tradable", + ), + ( + "UNRECOGNIZED_STATUS", + ExchangeStatusCode.UNKNOWN, + "market_status_unknown", + ), + ], +) +def test_get_symbol_runtime_status_preserves_status_classification( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, + raw_status: str, + expected_code: ExchangeStatusCode, + expected_reason: str, +) -> None: + validation = _make_valid_validation( + status=raw_status, + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda symbol: _quote_for_status( + symbol, + age_seconds=0.0, + ), + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert isinstance(result, ExchangeRuntimeStatus) + assert result.code == expected_code + assert result.reason == expected_reason + assert result.symbol == "BTC/USD" + + +def test_get_symbol_runtime_status_checks_freshness_only_for_open_market( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = _make_valid_validation( + status="BREAK", + ) + + quote_calls: list[str] = [] + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda symbol: quote_calls.append(symbol), + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert result.code == ExchangeStatusCode.BREAK + assert result.reason == "market_break" + assert quote_calls == [] + + +def test_get_symbol_runtime_status_returns_stale_status_for_old_market_data( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = _make_valid_validation( + status="TRADING", + ) + + fixed_timestamp = datetime( + 2026, + 7, + 10, + 12, + 0, + 0, + tzinfo=timezone.utc, + ) + + quote = Quote( + symbol="BTC/USD", + last_price=Decimal("100.0"), + bid_price=Decimal("99.0"), + ask_price=Decimal("101.0"), + exchange_timestamp=fixed_timestamp, + received_at=fixed_timestamp, + source="test", + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda symbol: quote, + ) + + monkeypatch.setattr( + service, + "_exchange_timestamp_age_seconds", + lambda raw_timestamp: 61.0, + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert result.code == ExchangeStatusCode.BREAK + assert result.is_open is False + assert result.is_available is True + assert result.reason == "market_data_stale" + assert result.raw_status == "STALE_MARKET_DATA" + assert result.symbol == "BTC/USD" + assert "61с" in result.message + assert "10.07.2026 12:00:00" in result.message + + +def test_get_symbol_runtime_status_keeps_open_status_at_stale_threshold( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = _make_valid_validation( + status="TRADING", + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda symbol: _quote_for_status( + symbol, + age_seconds=0.0, + ), + ) + + monkeypatch.setattr( + service, + "_exchange_timestamp_age_seconds", + lambda raw_timestamp: 60.0, + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert result.code == ExchangeStatusCode.OPEN + assert result.is_open is True + assert result.reason == "market_open" + + +def test_get_symbol_runtime_status_keeps_open_status_when_age_is_missing( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = _make_valid_validation( + status="TRADING", + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + monkeypatch.setattr( + service, + "_get_fresh_quote", + lambda symbol: _quote_for_status( + symbol, + age_seconds=None, + ), + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert result.code == ExchangeStatusCode.OPEN + assert result.is_open is True + assert result.reason == "market_open" + + +def test_get_symbol_runtime_status_keeps_open_status_when_quote_fails( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + validation = _make_valid_validation( + status="TRADING", + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + def fake_get_fresh_quote( + symbol: str, + ) -> Quote: + raise ExchangeError("ticker unavailable") + + monkeypatch.setattr( + service, + "_get_fresh_quote", + fake_get_fresh_quote, + ) + + result = service.get_symbol_runtime_status("BTC/USD") + + assert result.code == ExchangeStatusCode.OPEN + assert result.is_open is True + assert result.reason == "market_open" + + +def test_get_symbol_runtime_status_uses_normalized_matched_symbol( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + symbol_info = _make_instrument( + symbol="btc/usd", + status="BREAK", + ) + + validation = SymbolValidationResult( + requested_symbol="BTC/USD", + normalized_symbol="BTC/USD", + is_valid=True, + message="Символ найден в exchangeInfo.", + symbol_info=symbol_info, + ) + + monkeypatch.setattr( + service, + "validate_symbol", + lambda raw_symbol: validation, + ) + + result = service.get_symbol_runtime_status("btc/usd") + + assert result.symbol == "BTC/USD" + assert result.raw_status == "BREAK" + + +def test_get_symbol_market_status_preserves_legacy_dict_contract( + service: ExchangeService, + monkeypatch: pytest.MonkeyPatch, +) -> None: + runtime_status = ExchangeRuntimeStatus( + code=ExchangeStatusCode.BREAK, + is_open=False, + is_available=True, + is_auth_ok=True, + title="Перерыв в торгах", + message="Торги временно остановлены.", + ui_line="⏸️ Перерыв в торгах", + reason="market_break", + symbol="BTC/USD", + raw_status="BREAK", + raw_error=None, + ) + + monkeypatch.setattr( + service, + "get_symbol_runtime_status", + lambda symbol=None: runtime_status, + ) + + result = service.get_symbol_market_status("BTC/USD") + + assert result == runtime_status.as_dict() + + assert result == { + "code": "BREAK", + "status": "BREAK", + "symbol": "BTC/USD", + "is_open": False, + "is_available": True, + "is_auth_ok": True, + "title": "Перерыв в торгах", + "message": "Торги временно остановлены.", + "ui_line": "⏸️ Перерыв в торгах", + "reason": "market_break", + "raw_status": "BREAK", + "raw_error": None, + } \ No newline at end of file diff --git a/app/tests/unit/integrations/exchange/test_service_validate_symbol.py b/app/tests/unit/integrations/exchange/test_service_validate_symbol.py new file mode 100644 index 0000000..8748c2f --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_service_validate_symbol.py @@ -0,0 +1,530 @@ +# app/tests/unit/integrations/exchange/test_service_validate_symbol.py + +from __future__ import annotations + +from decimal import Decimal +from types import SimpleNamespace +from typing import cast + +import pytest + +from src.core.config import Settings +from src.integrations.exchange.models import SymbolValidationResult +from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.instrument import Instrument + + +def _settings( + *, + exchange_enabled: bool = True, +) -> SimpleNamespace: + return SimpleNamespace( + exchange_enabled=exchange_enabled, + ) + + +def _service( + *, + exchange_enabled: bool = True, +) -> ExchangeService: + service = object.__new__(ExchangeService) + service.settings = cast( + Settings, + _settings( + exchange_enabled=exchange_enabled, + ), + ) + + return service + + +def _instrument( + *, + symbol: str = "BTC/USD_LEVERAGE", + name: str = "BTC/USD", + base_asset: str = "BTC", +) -> Instrument: + return Instrument( + symbol=symbol, + name=name, + status="TRADING", + base_asset=base_asset, + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET", "STOP"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +def _set_instruments( + monkeypatch: pytest.MonkeyPatch, + service: ExchangeService, + instruments: tuple[Instrument, ...], +) -> list[int]: + call_count = [0] + + def get_instruments() -> tuple[Instrument, ...]: + call_count[0] += 1 + return instruments + + monkeypatch.setattr( + service, + "get_instruments", + get_instruments, + ) + + return call_count + + +def test_validate_symbol_rejects_empty_symbol( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + call_count = _set_instruments( + monkeypatch, + service, + (), + ) + + result = service.validate_symbol(" ") + + assert result == SymbolValidationResult( + requested_symbol="", + normalized_symbol="", + is_valid=False, + message="Символ пустой.", + symbol_info=None, + ) + assert call_count[0] == 0 + + +def test_validate_symbol_accepts_symbol_in_mock_mode( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service( + exchange_enabled=False, + ) + + call_count = _set_instruments( + monkeypatch, + service, + (), + ) + + result = service.validate_symbol( + " btc/usd_leverage " + ) + + assert result == SymbolValidationResult( + requested_symbol="BTC/USD_LEVERAGE", + normalized_symbol="BTC/USD_LEVERAGE", + is_valid=True, + message="Mock mode active.", + symbol_info=None, + ) + assert call_count[0] == 0 + + +def test_validate_symbol_finds_exact_instrument( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert result.is_valid + assert result.symbol_info is instrument + + +def test_validate_symbol_is_case_insensitive( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "btc/usd_leverage" + ) + + assert result.is_valid + assert result.normalized_symbol == "BTC/USD_LEVERAGE" + + +def test_validate_symbol_ignores_outer_spaces( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + " btc/usd_leverage " + ) + + assert result.is_valid + assert result.requested_symbol == "BTC/USD_LEVERAGE" + + +def test_validate_symbol_supports_encoded_separator( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "btc%2fusd_leverage" + ) + + assert result.is_valid + assert result.symbol_info is instrument + + +def test_validate_symbol_supports_internal_spaces( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "btc / usd_leverage" + ) + + assert result.is_valid + assert result.symbol_info is instrument + + +def test_validate_symbol_rejects_missing_symbol( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + _set_instruments( + monkeypatch, + service, + ( + _instrument(), + ), + ) + + result = service.validate_symbol( + "XRP/USD_LEVERAGE" + ) + + assert result == SymbolValidationResult( + requested_symbol="XRP/USD_LEVERAGE", + normalized_symbol="XRP/USD_LEVERAGE", + is_valid=False, + message=( + "Символ 'XRP/USD_LEVERAGE' " + "не найден в exchangeInfo." + ), + symbol_info=None, + ) + + +def test_validate_symbol_returns_original_instrument_object( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "btc/usd_leverage" + ) + + assert result.symbol_info is instrument + + +def test_validate_symbol_normalizes_actual_matched_symbol( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instrument = _instrument( + symbol=" btc/usd_leverage ", + ) + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert result.normalized_symbol == "BTC/USD_LEVERAGE" + + +def test_validate_symbol_preserves_success_message( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + _set_instruments( + monkeypatch, + service, + ( + _instrument(), + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert result.message == "Символ найден в exchangeInfo." + + +def test_validate_symbol_calls_get_instruments_once( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + call_count = _set_instruments( + monkeypatch, + service, + ( + _instrument(), + ), + ) + + service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert call_count[0] == 1 + + +def test_validate_symbol_uses_canonical_instruments( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instrument = _instrument() + + call_count = _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert result.is_valid + assert result.symbol_info is instrument + assert call_count[0] == 1 + + +def test_validate_symbol_does_not_call_acquisition_directly( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + _set_instruments( + monkeypatch, + service, + ( + _instrument(), + ), + ) + + def fail_if_called() -> tuple[Instrument, ...]: + raise AssertionError( + "validate_symbol() must use get_instruments() " + "and must not call acquisition directly." + ) + + monkeypatch.setattr( + service, + "_load_instruments_via_acquisition", + fail_if_called, + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert result.is_valid + + +def test_validate_symbol_preserves_candidate_priority( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + decoded_instrument = _instrument( + symbol="BTC/USD_LEVERAGE", + ) + encoded_instrument = _instrument( + symbol="BTC%2FUSD_LEVERAGE", + ) + + _set_instruments( + monkeypatch, + service, + ( + decoded_instrument, + encoded_instrument, + ), + ) + + result = service.validate_symbol( + "BTC%2FUSD_LEVERAGE" + ) + + assert result.symbol_info is encoded_instrument + + +def test_validate_symbol_returns_first_duplicate( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + first = _instrument( + name="First", + ) + second = _instrument( + name="Second", + ) + + _set_instruments( + monkeypatch, + service, + ( + first, + second, + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert result.symbol_info is first + + +def test_validate_symbol_returns_symbol_validation_result( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + _set_instruments( + monkeypatch, + service, + ( + _instrument(), + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert isinstance( + result, + SymbolValidationResult, + ) + + +def test_validate_symbol_returns_canonical_instrument( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument() + + _set_instruments( + monkeypatch, + service, + ( + instrument, + ), + ) + + result = service.validate_symbol( + "BTC/USD_LEVERAGE" + ) + + assert isinstance( + result.symbol_info, + Instrument, + ) + assert result.symbol_info is instrument + + +def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None: + assert not hasattr( + ExchangeService, + "get_exchange_symbols", + ) \ No newline at end of file diff --git a/app/tests/unit/integrations/exchange/test_status.py b/app/tests/unit/integrations/exchange/test_status.py new file mode 100644 index 0000000..750d67a --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_status.py @@ -0,0 +1,173 @@ +# app/tests/unit/integrations/exchange/test_status.py + +from __future__ import annotations + +import pytest + +from src.integrations.exchange.status import ( + ExchangeRuntimeStatus, + ExchangeStatusCode, + build_market_status_from_symbol_status, +) + + +_SYMBOL = "BTC/USD_LEVERAGE" + + +@pytest.mark.parametrize( + "raw_status", + [ + "TRADING", + "OPEN", + "ACTIVE", + "ENABLED", + "ONLINE", + " trading ", + ], +) +def test_open_instrument_status_preserves_legacy_contract( + raw_status: str, +) -> None: + status = build_market_status_from_symbol_status( + raw_status=raw_status, + symbol=_SYMBOL, + ) + + assert status == ExchangeRuntimeStatus( + code=ExchangeStatusCode.OPEN, + is_open=True, + is_available=True, + is_auth_ok=True, + title="Биржа доступна", + message="Рынок открыт.", + ui_line="🟢 Биржа доступна", + reason="market_open", + raw_status=raw_status.strip().upper(), + symbol=_SYMBOL, + ) + + +@pytest.mark.parametrize( + "raw_status", + [ + "NOT_TRADABLE", + "TRADING_DISABLED", + "MARKET_DISABLED", + "UNAVAILABLE_FOR_TRADING", + "CLOSE_ONLY", + "REDUCE_ONLY", + "VIEW_ONLY", + ], +) +def test_not_tradable_status_preserves_legacy_contract( + raw_status: str, +) -> None: + status = build_market_status_from_symbol_status( + raw_status=raw_status, + symbol=_SYMBOL, + ) + + assert status == ExchangeRuntimeStatus( + code=ExchangeStatusCode.BREAK, + is_open=False, + is_available=True, + is_auth_ok=True, + title="Рынок недоступен", + message=( + "Этот рынок недоступен для торговли: " + "BTC/USD_LEVERAGE." + ), + ui_line="⛔️ Рынок недоступен для торговли", + reason="market_not_tradable", + raw_status=raw_status, + symbol=_SYMBOL, + ) + + +@pytest.mark.parametrize( + "raw_status", + [ + "BREAK", + "CLOSED", + "HALT", + "HALTED", + "PAUSED", + "SUSPENDED", + "DISABLED", + "SETTLING", + "POST_ONLY", + ], +) +def test_break_status_preserves_legacy_contract( + raw_status: str, +) -> None: + status = build_market_status_from_symbol_status( + raw_status=raw_status, + symbol=_SYMBOL, + ) + + assert status == ExchangeRuntimeStatus( + code=ExchangeStatusCode.BREAK, + is_open=False, + is_available=True, + is_auth_ok=True, + title="Перерыв в торгах", + message="Торги по BTC/USD_LEVERAGE временно остановлены.", + ui_line="⏸️ Перерыв в торгах", + reason="market_break", + raw_status=raw_status, + symbol=_SYMBOL, + ) + + +def test_unknown_status_preserves_legacy_contract() -> None: + status = build_market_status_from_symbol_status( + raw_status=" maintenance ", + symbol=_SYMBOL, + ) + + assert status == ExchangeRuntimeStatus( + code=ExchangeStatusCode.UNKNOWN, + is_open=False, + is_available=True, + is_auth_ok=True, + title="Статус торгов неизвестен", + message=( + "Биржа вернула неизвестный статус инструмента: " + "MAINTENANCE." + ), + ui_line="⚠️ Статус торгов неизвестен", + reason="market_status_unknown", + raw_status="MAINTENANCE", + symbol=_SYMBOL, + ) + + +@pytest.mark.parametrize( + "raw_status", + [ + None, + "", + " ", + ], +) +def test_empty_status_preserves_legacy_unknown_contract( + raw_status: str | None, +) -> None: + status = build_market_status_from_symbol_status( + raw_status=raw_status, + symbol=_SYMBOL, + ) + + assert status == ExchangeRuntimeStatus( + code=ExchangeStatusCode.UNKNOWN, + is_open=False, + is_available=True, + is_auth_ok=True, + title="Статус торгов неизвестен", + message="Биржа вернула неизвестный статус инструмента.", + ui_line="⚠️ Статус торгов неизвестен", + reason="market_status_unknown", + raw_status=None, + symbol=_SYMBOL, + ) \ No newline at end of file diff --git a/app/tests/unit/integrations/exchange/test_symbol_utils.py b/app/tests/unit/integrations/exchange/test_symbol_utils.py new file mode 100644 index 0000000..de37c50 --- /dev/null +++ b/app/tests/unit/integrations/exchange/test_symbol_utils.py @@ -0,0 +1,74 @@ +# app/tests/unit/integrations/exchange/test_symbol_utils.py + +from __future__ import annotations + +import pytest + +from src.integrations.exchange.symbol_utils import ( + normalize_symbol as legacy_normalize_symbol, +) +from src.integrations.exchange.symbol_utils import ( + symbol_candidates as legacy_symbol_candidates, +) +from src.market_data.acquisition.symbols import ( + normalize_symbol as new_normalize_symbol, +) +from src.market_data.acquisition.symbols import ( + symbol_candidates as new_symbol_candidates, +) + + +@pytest.mark.parametrize( + "raw_symbol", + [ + "", + " ", + "btc/usd", + " BTC/USD ", + "btc%2fusd", + "btc / usd", + "btc%2f / usd", + "eth/usd_leverage", + "btc\t/usd", + "btc\n/usd", + ], +) +def test_legacy_normalize_symbol_matches_new_implementation( + raw_symbol: str, +) -> None: + assert ( + legacy_normalize_symbol(raw_symbol) + == new_normalize_symbol(raw_symbol) + ) + + +@pytest.mark.parametrize( + "raw_symbol", + [ + "", + " ", + "btc/usd", + " BTC/USD ", + "btc%2fusd", + "btc / usd", + "btc%2f / usd", + "eth/usd_leverage", + "btc\t/usd", + "btc\n/usd", + ], +) +def test_legacy_symbol_candidates_matches_new_implementation( + raw_symbol: str, +) -> None: + assert ( + legacy_symbol_candidates(raw_symbol) + == new_symbol_candidates(raw_symbol) + ) + + +def test_legacy_facade_exports_same_normalize_function() -> None: + assert legacy_normalize_symbol is new_normalize_symbol + + +def test_legacy_facade_exports_same_candidates_function() -> None: + assert legacy_symbol_candidates is new_symbol_candidates \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py new file mode 100644 index 0000000..a7bee04 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py @@ -0,0 +1,407 @@ +# app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py + +from __future__ import annotations + +from dataclasses import FrozenInstanceError, replace +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_exchange_info_to_instruments, + map_dzengi_symbol_to_instrument, +) +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiExchangeInfoPayload, + DzengiExchangeInfoResponse, + DzengiExchangeInfoSymbol, + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiUnknownFilter, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceMappingError, +) + + +def _complete_symbol() -> DzengiExchangeInfoSymbol: + return DzengiExchangeInfoSymbol( + symbol="ETH/EUR_LEVERAGE", + name="ETH/EUR", + status="TRADING", + asset_type="CRYPTOCURRENCY", + base_asset="ETH", + base_asset_precision=3, + quote_asset="EUR", + quote_asset_id="EUR_LEVERAGE", + quote_precision=3, + order_types=("LIMIT", "MARKET", "STOP"), + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", + ), + DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="2", + ), + ), + market_modes=("REGULAR",), + market_type="LEVERAGE", + country="", + sector="", + industry="", + trading_hours="UTC; Mon - 21:00, 21:05 -", + tick_size=0.01, + tick_value=18.3415, + trading_fee=0.06, + exchange_fee=None, + long_rate=-0.01, + short_rate=0.01, + swap_charge_interval=480, + min_sl_gap=0, + max_sl_gap=50.0, + min_tp_gap=0, + max_tp_gap=50.0, + ) + + +def _response( + *symbols: DzengiExchangeInfoSymbol, +) -> DzengiExchangeInfoResponse: + return DzengiExchangeInfoResponse( + payload=DzengiExchangeInfoPayload( + timezone="UTC", + server_time=1783537921471, + rate_limits=(), + exchange_filters=(), + symbols=tuple(symbols), + ), + ) + + +def test_map_complete_dzengi_symbol_to_instrument() -> None: + instrument = map_dzengi_symbol_to_instrument( + _complete_symbol() + ) + + assert instrument.symbol == "ETH/EUR_LEVERAGE" + assert instrument.name == "ETH/EUR" + assert instrument.status == "TRADING" + + assert instrument.base_asset == "ETH" + assert instrument.quote_asset == "EUR" + assert instrument.asset_type == "CRYPTOCURRENCY" + + assert instrument.market_type == "LEVERAGE" + assert instrument.market_modes == ("REGULAR",) + assert instrument.order_types == ("LIMIT", "MARKET", "STOP") + + assert instrument.base_asset_precision == 3 + assert instrument.quote_asset_precision == 3 + + assert instrument.tick_size == Decimal("0.01") + assert instrument.tick_value == Decimal("18.3415") + + assert instrument.step_size == Decimal("0.001") + assert instrument.min_qty == Decimal("0.001") + assert instrument.max_qty == Decimal("1000") + assert instrument.min_notional == Decimal("2") + + assert instrument.country is None + assert instrument.sector is None + assert instrument.industry is None + assert instrument.trading_hours == "UTC; Mon - 21:00, 21:05 -" + + +def test_map_exchange_info_to_instruments() -> None: + first = _complete_symbol() + second = replace( + _complete_symbol(), + symbol="BTC/USD_LEVERAGE", + name="BTC/USD", + base_asset="BTC", + quote_asset="USD", + ) + + instruments = map_dzengi_exchange_info_to_instruments( + _response(first, second) + ) + + assert isinstance(instruments, tuple) + assert len(instruments) == 2 + assert instruments[0].symbol == "ETH/EUR_LEVERAGE" + assert instruments[1].symbol == "BTC/USD_LEVERAGE" + + +def test_map_numeric_values_to_decimal_exactly() -> None: + symbol = replace( + _complete_symbol(), + tick_size=0.00000001, + tick_value=0, + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.00000001", + max_qty=10000000, + step_size="0.00000001", + ), + DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="0.00000069", + ), + ), + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.tick_size == Decimal("1E-8") + assert instrument.tick_value == Decimal("0") + assert instrument.min_qty == Decimal("1E-8") + assert instrument.max_qty == Decimal("10000000") + assert instrument.step_size == Decimal("1E-8") + assert instrument.min_notional == Decimal("6.9E-7") + + +def test_map_symbol_without_filters() -> None: + symbol = replace( + _complete_symbol(), + filters=(), + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.step_size is None + assert instrument.min_qty is None + assert instrument.max_qty is None + assert instrument.min_notional is None + + +def test_map_symbol_with_missing_optional_numeric_values() -> None: + symbol = replace( + _complete_symbol(), + tick_size=None, + tick_value=None, + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty=None, + max_qty=None, + step_size=None, + ), + DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional=None, + ), + ), + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.tick_size is None + assert instrument.tick_value is None + assert instrument.step_size is None + assert instrument.min_qty is None + assert instrument.max_qty is None + assert instrument.min_notional is None + + +def test_mapper_ignores_unknown_filters() -> None: + symbol = replace( + _complete_symbol(), + filters=( + DzengiUnknownFilter( + filter_type="FUTURE_FILTER", + fields=( + ("enabled", True), + ("limit", 10), + ), + ), + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", + ), + ), + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.min_qty == Decimal("0.001") + assert instrument.max_qty == Decimal("1000") + assert instrument.step_size == Decimal("0.001") + assert instrument.min_notional is None + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("asset_type", ""), + ("asset_type", " "), + ("country", ""), + ("country", " "), + ("sector", ""), + ("industry", " "), + ("trading_hours", ""), + ], +) +def test_mapper_converts_empty_optional_text_to_none( + field: str, + value: str, +) -> None: + symbol = replace( + _complete_symbol(), + **{field: value}, + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert getattr(instrument, field) is None + + +def test_mapper_strips_non_empty_optional_text() -> None: + symbol = replace( + _complete_symbol(), + asset_type=" CRYPTOCURRENCY ", + country=" DE ", + sector=" Technology ", + industry=" Software ", + trading_hours=" UTC; Mon 07:00 - 15:30 ", + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.asset_type == "CRYPTOCURRENCY" + assert instrument.country == "DE" + assert instrument.sector == "Technology" + assert instrument.industry == "Software" + assert instrument.trading_hours == "UTC; Mon 07:00 - 15:30" + + +def test_mapper_preserves_market_modes_order() -> None: + symbol = replace( + _complete_symbol(), + market_modes=("REGULAR", "CLOSE_ONLY", "EXTENDED"), + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.market_modes == ( + "REGULAR", + "CLOSE_ONLY", + "EXTENDED", + ) + + +def test_mapper_preserves_order_types_order() -> None: + symbol = replace( + _complete_symbol(), + order_types=("MARKET", "LIMIT", "STOP"), + ) + + instrument = map_dzengi_symbol_to_instrument(symbol) + + assert instrument.order_types == ( + "MARKET", + "LIMIT", + "STOP", + ) + + +def test_mapper_rejects_duplicate_lot_size_filters() -> None: + lot_size = DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", + ) + + symbol = replace( + _complete_symbol(), + filters=( + lot_size, + lot_size, + ), + ) + + with pytest.raises( + InstrumentReferenceMappingError, + match=r"несколько фильтров LOT_SIZE", + ): + map_dzengi_symbol_to_instrument(symbol) + + +def test_mapper_rejects_duplicate_min_notional_filters() -> None: + min_notional = DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="2", + ) + + symbol = replace( + _complete_symbol(), + filters=( + min_notional, + min_notional, + ), + ) + + with pytest.raises( + InstrumentReferenceMappingError, + match=r"несколько фильтров MIN_NOTIONAL", + ): + map_dzengi_symbol_to_instrument(symbol) + + +@pytest.mark.parametrize( + ("field_name", "invalid_value"), + [ + ("tick_size", float("nan")), + ("tick_value", float("inf")), + ], +) +def test_mapper_rejects_non_finite_direct_numeric_value( + field_name: str, + invalid_value: float, +) -> None: + symbol = replace( + _complete_symbol(), + **{field_name: invalid_value}, + ) + + with pytest.raises( + InstrumentReferenceMappingError, + match=r"должно быть конечным числом", + ): + map_dzengi_symbol_to_instrument(symbol) + + +def test_mapper_rejects_invalid_filter_numeric_value() -> None: + symbol = replace( + _complete_symbol(), + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="not-a-number", + max_qty="1000", + step_size="0.001", + ), + ), + ) + + with pytest.raises( + InstrumentReferenceMappingError, + match=r"minQty.*невозможно преобразовать в Decimal", + ): + map_dzengi_symbol_to_instrument(symbol) + + +def test_mapped_instrument_is_immutable() -> None: + instrument = map_dzengi_symbol_to_instrument( + _complete_symbol() + ) + + with pytest.raises(FrozenInstanceError): + instrument.status = "BREAK" # type: ignore[misc] \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py new file mode 100644 index 0000000..0d92010 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py @@ -0,0 +1,206 @@ +# app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py + +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiExchangeInfoPayload, + DzengiExchangeInfoResponse, + DzengiExchangeInfoSymbol, + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiRateLimit, + DzengiUnknownFilter, +) + + +def test_exchange_info_response_stores_complete_raw_contract() -> None: + lot_size = DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", + ) + min_notional = DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="2", + ) + + symbol = DzengiExchangeInfoSymbol( + symbol="ETH/EUR_LEVERAGE", + name="ETH/EUR", + status="TRADING", + asset_type="CRYPTOCURRENCY", + base_asset="ETH", + base_asset_precision=3, + quote_asset="EUR", + quote_asset_id="EUR_LEVERAGE", + quote_precision=3, + order_types=("LIMIT", "MARKET", "STOP"), + filters=(lot_size, min_notional), + market_modes=("REGULAR",), + market_type="LEVERAGE", + country="", + sector="", + industry="", + trading_hours="UTC; Mon - 21:00, 21:05 -", + tick_size=0.01, + tick_value=18.3415, + trading_fee=0.06, + exchange_fee=None, + long_rate=-0.01, + short_rate=0.01, + swap_charge_interval=480, + min_sl_gap=0, + max_sl_gap=50.0, + min_tp_gap=0, + max_tp_gap=50.0, + ) + + payload = DzengiExchangeInfoPayload( + timezone="UTC", + server_time=1783537921471, + rate_limits=( + DzengiRateLimit( + interval="MINUTE", + interval_num=1, + limit=1200, + rate_limit_type="REQUEST_WEIGHT", + ), + ), + exchange_filters=(), + symbols=(symbol,), + ) + + response = DzengiExchangeInfoResponse(payload=payload) + + assert response.status is None + assert response.correlation_id is None + + assert response.payload.timezone == "UTC" + assert response.payload.server_time == 1783537921471 + assert len(response.payload.symbols) == 1 + + parsed_symbol = response.payload.symbols[0] + + assert parsed_symbol.symbol == "ETH/EUR_LEVERAGE" + assert parsed_symbol.filters == (lot_size, min_notional) + assert parsed_symbol.tick_size == 0.01 + assert parsed_symbol.trading_fee == 0.06 + assert parsed_symbol.exchange_fee is None + + +def test_exchange_info_response_supports_wrapped_api_metadata() -> None: + payload = DzengiExchangeInfoPayload( + timezone="UTC", + server_time=1628193845310, + rate_limits=(), + exchange_filters=(), + symbols=(), + ) + + response = DzengiExchangeInfoResponse( + status="OK", + correlation_id="2", + payload=payload, + ) + + assert response.status == "OK" + assert response.correlation_id == "2" + assert response.payload.symbols == () + + +def test_exchange_info_symbol_accepts_optional_transport_fields() -> None: + symbol = DzengiExchangeInfoSymbol( + symbol="TUI1.", + name="TUI - EUR", + status="TRADING", + asset_type="EQUITY", + base_asset="TUI1.", + base_asset_precision=3, + quote_asset="EUR", + quote_asset_id="EUR_LEVERAGE", + quote_precision=3, + order_types=("LIMIT", "MARKET", "STOP"), + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.1", + max_qty="33000", + step_size="0.1", + ), + ), + market_modes=("REGULAR",), + market_type="LEVERAGE", + country="DE", + sector="Cyclical Consumer Goods & Services", + industry="Leisure & Recreation", + trading_hours="UTC; Mon 07:00 - 15:30", + tick_size=0.005, + tick_value=None, + trading_fee=0, + exchange_fee=None, + long_rate=-0.0165933, + short_rate=-0.0056289, + swap_charge_interval=1440, + min_sl_gap=0, + max_sl_gap=30.0, + min_tp_gap=0, + max_tp_gap=30.0, + ) + + assert symbol.tick_value is None + assert symbol.exchange_fee is None + assert len(symbol.filters) == 1 + + +def test_unknown_filter_preserves_unrecognized_scalar_fields() -> None: + unknown_filter = DzengiUnknownFilter( + filter_type="FUTURE_FILTER", + fields=( + ("enabled", True), + ("limit", 10), + ("mode", "STRICT"), + ("description", None), + ), + ) + + assert unknown_filter.filter_type == "FUTURE_FILTER" + assert unknown_filter.fields == ( + ("enabled", True), + ("limit", 10), + ("mode", "STRICT"), + ("description", None), + ) + + +def test_raw_models_use_immutable_sequences() -> None: + payload = DzengiExchangeInfoPayload( + timezone=None, + server_time=None, + rate_limits=(), + exchange_filters=(), + symbols=(), + ) + + assert isinstance(payload.rate_limits, tuple) + assert isinstance(payload.exchange_filters, tuple) + assert isinstance(payload.symbols, tuple) + + +def test_raw_models_are_immutable() -> None: + payload = DzengiExchangeInfoPayload( + timezone="UTC", + server_time=1783537921471, + rate_limits=(), + exchange_filters=(), + symbols=(), + ) + + response = DzengiExchangeInfoResponse(payload=payload) + + with pytest.raises(FrozenInstanceError): + response.status = "OK" # type: ignore[misc] \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py new file mode 100644 index 0000000..7fefd8b --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py @@ -0,0 +1,405 @@ +# app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py + +from __future__ import annotations + +from types import MappingProxyType + +import pytest + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiUnknownFilter, +) +from src.market_data.acquisition.adapters.dzengi.parser import ( + parse_exchange_info, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceParseError, +) +from src.market_data.acquisition.validation.schema import ( + ValidatedExchangeInfoDocument, +) + + +def _validated_document( + payload: dict[str, object], + *, + is_wrapped: bool = False, + status: object | None = None, + correlation_id: object | None = None, +) -> ValidatedExchangeInfoDocument: + return ValidatedExchangeInfoDocument( + payload=MappingProxyType(payload), + is_wrapped=is_wrapped, + status=status, + correlation_id=correlation_id, + ) + + +def _complete_symbol() -> dict[str, object]: + return { + "symbol": "ETH/EUR_LEVERAGE", + "name": "ETH/EUR", + "status": "TRADING", + "assetType": "CRYPTOCURRENCY", + "baseAsset": "ETH", + "baseAssetPrecision": 3, + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "orderTypes": ["LIMIT", "MARKET", "STOP"], + "filters": [ + { + "filterType": "LOT_SIZE", + "minQty": "0.001", + "maxQty": "1000", + "stepSize": "0.001", + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "2", + }, + ], + "marketModes": ["REGULAR"], + "marketType": "LEVERAGE", + "country": "", + "sector": "", + "industry": "", + "tradingHours": "UTC; Mon - 21:00, 21:05 -", + "tickSize": 0.01, + "tickValue": 18.3415, + "tradingFee": 0.06, + "longRate": -0.01, + "shortRate": 0.01, + "swapChargeInterval": 480, + "minSLGap": 0, + "maxSLGap": 50.0, + "minTPGap": 0, + "maxTPGap": 50.0, + } + + +def test_parse_complete_unwrapped_exchange_info() -> None: + document = _validated_document( + { + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [ + { + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200, + "rateLimitType": "REQUEST_WEIGHT", + } + ], + "exchangeFilters": [], + "symbols": [_complete_symbol()], + } + ) + + response = parse_exchange_info(document) + + assert response.status is None + assert response.correlation_id is None + assert response.payload.timezone == "UTC" + assert response.payload.server_time == 1783537921471 + assert len(response.payload.rate_limits) == 1 + assert len(response.payload.symbols) == 1 + + symbol = response.payload.symbols[0] + + assert symbol.symbol == "ETH/EUR_LEVERAGE" + assert symbol.name == "ETH/EUR" + assert symbol.status == "TRADING" + assert symbol.asset_type == "CRYPTOCURRENCY" + assert symbol.base_asset == "ETH" + assert symbol.quote_asset == "EUR" + assert symbol.order_types == ("LIMIT", "MARKET", "STOP") + assert symbol.market_modes == ("REGULAR",) + assert symbol.tick_size == 0.01 + assert symbol.tick_value == 18.3415 + assert symbol.trading_fee == 0.06 + assert symbol.exchange_fee is None + + +def test_parse_wrapped_exchange_info_metadata() -> None: + document = _validated_document( + { + "timezone": "UTC", + "serverTime": 1628193845310, + "symbols": [], + }, + is_wrapped=True, + status="OK", + correlation_id="2", + ) + + response = parse_exchange_info(document) + + assert response.status == "OK" + assert response.correlation_id == "2" + assert response.payload.symbols == () + + +def test_parse_known_instrument_filters() -> None: + document = _validated_document( + { + "symbols": [_complete_symbol()], + } + ) + + response = parse_exchange_info(document) + filters = response.payload.symbols[0].filters + + assert isinstance(filters[0], DzengiLotSizeFilter) + assert filters[0].min_qty == "0.001" + assert filters[0].max_qty == "1000" + assert filters[0].step_size == "0.001" + + assert isinstance(filters[1], DzengiMinNotionalFilter) + assert filters[1].min_notional == "2" + + +def test_parse_unknown_instrument_filter() -> None: + symbol = _complete_symbol() + symbol["filters"] = [ + { + "filterType": "FUTURE_FILTER", + "enabled": True, + "limit": 10, + "mode": "STRICT", + } + ] + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + response = parse_exchange_info(document) + parsed_filter = response.payload.symbols[0].filters[0] + + assert isinstance(parsed_filter, DzengiUnknownFilter) + assert parsed_filter.filter_type == "FUTURE_FILTER" + assert parsed_filter.fields == ( + ("enabled", True), + ("limit", 10), + ("mode", "STRICT"), + ) + + +def test_parse_exchange_filters_as_unknown_filters() -> None: + document = _validated_document( + { + "exchangeFilters": [ + { + "filterType": "GLOBAL_LIMIT", + "enabled": True, + "limit": 100, + } + ], + "symbols": [], + } + ) + + response = parse_exchange_info(document) + exchange_filter = response.payload.exchange_filters[0] + + assert exchange_filter.filter_type == "GLOBAL_LIMIT" + assert exchange_filter.fields == ( + ("enabled", True), + ("limit", 100), + ) + + +def test_parse_exchange_filter_without_filter_type() -> None: + document = _validated_document( + { + "exchangeFilters": [ + { + "enabled": True, + } + ], + "symbols": [], + } + ) + + response = parse_exchange_info(document) + + assert response.payload.exchange_filters[0].filter_type == "" + assert response.payload.exchange_filters[0].fields == ( + ("enabled", True), + ) + + +def test_parse_symbol_with_missing_optional_fields() -> None: + document = _validated_document( + { + "symbols": [ + { + "symbol": "TEST/USD", + "name": "Test", + "status": "BREAK", + "baseAsset": "TEST", + "quoteAsset": "USD", + "marketType": "SPOT", + } + ], + } + ) + + response = parse_exchange_info(document) + symbol = response.payload.symbols[0] + + assert symbol.asset_type is None + assert symbol.base_asset_precision is None + assert symbol.quote_asset_id is None + assert symbol.quote_precision is None + assert symbol.order_types == () + assert symbol.filters == () + assert symbol.market_modes == () + assert symbol.country is None + assert symbol.tick_size is None + assert symbol.min_sl_gap is None + + +@pytest.mark.parametrize( + "field", + [ + "symbol", + "name", + "status", + "baseAsset", + "quoteAsset", + "marketType", + ], +) +def test_reject_missing_required_symbol_field(field: str) -> None: + symbol = _complete_symbol() + symbol.pop(field) + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + with pytest.raises( + InstrumentReferenceParseError, + match=rf"\.{field} должен быть строкой", + ): + parse_exchange_info(document) + + +def test_reject_invalid_required_string_type() -> None: + symbol = _complete_symbol() + symbol["symbol"] = 123 + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + with pytest.raises( + InstrumentReferenceParseError, + match=r"\.symbol должен быть строкой", + ): + parse_exchange_info(document) + + +def test_reject_invalid_json_number_type() -> None: + symbol = _complete_symbol() + symbol["tickSize"] = "0.01" + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + with pytest.raises( + InstrumentReferenceParseError, + match=r"\.tickSize должен быть JSON-числом", + ): + parse_exchange_info(document) + + +def test_reject_bool_as_json_number() -> None: + symbol = _complete_symbol() + symbol["tickSize"] = True + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + with pytest.raises( + InstrumentReferenceParseError, + match=r"\.tickSize должен быть JSON-числом", + ): + parse_exchange_info(document) + + +def test_reject_float_as_integer_field() -> None: + symbol = _complete_symbol() + symbol["baseAssetPrecision"] = 3.0 + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + with pytest.raises( + InstrumentReferenceParseError, + match=r"\.baseAssetPrecision должен быть целым числом", + ): + parse_exchange_info(document) + + +def test_reject_nested_unknown_filter_value() -> None: + symbol = _complete_symbol() + symbol["filters"] = [ + { + "filterType": "FUTURE_FILTER", + "settings": { + "enabled": True, + }, + } + ] + + document = _validated_document( + { + "symbols": [symbol], + } + ) + + with pytest.raises( + InstrumentReferenceParseError, + match=r"\.settings должен быть скалярным JSON-значением", + ): + parse_exchange_info(document) + + +def test_parse_result_uses_immutable_sequences() -> None: + document = _validated_document( + { + "rateLimits": [], + "exchangeFilters": [], + "symbols": [_complete_symbol()], + } + ) + + response = parse_exchange_info(document) + symbol = response.payload.symbols[0] + + assert isinstance(response.payload.rate_limits, tuple) + assert isinstance(response.payload.exchange_filters, tuple) + assert isinstance(response.payload.symbols, tuple) + assert isinstance(symbol.order_types, tuple) + assert isinstance(symbol.filters, tuple) + assert isinstance(symbol.market_modes, tuple) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py new file mode 100644 index 0000000..43502de --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_ticker_to_quote, +) +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiTicker24hrResponse, +) +from src.market_data.acquisition.exceptions import QuoteMappingError +from src.market_data.acquisition.models.quote import Quote + + +def _response(**overrides: object) -> DzengiTicker24hrResponse: + values: dict[str, object] = { + "symbol": "BTC/USD_LEVERAGE", + "last_price": "64159.45", + "bid_price": "64159.45", + "ask_price": "64159.55", + "close_time": 1783887270312, + } + values.update(overrides) + return DzengiTicker24hrResponse(**values) # type: ignore[arg-type] + + +def test_mapper_returns_canonical_quote() -> None: + received_at = datetime(2026, 7, 12, 18, 0, tzinfo=timezone.utc) + + result = map_dzengi_ticker_to_quote( + _response(), + received_at=received_at, + ) + + assert result == Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("64159.45"), + bid_price=Decimal("64159.45"), + ask_price=Decimal("64159.55"), + exchange_timestamp=datetime.fromtimestamp( + 1783887270312 / 1000, + tz=timezone.utc, + ), + received_at=received_at, + source="dzengi", + ) + + +def test_mapper_preserves_decimal_precision() -> None: + result = map_dzengi_ticker_to_quote( + _response( + last_price="0.123456789123456789", + bid_price="0.123456789123456788", + ask_price="0.123456789123456790", + ), + received_at=datetime.now(timezone.utc), + ) + + assert result.last_price == Decimal("0.123456789123456789") + assert result.bid_price == Decimal("0.123456789123456788") + assert result.ask_price == Decimal("0.123456789123456790") + + +def test_mapper_strips_symbol_outer_spaces() -> None: + result = map_dzengi_ticker_to_quote( + _response(symbol=" BTC/USD_LEVERAGE "), + received_at=datetime.now(timezone.utc), + ) + + assert result.symbol == "BTC/USD_LEVERAGE" + + +def test_mapper_rejects_non_finite_price() -> None: + with pytest.raises( + QuoteMappingError, + match=r"lastPrice.*конечным числом", + ): + map_dzengi_ticker_to_quote( + _response(last_price="NaN"), + received_at=datetime.now(timezone.utc), + ) + + +def test_mapper_wraps_invalid_price_conversion() -> None: + with pytest.raises( + QuoteMappingError, + match=r"bidPrice.*Decimal", + ): + map_dzengi_ticker_to_quote( + _response(bid_price="not-a-number"), + received_at=datetime.now(timezone.utc), + ) + + +def test_mapper_rejects_naive_received_at() -> None: + with pytest.raises( + QuoteMappingError, + match=r"received_at.*timezone-aware", + ): + map_dzengi_ticker_to_quote( + _response(), + received_at=datetime(2026, 7, 12, 18, 0), + ) + + +def test_mapper_preserves_received_at_timezone() -> None: + received_at = datetime.fromisoformat("2026-07-12T21:00:00+03:00") + + result = map_dzengi_ticker_to_quote( + _response(), + received_at=received_at, + ) + + assert result.received_at is received_at + + +def test_mapper_wraps_invalid_close_time() -> None: + with pytest.raises( + QuoteMappingError, + match=r"closeTime.*UTC datetime", + ): + map_dzengi_ticker_to_quote( + _response(close_time=10**30), + received_at=datetime.now(timezone.utc), + ) diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py new file mode 100644 index 0000000..7f93ccb --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiTicker24hrResponse, +) +from src.market_data.acquisition.adapters.dzengi.parser import parse_quote +from src.market_data.acquisition.exceptions import QuoteParseError +from src.market_data.acquisition.validation.schema import validate_quote_schema + + +def _document() -> dict[str, object]: + return { + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + "bidPrice": "64159.45", + "askPrice": "64159.55", + "closeTime": 1783887270312, + "highPrice": "64261.45", + "volume": "9.6002", + } + + +def test_parse_quote_builds_dzengi_transport_model() -> None: + validated = validate_quote_schema(_document()) + + result = parse_quote(validated) + + assert result == DzengiTicker24hrResponse( + symbol="BTC/USD_LEVERAGE", + last_price="64159.45", + bid_price="64159.45", + ask_price="64159.55", + close_time=1783887270312, + ) + + +def test_parse_quote_ignores_unrelated_24hr_statistics() -> None: + document = _document() + document["openPrice"] = "63785.75" + document["weightedAvgPrice"] = "64159.50" + + result = parse_quote(validate_quote_schema(document)) + + assert result.symbol == "BTC/USD_LEVERAGE" + assert not hasattr(result, "open_price") + assert not hasattr(result, "weighted_avg_price") + + +def test_parse_quote_accepts_json_numbers_for_prices() -> None: + document = _document() + document["lastPrice"] = 64159.45 + document["bidPrice"] = 64159 + document["askPrice"] = 64160 + + result = parse_quote(validate_quote_schema(document)) + + assert result.last_price == 64159.45 + assert result.bid_price == 64159 + assert result.ask_price == 64160 + + +def test_parse_quote_rejects_boolean_price() -> None: + document = _document() + document["lastPrice"] = True + + with pytest.raises(QuoteParseError, match="lastPrice"): + parse_quote(validate_quote_schema(document)) + + +def test_parse_quote_rejects_non_integer_close_time() -> None: + document = _document() + document["closeTime"] = "1783887270312" + + with pytest.raises(QuoteParseError, match="closeTime"): + parse_quote(validate_quote_schema(document)) diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py new file mode 100644 index 0000000..c2cd30c --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py @@ -0,0 +1,371 @@ +# app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py + +from __future__ import annotations + +import pytest + +from src.integrations.exchange.exceptions import ( + ExchangeConnectionError, + ExchangeResponseError, +) +from src.market_data.acquisition.adapters.dzengi.rest import ( + DzengiInstrumentDocumentSource, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceTransportError, +) +from src.market_data.acquisition.protocol import ( + InstrumentDocumentSource, +) + + +class StubRestClient: + def __init__( + self, + *, + result: object = None, + error: Exception | None = None, + ) -> None: + self.result = result + self.error = error + self.calls: list[str] = [] + + def get_payload( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> object: + del params + del headers + + self.calls.append(path) + + if self.error is not None: + raise self.error + + return self.result + + +def test_source_satisfies_instrument_document_source_protocol() -> None: + source = DzengiInstrumentDocumentSource( + client=StubRestClient( + result={ + "symbols": [], + } + ) + ) + + assert isinstance(source, InstrumentDocumentSource) + + +def test_fetch_instrument_document_calls_exchange_info_endpoint() -> None: + client = StubRestClient( + result={ + "symbols": [], + } + ) + source = DzengiInstrumentDocumentSource(client=client) + + result = source.fetch_instrument_document() + + assert result == { + "symbols": [], + } + assert client.calls == [ + "/api/v1/exchangeInfo", + ] + + +def test_fetch_instrument_document_returns_dict_without_changes() -> None: + document = { + "timezone": "UTC", + "serverTime": 1783537921471, + "symbols": [ + { + "symbol": "BTC/USD_LEVERAGE", + } + ], + } + + source = DzengiInstrumentDocumentSource( + client=StubRestClient(result=document) + ) + + result = source.fetch_instrument_document() + + assert result is document + + +def test_fetch_instrument_document_returns_list_without_changes() -> None: + document = [ + { + "symbol": "BTC/USD_LEVERAGE", + } + ] + + source = DzengiInstrumentDocumentSource( + client=StubRestClient(result=document) + ) + + result = source.fetch_instrument_document() + + assert result is document + + +def test_source_uses_injected_client() -> None: + client = StubRestClient( + result={ + "symbols": [], + } + ) + source = DzengiInstrumentDocumentSource(client=client) + + source.fetch_instrument_document() + source.fetch_instrument_document() + + assert client.calls == [ + "/api/v1/exchangeInfo", + "/api/v1/exchangeInfo", + ] + + +@pytest.mark.parametrize( + "error", + [ + ExchangeConnectionError("Network error."), + ExchangeResponseError("Invalid response."), + RuntimeError("Unexpected transport failure."), + ], +) +def test_transport_errors_are_wrapped( + error: Exception, +) -> None: + source = DzengiInstrumentDocumentSource( + client=StubRestClient(error=error) + ) + + with pytest.raises( + InstrumentReferenceTransportError, + match=r"Не удалось получить Instrument Reference Data от Dzengi", + ) as exc_info: + source.fetch_instrument_document() + + assert exc_info.value.__cause__ is error + assert str(error) in str(exc_info.value) + + +def test_client_creation_error_is_wrapped( + monkeypatch: pytest.MonkeyPatch, +) -> None: + original_error = RuntimeError("EXCHANGE_BASE_URL is invalid.") + + def raise_client_creation_error() -> None: + raise original_error + + monkeypatch.setattr( + "src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient", + raise_client_creation_error, + ) + + source = DzengiInstrumentDocumentSource() + + with pytest.raises( + InstrumentReferenceTransportError, + match=r"Не удалось получить Instrument Reference Data от Dzengi", + ) as exc_info: + source.fetch_instrument_document() + + assert exc_info.value.__cause__ is original_error + + +def test_adapter_does_not_transform_returned_document() -> None: + document = { + "status": "OK", + "payload": { + "symbols": [], + }, + } + + source = DzengiInstrumentDocumentSource( + client=StubRestClient(result=document) + ) + + result = source.fetch_instrument_document() + + assert result is document + assert result == { + "status": "OK", + "payload": { + "symbols": [], + }, + } + +# Quotes Feed REST source tests. +from src.market_data.acquisition.adapters.dzengi.rest import ( + DzengiQuoteDocumentSource, +) +from src.market_data.acquisition.exceptions import QuoteTransportError +from src.market_data.acquisition.protocol import QuoteDocumentSource + + +class RecordingQuoteRestClient: + def __init__( + self, + *, + result: object = None, + error: Exception | None = None, + ) -> None: + self.result = result + self.error = error + self.calls: list[dict[str, object]] = [] + + def get_payload( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> object: + self.calls.append( + { + "path": path, + "params": params, + "headers": headers, + } + ) + + if self.error is not None: + raise self.error + + return self.result + + +def test_quote_source_satisfies_quote_document_source_protocol() -> None: + source = DzengiQuoteDocumentSource( + client=RecordingQuoteRestClient(result={}) + ) + + assert isinstance(source, QuoteDocumentSource) + + +def test_fetch_quote_document_calls_ticker_endpoint_with_symbol() -> None: + client = RecordingQuoteRestClient(result={}) + source = DzengiQuoteDocumentSource(client=client) + + source.fetch_quote_document("BTC/USD_LEVERAGE") + + assert client.calls == [ + { + "path": "/api/v1/ticker/24hr", + "params": { + "symbol": "BTC/USD_LEVERAGE", + }, + "headers": None, + } + ] + + +def test_fetch_quote_document_passes_symbol_without_changes() -> None: + client = RecordingQuoteRestClient(result={}) + source = DzengiQuoteDocumentSource(client=client) + + source.fetch_quote_document(" btc/usd_leverage ") + + assert client.calls[0]["params"] == { + "symbol": " btc/usd_leverage ", + } + + +def test_fetch_quote_document_returns_payload_without_changes() -> None: + document = { + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + "bidPrice": "64159.45", + "askPrice": "64159.55", + "closeTime": 1783887270312, + } + source = DzengiQuoteDocumentSource( + client=RecordingQuoteRestClient(result=document) + ) + + result = source.fetch_quote_document("BTC/USD_LEVERAGE") + + assert result is document + + +def test_quote_source_uses_injected_client_once() -> None: + client = RecordingQuoteRestClient(result={}) + source = DzengiQuoteDocumentSource(client=client) + + source.fetch_quote_document("BTC/USD_LEVERAGE") + + assert len(client.calls) == 1 + + +def test_quote_source_creates_default_client( + monkeypatch: pytest.MonkeyPatch, +) -> None: + client = RecordingQuoteRestClient(result={}) + client_creation_count = 0 + + def create_client() -> RecordingQuoteRestClient: + nonlocal client_creation_count + client_creation_count += 1 + return client + + monkeypatch.setattr( + "src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient", + create_client, + ) + + source = DzengiQuoteDocumentSource() + source.fetch_quote_document("BTC/USD_LEVERAGE") + + assert client_creation_count == 1 + assert len(client.calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + ExchangeConnectionError("Network error."), + ExchangeResponseError("Invalid response."), + RuntimeError("Unexpected transport failure."), + ], +) +def test_quote_transport_errors_are_wrapped( + error: Exception, +) -> None: + source = DzengiQuoteDocumentSource( + client=RecordingQuoteRestClient(error=error) + ) + + with pytest.raises( + QuoteTransportError, + match=r"Не удалось получить текущую котировку от Dzengi", + ) as exc_info: + source.fetch_quote_document("BTC/USD_LEVERAGE") + + assert exc_info.value.__cause__ is error + assert str(error) in str(exc_info.value) + + +def test_quote_client_creation_error_is_wrapped( + monkeypatch: pytest.MonkeyPatch, +) -> None: + original_error = RuntimeError("EXCHANGE_BASE_URL is invalid.") + + def raise_client_creation_error() -> None: + raise original_error + + monkeypatch.setattr( + "src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient", + raise_client_creation_error, + ) + + source = DzengiQuoteDocumentSource() + + with pytest.raises(QuoteTransportError) as exc_info: + source.fetch_quote_document("BTC/USD_LEVERAGE") + + assert exc_info.value.__cause__ is original_error diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py new file mode 100644 index 0000000..d3f33c6 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest + +from src.market_data.acquisition.adapters.dzengi.websocket import ( + DzengiWebSocketQuoteAdapter, +) +from src.market_data.acquisition.exceptions import QuoteValueError + + +def test_adapter_maps_document_to_quote() -> None: + received_at = datetime(2026, 7, 13, tzinfo=timezone.utc) + result = DzengiWebSocketQuoteAdapter().map_message( + { + "Payload": { + "symbolName": "BTC/USD", + "bids": [["10", "1"]], + "asks": [["12", "1"]], + "timestamp": 1000, + } + }, + received_at=received_at, + ) + assert str(result.last_price) == "11" + assert result.received_at is received_at + + +def test_adapter_preserves_layer_error() -> None: + with pytest.raises(QuoteValueError): + DzengiWebSocketQuoteAdapter().map_message( + {"symbol": "BTC/USD", "bid": "12", "ask": "11"} + ) diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py new file mode 100644 index 0000000..77a76f7 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.adapters.dzengi.mapper import ( + map_dzengi_websocket_quote_to_quote, +) +from src.market_data.acquisition.adapters.dzengi.models import DzengiWebSocketQuoteResponse +from src.market_data.acquisition.exceptions import QuoteMappingError + + +def _response(timestamp: int | None = 1000) -> DzengiWebSocketQuoteResponse: + return DzengiWebSocketQuoteResponse( + symbol="BTC/USD", + bid_price="10.1", + ask_price="10.3", + timestamp=timestamp, + ) + + +def test_maps_midpoint_and_timestamps() -> None: + received_at = datetime(2026, 7, 13, tzinfo=timezone.utc) + result = map_dzengi_websocket_quote_to_quote(_response(), received_at=received_at) + assert result.last_price == Decimal("10.2") + assert result.bid_price == Decimal("10.1") + assert result.ask_price == Decimal("10.3") + assert result.exchange_timestamp == datetime.fromtimestamp(1, tz=timezone.utc) + assert result.received_at is received_at + assert result.source == "dzengi" + + +def test_allows_missing_exchange_timestamp() -> None: + result = map_dzengi_websocket_quote_to_quote( + _response(None), + received_at=datetime.now(timezone.utc), + ) + assert result.exchange_timestamp is None + + +def test_rejects_naive_received_at() -> None: + with pytest.raises(QuoteMappingError): + map_dzengi_websocket_quote_to_quote( + _response(), + received_at=datetime(2026, 7, 13), + ) diff --git a/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py new file mode 100644 index 0000000..d6c051b --- /dev/null +++ b/app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.adapters.dzengi.parser import ( + parse_dzengi_websocket_quote, +) +from src.market_data.acquisition.exceptions import QuoteParseError +from src.market_data.acquisition.validation.schema import ( + validate_dzengi_websocket_quote_schema, +) + + +def _parse(document: object): + return parse_dzengi_websocket_quote( + validate_dzengi_websocket_quote_schema(document) + ) + + +def test_parses_direct_quote() -> None: + result = _parse( + {"symbolName": "BTC/USD", "bid": "10", "ofr": "11", "timestamp": 1000} + ) + assert result.symbol == "BTC/USD" + assert result.bid_price == "10" + assert result.ask_price == "11" + assert result.timestamp == 1000 + + +def test_parses_depth_list_entries() -> None: + result = _parse( + {"symbol": "BTC/USD", "bids": [["10", "2"]], "asks": [["11", "3"]]} + ) + assert result.bid_price == "10" + assert result.ask_price == "11" + assert result.timestamp is None + + +def test_parses_depth_dict_aliases() -> None: + result = _parse( + {"symbol": "BTC/USD", "bids": [{"p": "10"}], "asks": [{"askPrice": "11"}]} + ) + assert result.bid_price == "10" + assert result.ask_price == "11" + + +def test_rejects_invalid_depth_item() -> None: + validated = validate_dzengi_websocket_quote_schema( + {"symbol": "BTC/USD", "bids": ["10"], "asks": [["11"]]} + ) + with pytest.raises(QuoteParseError): + parse_dzengi_websocket_quote(validated) diff --git a/app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py b/app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py new file mode 100644 index 0000000..894aa73 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py @@ -0,0 +1,328 @@ +# app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py + +from __future__ import annotations + +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceTransportError, + InstrumentReferenceValueError, +) +from src.market_data.acquisition.feeds.instrument_feed import InstrumentFeed +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.protocol import InstrumentFeedProtocol + + +def _instrument( + *, + symbol: str = "BTC/USD_LEVERAGE", +) -> Instrument: + return Instrument( + symbol=symbol, + name=symbol, + status="TRADING", + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +class StubInstrumentDocumentSource: + def __init__( + self, + *, + document: object, + error: Exception | None = None, + ) -> None: + self.document = document + self.error = error + self.call_count = 0 + + def fetch_instrument_document(self) -> object: + self.call_count += 1 + + if self.error is not None: + raise self.error + + return self.document + + +class StubInstrumentDocumentHandler: + def __init__( + self, + *, + instruments: tuple[Instrument, ...], + error: Exception | None = None, + ) -> None: + self.instruments = instruments + self.error = error + self.documents: list[object] = [] + + def handle_instrument_document( + self, + document: object, + ) -> tuple[Instrument, ...]: + self.documents.append(document) + + if self.error is not None: + raise self.error + + return self.instruments + + +def test_feed_satisfies_instrument_feed_protocol() -> None: + source = StubInstrumentDocumentSource( + document={ + "symbols": [], + } + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + assert isinstance(feed, InstrumentFeedProtocol) + + +def test_feed_calls_source_once() -> None: + source = StubInstrumentDocumentSource( + document={ + "symbols": [], + } + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + feed.load_instruments() + + assert source.call_count == 1 + + +def test_feed_calls_handler_once() -> None: + source = StubInstrumentDocumentSource( + document={ + "symbols": [], + } + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + feed.load_instruments() + + assert len(handler.documents) == 1 + + +def test_feed_passes_document_to_handler_without_changes() -> None: + document = { + "status": "OK", + "payload": { + "symbols": [], + }, + } + + source = StubInstrumentDocumentSource( + document=document, + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + feed.load_instruments() + + assert handler.documents == [document] + assert handler.documents[0] is document + + +def test_feed_returns_handler_result_without_changes() -> None: + instruments = ( + _instrument(), + ) + + source = StubInstrumentDocumentSource( + document={ + "symbols": [], + } + ) + handler = StubInstrumentDocumentHandler( + instruments=instruments, + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + result = feed.load_instruments() + + assert result is instruments + + +def test_feed_preserves_instrument_order() -> None: + instruments = ( + _instrument(symbol="BTC/USD_LEVERAGE"), + _instrument(symbol="ETH/USD_LEVERAGE"), + _instrument(symbol="XRP/USD_LEVERAGE"), + ) + + source = StubInstrumentDocumentSource( + document={ + "symbols": [], + } + ) + handler = StubInstrumentDocumentHandler( + instruments=instruments, + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + result = feed.load_instruments() + + assert tuple(item.symbol for item in result) == ( + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE", + "XRP/USD_LEVERAGE", + ) + + +def test_feed_returns_empty_tuple_without_error() -> None: + source = StubInstrumentDocumentSource( + document={ + "symbols": [], + } + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + result = feed.load_instruments() + + assert result == () + + +def test_feed_preserves_transport_error_without_wrapping() -> None: + original_error = InstrumentReferenceTransportError( + "Не удалось получить exchangeInfo." + ) + + source = StubInstrumentDocumentSource( + document=None, + error=original_error, + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + with pytest.raises( + InstrumentReferenceTransportError, + ) as exc_info: + feed.load_instruments() + + assert exc_info.value is original_error + assert source.call_count == 1 + assert handler.documents == [] + + +def test_feed_preserves_processing_error_without_wrapping() -> None: + document = { + "symbols": [], + } + original_error = InstrumentReferenceValueError( + "Некорректное значение." + ) + + source = StubInstrumentDocumentSource( + document=document, + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + error=original_error, + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + with pytest.raises( + InstrumentReferenceValueError, + ) as exc_info: + feed.load_instruments() + + assert exc_info.value is original_error + assert source.call_count == 1 + assert handler.documents == [document] + + +def test_feed_does_not_retry_source_after_transport_error() -> None: + original_error = InstrumentReferenceTransportError( + "Network error." + ) + + source = StubInstrumentDocumentSource( + document=None, + error=original_error, + ) + handler = StubInstrumentDocumentHandler( + instruments=(), + ) + + feed = InstrumentFeed( + source=source, + handler=handler, + ) + + with pytest.raises(InstrumentReferenceTransportError): + feed.load_instruments() + + assert source.call_count == 1 \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/feeds/test_quotes_feed.py b/app/tests/unit/market_data/acquisition/feeds/test_quotes_feed.py new file mode 100644 index 0000000..99e59ed --- /dev/null +++ b/app/tests/unit/market_data/acquisition/feeds/test_quotes_feed.py @@ -0,0 +1,220 @@ +# app/tests/unit/market_data/acquisition/feeds/test_quotes_feed.py + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.exceptions import ( + QuoteTransportError, + QuoteValueError, +) +from src.market_data.acquisition.feeds.quotes_feed import QuotesFeed +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.protocol import QuoteFeedProtocol + + +def _quote( + *, + symbol: str = "BTC/USD_LEVERAGE", +) -> Quote: + return Quote( + symbol=symbol, + last_price=Decimal("64159.45"), + bid_price=Decimal("64159.45"), + ask_price=Decimal("64159.55"), + exchange_timestamp=datetime( + 2026, + 7, + 12, + 16, + 14, + 30, + tzinfo=timezone.utc, + ), + received_at=datetime( + 2026, + 7, + 12, + 16, + 14, + 31, + tzinfo=timezone.utc, + ), + source="dzengi", + ) + + +class StubQuoteDocumentSource: + def __init__( + self, + *, + document: object, + error: Exception | None = None, + ) -> None: + self.document = document + self.error = error + self.symbols: list[str] = [] + + def fetch_quote_document( + self, + symbol: str, + ) -> object: + self.symbols.append(symbol) + + if self.error is not None: + raise self.error + + return self.document + + +class StubQuoteDocumentHandler: + def __init__( + self, + *, + quote: Quote, + error: Exception | None = None, + ) -> None: + self.quote = quote + self.error = error + self.documents: list[object] = [] + + def handle_quote_document( + self, + document: object, + ) -> Quote: + self.documents.append(document) + + if self.error is not None: + raise self.error + + return self.quote + + +def test_feed_satisfies_quote_feed_protocol() -> None: + feed = QuotesFeed( + source=StubQuoteDocumentSource(document={}), + handler=StubQuoteDocumentHandler(quote=_quote()), + ) + + assert isinstance(feed, QuoteFeedProtocol) + + +def test_feed_calls_source_once() -> None: + source = StubQuoteDocumentSource(document={}) + feed = QuotesFeed( + source=source, + handler=StubQuoteDocumentHandler(quote=_quote()), + ) + + feed.load_quote("BTC/USD_LEVERAGE") + + assert source.symbols == ["BTC/USD_LEVERAGE"] + + +def test_feed_passes_symbol_to_source_without_changes() -> None: + source = StubQuoteDocumentSource(document={}) + feed = QuotesFeed( + source=source, + handler=StubQuoteDocumentHandler(quote=_quote()), + ) + + feed.load_quote(" btc/usd_leverage ") + + assert source.symbols == [" btc/usd_leverage "] + + +def test_feed_calls_handler_once() -> None: + handler = StubQuoteDocumentHandler(quote=_quote()) + feed = QuotesFeed( + source=StubQuoteDocumentSource(document={}), + handler=handler, + ) + + feed.load_quote("BTC/USD_LEVERAGE") + + assert len(handler.documents) == 1 + + +def test_feed_passes_document_to_handler_without_changes() -> None: + document = { + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + } + handler = StubQuoteDocumentHandler(quote=_quote()) + feed = QuotesFeed( + source=StubQuoteDocumentSource(document=document), + handler=handler, + ) + + feed.load_quote("BTC/USD_LEVERAGE") + + assert handler.documents == [document] + assert handler.documents[0] is document + + +def test_feed_returns_handler_result_without_copying() -> None: + quote = _quote() + feed = QuotesFeed( + source=StubQuoteDocumentSource(document={}), + handler=StubQuoteDocumentHandler(quote=quote), + ) + + result = feed.load_quote("BTC/USD_LEVERAGE") + + assert result is quote + + +def test_feed_preserves_transport_error_without_wrapping() -> None: + original_error = QuoteTransportError("Network error.") + handler = StubQuoteDocumentHandler(quote=_quote()) + feed = QuotesFeed( + source=StubQuoteDocumentSource( + document=None, + error=original_error, + ), + handler=handler, + ) + + with pytest.raises(QuoteTransportError) as exc_info: + feed.load_quote("BTC/USD_LEVERAGE") + + assert exc_info.value is original_error + assert handler.documents == [] + + +def test_feed_preserves_handler_error_without_wrapping() -> None: + document = {"symbol": "BTC/USD_LEVERAGE"} + original_error = QuoteValueError("Invalid quote.") + handler = StubQuoteDocumentHandler( + quote=_quote(), + error=original_error, + ) + feed = QuotesFeed( + source=StubQuoteDocumentSource(document=document), + handler=handler, + ) + + with pytest.raises(QuoteValueError) as exc_info: + feed.load_quote("BTC/USD_LEVERAGE") + + assert exc_info.value is original_error + assert handler.documents == [document] + + +def test_feed_does_not_retry_source_after_error() -> None: + source = StubQuoteDocumentSource( + document=None, + error=QuoteTransportError("Network error."), + ) + feed = QuotesFeed( + source=source, + handler=StubQuoteDocumentHandler(quote=_quote()), + ) + + with pytest.raises(QuoteTransportError): + feed.load_quote("BTC/USD_LEVERAGE") + + assert source.symbols == ["BTC/USD_LEVERAGE"] diff --git a/app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py b/app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py new file mode 100644 index 0000000..61da5fe --- /dev/null +++ b/app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py @@ -0,0 +1,220 @@ +# app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py + +from __future__ import annotations + +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceMappingError, + InstrumentReferenceParseError, + InstrumentReferenceSchemaError, + InstrumentReferenceValueError, +) +from src.market_data.acquisition.handlers.instrument_handler import ( + DzengiInstrumentDocumentHandler, +) +from src.market_data.acquisition.protocol import ( + InstrumentDocumentHandler, +) + + +def _valid_symbol_document() -> dict[str, object]: + return { + "symbol": "BTC/USD_LEVERAGE", + "name": "BTC/USD", + "status": "TRADING", + "assetType": "CRYPTOCURRENCY", + "baseAsset": "BTC", + "baseAssetPrecision": 4, + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 4, + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP", + ], + "filters": [ + { + "filterType": "LOT_SIZE", + "minQty": "0.0001", + "maxQty": "1000", + "stepSize": "0.0001", + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "1", + }, + ], + "marketModes": [ + "REGULAR", + ], + "marketType": "LEVERAGE", + "country": "", + "sector": "", + "industry": "", + "tradingHours": None, + "tickSize": 0.05, + "tickValue": 3878.86, + "tradingFee": 0.06, + "exchangeFee": None, + "longRate": -0.01, + "shortRate": 0.01, + "swapChargeInterval": 480, + "minSLGap": 0, + "maxSLGap": 50.0, + "minTPGap": 0, + "maxTPGap": 50.0, + } + + +def _valid_unwrapped_document() -> dict[str, object]: + return { + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [ + _valid_symbol_document(), + ], + } + + +def _valid_wrapped_document() -> dict[str, object]: + return { + "status": "OK", + "correlationId": "2", + "payload": _valid_unwrapped_document(), + } + + +def test_handler_satisfies_instrument_document_handler_protocol() -> None: + handler = DzengiInstrumentDocumentHandler() + + assert isinstance(handler, InstrumentDocumentHandler) + + +def test_handler_processes_valid_unwrapped_document() -> None: + handler = DzengiInstrumentDocumentHandler() + + instruments = handler.handle_instrument_document( + _valid_unwrapped_document() + ) + + assert isinstance(instruments, tuple) + assert len(instruments) == 1 + + instrument = instruments[0] + + assert instrument.symbol == "BTC/USD_LEVERAGE" + assert instrument.name == "BTC/USD" + assert instrument.status == "TRADING" + assert instrument.base_asset == "BTC" + assert instrument.quote_asset == "USD" + assert instrument.asset_type == "CRYPTOCURRENCY" + assert instrument.market_type == "LEVERAGE" + assert instrument.market_modes == ("REGULAR",) + assert instrument.order_types == ( + "LIMIT", + "MARKET", + "STOP", + ) + + +def test_handler_processes_valid_wrapped_document() -> None: + handler = DzengiInstrumentDocumentHandler() + + instruments = handler.handle_instrument_document( + _valid_wrapped_document() + ) + + assert len(instruments) == 1 + assert instruments[0].symbol == "BTC/USD_LEVERAGE" + + +def test_handler_returns_exact_decimal_values() -> None: + handler = DzengiInstrumentDocumentHandler() + + instruments = handler.handle_instrument_document( + _valid_unwrapped_document() + ) + + instrument = instruments[0] + + assert instrument.tick_size == Decimal("0.05") + assert instrument.tick_value == Decimal("3878.86") + assert instrument.step_size == Decimal("0.0001") + assert instrument.min_qty == Decimal("0.0001") + assert instrument.max_qty == Decimal("1000") + assert instrument.min_notional == Decimal("1") + + +def test_handler_returns_empty_tuple_for_empty_symbols() -> None: + document = _valid_unwrapped_document() + document["symbols"] = [] + + handler = DzengiInstrumentDocumentHandler() + + instruments = handler.handle_instrument_document(document) + + assert instruments == () + + +def test_handler_preserves_schema_error() -> None: + handler = DzengiInstrumentDocumentHandler() + + with pytest.raises(InstrumentReferenceSchemaError): + handler.handle_instrument_document([]) + + +def test_handler_preserves_parse_error() -> None: + document = _valid_unwrapped_document() + symbol = _valid_symbol_document() + symbol["baseAssetPrecision"] = True + document["symbols"] = [symbol] + + handler = DzengiInstrumentDocumentHandler() + + with pytest.raises(InstrumentReferenceParseError): + handler.handle_instrument_document(document) + + +def test_handler_preserves_value_error() -> None: + document = _valid_unwrapped_document() + symbol = _valid_symbol_document() + symbol["tickSize"] = 0 + document["symbols"] = [symbol] + + handler = DzengiInstrumentDocumentHandler() + + with pytest.raises(InstrumentReferenceValueError): + handler.handle_instrument_document(document) + + +def test_handler_preserves_mapping_error() -> None: + document = _valid_unwrapped_document() + symbol = _valid_symbol_document() + + lot_size = { + "filterType": "LOT_SIZE", + "minQty": "0.0001", + "maxQty": "1000", + "stepSize": "0.0001", + } + + symbol["filters"] = [ + lot_size, + lot_size.copy(), + ] + + document["symbols"] = [symbol] + + handler = DzengiInstrumentDocumentHandler() + + with pytest.raises( + InstrumentReferenceMappingError, + match=r"несколько фильтров LOT_SIZE", + ): + handler.handle_instrument_document(document) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/handlers/test_quotes_handler.py b/app/tests/unit/market_data/acquisition/handlers/test_quotes_handler.py new file mode 100644 index 0000000..197a9f9 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/handlers/test_quotes_handler.py @@ -0,0 +1,156 @@ +# app/tests/unit/market_data/acquisition/handlers/test_quotes_handler.py + +from __future__ import annotations + +from datetime import timezone +from decimal import Decimal +from typing import TypeAlias + +import pytest + +import src.market_data.acquisition.handlers.quotes_handler as handler_module +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiTicker24hrResponse, +) +from src.market_data.acquisition.exceptions import QuoteSchemaError +from src.market_data.acquisition.handlers.quotes_handler import ( + DzengiQuoteDocumentHandler, +) +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.protocol import QuoteDocumentHandler + + +PipelineCall: TypeAlias = ( + tuple[str, object] + | tuple[str, object, object] +) + + +def _document() -> dict[str, object]: + return { + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + "bidPrice": "64159.45", + "askPrice": "64159.55", + "closeTime": 1783887270312, + "volume": "9.6002", + } + + +def test_handler_implements_quote_document_handler_protocol() -> None: + handler = DzengiQuoteDocumentHandler() + + assert isinstance(handler, QuoteDocumentHandler) + + +def test_handler_returns_canonical_quote() -> None: + before = handler_module.datetime.now(timezone.utc) + + result = DzengiQuoteDocumentHandler().handle_quote_document( + _document() + ) + + after = handler_module.datetime.now(timezone.utc) + + assert isinstance(result, Quote) + assert result.symbol == "BTC/USD_LEVERAGE" + assert result.last_price == Decimal("64159.45") + assert result.bid_price == Decimal("64159.45") + assert result.ask_price == Decimal("64159.55") + assert result.source == "dzengi" + assert result.exchange_timestamp is not None + assert result.exchange_timestamp.tzinfo is timezone.utc + assert before <= result.received_at <= after + + +def test_handler_executes_pipeline_in_order( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[PipelineCall] = [] + validated = object() + response = DzengiTicker24hrResponse( + symbol="BTC/USD_LEVERAGE", + last_price="64159.45", + bid_price="64159.45", + ask_price="64159.55", + close_time=1783887270312, + ) + quote = Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("64159.45"), + bid_price=Decimal("64159.45"), + ask_price=Decimal("64159.55"), + exchange_timestamp=None, + received_at=handler_module.datetime.now(timezone.utc), + source="dzengi", + ) + + def validate_schema(document: object) -> object: + calls.append(("schema", document)) + return validated + + def parse(document: object) -> DzengiTicker24hrResponse: + calls.append(("parser", document)) + return response + + def validate_values( + value: DzengiTicker24hrResponse, + ) -> None: + calls.append(("values", value)) + + def map_quote( + value: DzengiTicker24hrResponse, + *, + received_at: object, + ) -> Quote: + calls.append( + ( + "mapper", + value, + received_at, + ) + ) + return quote + + monkeypatch.setattr( + handler_module, + "validate_quote_schema", + validate_schema, + ) + monkeypatch.setattr( + handler_module, + "parse_quote", + parse, + ) + monkeypatch.setattr( + handler_module, + "validate_quote_values", + validate_values, + ) + monkeypatch.setattr( + handler_module, + "map_dzengi_ticker_to_quote", + map_quote, + ) + + document = _document() + result = ( + DzengiQuoteDocumentHandler() + .handle_quote_document(document) + ) + + assert result is quote + assert calls[0] == ("schema", document) + assert calls[1] == ("parser", validated) + assert calls[2] == ("values", response) + + mapper_call = calls[3] + + assert len(mapper_call) == 3 + assert mapper_call[0] == "mapper" + assert mapper_call[1] is response + + +def test_handler_propagates_schema_error() -> None: + with pytest.raises(QuoteSchemaError): + DzengiQuoteDocumentHandler().handle_quote_document({}) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/models/test_instrument.py b/app/tests/unit/market_data/acquisition/models/test_instrument.py new file mode 100644 index 0000000..1e77c7d --- /dev/null +++ b/app/tests/unit/market_data/acquisition/models/test_instrument.py @@ -0,0 +1,164 @@ +# app/tests/unit/market_data/acquisition/models/test_instrument.py + +from __future__ import annotations + +from dataclasses import FrozenInstanceError +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.models.instrument import Instrument + + +def test_instrument_stores_complete_reference_data() -> None: + instrument = Instrument( + symbol="ETH/EUR_LEVERAGE", + name="ETH/EUR", + status="TRADING", + base_asset="ETH", + quote_asset="EUR", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET", "STOP"), + base_asset_precision=3, + quote_asset_precision=3, + tick_size=Decimal("0.01"), + tick_value=Decimal("18.3415"), + step_size=Decimal("0.001"), + min_qty=Decimal("0.001"), + max_qty=Decimal("1000"), + min_notional=Decimal("2"), + country=None, + sector=None, + industry=None, + trading_hours=( + "UTC; Mon - 21:00, 21:05 -; " + "Tue - 21:00, 21:05 -" + ), + ) + + assert instrument.symbol == "ETH/EUR_LEVERAGE" + assert instrument.name == "ETH/EUR" + assert instrument.status == "TRADING" + + assert instrument.base_asset == "ETH" + assert instrument.quote_asset == "EUR" + assert instrument.asset_type == "CRYPTOCURRENCY" + + assert instrument.market_type == "LEVERAGE" + assert instrument.market_modes == ("REGULAR",) + assert instrument.order_types == ("LIMIT", "MARKET", "STOP") + + assert instrument.base_asset_precision == 3 + assert instrument.quote_asset_precision == 3 + + assert instrument.tick_size == Decimal("0.01") + assert instrument.tick_value == Decimal("18.3415") + + assert instrument.step_size == Decimal("0.001") + assert instrument.min_qty == Decimal("0.001") + assert instrument.max_qty == Decimal("1000") + assert instrument.min_notional == Decimal("2") + + assert instrument.country is None + assert instrument.sector is None + assert instrument.industry is None + assert instrument.trading_hours is not None + + +def test_instrument_accepts_missing_optional_reference_values() -> None: + instrument = Instrument( + symbol="TEST/USD", + name="Test instrument", + status="BREAK", + base_asset="TEST", + quote_asset="USD", + asset_type=None, + market_type="SPOT", + market_modes=(), + order_types=(), + base_asset_precision=None, + quote_asset_precision=None, + tick_size=None, + tick_value=None, + step_size=None, + min_qty=None, + max_qty=None, + min_notional=None, + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + assert instrument.asset_type is None + assert instrument.market_modes == () + assert instrument.order_types == () + assert instrument.base_asset_precision is None + assert instrument.quote_asset_precision is None + assert instrument.tick_size is None + assert instrument.tick_value is None + assert instrument.step_size is None + assert instrument.min_qty is None + assert instrument.max_qty is None + assert instrument.min_notional is None + assert instrument.trading_hours is None + + +def test_instrument_uses_immutable_sequences() -> None: + instrument = Instrument( + symbol="BTC/USD", + name="BTC/USD", + status="TRADING", + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="SPOT", + market_modes=("REGULAR",), + order_types=("MARKET",), + base_asset_precision=8, + quote_asset_precision=2, + tick_size=Decimal("0.01"), + tick_value=None, + step_size=Decimal("0.00000001"), + min_qty=Decimal("0.00000001"), + max_qty=Decimal("100"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + assert isinstance(instrument.market_modes, tuple) + assert isinstance(instrument.order_types, tuple) + + +def test_instrument_is_immutable() -> None: + instrument = Instrument( + symbol="BTC/USD", + name="BTC/USD", + status="TRADING", + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="SPOT", + market_modes=("REGULAR",), + order_types=("MARKET",), + base_asset_precision=8, + quote_asset_precision=2, + tick_size=Decimal("0.01"), + tick_value=None, + step_size=Decimal("0.00000001"), + min_qty=Decimal("0.00000001"), + max_qty=Decimal("100"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + with pytest.raises(FrozenInstanceError): + instrument.status = "BREAK" # type: ignore[misc] \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/models/test_instrument_status.py b/app/tests/unit/market_data/acquisition/models/test_instrument_status.py new file mode 100644 index 0000000..5013ec4 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/models/test_instrument_status.py @@ -0,0 +1,127 @@ +# app/tests/unit/market_data/acquisition/models/test_status.py + +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.market_data.acquisition.models.status import ( + InstrumentStatusClassification, + InstrumentTradingState, + classify_instrument_status, +) + + +@pytest.mark.parametrize( + "raw_status", + [ + "TRADING", + "OPEN", + "ACTIVE", + "ENABLED", + "ONLINE", + ], +) +def test_classify_open_statuses(raw_status: str) -> None: + result = classify_instrument_status(raw_status) + + assert result == InstrumentStatusClassification( + state=InstrumentTradingState.OPEN, + normalized_status=raw_status, + ) + + +@pytest.mark.parametrize( + "raw_status", + [ + "NOT_TRADABLE", + "TRADING_DISABLED", + "MARKET_DISABLED", + "UNAVAILABLE_FOR_TRADING", + "CLOSE_ONLY", + "REDUCE_ONLY", + "VIEW_ONLY", + ], +) +def test_classify_not_tradable_statuses(raw_status: str) -> None: + result = classify_instrument_status(raw_status) + + assert result == InstrumentStatusClassification( + state=InstrumentTradingState.NOT_TRADABLE, + normalized_status=raw_status, + ) + + +@pytest.mark.parametrize( + "raw_status", + [ + "BREAK", + "CLOSED", + "HALT", + "HALTED", + "PAUSED", + "SUSPENDED", + "DISABLED", + "SETTLING", + "POST_ONLY", + ], +) +def test_classify_break_statuses(raw_status: str) -> None: + result = classify_instrument_status(raw_status) + + assert result == InstrumentStatusClassification( + state=InstrumentTradingState.BREAK, + normalized_status=raw_status, + ) + + +def test_classification_normalizes_case_and_outer_spaces() -> None: + result = classify_instrument_status( + " trading " + ) + + assert result == InstrumentStatusClassification( + state=InstrumentTradingState.OPEN, + normalized_status="TRADING", + ) + + +@pytest.mark.parametrize( + "raw_status", + [ + None, + "", + " ", + " ", + ], +) +def test_empty_status_is_unknown( + raw_status: str | None, +) -> None: + result = classify_instrument_status(raw_status) + + assert result == InstrumentStatusClassification( + state=InstrumentTradingState.UNKNOWN, + normalized_status=None, + ) + + +def test_unknown_status_preserves_normalized_value() -> None: + result = classify_instrument_status( + " maintenance " + ) + + assert result == InstrumentStatusClassification( + state=InstrumentTradingState.UNKNOWN, + normalized_status="MAINTENANCE", + ) + + +def test_classification_result_is_frozen() -> None: + result = classify_instrument_status( + "TRADING" + ) + + with pytest.raises(FrozenInstanceError): + result.normalized_status = "BREAK" # type: ignore[misc] \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/test_protocol.py b/app/tests/unit/market_data/acquisition/test_protocol.py new file mode 100644 index 0000000..de46ee1 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/test_protocol.py @@ -0,0 +1,159 @@ +# app/tests/unit/market_data/acquisition/test_protocol.py + +from __future__ import annotations + +from decimal import Decimal + +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceMappingError, + InstrumentReferenceParseError, + InstrumentReferenceSchemaError, + InstrumentReferenceTransportError, + InstrumentReferenceValueError, + MarketDataAcquisitionError, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.protocol import ( + InstrumentDocumentHandler, + InstrumentDocumentSource, + InstrumentFeedProtocol, +) + + +def _instrument() -> Instrument: + return Instrument( + symbol="BTC/USD_LEVERAGE", + name="BTC/USD", + status="TRADING", + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +class StubInstrumentDocumentSource: + def fetch_instrument_document(self) -> object: + return { + "symbols": [], + } + + +class StubInstrumentDocumentHandler: + def handle_instrument_document( + self, + document: object, + ) -> tuple[Instrument, ...]: + del document + return (_instrument(),) + + +class StubInstrumentFeed: + def load_instruments(self) -> tuple[Instrument, ...]: + return (_instrument(),) + + +class InvalidSource: + pass + + +class InvalidHandler: + pass + + +class InvalidFeed: + pass + + +def test_document_source_satisfies_protocol() -> None: + source = StubInstrumentDocumentSource() + + assert isinstance(source, InstrumentDocumentSource) + assert source.fetch_instrument_document() == { + "symbols": [], + } + + +def test_document_handler_satisfies_protocol() -> None: + handler = StubInstrumentDocumentHandler() + + assert isinstance(handler, InstrumentDocumentHandler) + + instruments = handler.handle_instrument_document( + { + "symbols": [], + } + ) + + assert isinstance(instruments, tuple) + assert len(instruments) == 1 + assert instruments[0].symbol == "BTC/USD_LEVERAGE" + + +def test_instrument_feed_satisfies_protocol() -> None: + feed = StubInstrumentFeed() + + assert isinstance(feed, InstrumentFeedProtocol) + + instruments = feed.load_instruments() + + assert isinstance(instruments, tuple) + assert len(instruments) == 1 + assert instruments[0].symbol == "BTC/USD_LEVERAGE" + + +def test_objects_without_required_methods_do_not_satisfy_protocols() -> None: + assert not isinstance(InvalidSource(), InstrumentDocumentSource) + assert not isinstance(InvalidHandler(), InstrumentDocumentHandler) + assert not isinstance(InvalidFeed(), InstrumentFeedProtocol) + + +def test_protocols_support_structural_typing_without_inheritance() -> None: + source: InstrumentDocumentSource = StubInstrumentDocumentSource() + handler: InstrumentDocumentHandler = StubInstrumentDocumentHandler() + feed: InstrumentFeedProtocol = StubInstrumentFeed() + + document = source.fetch_instrument_document() + handled_instruments = handler.handle_instrument_document(document) + loaded_instruments = feed.load_instruments() + + assert handled_instruments[0].symbol == "BTC/USD_LEVERAGE" + assert loaded_instruments[0].symbol == "BTC/USD_LEVERAGE" + + +def test_transport_error_inherits_acquisition_error() -> None: + error = InstrumentReferenceTransportError( + "Не удалось получить exchangeInfo." + ) + + assert isinstance(error, MarketDataAcquisitionError) + assert str(error) == "Не удалось получить exchangeInfo." + + +def test_all_instrument_reference_errors_share_base_type() -> None: + errors = ( + InstrumentReferenceTransportError(), + InstrumentReferenceSchemaError(), + InstrumentReferenceParseError(), + InstrumentReferenceValueError(), + InstrumentReferenceMappingError(), + ) + + assert all( + isinstance(error, MarketDataAcquisitionError) + for error in errors + ) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/test_registry.py b/app/tests/unit/market_data/acquisition/test_registry.py new file mode 100644 index 0000000..b3d8174 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/test_registry.py @@ -0,0 +1,433 @@ +# app/tests/unit/market_data/acquisition/test_registry.py + +from __future__ import annotations + +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.exceptions import ( + InstrumentFeedRegistryError, + MarketDataAcquisitionError, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.protocol import InstrumentFeedProtocol +from src.market_data.acquisition.registry import InstrumentFeedRegistry + + +def _instrument( + *, + symbol: str = "BTC/USD_LEVERAGE", +) -> Instrument: + return Instrument( + symbol=symbol, + name=symbol, + status="TRADING", + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +class StubInstrumentFeed: + def __init__( + self, + *, + instruments: tuple[Instrument, ...] = (), + ) -> None: + self.instruments = instruments + self.load_call_count = 0 + + def load_instruments(self) -> tuple[Instrument, ...]: + self.load_call_count += 1 + return self.instruments + + +class InvalidFeed: + pass + + +def test_register_and_get_feed() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register("dzengi", feed) + + result = registry.get("dzengi") + + assert result is feed + + +def test_registry_preserves_feed_identity() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed( + instruments=( + _instrument(), + ) + ) + + registry.register("dzengi", feed) + + registered_feed = registry.get("dzengi") + + assert registered_feed is feed + assert registered_feed.load_instruments() is feed.instruments + + +def test_registry_accepts_instrument_feed_protocol() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + assert isinstance(feed, InstrumentFeedProtocol) + + registry.register("dzengi", feed) + + assert registry.get("dzengi") is feed + + +def test_registry_supports_multiple_source_names() -> None: + registry = InstrumentFeedRegistry() + + dzengi_feed = StubInstrumentFeed( + instruments=( + _instrument(symbol="BTC/USD_LEVERAGE"), + ) + ) + secondary_feed = StubInstrumentFeed( + instruments=( + _instrument(symbol="ETH/USD_LEVERAGE"), + ) + ) + + registry.register("dzengi", dzengi_feed) + registry.register("secondary", secondary_feed) + + assert registry.get("dzengi") is dzengi_feed + assert registry.get("secondary") is secondary_feed + + +def test_registry_strips_outer_whitespace_from_source_name() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register(" dzengi ", feed) + + assert registry.get("dzengi") is feed + assert registry.get(" dzengi ") is feed + + +@pytest.mark.parametrize( + "source_name", + [ + "", + " ", + " ", + "\t", + "\n", + ], +) +def test_registry_rejects_empty_source_name( + source_name: str, +) -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + with pytest.raises( + InstrumentFeedRegistryError, + match=r"Имя источника Instrument Feed не должно быть пустым", + ): + registry.register(source_name, feed) + + +@pytest.mark.parametrize( + "source_name", + [ + "", + " ", + " ", + "\t", + "\n", + ], +) +def test_registry_rejects_empty_source_name_on_get( + source_name: str, +) -> None: + registry = InstrumentFeedRegistry() + + with pytest.raises( + InstrumentFeedRegistryError, + match=r"Имя источника Instrument Feed не должно быть пустым", + ): + registry.get(source_name) + + +def test_registry_rejects_duplicate_registration() -> None: + registry = InstrumentFeedRegistry() + + first_feed = StubInstrumentFeed() + second_feed = StubInstrumentFeed() + + registry.register("dzengi", first_feed) + + with pytest.raises( + InstrumentFeedRegistryError, + match=r"уже зарегистрирован", + ): + registry.register("dzengi", second_feed) + + +def test_duplicate_registration_does_not_replace_original_feed() -> None: + registry = InstrumentFeedRegistry() + + first_feed = StubInstrumentFeed() + second_feed = StubInstrumentFeed() + + registry.register("dzengi", first_feed) + + with pytest.raises(InstrumentFeedRegistryError): + registry.register("dzengi", second_feed) + + assert registry.get("dzengi") is first_feed + + +def test_duplicate_registration_uses_normalized_source_name() -> None: + registry = InstrumentFeedRegistry() + + first_feed = StubInstrumentFeed() + second_feed = StubInstrumentFeed() + + registry.register("dzengi", first_feed) + + with pytest.raises( + InstrumentFeedRegistryError, + match=r"уже зарегистрирован", + ): + registry.register(" dzengi ", second_feed) + + +def test_registry_keeps_source_name_case_sensitive() -> None: + registry = InstrumentFeedRegistry() + + lowercase_feed = StubInstrumentFeed() + uppercase_feed = StubInstrumentFeed() + + registry.register("dzengi", lowercase_feed) + registry.register("DZENGI", uppercase_feed) + + assert registry.get("dzengi") is lowercase_feed + assert registry.get("DZENGI") is uppercase_feed + + +def test_registry_rejects_unregistered_source() -> None: + registry = InstrumentFeedRegistry() + + with pytest.raises( + InstrumentFeedRegistryError, + match=r"не зарегистрирован", + ): + registry.get("dzengi") + + +def test_registry_rejects_object_without_feed_protocol() -> None: + registry = InstrumentFeedRegistry() + + with pytest.raises( + InstrumentFeedRegistryError, + match=r"не соответствует InstrumentFeedProtocol", + ): + registry.register( + "invalid", + InvalidFeed(), # type: ignore[arg-type] + ) + + +def test_registry_does_not_load_feed_during_registration() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register("dzengi", feed) + + assert feed.load_call_count == 0 + + +def test_registry_does_not_load_feed_during_get() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register("dzengi", feed) + result = registry.get("dzengi") + + assert result is feed + assert feed.load_call_count == 0 + + +def test_registry_stores_feed_not_instrument_result() -> None: + registry = InstrumentFeedRegistry() + instruments = ( + _instrument(), + ) + feed = StubInstrumentFeed( + instruments=instruments, + ) + + registry.register("dzengi", feed) + + registered_feed = registry.get("dzengi") + + assert registered_feed is feed + assert registered_feed is not instruments + + +def test_registry_error_inherits_acquisition_error() -> None: + error = InstrumentFeedRegistryError( + "Registry error." + ) + + assert isinstance(error, MarketDataAcquisitionError) + assert str(error) == "Registry error." + +# Quotes Feed Registry tests. +from datetime import datetime, timezone + +from src.market_data.acquisition.exceptions import QuoteFeedRegistryError +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.protocol import QuoteFeedProtocol +from src.market_data.acquisition.registry import QuoteFeedRegistry + + +def _quote() -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("64159.45"), + bid_price=Decimal("64159.45"), + ask_price=Decimal("64159.55"), + exchange_timestamp=datetime.now(timezone.utc), + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + + +class StubQuoteFeed: + def __init__(self) -> None: + self.quote = _quote() + self.symbols: list[str] = [] + + def load_quote( + self, + symbol: str, + ) -> Quote: + self.symbols.append(symbol) + return self.quote + + +def test_quote_registry_registers_and_returns_feed() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + + registry.register("dzengi", feed) + + assert registry.get("dzengi") is feed + + +def test_quote_registry_accepts_quote_feed_protocol() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + + assert isinstance(feed, QuoteFeedProtocol) + + registry.register("dzengi", feed) + + assert registry.get("dzengi") is feed + + +def test_quote_registry_strips_outer_whitespace() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + + registry.register(" dzengi ", feed) + + assert registry.get("dzengi") is feed + assert registry.get(" dzengi ") is feed + + +@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"]) +def test_quote_registry_rejects_empty_source_name( + source_name: str, +) -> None: + registry = QuoteFeedRegistry() + + with pytest.raises( + QuoteFeedRegistryError, + match=r"Имя источника Quotes Feed не должно быть пустым", + ): + registry.register(source_name, StubQuoteFeed()) + + +def test_quote_registry_rejects_duplicate_registration() -> None: + registry = QuoteFeedRegistry() + first_feed = StubQuoteFeed() + + registry.register("dzengi", first_feed) + + with pytest.raises( + QuoteFeedRegistryError, + match=r"уже зарегистрирован", + ): + registry.register("dzengi", StubQuoteFeed()) + + assert registry.get("dzengi") is first_feed + + +def test_quote_registry_rejects_unregistered_source() -> None: + registry = QuoteFeedRegistry() + + with pytest.raises( + QuoteFeedRegistryError, + match=r"не зарегистрирован", + ): + registry.get("dzengi") + + +def test_quote_registry_rejects_invalid_feed() -> None: + registry = QuoteFeedRegistry() + + with pytest.raises( + QuoteFeedRegistryError, + match=r"не соответствует QuoteFeedProtocol", + ): + registry.register( + "invalid", + InvalidFeed(), # type: ignore[arg-type] + ) + + +def test_quote_registry_does_not_load_feed() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + + registry.register("dzengi", feed) + result = registry.get("dzengi") + + assert result is feed + assert feed.symbols == [] + + +def test_quote_registry_error_inherits_acquisition_error() -> None: + error = QuoteFeedRegistryError("Registry error.") + + assert isinstance(error, MarketDataAcquisitionError) diff --git a/app/tests/unit/market_data/acquisition/test_service.py b/app/tests/unit/market_data/acquisition/test_service.py new file mode 100644 index 0000000..b1d5a3d --- /dev/null +++ b/app/tests/unit/market_data/acquisition/test_service.py @@ -0,0 +1,475 @@ +# app/tests/unit/market_data/acquisition/test_service.py + +from __future__ import annotations + +from decimal import Decimal + +import pytest + +from src.market_data.acquisition.exceptions import ( + InstrumentFeedRegistryError, + InstrumentReferenceMappingError, + InstrumentReferenceTransportError, + InstrumentReferenceValueError, +) +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.protocol import InstrumentFeedProtocol +from src.market_data.acquisition.registry import InstrumentFeedRegistry +from src.market_data.acquisition.service import InstrumentAcquisitionService + + +def _instrument( + *, + symbol: str = "BTC/USD_LEVERAGE", +) -> Instrument: + return Instrument( + symbol=symbol, + name=symbol, + status="TRADING", + base_asset="BTC", + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +class StubInstrumentFeed: + def __init__( + self, + *, + instruments: tuple[Instrument, ...] = (), + error: Exception | None = None, + ) -> None: + self.instruments = instruments + self.error = error + self.load_call_count = 0 + + def load_instruments(self) -> tuple[Instrument, ...]: + self.load_call_count += 1 + + if self.error is not None: + raise self.error + + return self.instruments + + +class RecordingInstrumentFeedRegistry(InstrumentFeedRegistry): + def __init__(self) -> None: + super().__init__() + self.requested_source_names: list[str] = [] + self.get_call_count = 0 + + def get( + self, + source_name: str, + ) -> InstrumentFeedProtocol: + self.get_call_count += 1 + self.requested_source_names.append(source_name) + + return super().get(source_name) + + +def test_service_loads_instruments_from_registered_feed() -> None: + registry = InstrumentFeedRegistry() + instruments = ( + _instrument(), + ) + feed = StubInstrumentFeed( + instruments=instruments, + ) + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + result = service.load_instruments("dzengi") + + assert result is instruments + + +def test_service_passes_source_name_to_registry_without_changes() -> None: + registry = RecordingInstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + service.load_instruments(" dzengi ") + + assert registry.requested_source_names == [ + " dzengi ", + ] + + +def test_service_calls_registry_once() -> None: + registry = RecordingInstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + service.load_instruments("dzengi") + + assert registry.get_call_count == 1 + + +def test_service_calls_feed_once() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed() + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + service.load_instruments("dzengi") + + assert feed.load_call_count == 1 + + +def test_service_returns_feed_result_without_copying() -> None: + registry = InstrumentFeedRegistry() + instruments = ( + _instrument(), + _instrument(symbol="ETH/USD_LEVERAGE"), + ) + feed = StubInstrumentFeed( + instruments=instruments, + ) + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + result = service.load_instruments("dzengi") + + assert result is instruments + + +def test_service_preserves_instrument_order() -> None: + registry = InstrumentFeedRegistry() + instruments = ( + _instrument(symbol="BTC/USD_LEVERAGE"), + _instrument(symbol="ETH/USD_LEVERAGE"), + _instrument(symbol="XRP/USD_LEVERAGE"), + ) + feed = StubInstrumentFeed( + instruments=instruments, + ) + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + result = service.load_instruments("dzengi") + + assert tuple(item.symbol for item in result) == ( + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE", + "XRP/USD_LEVERAGE", + ) + + +def test_service_returns_empty_tuple_without_error() -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed( + instruments=(), + ) + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + result = service.load_instruments("dzengi") + + assert result == () + + +def test_service_preserves_registry_error_without_wrapping() -> None: + registry = InstrumentFeedRegistry() + + service = InstrumentAcquisitionService( + registry=registry, + ) + + with pytest.raises( + InstrumentFeedRegistryError, + ) as exc_info: + service.load_instruments("dzengi") + + assert "не зарегистрирован" in str(exc_info.value) + + +def test_service_does_not_call_feed_when_registry_fails() -> None: + registry = InstrumentFeedRegistry() + registered_feed = StubInstrumentFeed() + + registry.register("registered", registered_feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + with pytest.raises(InstrumentFeedRegistryError): + service.load_instruments("missing") + + assert registered_feed.load_call_count == 0 + + +@pytest.mark.parametrize( + "error", + [ + InstrumentReferenceTransportError("Network error."), + InstrumentReferenceValueError("Invalid value."), + InstrumentReferenceMappingError("Mapping error."), + ], +) +def test_service_preserves_feed_error_without_wrapping( + error: Exception, +) -> None: + registry = InstrumentFeedRegistry() + feed = StubInstrumentFeed( + error=error, + ) + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + with pytest.raises(type(error)) as exc_info: + service.load_instruments("dzengi") + + assert exc_info.value is error + assert feed.load_call_count == 1 + + +def test_service_does_not_retry_feed_after_error() -> None: + registry = InstrumentFeedRegistry() + original_error = InstrumentReferenceTransportError( + "Network error." + ) + feed = StubInstrumentFeed( + error=original_error, + ) + + registry.register("dzengi", feed) + + service = InstrumentAcquisitionService( + registry=registry, + ) + + with pytest.raises(InstrumentReferenceTransportError): + service.load_instruments("dzengi") + + assert feed.load_call_count == 1 + +# Quote Acquisition Service tests. +from datetime import datetime, timezone + +from src.market_data.acquisition.exceptions import ( + QuoteFeedRegistryError, + QuoteTransportError, + QuoteValueError, +) +from src.market_data.acquisition.models.quote import Quote +from src.market_data.acquisition.protocol import QuoteFeedProtocol +from src.market_data.acquisition.registry import QuoteFeedRegistry +from src.market_data.acquisition.service import QuoteAcquisitionService + + +def _quote() -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("64159.45"), + bid_price=Decimal("64159.45"), + ask_price=Decimal("64159.55"), + exchange_timestamp=datetime.now(timezone.utc), + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + + +class StubQuoteFeed: + def __init__( + self, + *, + quote: Quote | None = None, + error: Exception | None = None, + ) -> None: + self.quote = quote or _quote() + self.error = error + self.symbols: list[str] = [] + + def load_quote( + self, + symbol: str, + ) -> Quote: + self.symbols.append(symbol) + + if self.error is not None: + raise self.error + + return self.quote + + +class RecordingQuoteFeedRegistry(QuoteFeedRegistry): + def __init__(self) -> None: + super().__init__() + self.requested_source_names: list[str] = [] + self.get_call_count = 0 + + def get( + self, + source_name: str, + ) -> QuoteFeedProtocol: + self.get_call_count += 1 + self.requested_source_names.append(source_name) + return super().get(source_name) + + +def test_quote_service_loads_quote_from_registered_feed() -> None: + registry = QuoteFeedRegistry() + quote = _quote() + feed = StubQuoteFeed(quote=quote) + registry.register("dzengi", feed) + service = QuoteAcquisitionService(registry=registry) + + result = service.load_quote( + "dzengi", + "BTC/USD_LEVERAGE", + ) + + assert result is quote + + +def test_quote_service_passes_source_name_without_changes() -> None: + registry = RecordingQuoteFeedRegistry() + registry.register("dzengi", StubQuoteFeed()) + service = QuoteAcquisitionService(registry=registry) + + service.load_quote( + " dzengi ", + "BTC/USD_LEVERAGE", + ) + + assert registry.requested_source_names == [" dzengi "] + + +def test_quote_service_calls_registry_once() -> None: + registry = RecordingQuoteFeedRegistry() + registry.register("dzengi", StubQuoteFeed()) + service = QuoteAcquisitionService(registry=registry) + + service.load_quote("dzengi", "BTC/USD_LEVERAGE") + + assert registry.get_call_count == 1 + + +def test_quote_service_passes_symbol_without_changes() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + registry.register("dzengi", feed) + service = QuoteAcquisitionService(registry=registry) + + service.load_quote("dzengi", " btc/usd_leverage ") + + assert feed.symbols == [" btc/usd_leverage "] + + +def test_quote_service_calls_feed_once() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + registry.register("dzengi", feed) + service = QuoteAcquisitionService(registry=registry) + + service.load_quote("dzengi", "BTC/USD_LEVERAGE") + + assert feed.symbols == ["BTC/USD_LEVERAGE"] + + +def test_quote_service_preserves_registry_error() -> None: + service = QuoteAcquisitionService( + registry=QuoteFeedRegistry(), + ) + + with pytest.raises(QuoteFeedRegistryError): + service.load_quote("missing", "BTC/USD_LEVERAGE") + + +def test_quote_service_does_not_call_registered_feed_when_registry_fails() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed() + registry.register("registered", feed) + service = QuoteAcquisitionService(registry=registry) + + with pytest.raises(QuoteFeedRegistryError): + service.load_quote("missing", "BTC/USD_LEVERAGE") + + assert feed.symbols == [] + + +@pytest.mark.parametrize( + "error", + [ + QuoteTransportError("Network error."), + QuoteValueError("Invalid quote."), + ], +) +def test_quote_service_preserves_feed_error( + error: Exception, +) -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed(error=error) + registry.register("dzengi", feed) + service = QuoteAcquisitionService(registry=registry) + + with pytest.raises(type(error)) as exc_info: + service.load_quote("dzengi", "BTC/USD_LEVERAGE") + + assert exc_info.value is error + assert feed.symbols == ["BTC/USD_LEVERAGE"] + + +def test_quote_service_does_not_retry_after_error() -> None: + registry = QuoteFeedRegistry() + feed = StubQuoteFeed( + error=QuoteTransportError("Network error."), + ) + registry.register("dzengi", feed) + service = QuoteAcquisitionService(registry=registry) + + with pytest.raises(QuoteTransportError): + service.load_quote("dzengi", "BTC/USD_LEVERAGE") + + assert feed.symbols == ["BTC/USD_LEVERAGE"] diff --git a/app/tests/unit/market_data/acquisition/test_symbols.py b/app/tests/unit/market_data/acquisition/test_symbols.py new file mode 100644 index 0000000..f01ab55 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/test_symbols.py @@ -0,0 +1,366 @@ +# app/tests/unit/market_data/acquisition/test_symbols.py + +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.symbols import ( + normalize_symbol, + resolve_symbol_index, + symbol_candidates, +) + + +@pytest.mark.parametrize( + ("raw_symbol", "expected"), + [ + ( + "BTC/USD", + "BTC/USD", + ), + ( + "btc/usd", + "BTC/USD", + ), + ( + " btc/usd ", + "BTC/USD", + ), + ( + "", + "", + ), + ( + " ", + "", + ), + ( + "btc / usd", + "BTC / USD", + ), + ( + "btc%2fusd", + "BTC%2FUSD", + ), + ( + "eth/usd_leverage", + "ETH/USD_LEVERAGE", + ), + ], +) +def test_normalize_symbol_preserves_existing_contract( + raw_symbol: str, + expected: str, +) -> None: + assert normalize_symbol(raw_symbol) == expected + + +def test_normalize_symbol_does_not_decode_encoded_separator() -> None: + result = normalize_symbol( + "btc%2fusd" + ) + + assert result == "BTC%2FUSD" + + +def test_normalize_symbol_does_not_remove_internal_spaces() -> None: + result = normalize_symbol( + " btc / usd " + ) + + assert result == "BTC / USD" + + +def test_normalize_symbol_does_not_add_leverage_suffix() -> None: + result = normalize_symbol( + "btc/usd" + ) + + assert result == "BTC/USD" + + +def test_normalize_symbol_preserves_existing_leverage_suffix() -> None: + result = normalize_symbol( + "btc/usd_leverage" + ) + + assert result == "BTC/USD_LEVERAGE" + + +@pytest.mark.parametrize( + "raw_symbol", + [ + "", + " ", + " ", + "\t", + "\n", + ], +) +def test_symbol_candidates_returns_empty_list_for_empty_value( + raw_symbol: str, +) -> None: + assert symbol_candidates(raw_symbol) == [] + + +def test_symbol_candidates_returns_single_normalized_candidate() -> None: + result = symbol_candidates( + " btc/usd " + ) + + assert result == [ + "BTC/USD", + ] + + +def test_symbol_candidates_adds_decoded_separator_candidate() -> None: + result = symbol_candidates( + "btc%2fusd" + ) + + assert result == [ + "BTC%2FUSD", + "BTC/USD", + ] + + +def test_symbol_candidates_adds_no_spaces_candidate() -> None: + result = symbol_candidates( + "btc / usd" + ) + + assert result == [ + "BTC / USD", + "BTC/USD", + ] + + +def test_symbol_candidates_preserves_transformation_order() -> None: + result = symbol_candidates( + " btc%2f / usd " + ) + + assert result == [ + "BTC%2F / USD", + "BTC/ / USD", + "BTC//USD", + ] + + +def test_symbol_candidates_does_not_add_duplicate_after_separator_decode() -> None: + result = symbol_candidates( + "btc/usd" + ) + + assert result == [ + "BTC/USD", + ] + + +def test_symbol_candidates_does_not_add_duplicate_after_space_removal() -> None: + result = symbol_candidates( + "btc%2fusd" + ) + + assert result == [ + "BTC%2FUSD", + "BTC/USD", + ] + + +def test_symbol_candidates_returns_new_list_for_each_call() -> None: + first = symbol_candidates( + "btc/usd" + ) + second = symbol_candidates( + "btc/usd" + ) + + assert first == second + assert first is not second + + +def test_symbol_candidates_does_not_modify_source_string() -> None: + raw_symbol = " btc%2f / usd " + + symbol_candidates(raw_symbol) + + assert raw_symbol == " btc%2f / usd " + + +def test_symbol_candidates_does_not_remove_internal_tab() -> None: + result = symbol_candidates( + "btc\t/usd" + ) + + assert result == [ + "BTC\t/USD", + ] + + +def test_symbol_candidates_does_not_remove_internal_newline() -> None: + result = symbol_candidates( + "btc\n/usd" + ) + + assert result == [ + "BTC\n/USD", + ] + + +def test_symbol_candidates_preserves_leverage_suffix() -> None: + result = symbol_candidates( + " btc / usd_leverage " + ) + + assert result == [ + "BTC / USD_LEVERAGE", + "BTC/USD_LEVERAGE", + ] + + +def test_symbol_candidates_returns_list() -> None: + result = symbol_candidates( + "btc/usd" + ) + + assert isinstance(result, list) + + +def test_resolve_symbol_index_finds_exact_match() -> None: + result = resolve_symbol_index( + "BTC/USD_LEVERAGE", + ( + "ETH/USD_LEVERAGE", + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 1 + + +def test_resolve_symbol_index_is_case_insensitive() -> None: + result = resolve_symbol_index( + "btc/usd_leverage", + ( + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 0 + + +def test_resolve_symbol_index_ignores_outer_spaces() -> None: + result = resolve_symbol_index( + " btc/usd_leverage ", + ( + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 0 + + +def test_resolve_symbol_index_supports_encoded_separator() -> None: + result = resolve_symbol_index( + "btc%2fusd_leverage", + ( + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 0 + + +def test_resolve_symbol_index_supports_internal_spaces() -> None: + result = resolve_symbol_index( + "btc / usd_leverage", + ( + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 0 + + +def test_resolve_symbol_index_returns_none_for_missing_symbol() -> None: + result = resolve_symbol_index( + "XRP/USD_LEVERAGE", + ( + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE", + ), + ) + + assert result is None + + +def test_resolve_symbol_index_returns_none_for_empty_request() -> None: + result = resolve_symbol_index( + " ", + ( + "BTC/USD_LEVERAGE", + ), + ) + + assert result is None + + +def test_resolve_symbol_index_returns_none_for_empty_available_symbols() -> None: + result = resolve_symbol_index( + "BTC/USD_LEVERAGE", + (), + ) + + assert result is None + + +def test_resolve_symbol_index_preserves_candidate_priority() -> None: + result = resolve_symbol_index( + "BTC%2FUSD_LEVERAGE", + ( + "BTC/USD_LEVERAGE", + "BTC%2FUSD_LEVERAGE", + ), + ) + + assert result == 1 + + +def test_resolve_symbol_index_preserves_available_symbol_order() -> None: + result = resolve_symbol_index( + "BTC/USD_LEVERAGE", + ( + "btc/usd_leverage", + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 0 + + +def test_resolve_symbol_index_returns_first_duplicate() -> None: + result = resolve_symbol_index( + "BTC/USD_LEVERAGE", + ( + "BTC/USD_LEVERAGE", + "BTC/USD_LEVERAGE", + ), + ) + + assert result == 0 + + +def test_resolve_symbol_index_does_not_modify_available_symbols() -> None: + available_symbols = [ + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE", + ] + original_symbols = list(available_symbols) + + resolve_symbol_index( + "BTC/USD_LEVERAGE", + available_symbols, + ) + + assert available_symbols == original_symbols \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/validation/test_quote_schema.py b/app/tests/unit/market_data/acquisition/validation/test_quote_schema.py new file mode 100644 index 0000000..6b12317 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_quote_schema.py @@ -0,0 +1,72 @@ +# app/tests/unit/market_data/acquisition/validation/test_quote_schema.py + +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.exceptions import QuoteSchemaError +from src.market_data.acquisition.validation.schema import validate_quote_schema + + +def _document() -> dict[str, object]: + return { + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + "bidPrice": "64159.45", + "askPrice": "64159.55", + "closeTime": 1783887270312, + "highPrice": "64261.45", + } + + +def test_validate_quote_schema_accepts_real_unwrapped_document() -> None: + document = _document() + + result = validate_quote_schema(document) + + assert result.is_wrapped is False + assert result.status is None + assert result.correlation_id is None + assert dict(result.payload) == document + + +def test_validate_quote_schema_accepts_wrapped_document() -> None: + payload = _document() + + result = validate_quote_schema( + { + "status": "OK", + "correlationId": "quote-1", + "payload": payload, + } + ) + + assert result.is_wrapped is True + assert result.status == "OK" + assert result.correlation_id == "quote-1" + assert dict(result.payload) == payload + + +@pytest.mark.parametrize( + "missing_key", + [ + "symbol", + "lastPrice", + "bidPrice", + "askPrice", + "closeTime", + ], +) +def test_validate_quote_schema_rejects_missing_required_field( + missing_key: str, +) -> None: + document = _document() + document.pop(missing_key) + + with pytest.raises(QuoteSchemaError, match=missing_key): + validate_quote_schema(document) + + +def test_validate_quote_schema_rejects_non_mapping_root() -> None: + with pytest.raises(QuoteSchemaError, match="JSON-объектом"): + validate_quote_schema([]) diff --git a/app/tests/unit/market_data/acquisition/validation/test_quote_values.py b/app/tests/unit/market_data/acquisition/validation/test_quote_values.py new file mode 100644 index 0000000..2de5d5c --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_quote_values.py @@ -0,0 +1,91 @@ +# app/tests/unit/market_data/acquisition/validation/test_quote_values.py + +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiTicker24hrResponse, +) +from src.market_data.acquisition.exceptions import QuoteValueError +from src.market_data.acquisition.validation.values import validate_quote_values + + +def _response( + *, + symbol: str = "BTC/USD_LEVERAGE", + last_price: str | int | float = "64159.45", + bid_price: str | int | float = "64159.45", + ask_price: str | int | float = "64159.55", + close_time: int = 1783887270312, +) -> DzengiTicker24hrResponse: + return DzengiTicker24hrResponse( + symbol=symbol, + last_price=last_price, + bid_price=bid_price, + ask_price=ask_price, + close_time=close_time, + ) + + +def test_validate_quote_values_accepts_real_response() -> None: + validate_quote_values(_response()) + + +@pytest.mark.parametrize( + ("field_name", "value"), + [ + ("last_price", "0"), + ("bid_price", "-1"), + ("ask_price", "NaN"), + ("last_price", "not-a-number"), + ], +) +def test_validate_quote_values_rejects_invalid_price( + field_name: str, + value: str, +) -> None: + values = { + "last_price": "64159.45", + "bid_price": "64159.45", + "ask_price": "64159.55", + } + values[field_name] = value + + with pytest.raises(QuoteValueError): + validate_quote_values( + _response( + last_price=values["last_price"], + bid_price=values["bid_price"], + ask_price=values["ask_price"], + ) + ) + + +def test_validate_quote_values_rejects_empty_symbol() -> None: + with pytest.raises(QuoteValueError, match="symbol"): + validate_quote_values(_response(symbol=" ")) + + +def test_validate_quote_values_rejects_non_positive_close_time() -> None: + with pytest.raises(QuoteValueError, match="closeTime"): + validate_quote_values(_response(close_time=0)) + + +def test_validate_quote_values_rejects_crossed_market() -> None: + with pytest.raises(QuoteValueError, match="bidPrice"): + validate_quote_values( + _response( + bid_price="64160.00", + ask_price="64159.55", + ) + ) + + +def test_validate_quote_values_accepts_equal_bid_and_ask() -> None: + validate_quote_values( + _response( + bid_price="64159.45", + ask_price="64159.45", + ) + ) diff --git a/app/tests/unit/market_data/acquisition/validation/test_schema.py b/app/tests/unit/market_data/acquisition/validation/test_schema.py new file mode 100644 index 0000000..7a7d01c --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_schema.py @@ -0,0 +1,268 @@ +# app/tests/unit/market_data/acquisition/validation/test_schema.py + +from __future__ import annotations + +from types import MappingProxyType + +import pytest + +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceSchemaError, +) +from src.market_data.acquisition.validation.schema import ( + validate_exchange_info_schema, +) + + +def test_validate_unwrapped_exchange_info_document() -> None: + document = { + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [ + { + "symbol": "BTC/USD_LEVERAGE", + "filters": [ + { + "filterType": "LOT_SIZE", + "minQty": "0.0001", + "maxQty": "1000", + "stepSize": "0.0001", + } + ], + "marketModes": ["REGULAR"], + "orderTypes": ["LIMIT", "MARKET", "STOP"], + } + ], + } + + validated = validate_exchange_info_schema(document) + + assert validated.is_wrapped is False + assert validated.status is None + assert validated.correlation_id is None + assert validated.payload["symbols"] == document["symbols"] + assert isinstance(validated.payload, MappingProxyType) + + +def test_validate_wrapped_exchange_info_document() -> None: + document = { + "status": "OK", + "correlationId": "2", + "payload": { + "timezone": "UTC", + "serverTime": 1628193845310, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [], + }, + } + + validated = validate_exchange_info_schema(document) + + assert validated.is_wrapped is True + assert validated.status == "OK" + assert validated.correlation_id == "2" + assert validated.payload["symbols"] == [] + + +@pytest.mark.parametrize( + "document", + [ + None, + [], + "invalid", + 123, + ], +) +def test_reject_non_object_root(document: object) -> None: + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\$ должен быть JSON-объектом", + ): + validate_exchange_info_schema(document) + + +def test_reject_non_object_wrapped_payload() -> None: + document = { + "status": "OK", + "payload": [], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\$\.payload должен быть JSON-объектом", + ): + validate_exchange_info_schema(document) + + +def test_reject_missing_symbols() -> None: + document = { + "timezone": "UTC", + "serverTime": 1783537921471, + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\$\.payload\.symbols должен быть JSON-массивом", + ): + validate_exchange_info_schema(document) + + +def test_reject_non_list_symbols() -> None: + document = { + "symbols": {}, + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\$\.payload\.symbols должен быть JSON-массивом", + ): + validate_exchange_info_schema(document) + + +def test_reject_non_object_symbol_item() -> None: + document = { + "symbols": [ + "BTC/USD", + ], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\$\.payload\.symbols\[0\] должен быть JSON-объектом", + ): + validate_exchange_info_schema(document) + + +def test_reject_non_list_filters() -> None: + document = { + "symbols": [ + { + "symbol": "BTC/USD", + "filters": {}, + } + ], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\.filters должен быть JSON-массивом", + ): + validate_exchange_info_schema(document) + + +def test_reject_non_object_filter_item() -> None: + document = { + "symbols": [ + { + "symbol": "BTC/USD", + "filters": [ + "LOT_SIZE", + ], + } + ], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=r"\.filters\[0\] должен быть JSON-объектом", + ): + validate_exchange_info_schema(document) + + +@pytest.mark.parametrize( + ("key", "invalid_value"), + [ + ("marketModes", {}), + ("orderTypes", "MARKET"), + ], +) +def test_reject_non_list_string_collections( + key: str, + invalid_value: object, +) -> None: + document = { + "symbols": [ + { + "symbol": "BTC/USD", + key: invalid_value, + } + ], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=rf"\.{key} должен быть JSON-массивом", + ): + validate_exchange_info_schema(document) + + +@pytest.mark.parametrize( + "key", + [ + "marketModes", + "orderTypes", + ], +) +def test_reject_non_string_collection_item(key: str) -> None: + document = { + "symbols": [ + { + "symbol": "BTC/USD", + key: [ + "REGULAR", + 123, + ], + } + ], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=rf"\.{key}\[1\] должен быть строкой", + ): + validate_exchange_info_schema(document) + + +@pytest.mark.parametrize( + "key", + [ + "rateLimits", + "exchangeFilters", + ], +) +def test_reject_non_list_payload_collections(key: str) -> None: + document = { + "symbols": [], + key: {}, + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=rf"\.{key} должен быть JSON-массивом", + ): + validate_exchange_info_schema(document) + + +@pytest.mark.parametrize( + "key", + [ + "rateLimits", + "exchangeFilters", + ], +) +def test_reject_non_object_payload_collection_item(key: str) -> None: + document = { + "symbols": [], + key: [ + "invalid", + ], + } + + with pytest.raises( + InstrumentReferenceSchemaError, + match=rf"\.{key}\[0\] должен быть JSON-объектом", + ): + validate_exchange_info_schema(document) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/validation/test_values.py b/app/tests/unit/market_data/acquisition/validation/test_values.py new file mode 100644 index 0000000..035172e --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_values.py @@ -0,0 +1,481 @@ +# app/tests/unit/market_data/acquisition/validation/test_values.py + +from __future__ import annotations + +from dataclasses import replace + +import pytest + +from src.market_data.acquisition.adapters.dzengi.models import ( + DzengiExchangeInfoPayload, + DzengiExchangeInfoResponse, + DzengiExchangeInfoSymbol, + DzengiLotSizeFilter, + DzengiMinNotionalFilter, + DzengiRateLimit, + DzengiUnknownFilter, +) +from src.market_data.acquisition.exceptions import ( + InstrumentReferenceValueError, +) +from src.market_data.acquisition.validation.values import ( + validate_exchange_info_values, +) + + +def _valid_symbol() -> DzengiExchangeInfoSymbol: + return DzengiExchangeInfoSymbol( + symbol="ETH/EUR_LEVERAGE", + name="ETH/EUR", + status="TRADING", + asset_type="CRYPTOCURRENCY", + base_asset="ETH", + base_asset_precision=3, + quote_asset="EUR", + quote_asset_id="EUR_LEVERAGE", + quote_precision=3, + order_types=("LIMIT", "MARKET", "STOP"), + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", + ), + DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="2", + ), + ), + market_modes=("REGULAR",), + market_type="LEVERAGE", + country="", + sector="", + industry="", + trading_hours="UTC; Mon - 21:00, 21:05 -", + tick_size=0.01, + tick_value=18.3415, + trading_fee=0.06, + exchange_fee=None, + long_rate=-0.01, + short_rate=0.01, + swap_charge_interval=480, + min_sl_gap=0, + max_sl_gap=50.0, + min_tp_gap=0, + max_tp_gap=50.0, + ) + + +def _valid_response( + *, + symbol: DzengiExchangeInfoSymbol | None = None, + rate_limits: tuple[DzengiRateLimit, ...] = (), + exchange_filters: tuple[DzengiUnknownFilter, ...] = (), +) -> DzengiExchangeInfoResponse: + return DzengiExchangeInfoResponse( + status="OK", + correlation_id="2", + payload=DzengiExchangeInfoPayload( + timezone="UTC", + server_time=1783537921471, + rate_limits=rate_limits, + exchange_filters=exchange_filters, + symbols=(symbol or _valid_symbol(),), + ), + ) + + +def test_validate_complete_exchange_info_values() -> None: + response = _valid_response( + rate_limits=( + DzengiRateLimit( + interval="MINUTE", + interval_num=1, + limit=1200, + rate_limit_type="REQUEST_WEIGHT", + ), + ), + ) + + assert validate_exchange_info_values(response) is None + + +@pytest.mark.parametrize( + "field", + [ + "symbol", + "name", + "status", + "base_asset", + "quote_asset", + "market_type", + ], +) +def test_reject_empty_required_symbol_string(field: str) -> None: + symbol = replace( + _valid_symbol(), + **{field: " "}, + ) + + with pytest.raises( + InstrumentReferenceValueError, + match="не должен быть пустым", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_reject_empty_order_type() -> None: + symbol = replace( + _valid_symbol(), + order_types=("LIMIT", " "), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"orderTypes\[1\] не должен быть пустым", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_reject_empty_market_mode() -> None: + symbol = replace( + _valid_symbol(), + market_modes=("REGULAR", ""), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"marketModes\[1\] не должен быть пустым", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +@pytest.mark.parametrize( + "field", + [ + "base_asset_precision", + "quote_precision", + "swap_charge_interval", + ], +) +def test_reject_negative_non_negative_integer_field(field: str) -> None: + symbol = replace( + _valid_symbol(), + **{field: -1}, + ) + + with pytest.raises( + InstrumentReferenceValueError, + match="должно быть больше или равно нулю", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +@pytest.mark.parametrize( + "tick_size", + [ + 0, + -0.01, + ], +) +def test_reject_non_positive_tick_size(tick_size: float) -> None: + symbol = replace( + _valid_symbol(), + tick_size=tick_size, + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"tickSize должно быть больше нуля", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +@pytest.mark.parametrize( + "tick_size", + [ + float("nan"), + float("inf"), + float("-inf"), + ], +) +def test_reject_non_finite_tick_size(tick_size: float) -> None: + symbol = replace( + _valid_symbol(), + tick_size=tick_size, + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"tickSize должно быть конечным числом", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_reject_non_numeric_lot_size_value() -> None: + symbol = replace( + _valid_symbol(), + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="not-a-number", + max_qty="1000", + step_size="0.001", + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"minQty должно быть корректным числом", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("min_qty", "0"), + ("max_qty", 0), + ("step_size", -1), + ], +) +def test_reject_non_positive_lot_size_values( + field: str, + value: str | int, +) -> None: + lot_size = DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", + ) + + lot_size = replace( + lot_size, + **{field: value}, + ) + + symbol = replace( + _valid_symbol(), + filters=(lot_size,), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match="должно быть больше нуля", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_reject_min_qty_greater_than_max_qty() -> None: + symbol = replace( + _valid_symbol(), + filters=( + DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="10", + max_qty="1", + step_size="0.1", + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match="minQty не должно превышать", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_reject_negative_min_notional() -> None: + symbol = replace( + _valid_symbol(), + filters=( + DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="-1", + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"minNotional должно быть больше или равно нулю", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_accept_zero_min_notional() -> None: + symbol = replace( + _valid_symbol(), + filters=( + DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="0", + ), + ), + ) + + assert ( + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + is None + ) + + +def test_accept_negative_long_and_short_rates() -> None: + symbol = replace( + _valid_symbol(), + long_rate=-0.15, + short_rate=-0.25, + ) + + assert ( + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + is None + ) + + +def test_accept_zero_optional_numeric_values() -> None: + symbol = replace( + _valid_symbol(), + tick_value=0, + trading_fee=0, + exchange_fee=0, + min_sl_gap=0, + max_sl_gap=0, + min_tp_gap=0, + max_tp_gap=0, + ) + + assert ( + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + is None + ) + + +@pytest.mark.parametrize( + ("interval_num", "limit"), + [ + (0, 1200), + (1, 0), + (-1, 1200), + (1, -100), + ], +) +def test_reject_invalid_rate_limit_values( + interval_num: int, + limit: int, +) -> None: + response = _valid_response( + rate_limits=( + DzengiRateLimit( + interval="MINUTE", + interval_num=interval_num, + limit=limit, + rate_limit_type="REQUEST_WEIGHT", + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match="должно быть больше нуля", + ): + validate_exchange_info_values(response) + + +def test_reject_empty_rate_limit_string() -> None: + response = _valid_response( + rate_limits=( + DzengiRateLimit( + interval=" ", + interval_num=1, + limit=1200, + rate_limit_type="REQUEST_WEIGHT", + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"interval не должен быть пустым", + ): + validate_exchange_info_values(response) + + +def test_reject_empty_unknown_instrument_filter_type() -> None: + symbol = replace( + _valid_symbol(), + filters=( + DzengiUnknownFilter( + filter_type=" ", + fields=(("enabled", True),), + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"filterType не должен быть пустым", + ): + validate_exchange_info_values( + _valid_response(symbol=symbol) + ) + + +def test_accept_empty_global_exchange_filter_type() -> None: + response = _valid_response( + exchange_filters=( + DzengiUnknownFilter( + filter_type="", + fields=(("enabled", True),), + ), + ), + ) + + assert validate_exchange_info_values(response) is None + + +def test_reject_whitespace_global_exchange_filter_type() -> None: + response = _valid_response( + exchange_filters=( + DzengiUnknownFilter( + filter_type=" ", + fields=(("enabled", True),), + ), + ), + ) + + with pytest.raises( + InstrumentReferenceValueError, + match=r"filterType не должен состоять только из пробелов", + ): + validate_exchange_info_values(response) \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py b/app/tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py new file mode 100644 index 0000000..fb301a3 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.exceptions import QuoteSchemaError +from src.market_data.acquisition.validation.schema import ( + validate_dzengi_websocket_quote_schema, +) + + +def test_accepts_direct_unwrapped_message() -> None: + result = validate_dzengi_websocket_quote_schema( + {"symbol": "BTC/USD", "bid": "10", "ask": "11"} + ) + assert result.payload["bid"] == "10" + + +def test_accepts_double_payload_wrapper_and_root_symbol() -> None: + result = validate_dzengi_websocket_quote_schema( + { + "symbol": "BTC/USD", + "Payload": {"payload": {"bids": [["10", "1"]], "asks": [["11", "1"]]}}, + } + ) + assert result.root_symbol == "BTC/USD" + + +def test_accepts_ofr_alias() -> None: + validate_dzengi_websocket_quote_schema( + {"symbolName": "BTC/USD", "bid": "10", "ofr": "11"} + ) + + +@pytest.mark.parametrize( + "document", + [ + [], + {"bid": "10", "ask": "11"}, + {"symbol": "BTC/USD", "bid": "10"}, + {"symbol": "BTC/USD", "bids": [], "asks": [["11"]]}, + {"symbol": "BTC/USD", "bids": [["10"]], "asks": []}, + ], +) +def test_rejects_invalid_structure(document: object) -> None: + with pytest.raises(QuoteSchemaError): + validate_dzengi_websocket_quote_schema(document) diff --git a/app/tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py b/app/tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py new file mode 100644 index 0000000..68c0d37 --- /dev/null +++ b/app/tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import pytest + +from src.market_data.acquisition.adapters.dzengi.models import DzengiWebSocketQuoteResponse +from src.market_data.acquisition.exceptions import QuoteValueError +from src.market_data.acquisition.validation.values import ( + validate_dzengi_websocket_quote_values, +) + + +def _response(**overrides: object) -> DzengiWebSocketQuoteResponse: + values = { + "symbol": "BTC/USD", + "bid_price": "10", + "ask_price": "11", + "timestamp": 1000, + } + values.update(overrides) + return DzengiWebSocketQuoteResponse(**values) # type: ignore[arg-type] + + +def test_accepts_valid_values_and_missing_timestamp() -> None: + validate_dzengi_websocket_quote_values(_response()) + validate_dzengi_websocket_quote_values(_response(timestamp=None)) + + +@pytest.mark.parametrize( + "overrides", + [ + {"symbol": " "}, + {"bid_price": "0"}, + {"ask_price": "-1"}, + {"bid_price": "NaN"}, + {"bid_price": "12", "ask_price": "11"}, + {"timestamp": 0}, + ], +) +def test_rejects_invalid_values(overrides: dict[str, object]) -> None: + with pytest.raises(QuoteValueError): + validate_dzengi_websocket_quote_values(_response(**overrides)) diff --git a/app/tests/unit/storage/test_instrument_store.py b/app/tests/unit/storage/test_instrument_store.py new file mode 100644 index 0000000..2854704 --- /dev/null +++ b/app/tests/unit/storage/test_instrument_store.py @@ -0,0 +1,463 @@ +# app/tests/unit/storage/test_instrument_store.py + +from __future__ import annotations + +from decimal import Decimal +from typing import cast + +import pytest + +from src.market_data.acquisition.models.instrument import Instrument +from src.storage.exceptions import ( + InstrumentStoreError, + StorageError, +) +from src.storage.instrument_store import ( + InMemoryInstrumentStore, + InstrumentStoreProtocol, +) + + +def _instrument( + *, + symbol: str = "BTC/USD_LEVERAGE", + name: str = "BTC/USD", + base_asset: str = "BTC", +) -> Instrument: + return Instrument( + symbol=symbol, + name=name, + status="TRADING", + base_asset=base_asset, + quote_asset="USD", + asset_type="CRYPTOCURRENCY", + market_type="LEVERAGE", + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET", "STOP"), + base_asset_precision=4, + quote_asset_precision=4, + tick_size=Decimal("0.05"), + tick_value=Decimal("3878.86"), + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=Decimal("1000"), + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +def test_in_memory_store_matches_protocol() -> None: + store = InMemoryInstrumentStore() + + assert isinstance( + store, + InstrumentStoreProtocol, + ) + + +def test_get_returns_none_for_unknown_source() -> None: + store = InMemoryInstrumentStore() + + assert store.get("dzengi") is None + + +def test_set_and_get_preserve_same_tuple_object() -> None: + store = InMemoryInstrumentStore() + instruments = ( + _instrument(), + ) + + store.set( + "dzengi", + instruments, + ) + + result = store.get("dzengi") + + assert result is instruments + + +def test_empty_tuple_is_distinct_from_cache_miss() -> None: + store = InMemoryInstrumentStore() + + assert store.get("dzengi") is None + + store.set( + "dzengi", + (), + ) + + result = store.get("dzengi") + + assert result == () + assert result is not None + + +def test_different_sources_are_isolated() -> None: + store = InMemoryInstrumentStore() + + dzengi_instruments = ( + _instrument( + symbol="BTC/USD_LEVERAGE", + ), + ) + secondary_instruments = ( + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + ) + + store.set( + "dzengi", + dzengi_instruments, + ) + store.set( + "secondary", + secondary_instruments, + ) + + assert store.get("dzengi") is dzengi_instruments + assert store.get("secondary") is secondary_instruments + + +def test_clear_removes_only_requested_source() -> None: + store = InMemoryInstrumentStore() + + dzengi_instruments = ( + _instrument( + symbol="BTC/USD_LEVERAGE", + ), + ) + secondary_instruments = ( + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + ) + + store.set( + "dzengi", + dzengi_instruments, + ) + store.set( + "secondary", + secondary_instruments, + ) + + store.clear("dzengi") + + assert store.get("dzengi") is None + assert store.get("secondary") is secondary_instruments + + +def test_clear_without_source_removes_all_sources() -> None: + store = InMemoryInstrumentStore() + + store.set( + "dzengi", + ( + _instrument(), + ), + ) + store.set( + "secondary", + ( + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + ), + ) + + store.clear() + + assert store.get("dzengi") is None + assert store.get("secondary") is None + + +def test_set_replaces_existing_value() -> None: + store = InMemoryInstrumentStore() + + first = ( + _instrument( + symbol="BTC/USD_LEVERAGE", + ), + ) + second = ( + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + ) + + store.set( + "dzengi", + first, + ) + store.set( + "dzengi", + second, + ) + + assert store.get("dzengi") is second + + +def test_source_name_outer_spaces_are_normalized() -> None: + store = InMemoryInstrumentStore() + instruments = ( + _instrument(), + ) + + store.set( + " dzengi ", + instruments, + ) + + assert store.get("dzengi") is instruments + assert store.get(" dzengi ") is instruments + + +def test_source_name_case_is_preserved() -> None: + store = InMemoryInstrumentStore() + + lowercase = ( + _instrument( + symbol="BTC/USD_LEVERAGE", + ), + ) + uppercase = ( + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + ) + + store.set( + "dzengi", + lowercase, + ) + store.set( + "DZENGI", + uppercase, + ) + + assert store.get("dzengi") is lowercase + assert store.get("DZENGI") is uppercase + + +@pytest.mark.parametrize( + "source_name", + [ + "", + " ", + " ", + "\t", + "\n", + ], +) +def test_get_rejects_empty_source_name( + source_name: str, +) -> None: + store = InMemoryInstrumentStore() + + with pytest.raises( + InstrumentStoreError, + match=r"Имя источника Instrument Store не должно быть пустым", + ): + store.get(source_name) + + +@pytest.mark.parametrize( + "source_name", + [ + "", + " ", + " ", + "\t", + "\n", + ], +) +def test_set_rejects_empty_source_name( + source_name: str, +) -> None: + store = InMemoryInstrumentStore() + + with pytest.raises( + InstrumentStoreError, + match=r"Имя источника Instrument Store не должно быть пустым", + ): + store.set( + source_name, + (), + ) + + +@pytest.mark.parametrize( + "source_name", + [ + "", + " ", + " ", + "\t", + "\n", + ], +) +def test_targeted_clear_rejects_empty_source_name( + source_name: str, +) -> None: + store = InMemoryInstrumentStore() + + with pytest.raises( + InstrumentStoreError, + match=r"Имя источника Instrument Store не должно быть пустым", + ): + store.clear(source_name) + + +def test_set_rejects_list_instead_of_tuple() -> None: + store = InMemoryInstrumentStore() + + invalid_instruments = cast( + tuple[Instrument, ...], + [ + _instrument(), + ], + ) + + with pytest.raises( + InstrumentStoreError, + match=r"должен быть передан как tuple", + ): + store.set( + "dzengi", + invalid_instruments, + ) + + +def test_set_rejects_non_instrument_item() -> None: + store = InMemoryInstrumentStore() + + invalid_instruments = cast( + tuple[Instrument, ...], + ( + "BTC/USD_LEVERAGE", + ), + ) + + with pytest.raises( + InstrumentStoreError, + match=r"не являющийся Instrument", + ): + store.set( + "dzengi", + invalid_instruments, + ) + + +def test_store_preserves_instrument_order() -> None: + store = InMemoryInstrumentStore() + + instruments = ( + _instrument( + symbol="BTC/USD_LEVERAGE", + ), + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + _instrument( + symbol="XRP/USD_LEVERAGE", + name="XRP/USD", + base_asset="XRP", + ), + ) + + store.set( + "dzengi", + instruments, + ) + + result = store.get("dzengi") + + assert result is not None + assert [ + instrument.symbol + for instrument in result + ] == [ + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE", + "XRP/USD_LEVERAGE", + ] + + +def test_store_does_not_modify_input_tuple() -> None: + store = InMemoryInstrumentStore() + + instruments = ( + _instrument( + symbol="BTC/USD_LEVERAGE", + ), + _instrument( + symbol="ETH/USD_LEVERAGE", + name="ETH/USD", + base_asset="ETH", + ), + ) + + original = instruments + + store.set( + "dzengi", + instruments, + ) + + assert instruments is original + assert store.get("dzengi") is original + + +def test_store_instances_are_isolated() -> None: + first_store = InMemoryInstrumentStore() + second_store = InMemoryInstrumentStore() + + instruments = ( + _instrument(), + ) + + first_store.set( + "dzengi", + instruments, + ) + + assert first_store.get("dzengi") is instruments + assert second_store.get("dzengi") is None + + +def test_clear_unknown_source_is_idempotent() -> None: + store = InMemoryInstrumentStore() + + store.clear("dzengi") + store.clear("dzengi") + + assert store.get("dzengi") is None + + +def test_instrument_store_error_inherits_storage_error() -> None: + error = InstrumentStoreError( + "Storage failure." + ) + + assert isinstance( + error, + StorageError, + ) + assert str(error) == "Storage failure." \ No newline at end of file diff --git a/app/tests/unit/storage/test_quote_store.py b/app/tests/unit/storage/test_quote_store.py new file mode 100644 index 0000000..7e41f68 --- /dev/null +++ b/app/tests/unit/storage/test_quote_store.py @@ -0,0 +1,448 @@ +# app/tests/unit/storage/test_quote_store.py + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal +from typing import cast + +import pytest + +from src.market_data.acquisition.models.quote import Quote +from src.storage.exceptions import QuoteStoreError, StorageError +from src.storage.quote_store import ( + InMemoryQuoteStore, + QuoteStoreProtocol, +) + + +def _quote( + *, + symbol: str = "BTC/USD_LEVERAGE", + last_price: Decimal = Decimal("64159.45"), + source: str = "dzengi", +) -> Quote: + return Quote( + symbol=symbol, + last_price=last_price, + bid_price=last_price - Decimal("0.05"), + ask_price=last_price + Decimal("0.05"), + exchange_timestamp=datetime( + 2026, + 7, + 12, + 18, + 0, + tzinfo=timezone.utc, + ), + received_at=datetime( + 2026, + 7, + 12, + 18, + 0, + 1, + tzinfo=timezone.utc, + ), + source=source, + ) + + +def test_in_memory_quote_store_matches_protocol() -> None: + assert isinstance( + InMemoryQuoteStore(), + QuoteStoreProtocol, + ) + + +def test_get_returns_none_for_unknown_key() -> None: + store = InMemoryQuoteStore() + + assert store.get("dzengi", "BTC/USD_LEVERAGE") is None + + +def test_set_and_get_preserve_quote_identity() -> None: + store = InMemoryQuoteStore() + quote = _quote() + + store.set("dzengi", quote) + + assert store.get("dzengi", quote.symbol) is quote + + +def test_set_replaces_existing_quote() -> None: + store = InMemoryQuoteStore() + first = _quote(last_price=Decimal("100")) + second = _quote(last_price=Decimal("101")) + + store.set("dzengi", first) + store.set("dzengi", second) + + assert store.get("dzengi", second.symbol) is second + + +def test_different_sources_are_isolated() -> None: + store = InMemoryQuoteStore() + first = _quote(source="dzengi") + second = _quote(source="secondary") + + store.set("dzengi", first) + store.set("secondary", second) + + assert store.get("dzengi", first.symbol) is first + assert store.get("secondary", second.symbol) is second + + +def test_source_name_alias_does_not_have_to_match_quote_source() -> None: + store = InMemoryQuoteStore() + quote = _quote(source="dzengi") + + store.set("dzengi-primary", quote) + + assert store.get("dzengi-primary", quote.symbol) is quote + + +def test_source_name_outer_spaces_are_normalized() -> None: + store = InMemoryQuoteStore() + quote = _quote() + + store.set(" dzengi ", quote) + + assert store.get("dzengi", quote.symbol) is quote + assert store.get(" dzengi ", quote.symbol) is quote + + +def test_source_name_case_is_preserved() -> None: + store = InMemoryQuoteStore() + lowercase = _quote(last_price=Decimal("100")) + uppercase = _quote(last_price=Decimal("200")) + + store.set("dzengi", lowercase) + store.set("DZENGI", uppercase) + + assert store.get("dzengi", lowercase.symbol) is lowercase + assert store.get("DZENGI", uppercase.symbol) is uppercase + + +def test_runtime_keys_are_isolated() -> None: + store = InMemoryQuoteStore() + auto_quote = _quote(last_price=Decimal("100")) + debug_quote = _quote(last_price=Decimal("200")) + + store.set("dzengi", auto_quote, runtime_key="auto") + store.set("dzengi", debug_quote, runtime_key="debug_auto") + + assert store.get( + "dzengi", + auto_quote.symbol, + runtime_key="auto", + ) is auto_quote + assert store.get( + "dzengi", + debug_quote.symbol, + runtime_key="debug_auto", + ) is debug_quote + + +def test_runtime_key_is_trimmed_and_lowercased() -> None: + store = InMemoryQuoteStore() + quote = _quote() + + store.set("dzengi", quote, runtime_key=" AUTO ") + + assert store.get( + "dzengi", + quote.symbol, + runtime_key="auto", + ) is quote + assert store.get( + "dzengi", + quote.symbol, + runtime_key=" AUTO ", + ) is quote + + +def test_symbols_are_isolated() -> None: + store = InMemoryQuoteStore() + btc = _quote(symbol="BTC/USD_LEVERAGE") + eth = _quote( + symbol="ETH/USD_LEVERAGE", + last_price=Decimal("3500"), + ) + + store.set("dzengi", btc) + store.set("dzengi", eth) + + assert store.get("dzengi", btc.symbol) is btc + assert store.get("dzengi", eth.symbol) is eth + + +def test_symbol_key_is_trimmed_and_uppercased() -> None: + store = InMemoryQuoteStore() + quote = _quote(symbol=" btc/usd_leverage ") + + store.set("dzengi", quote) + + assert store.get("dzengi", "BTC/USD_LEVERAGE") is quote + assert store.get("dzengi", " btc/usd_leverage ") is quote + + +@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"]) +def test_get_rejects_empty_source_name(source_name: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises( + QuoteStoreError, + match=r"Имя источника Quote Store не должно быть пустым", + ): + store.get(source_name, "BTC/USD_LEVERAGE") + + +@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"]) +def test_set_rejects_empty_source_name(source_name: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises(QuoteStoreError): + store.set(source_name, _quote()) + + +@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"]) +def test_clear_rejects_empty_source_name(source_name: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises(QuoteStoreError): + store.clear(source_name=source_name) + + +@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"]) +def test_get_rejects_empty_runtime_key(runtime_key: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises( + QuoteStoreError, + match=r"Runtime key Quote Store не должен быть пустым", + ): + store.get( + "dzengi", + "BTC/USD_LEVERAGE", + runtime_key=runtime_key, + ) + + +@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"]) +def test_set_rejects_empty_runtime_key(runtime_key: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises(QuoteStoreError): + store.set( + "dzengi", + _quote(), + runtime_key=runtime_key, + ) + + +@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"]) +def test_clear_rejects_empty_runtime_key(runtime_key: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises(QuoteStoreError): + store.clear(runtime_key=runtime_key) + + +@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"]) +def test_get_rejects_empty_symbol(symbol: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises( + QuoteStoreError, + match=r"Символ Quote Store не должен быть пустым", + ): + store.get("dzengi", symbol) + + +@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"]) +def test_set_rejects_quote_with_empty_symbol(symbol: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises(QuoteStoreError): + store.set("dzengi", _quote(symbol=symbol)) + + +@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"]) +def test_clear_rejects_empty_symbol(symbol: str) -> None: + store = InMemoryQuoteStore() + + with pytest.raises(QuoteStoreError): + store.clear(symbol=symbol) + + +def test_set_rejects_non_quote_object() -> None: + store = InMemoryQuoteStore() + + with pytest.raises( + QuoteStoreError, + match=r"принимает только объект Quote", + ): + store.set( + "dzengi", + cast(Quote, object()), + ) + + +def test_clear_without_filters_removes_all_quotes() -> None: + store = InMemoryQuoteStore() + store.set("dzengi", _quote(symbol="BTC/USD_LEVERAGE")) + store.set("secondary", _quote(symbol="ETH/USD_LEVERAGE")) + + store.clear() + + assert store.get("dzengi", "BTC/USD_LEVERAGE") is None + assert store.get("secondary", "ETH/USD_LEVERAGE") is None + + +def test_clear_by_source_removes_all_source_quotes() -> None: + store = InMemoryQuoteStore() + dzengi_btc = _quote(symbol="BTC/USD_LEVERAGE") + dzengi_eth = _quote(symbol="ETH/USD_LEVERAGE") + secondary_btc = _quote(symbol="BTC/USD_LEVERAGE") + + store.set("dzengi", dzengi_btc, runtime_key="auto") + store.set("dzengi", dzengi_eth, runtime_key="debug_auto") + store.set("secondary", secondary_btc, runtime_key="auto") + + store.clear(source_name="dzengi") + + assert store.get("dzengi", dzengi_btc.symbol, runtime_key="auto") is None + assert store.get("dzengi", dzengi_eth.symbol, runtime_key="debug_auto") is None + assert store.get("secondary", secondary_btc.symbol, runtime_key="auto") is secondary_btc + + +def test_clear_by_runtime_removes_all_runtime_quotes() -> None: + store = InMemoryQuoteStore() + auto_btc = _quote(symbol="BTC/USD_LEVERAGE") + auto_eth = _quote(symbol="ETH/USD_LEVERAGE") + debug_btc = _quote(symbol="BTC/USD_LEVERAGE") + + store.set("dzengi", auto_btc, runtime_key="auto") + store.set("secondary", auto_eth, runtime_key="auto") + store.set("dzengi", debug_btc, runtime_key="debug_auto") + + store.clear(runtime_key="AUTO") + + assert store.get("dzengi", auto_btc.symbol, runtime_key="auto") is None + assert store.get("secondary", auto_eth.symbol, runtime_key="auto") is None + assert store.get("dzengi", debug_btc.symbol, runtime_key="debug_auto") is debug_btc + + +def test_clear_by_symbol_removes_symbol_from_all_namespaces() -> None: + store = InMemoryQuoteStore() + dzengi_btc = _quote(symbol="BTC/USD_LEVERAGE") + secondary_btc = _quote(symbol="BTC/USD_LEVERAGE") + dzengi_eth = _quote(symbol="ETH/USD_LEVERAGE") + + store.set("dzengi", dzengi_btc, runtime_key="auto") + store.set("secondary", secondary_btc, runtime_key="debug_auto") + store.set("dzengi", dzengi_eth, runtime_key="auto") + + store.clear(symbol=" btc/usd_leverage ") + + assert store.get("dzengi", dzengi_btc.symbol, runtime_key="auto") is None + assert store.get("secondary", secondary_btc.symbol, runtime_key="debug_auto") is None + assert store.get("dzengi", dzengi_eth.symbol, runtime_key="auto") is dzengi_eth + + +def test_clear_exact_record_removes_only_that_record() -> None: + store = InMemoryQuoteStore() + auto_quote = _quote() + debug_quote = _quote(last_price=Decimal("65000")) + + store.set("dzengi", auto_quote, runtime_key="auto") + store.set("dzengi", debug_quote, runtime_key="debug_auto") + + store.clear( + source_name="dzengi", + symbol="BTC/USD_LEVERAGE", + runtime_key="auto", + ) + + assert store.get("dzengi", auto_quote.symbol, runtime_key="auto") is None + assert store.get("dzengi", debug_quote.symbol, runtime_key="debug_auto") is debug_quote + + +def test_clear_combined_source_and_runtime_filters() -> None: + store = InMemoryQuoteStore() + dzengi_auto_btc = _quote(symbol="BTC/USD_LEVERAGE") + dzengi_auto_eth = _quote(symbol="ETH/USD_LEVERAGE") + dzengi_debug_btc = _quote(symbol="BTC/USD_LEVERAGE") + secondary_auto_btc = _quote(symbol="BTC/USD_LEVERAGE") + + store.set("dzengi", dzengi_auto_btc, runtime_key="auto") + store.set("dzengi", dzengi_auto_eth, runtime_key="auto") + store.set("dzengi", dzengi_debug_btc, runtime_key="debug_auto") + store.set("secondary", secondary_auto_btc, runtime_key="auto") + + store.clear(source_name="dzengi", runtime_key="auto") + + assert store.get("dzengi", dzengi_auto_btc.symbol, runtime_key="auto") is None + assert store.get("dzengi", dzengi_auto_eth.symbol, runtime_key="auto") is None + assert store.get("dzengi", dzengi_debug_btc.symbol, runtime_key="debug_auto") is dzengi_debug_btc + assert store.get("secondary", secondary_auto_btc.symbol, runtime_key="auto") is secondary_auto_btc + + +def test_clear_unknown_record_is_idempotent() -> None: + store = InMemoryQuoteStore() + quote = _quote() + store.set("dzengi", quote) + + store.clear( + source_name="unknown", + symbol="ETH/USD_LEVERAGE", + runtime_key="missing", + ) + + assert store.get("dzengi", quote.symbol) is quote + + +def test_store_instances_are_independent() -> None: + first = InMemoryQuoteStore() + second = InMemoryQuoteStore() + quote = _quote() + + first.set("dzengi", quote) + + assert first.get("dzengi", quote.symbol) is quote + assert second.get("dzengi", quote.symbol) is None + + +def test_store_preserves_decimal_values() -> None: + store = InMemoryQuoteStore() + quote = _quote( + last_price=Decimal("64159.45000001"), + ) + + store.set("dzengi", quote) + result = store.get("dzengi", quote.symbol) + + assert result is not None + assert result is quote + assert isinstance(result.last_price, Decimal) + assert result.last_price == Decimal("64159.45000001") + + +def test_store_preserves_datetime_values() -> None: + store = InMemoryQuoteStore() + quote = _quote() + + store.set("dzengi", quote) + result = store.get("dzengi", quote.symbol) + + assert result is not None + assert result is quote + assert result.exchange_timestamp is quote.exchange_timestamp + assert result.received_at is quote.received_at + + +def test_quote_store_error_inherits_storage_error() -> None: + error = QuoteStoreError("test") + + assert isinstance(error, StorageError) diff --git a/app/tests/unit/telegram/__init__.py b/app/tests/unit/telegram/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/unit/telegram/ui/__init__.py b/app/tests/unit/telegram/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/unit/telegram/ui/test_currency_ui.py b/app/tests/unit/telegram/ui/test_currency_ui.py new file mode 100644 index 0000000..11a7ad1 --- /dev/null +++ b/app/tests/unit/telegram/ui/test_currency_ui.py @@ -0,0 +1,607 @@ +# app/tests/unit/telegram/ui/test_currency_ui.py + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal + +from decimal import Decimal +from types import SimpleNamespace +from typing import cast + +import pytest + +from src.integrations.exchange.exceptions import ExchangeError +from src.integrations.exchange.models import ( + BalanceSummary, +) +from src.integrations.exchange.service import ExchangeService +from src.market_data.acquisition.models.instrument import Instrument +from src.market_data.acquisition.models.quote import Quote +from src.telegram.ui.currency_ui import ( + _resolve_asset_quote_instrument, + estimate_balance_usd, + get_asset_usd_rate, +) + + +def _instrument( + *, + symbol: str, + base_asset: str, + quote_asset: str = "USD", + status: str = "TRADING", + market_type: str = "SPOT", +) -> Instrument: + return Instrument( + symbol=symbol, + name=symbol, + status=status, + base_asset=base_asset, + quote_asset=quote_asset, + asset_type="CRYPTOCURRENCY", + market_type=market_type, + market_modes=("REGULAR",), + order_types=("LIMIT", "MARKET"), + base_asset_precision=8, + quote_asset_precision=8, + tick_size=Decimal("0.01"), + tick_value=None, + step_size=Decimal("0.0001"), + min_qty=Decimal("0.0001"), + max_qty=None, + min_notional=Decimal("1"), + country=None, + sector=None, + industry=None, + trading_hours=None, + ) + + +def _service() -> ExchangeService: + return object.__new__( + ExchangeService + ) + + +def test_resolver_uses_get_instruments( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instruments = ( + _instrument( + symbol="BTC/USD", + base_asset="BTC", + ), + ) + call_count = 0 + + def get_instruments() -> tuple[Instrument, ...]: + nonlocal call_count + + call_count += 1 + return instruments + + monkeypatch.setattr( + service, + "get_instruments", + get_instruments, + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is instruments[0] + assert call_count == 1 + + +def test_resolver_uses_canonical_instrument_reference( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + instrument = _instrument( + symbol="BTC/USD", + base_asset="BTC", + ) + + calls = 0 + + def get_instruments() -> tuple[Instrument, ...]: + nonlocal calls + + calls += 1 + return ( + instrument, + ) + + monkeypatch.setattr( + service, + "get_instruments", + get_instruments, + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is instrument + assert calls == 1 + + +def test_resolver_prefers_usd_over_usdt( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + usd = _instrument( + symbol="BTC/USD", + base_asset="BTC", + quote_asset="USD", + market_type="LEVERAGE", + ) + usdt = _instrument( + symbol="BTC/USDT", + base_asset="BTC", + quote_asset="USDT", + market_type="SPOT", + ) + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + usdt, + usd, + ), + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is usd + + +def test_resolver_prefers_trading_status( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + active = _instrument( + symbol="BTC/USD_ACTIVE", + base_asset="BTC", + status="TRADING", + market_type="LEVERAGE", + ) + inactive = _instrument( + symbol="BTC/USD_INACTIVE", + base_asset="BTC", + status="UNKNOWN", + market_type="SPOT", + ) + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + inactive, + active, + ), + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is active + + +def test_resolver_prefers_spot_when_other_priority_is_equal( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + spot = _instrument( + symbol="BTC/USD_SPOT", + base_asset="BTC", + market_type="SPOT", + ) + leverage = _instrument( + symbol="BTC/USD_LEVERAGE", + base_asset="BTC", + market_type="LEVERAGE", + ) + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + leverage, + spot, + ), + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is spot + + +def test_resolver_ignores_other_base_assets( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + _instrument( + symbol="ETH/USD", + base_asset="ETH", + ), + ), + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is None + + +def test_resolver_ignores_unsupported_quote_assets( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + _instrument( + symbol="BTC/EUR", + base_asset="BTC", + quote_asset="EUR", + ), + ), + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is None + + +def test_resolver_returns_none_when_get_instruments_fails( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + def raise_error() -> tuple[Instrument, ...]: + raise ExchangeError( + "exchangeInfo unavailable" + ) + + monkeypatch.setattr( + service, + "get_instruments", + raise_error, + ) + + result = _resolve_asset_quote_instrument( + service, + "BTC", + ) + + assert result is None + + +@pytest.mark.parametrize( + "currency", + [ + "USD", + "usd", + "USDT", + "usdt", + ], +) +def test_usd_and_usdt_rates_are_one( + currency: str, +) -> None: + service = _service() + cache: dict[str, float | None] = {} + + result = get_asset_usd_rate( + service, + currency, + cache, + ) + + assert result == 1.0 + assert cache == {} + + +def test_get_asset_usd_rate_uses_existing_price_cache( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + cache: dict[str, float | None] = { + "BTC": 50_000.0, + } + + def fail_if_called() -> tuple[Instrument, ...]: + raise AssertionError( + "Instrument lookup must not run on price cache hit." + ) + + monkeypatch.setattr( + service, + "get_instruments", + fail_if_called, + ) + + result = get_asset_usd_rate( + service, + "BTC", + cache, + ) + + assert result == 50_000.0 + + +def test_get_asset_usd_rate_loads_price_by_instrument_symbol( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + instrument = _instrument( + symbol="BTC/USD", + base_asset="BTC", + ) + requested_symbols: list[str] = [] + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + instrument, + ), + ) + + def get_quote( + symbol: str, + *, + runtime_key: str | None = None, + ) -> Quote: + del runtime_key + + requested_symbols.append(symbol) + + return Quote( + symbol=symbol, + last_price=Decimal("50000"), + bid_price=Decimal("49999"), + ask_price=Decimal("50001"), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="test", + ) + + monkeypatch.setattr( + service, + "get_quote", + get_quote, + ) + + cache: dict[str, float | None] = {} + + result = get_asset_usd_rate( + service, + "BTC", + cache, + ) + + assert result == 50_000.0 + assert requested_symbols == [ + "BTC/USD", + ] + assert cache == { + "BTC": 50_000.0, + } + + +def test_get_asset_usd_rate_caches_missing_instrument( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + monkeypatch.setattr( + service, + "get_instruments", + lambda: (), + ) + + cache: dict[str, float | None] = {} + + result = get_asset_usd_rate( + service, + "BTC", + cache, + ) + + assert result is None + assert cache == { + "BTC": None, + } + + +def test_get_asset_usd_rate_caches_price_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + _instrument( + symbol="BTC/USD", + base_asset="BTC", + ), + ), + ) + + def raise_error( + symbol: str, + *, + runtime_key: str | None = None, + ) -> Quote: + del symbol + del runtime_key + + raise ExchangeError( + "ticker unavailable" + ) + + monkeypatch.setattr( + service, + "get_quote", + raise_error, + ) + + cache: dict[str, float | None] = {} + + result = get_asset_usd_rate( + service, + "BTC", + cache, + ) + + assert result is None + assert cache == { + "BTC": None, + } + + +def test_estimate_balance_usd_for_fiat_balance() -> None: + service = _service() + + balance = BalanceSummary( + currency="USD", + available=100.0, + locked=25.0, + source="test", + ) + + result = estimate_balance_usd( + balance, + service, + {}, + ) + + assert result == 125.0 + + +def test_estimate_balance_usd_for_crypto_balance( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + monkeypatch.setattr( + service, + "get_instruments", + lambda: ( + _instrument( + symbol="BTC/USD", + base_asset="BTC", + ), + ), + ) + + monkeypatch.setattr( + service, + "get_quote", + lambda symbol, **_: Quote( + symbol=symbol, + last_price=Decimal("50000"), + bid_price=Decimal("49999"), + ask_price=Decimal("50001"), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="test", + ), + ) + + balance = BalanceSummary( + currency="BTC", + available=0.01, + locked=0.005, + source="test", + ) + + result = estimate_balance_usd( + balance, + service, + {}, + ) + + assert result == pytest.approx( + 750.0 + ) + + +def test_estimate_balance_usd_returns_none_for_zero_total() -> None: + service = _service() + + balance = BalanceSummary( + currency="BTC", + available=0.0, + locked=0.0, + source="test", + ) + + result = estimate_balance_usd( + balance, + service, + {}, + ) + + assert result is None + + +def test_estimate_balance_usd_returns_none_when_rate_is_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = _service() + + monkeypatch.setattr( + service, + "get_instruments", + lambda: (), + ) + + balance = BalanceSummary( + currency="BTC", + available=1.0, + locked=0.0, + source="test", + ) + + result = estimate_balance_usd( + balance, + service, + {}, + ) + + assert result is None \ No newline at end of file diff --git a/app/tests/unit/trading/auto/test_execution_quality.py b/app/tests/unit/trading/auto/test_execution_quality.py new file mode 100644 index 0000000..c9e4ad8 --- /dev/null +++ b/app/tests/unit/trading/auto/test_execution_quality.py @@ -0,0 +1,83 @@ +# app/tests/unit/trading/auto/test_execution_quality.py + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +import src.trading.auto.execution_quality as module +from src.integrations.exchange.models import ExecutionPriceSnapshot +from src.trading.auto.execution_quality import AutoExecutionQualityMixin + + +class Harness(AutoExecutionQualityMixin): + _spread_thresholds_by_asset = {} + _default_spread_thresholds = { + "warning_enter": 1.0, + "warning_exit": 0.8, + "block_enter": 2.0, + "block_exit": 1.5, + } + _max_snapshot_age_seconds = 5.0 + _warning_snapshot_age_seconds = 2.0 + _last_logged_execution_quality_key = None + + def _log_execution_quality_if_changed(self, **_: object) -> None: + return None + + def _apply_exchange_block_state(self, **_: object) -> None: + raise AssertionError("exchange block must not be applied") + + +def _state() -> SimpleNamespace: + return SimpleNamespace( + market_is_open=True, + symbol="BTC/USD_LEVERAGE", + strategy="trend", + status="RUNNING", + + execution_quality=None, + execution_quality_reason=None, + execution_quality_message=None, + execution_block_reason=None, + + market_runtime_degraded=False, + snapshot_age_seconds=None, + spread_percent=None, + + execution_price_age_seconds=None, + execution_bid_price=None, + execution_ask_price=None, + execution_last_price=None, + execution_price_freshness=None, + ) + +def test_execution_quality_uses_typed_execution_snapshot( + monkeypatch: pytest.MonkeyPatch, +) -> None: + snapshot = ExecutionPriceSnapshot( + symbol="BTC/USD_LEVERAGE", + last_price=100.5, + bid_price=100.0, + ask_price=101.0, + updated_at="13.07.2026 15:00:00", + source="dzengi:fresh_cache", + is_fresh=True, + age_seconds=0.5, + ) + + class Service: + def get_execution_snapshot(self, *_: object, **__: object) -> ExecutionPriceSnapshot: + return snapshot + + monkeypatch.setattr(module, "ExchangeService", Service) + + state = _state() + Harness()._sync_execution_quality_state(state) # type: ignore[arg-type] + + assert state.execution_bid_price == 100.0 + assert state.execution_ask_price == 101.0 + assert state.execution_last_price == 100.5 + assert state.execution_price_source == "dzengi:fresh_cache" + assert state.snapshot_age_seconds == 0.5 diff --git a/app/tests/unit/trading/auto/test_signal_runtime_quote.py b/app/tests/unit/trading/auto/test_signal_runtime_quote.py new file mode 100644 index 0000000..c4d8df8 --- /dev/null +++ b/app/tests/unit/trading/auto/test_signal_runtime_quote.py @@ -0,0 +1,75 @@ +# app/tests/unit/trading/auto/test_signal_runtime_quote.py + +from __future__ import annotations + +import inspect +from datetime import datetime, timezone +from decimal import Decimal +from types import SimpleNamespace + +import pytest + +import src.trading.auto.signal_runtime as module +from src.market_data.acquisition.models.quote import Quote +from src.trading.auto.signal_runtime import AutoSignalRuntimeMixin + + +def _quote() -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal("100.5"), + bid_price=Decimal("100.0"), + ask_price=Decimal("101.0"), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + + +class Harness(AutoSignalRuntimeMixin): + _ready_confidence = 0.3 + + +def test_ready_signal_uses_canonical_quote( + monkeypatch: pytest.MonkeyPatch, +) -> None: + quote = _quote() + requested: list[tuple[str, str]] = [] + captured: dict[str, object] = {} + + class Service: + def get_quote(self, symbol: str, *, runtime_key: str) -> Quote: + requested.append((symbol, runtime_key)) + return quote + + class Journal: + def log_ui_info(self, **kwargs: object) -> None: + captured.update(kwargs) + + harness = Harness() + + def build_payload(**kwargs: object) -> dict[str, object]: + captured["quote"] = kwargs["quote"] + return {"ok": True} + + monkeypatch.setattr(module, "ExchangeService", Service) + monkeypatch.setattr(module, "JournalService", Journal) + monkeypatch.setattr(harness, "_build_ready_signal_payload", build_payload) + + state = SimpleNamespace(symbol="BTC/USD_LEVERAGE") + harness._log_ready_signal( + state=state, # type: ignore[arg-type] + signal="BUY", + reason="test", + confidence=0.9, + signal_intent="ENTRY", + ) + + assert requested == [("BTC/USD_LEVERAGE", "auto")] + assert captured["quote"] is quote + + +def test_signal_runtime_has_no_legacy_market_snapshot_call() -> None: + source = inspect.getsource(module.AutoSignalRuntimeMixin) + + assert "get_quote(" in source diff --git a/app/tests/unit/trading/debug/test_execution.py b/app/tests/unit/trading/debug/test_execution.py new file mode 100644 index 0000000..3dc8951 --- /dev/null +++ b/app/tests/unit/trading/debug/test_execution.py @@ -0,0 +1,49 @@ +# app/tests/unit/trading/debug/test_execution.py + +from __future__ import annotations + +import pytest + +import src.trading.debug.execution as module +from src.integrations.exchange.models import ExecutionPriceSnapshot +from src.trading.debug.execution import DebugExecutionEngine + + +def _snapshot() -> ExecutionPriceSnapshot: + return ExecutionPriceSnapshot( + symbol="BTC/USD_LEVERAGE", + last_price=100.5, + bid_price=100.0, + ask_price=101.0, + updated_at="13.07.2026 15:00:00", + source="rest_fallback", + is_fresh=True, + age_seconds=0.0, + ) + + +def test_debug_execution_uses_execution_snapshot( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[tuple[str, str | None]] = [] + + class Service: + def get_execution_snapshot( + self, + symbol: str, + *, + runtime_key: str | None = None, + ) -> ExecutionPriceSnapshot: + calls.append((symbol, runtime_key)) + return _snapshot() + + monkeypatch.setattr(module, "ExchangeService", Service) + + engine = DebugExecutionEngine() + + assert engine._entry_price_for_side("BTC/USD_LEVERAGE", "LONG") == 101.0 + assert engine._entry_price_for_side("BTC/USD_LEVERAGE", "SHORT") == 100.0 + assert engine._exit_price_for_side("BTC/USD_LEVERAGE", "LONG") == 100.0 + assert engine._exit_price_for_side("BTC/USD_LEVERAGE", "SHORT") == 101.0 + assert engine._market_last_price("BTC/USD_LEVERAGE") == 100.5 + assert calls == [("BTC/USD_LEVERAGE", "debug_auto")] * 5 diff --git a/app/tests/unit/trading/strategies/test_scalp_quote.py b/app/tests/unit/trading/strategies/test_scalp_quote.py new file mode 100644 index 0000000..8137a70 --- /dev/null +++ b/app/tests/unit/trading/strategies/test_scalp_quote.py @@ -0,0 +1,52 @@ +# app/tests/unit/trading/strategies/test_scalp_quote.py + +from __future__ import annotations + +import inspect +from datetime import datetime, timezone +from decimal import Decimal + +import src.trading.strategies.scalp as module +from src.market_data.acquisition.models.quote import Quote +from src.trading.strategies.scalp import ScalpStrategy + + +def _quote( + *, + last: str = "100.5", + bid: str = "100.0", + ask: str = "101.0", +) -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal(last), + bid_price=Decimal(bid), + ask_price=Decimal(ask), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + + +def test_scalp_uses_midpoint_from_quote() -> None: + result = ScalpStrategy()._analysis_price(_quote()) + + assert result == 100.5 + + +def test_scalp_quote_snapshot_is_json_compatible_projection() -> None: + result = ScalpStrategy()._quote_snapshot(_quote()) + + assert result == { + "symbol": "BTC/USD_LEVERAGE", + "last_price": 100.5, + "bid_price": 100.0, + "ask_price": 101.0, + "source": "dzengi", + } + + +def test_scalp_has_no_legacy_market_snapshot_call() -> None: + source = inspect.getsource(module.ScalpStrategy) + + assert "get_quote(" in source diff --git a/app/tests/unit/trading/strategies/test_trend_quote.py b/app/tests/unit/trading/strategies/test_trend_quote.py new file mode 100644 index 0000000..1b50d4d --- /dev/null +++ b/app/tests/unit/trading/strategies/test_trend_quote.py @@ -0,0 +1,52 @@ +# app/tests/unit/trading/strategies/test_trend_quote.py + +from __future__ import annotations + +import inspect +from datetime import datetime, timezone +from decimal import Decimal + +import src.trading.strategies.trend as module +from src.market_data.acquisition.models.quote import Quote +from src.trading.strategies.trend import TrendStrategy + + +def _quote( + *, + last: str = "100.5", + bid: str = "100.0", + ask: str = "101.0", +) -> Quote: + return Quote( + symbol="BTC/USD_LEVERAGE", + last_price=Decimal(last), + bid_price=Decimal(bid), + ask_price=Decimal(ask), + exchange_timestamp=None, + received_at=datetime.now(timezone.utc), + source="dzengi", + ) + + +def test_trend_uses_midpoint_from_quote() -> None: + result = TrendStrategy()._analysis_price(_quote()) + + assert result == 100.5 + + +def test_trend_quote_snapshot_is_json_compatible_projection() -> None: + result = TrendStrategy()._quote_snapshot(_quote()) + + assert result == { + "symbol": "BTC/USD_LEVERAGE", + "last_price": 100.5, + "bid_price": 100.0, + "ask_price": 101.0, + "source": "dzengi", + } + + +def test_trend_has_no_legacy_market_snapshot_call() -> None: + source = inspect.getsource(module.TrendStrategy) + + assert "get_quote(" in source diff --git a/app/ticker_24hr_response.json b/app/ticker_24hr_response.json new file mode 100644 index 0000000..2d8cb0b --- /dev/null +++ b/app/ticker_24hr_response.json @@ -0,0 +1,18 @@ +{ + "askPrice": "64159.55", + "bidPrice": "64159.45", + "closeTime": 1783887270312, + "highPrice": "64261.45", + "lastPrice": "64159.45", + "lastQty": "5.0", + "lowPrice": "63590.7", + "openPrice": "63785.75", + "openTime": 1783814400000, + "prevClosePrice": "63785.75", + "priceChange": "368.85", + "priceChangePercent": "0.57822", + "quoteVolume": "616402.92146", + "symbol": "BTC/USD_LEVERAGE", + "volume": "9.6002", + "weightedAvgPrice": "64159.50" +} diff --git a/app/tools/dzengi_probe/README.md b/app/tools/dzengi_probe/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/__init__.py b/app/tools/dzengi_probe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/ask_price_equivalence_probe.py b/app/tools/dzengi_probe/ask_price_equivalence_probe.py new file mode 100644 index 0000000..dd1c074 --- /dev/null +++ b/app/tools/dzengi_probe/ask_price_equivalence_probe.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import asyncio +import csv +import json +from dataclasses import dataclass +from decimal import Decimal +from pathlib import Path +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +import websockets +from websockets.typing import Subprotocol + +from app.tools.dzengi_probe.config import DzengiProbeConfig + + +@dataclass(frozen=True) +class AskPriceSample: + sample: int + depth_ask: str | None + stream_ofr: str | None + ticker_ask_price: str | None + + +class AskPriceEquivalenceProbe: + def __init__(self, config: DzengiProbeConfig) -> None: + self.config = config + + async def run(self, *, samples: int = 30) -> Path: + output_path = ( + self.config.output_dir + / "reports" + / f"ask_price_equivalence_{self.config.symbol_file_name}.csv" + ) + output_path.parent.mkdir(parents=True, exist_ok=True) + + rows: list[AskPriceSample] = [] + + async with websockets.connect( + self.config.ws_url, + extra_headers={ + "Origin": self.config.base_url, + "Content-Type": "application/json", + }, + subprotocols=[Subprotocol("json")], + ping_interval=20, + ping_timeout=20, + close_timeout=5, + ) as websocket: + await websocket.send( + json.dumps( + { + "correlationId": "probe-market-data-subscribe", + "destination": "marketData.subscribe", + "payload": {"symbols": [self.config.symbol]}, + } + ) + ) + + for index in range(1, samples + 1): + stream_ofr = await self._read_next_ofr(websocket) + depth_ask = self._get_depth_ask() + ticker_ask_price = self._get_ticker_ask_price() + + rows.append( + AskPriceSample( + sample=index, + depth_ask=depth_ask, + stream_ofr=stream_ofr, + ticker_ask_price=ticker_ask_price, + ) + ) + + await asyncio.sleep(0.2) + + self._save_csv(output_path, rows) + return output_path + + async def _read_next_ofr(self, websocket) -> str | None: + while True: + raw_message = await asyncio.wait_for( + websocket.recv(), + timeout=self.config.timeout_seconds, + ) + payload = json.loads(raw_message) + + if payload.get("destination") != "internal.quote": + continue + + data = payload.get("payload") + if not isinstance(data, dict): + continue + + value = data.get("ofr") + if value is None: + continue + + return str(Decimal(str(value))) + + def _get_depth_ask(self) -> str | None: + payload = self._get_json( + f"/api/{self.config.api_version}/depth", + { + "symbol": self.config.symbol, + "limit": self.config.depth_limit, + }, + ) + + asks = payload.get("asks") + if not asks: + return None + + return str(Decimal(str(asks[0][0]))) + + def _get_ticker_ask_price(self) -> str | None: + payload = self._get_json( + f"/api/{self.config.api_version}/ticker/24hr", + {"symbol": self.config.symbol}, + ) + + value = payload.get("askPrice") + if value is None: + return None + + return str(Decimal(str(value))) + + def _get_json(self, path: str, params: dict[str, str]) -> dict: + query = urlencode(params) + url = f"{self.config.base_url}{path}?{query}" + + request = Request( + url=url, + method="GET", + headers={ + "Accept": "application/json", + "User-Agent": "dzentra-dzengi-probe/1.0", + }, + ) + + with urlopen(request, timeout=self.config.timeout_seconds) as response: + return json.loads(response.read().decode("utf-8")) + + def _save_csv(self, path: Path, rows: list[AskPriceSample]) -> None: + with path.open("w", encoding="utf-8", newline="") as file: + writer = csv.writer(file) + writer.writerow( + [ + "sample", + "depth_ask", + "stream_ofr", + "ticker_ask_price", + "depth_equals_stream", + "ticker_equals_depth", + "ticker_equals_stream", + ] + ) + + for row in rows: + writer.writerow( + [ + row.sample, + row.depth_ask, + row.stream_ofr, + row.ticker_ask_price, + row.depth_ask == row.stream_ofr, + row.ticker_ask_price == row.depth_ask, + row.ticker_ask_price == row.stream_ofr, + ] + ) \ No newline at end of file diff --git a/app/tools/dzengi_probe/config.py b/app/tools/dzengi_probe/config.py new file mode 100644 index 0000000..1ba5c4d --- /dev/null +++ b/app/tools/dzengi_probe/config.py @@ -0,0 +1,49 @@ +# app/tools/dzengi_probe/config.py + +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + +from dotenv import load_dotenv + + +@dataclass(frozen=True) +class DzengiProbeConfig: + base_url: str + ws_url: str + api_version: str + symbol: str + symbol_file_name: str + interval: str + depth_limit: str + agg_trades_limit: str + timeout_seconds: float + output_dir: Path + + +def load_config() -> DzengiProbeConfig: + load_dotenv("app/.env") + + base_url = os.getenv("EXCHANGE_BASE_URL", "").rstrip("/") + ws_url = os.getenv("EXCHANGE_WS_URL", "").rstrip("/") + + if not base_url: + raise RuntimeError("EXCHANGE_BASE_URL is empty.") + + symbol = os.getenv("PROBE_SYMBOL", "BTC/USD_LEVERAGE") + symbol_file_name = symbol.replace("/", "_") + + return DzengiProbeConfig( + base_url=base_url, + ws_url=ws_url, + api_version=os.getenv("PROBE_API_VERSION", "v1"), + symbol=symbol, + symbol_file_name=symbol_file_name, + interval=os.getenv("PROBE_KLINE_INTERVAL", "1m"), + depth_limit=os.getenv("PROBE_DEPTH_LIMIT", "20"), + agg_trades_limit=os.getenv("PROBE_AGG_TRADES_LIMIT", "20"), + timeout_seconds=float(os.getenv("EXCHANGE_TIMEOUT_SEC", "10")), + output_dir=Path("app/tools/dzengi_probe/runtime_samples"), + ) \ No newline at end of file diff --git a/app/tools/dzengi_probe/connection_probe.py b/app/tools/dzengi_probe/connection_probe.py new file mode 100644 index 0000000..ca741b3 --- /dev/null +++ b/app/tools/dzengi_probe/connection_probe.py @@ -0,0 +1,118 @@ +# app/tools/dzengi_probe/connection_probe.py + +from __future__ import annotations + +import asyncio +import json +from dataclasses import dataclass +from typing import Any + +import websockets +from websockets.typing import Subprotocol + +from app.tools.dzengi_probe.config import DzengiProbeConfig + + +@dataclass(frozen=True) +class ConnectionProbeResult: + name: str + ok: bool + error: str | None = None + + +class DzengiConnectionProbe: + def __init__(self, config: DzengiProbeConfig) -> None: + self.config = config + + async def run_all(self) -> list[ConnectionProbeResult]: + return [ + await self._check( + name="baseline", + headers={ + "Origin": self.config.base_url, + "Content-Type": "application/json", + }, + subprotocols=[Subprotocol("json")], + ), + await self._check( + name="without_origin", + headers={ + "Content-Type": "application/json", + }, + subprotocols=[Subprotocol("json")], + ), + await self._check( + name="without_content_type", + headers={ + "Origin": self.config.base_url, + }, + subprotocols=[Subprotocol("json")], + ), + await self._check( + name="without_headers", + headers=None, + subprotocols=[Subprotocol("json")], + ), + await self._check( + name="without_subprotocol", + headers={ + "Origin": self.config.base_url, + "Content-Type": "application/json", + }, + subprotocols=None, + ), + await self._check( + name="minimal_connection", + headers=None, + subprotocols=None, + ), + ] + + async def _check( + self, + *, + name: str, + headers: dict[str, str] | None, + subprotocols: list[Subprotocol] | None, + ) -> ConnectionProbeResult: + subscribe_message: dict[str, Any] = { + "correlationId": f"connection-probe-{name}", + "destination": "marketData.subscribe", + "payload": { + "symbols": [self.config.symbol], + }, + } + + try: + async with websockets.connect( + self.config.ws_url, + extra_headers=headers, + subprotocols=subprotocols, + ping_interval=20, + ping_timeout=20, + close_timeout=5, + ) as websocket: + await websocket.send(json.dumps(subscribe_message)) + + raw_message = await asyncio.wait_for( + websocket.recv(), + timeout=self.config.timeout_seconds, + ) + + payload = json.loads(raw_message) + + if payload.get("status") != "OK": + return ConnectionProbeResult( + name=name, + ok=False, + error=json.dumps(payload, ensure_ascii=False), + ) + + return ConnectionProbeResult(name=name, ok=True) + + except Exception as exc: + return ConnectionProbeResult( + name=name, + ok=False, + error=str(exc), + ) \ No newline at end of file diff --git a/app/tools/dzengi_probe/probe.py b/app/tools/dzengi_probe/probe.py new file mode 100644 index 0000000..53c6fb7 --- /dev/null +++ b/app/tools/dzengi_probe/probe.py @@ -0,0 +1,150 @@ +# app/tools/dzengi_probe/probe.py + +from __future__ import annotations + +import argparse +import asyncio + +from app.tools.dzengi_probe.config import load_config +from app.tools.dzengi_probe.connection_probe import DzengiConnectionProbe +from app.tools.dzengi_probe.rest_probe import DzengiRestProbe +from app.tools.dzengi_probe.stream_market_data_probe import MarketDataStreamProbe +from app.tools.dzengi_probe.websocket_probe import DzengiWebSocketProbe + + +def run_rest_probe() -> None: + config = load_config() + probe = DzengiRestProbe(config) + + results = probe.run_all() + + print("Dzengi REST probe results:") + for result in results: + status = "OK" if result.ok else "FAIL" + print(f"- {status} {result.endpoint} -> {result.output_file}") + + if result.error: + print(f" error: {result.error}") + + +async def run_connection_probe() -> None: + config = load_config() + probe = DzengiConnectionProbe(config) + + results = await probe.run_all() + + print("Dzengi connection probe results:") + for result in results: + status = "OK" if result.ok else "FAIL" + print(f"- {status} {result.name}") + + if result.error: + print(f" error: {result.error}") + + +async def run_ws_depth_probe() -> None: + config = load_config() + probe = DzengiWebSocketProbe(config) + + result = await probe.probe_depth_request() + + status = "OK" if result.ok else "FAIL" + print("Dzengi WebSocket Request probe results:") + print(f"- {status} {result.stream} -> {result.output_file}") + print(f" messages saved: {result.messages_saved}") + + if result.error: + print(f" error: {result.error}") + + +async def run_market_data_stream_probe( + duration: int | None = None, + scenario: str = "valid", + repeat: int = 1, +) -> None: + config = load_config() + probe = MarketDataStreamProbe(config) + + result = await probe.run( + max_messages=None if duration else 10, + duration_seconds=duration, + scenario=scenario, + repeat=repeat, + ) + + status = "OK" if result.ok else "FAIL" + print("Dzengi Stream probe results:") + print(f"- {status} {result.stream} -> {result.output_file}") + print(f" messages saved: {result.messages_saved}") + + if result.error: + print(f" error: {result.error}") + + +def main() -> None: + parser = argparse.ArgumentParser() + + parser.add_argument( + "target", + choices=[ + "rest", + "ws-depth", + "stream-market-data", + "connection", + ], + ) + + parser.add_argument( + "--duration", + type=int, + default=None, + ) + + parser.add_argument( + "--repeat", + type=int, + default=1, + ) + + parser.add_argument( + "--scenario", + default="valid", + choices=[ + "valid", + "invalid-symbol", + "empty-symbols", + "missing-symbols", + "missing-payload", + "invalid-destination", + "unsubscribe", + "multi-symbol", + ], + ) + + args = parser.parse_args() + + if args.target == "rest": + run_rest_probe() + return + + if args.target == "ws-depth": + asyncio.run(run_ws_depth_probe()) + return + + if args.target == "stream-market-data": + asyncio.run( + run_market_data_stream_probe( + duration=args.duration, + scenario=args.scenario, + repeat=args.repeat, + ) + ) + return + + if args.target == "connection": + asyncio.run(run_connection_probe()) + return + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app/tools/dzengi_probe/reports/README.md b/app/tools/dzengi_probe/reports/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/response_store.py b/app/tools/dzengi_probe/response_store.py new file mode 100644 index 0000000..b6d2f53 --- /dev/null +++ b/app/tools/dzengi_probe/response_store.py @@ -0,0 +1,23 @@ +# app/tools/dzengi_probe/response_store.py + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +def save_json(path: Path, payload: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + with path.open("w", encoding="utf-8") as file: + json.dump(payload, file, ensure_ascii=False, indent=2, sort_keys=True) + file.write("\n") + + +def append_jsonl(path: Path, payload: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + + with path.open("a", encoding="utf-8") as file: + file.write(json.dumps(payload, ensure_ascii=False, sort_keys=True)) + file.write("\n") \ No newline at end of file diff --git a/app/tools/dzengi_probe/rest_probe.py b/app/tools/dzengi_probe/rest_probe.py new file mode 100644 index 0000000..0646cc5 --- /dev/null +++ b/app/tools/dzengi_probe/rest_probe.py @@ -0,0 +1,195 @@ +# app/tools/dzengi_probe/rest_probe.py + +from __future__ import annotations + +import json +from dataclasses import dataclass +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from app.tools.dzengi_probe.config import DzengiProbeConfig +from app.tools.dzengi_probe.response_store import save_json + + +@dataclass(frozen=True) +class RestProbeResult: + endpoint: str + path: str + output_file: str + ok: bool + error: str | None = None + + +class DzengiRestProbe: + def __init__(self, config: DzengiProbeConfig) -> None: + self.config = config + + def run_all(self) -> list[RestProbeResult]: + return [ + self.probe_time(), + self.probe_exchange_info(), + self.probe_ticker24hr(), + self.probe_depth(), + self.probe_klines(), + self.probe_agg_trades(), + ] + + def probe_time(self) -> RestProbeResult: + return self._get_and_save( + endpoint="time", + path=f"/api/{self.config.api_version}/time", + params=None, + output_file="rest/time/response.json", + ) + + def probe_exchange_info(self) -> RestProbeResult: + return self._get_and_save( + endpoint="exchangeInfo", + path=f"/api/{self.config.api_version}/exchangeInfo", + params=None, + output_file="rest/exchangeInfo/all.json", + ) + + def probe_ticker24hr(self) -> RestProbeResult: + return self._get_and_save( + endpoint="ticker24hr", + path=f"/api/{self.config.api_version}/ticker/24hr", + params={"symbol": self.config.symbol}, + output_file=f"rest/ticker24hr/{self.config.symbol_file_name}.json", + ) + + def probe_depth(self) -> RestProbeResult: + return self._get_and_save( + endpoint="depth", + path=f"/api/{self.config.api_version}/depth", + params={ + "symbol": self.config.symbol, + "limit": self.config.depth_limit, + }, + output_file=f"rest/depth/{self.config.symbol_file_name}.json", + ) + + def probe_klines(self) -> RestProbeResult: + return self._get_and_save( + endpoint="klines", + path=f"/api/{self.config.api_version}/klines", + params={ + "symbol": self.config.symbol, + "interval": self.config.interval, + "limit": "10", + }, + output_file=( + f"rest/klines/" + f"{self.config.symbol_file_name}_{self.config.interval}.json" + ), + ) + + def probe_agg_trades(self) -> RestProbeResult: + return self._get_and_save( + endpoint="aggTrades", + path=f"/api/{self.config.api_version}/aggTrades", + params={ + "symbol": self.config.symbol, + "limit": self.config.agg_trades_limit, + }, + output_file=f"rest/aggTrades/{self.config.symbol_file_name}.json", + ) + + def _get_and_save( + self, + *, + endpoint: str, + path: str, + params: dict[str, str] | None, + output_file: str, + ) -> RestProbeResult: + url = self._build_url(path, params) + + request = Request( + url=url, + method="GET", + headers={ + "Accept": "application/json", + "User-Agent": "dzentra-dzengi-probe/1.0", + }, + ) + + try: + with urlopen(request, timeout=self.config.timeout_seconds) as response: + status = getattr(response, "status", 200) + body = response.read().decode("utf-8") + + if status != 200: + return RestProbeResult( + endpoint=endpoint, + path=path, + output_file=output_file, + ok=False, + error=f"Unexpected HTTP status: {status}", + ) + + payload = json.loads(body) + output_path = self.config.output_dir / output_file + save_json(output_path, payload) + + return RestProbeResult( + endpoint=endpoint, + path=path, + output_file=str(output_path), + ok=True, + ) + + except HTTPError as exc: + error_body = "" + try: + error_body = exc.read().decode("utf-8") + except Exception: + pass + + message = f"HTTP {exc.code}: {exc.reason}" + if error_body: + message += f" | body: {error_body}" + + return RestProbeResult( + endpoint=endpoint, + path=path, + output_file=output_file, + ok=False, + error=message, + ) + + except URLError as exc: + return RestProbeResult( + endpoint=endpoint, + path=path, + output_file=output_file, + ok=False, + error=f"Network error: {exc.reason}", + ) + + except TimeoutError: + return RestProbeResult( + endpoint=endpoint, + path=path, + output_file=output_file, + ok=False, + error="Timeout while calling Dzengi API.", + ) + + except json.JSONDecodeError as exc: + return RestProbeResult( + endpoint=endpoint, + path=path, + output_file=output_file, + ok=False, + error=f"Non-JSON response: {exc}", + ) + + def _build_url( + self, + path: str, + params: dict[str, str] | None, + ) -> str: + query = f"?{urlencode(params)}" if params else "" + return f"{self.config.base_url}{path}{query}" \ No newline at end of file diff --git a/app/tools/dzengi_probe/runtime_samples/README.md b/app/tools/dzengi_probe/runtime_samples/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/runtime_samples/reports/ask_price_equivalence_BTC_USD_LEVERAGE.csv b/app/tools/dzengi_probe/runtime_samples/reports/ask_price_equivalence_BTC_USD_LEVERAGE.csv new file mode 100644 index 0000000..424f5ed --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/reports/ask_price_equivalence_BTC_USD_LEVERAGE.csv @@ -0,0 +1,31 @@ +sample,depth_ask,stream_ofr,ticker_ask_price,depth_equals_stream,ticker_equals_depth,ticker_equals_stream +1,62217.3,62215.3,62215.80,False,False,False +2,62216.9,62216.55,62215.80,False,False,False +3,62216.9,62220.8,62215.80,False,False,False +4,62216.9,62218.05,62215.80,False,False,False +5,62216.9,62212.45,62215.80,False,False,False +6,62216.9,62217.3,62215.80,False,False,False +7,62212.85,62216.05,62215.80,False,False,False +8,62212.7,62220.7,62215.80,False,False,False +9,62213.8,62217.3,62215.80,False,False,False +10,62211.9,62212.65,62215.80,False,False,False +11,62213.55,62217.3,62215.80,False,False,False +12,62213.55,62216.9,62215.80,False,False,False +13,62213.55,62218.7,62215.80,False,False,False +14,62213.55,62214.05,62215.80,False,False,False +15,62213.55,62213.2,62215.80,False,False,False +16,62213.55,62216.45,62215.80,False,False,False +17,62236.6,62213.45,62215.80,False,False,False +18,62239.4,62212.85,62215.80,False,False,False +19,62236.4,62213.7,62215.80,False,False,False +20,62221.55,62208.45,62221.60,False,False,False +21,62225,62210.6,62221.60,False,False,False +22,62219.75,62213.8,62221.60,False,False,False +23,62217.1,62209.8,62221.60,False,False,False +24,62224.4,62212.6,62221.60,False,False,False +25,62238.75,62212.7,62221.60,False,False,False +26,62241.5,62214.0,62221.60,False,False,False +27,62245.45,62211.6,62221.60,False,False,False +28,62245.05,62210.85,62221.60,False,False,False +29,62245.05,62213.8,62221.60,False,False,False +30,62245.05,62212.0,62221.60,False,False,False diff --git a/app/tools/dzengi_probe/runtime_samples/reports/ticker24hr_update_BTC_USD_LEVERAGE.csv b/app/tools/dzengi_probe/runtime_samples/reports/ticker24hr_update_BTC_USD_LEVERAGE.csv new file mode 100644 index 0000000..6b67097 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/reports/ticker24hr_update_BTC_USD_LEVERAGE.csv @@ -0,0 +1,301 @@ +sample,local_time_ms,close_time,ask_price,bid_price,last_price,ask_changed,bid_changed,last_changed,close_time_changed,last_equals_bid +1,1783573006101,1783572989542,62334.20,62334.10,62334.10,False,False,False,False,True +2,1783573006593,1783572989542,62334.20,62334.10,62334.10,False,False,False,False,True +3,1783573007060,1783572989542,62334.20,62334.10,62334.10,False,False,False,False,True +4,1783573007571,1783572989542,62334.20,62334.10,62334.10,False,False,False,False,True +5,1783573008101,1783573008128,62349.30,62349.20,62349.20,True,True,True,True,True +6,1783573008616,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +7,1783573009148,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +8,1783573009669,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +9,1783573010207,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +10,1783573010718,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +11,1783573011239,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +12,1783573011765,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +13,1783573012292,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +14,1783573012817,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +15,1783573013337,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +16,1783573013867,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +17,1783573014393,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +18,1783573014919,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +19,1783573015476,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +20,1783573015959,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +21,1783573016483,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +22,1783573017015,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +23,1783573017528,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +24,1783573018062,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +25,1783573018578,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +26,1783573019113,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +27,1783573019630,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +28,1783573020157,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +29,1783573020686,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +30,1783573021214,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +31,1783573021737,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +32,1783573022250,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +33,1783573022771,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +34,1783573023305,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +35,1783573023830,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +36,1783573024372,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +37,1783573024877,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +38,1783573025405,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +39,1783573025931,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +40,1783573026452,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +41,1783573026993,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +42,1783573027496,1783573008128,62349.30,62349.20,62349.20,False,False,False,False,True +43,1783573028025,1783573028055,62363.40,62363.30,62363.30,True,True,True,True,True +44,1783573028547,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +45,1783573029074,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +46,1783573029597,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +47,1783573030117,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +48,1783573030695,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +49,1783573031172,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +50,1783573031696,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +51,1783573032221,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +52,1783573032742,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +53,1783573033266,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +54,1783573033788,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +55,1783573034317,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +56,1783573034839,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +57,1783573035362,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +58,1783573035893,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +59,1783573036435,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +60,1783573036939,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +61,1783573037462,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +62,1783573037985,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +63,1783573038507,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +64,1783573039038,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +65,1783573039562,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +66,1783573040077,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +67,1783573040605,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +68,1783573041135,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +69,1783573041658,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +70,1783573042184,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +71,1783573042710,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +72,1783573043228,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +73,1783573043748,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +74,1783573044270,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +75,1783573044807,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +76,1783573045327,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +77,1783573045850,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +78,1783573046375,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +79,1783573046901,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +80,1783573047418,1783573028055,62363.40,62363.30,62363.30,False,False,False,False,True +81,1783573047962,1783573047988,62387.30,62387.20,62387.20,True,True,True,True,True +82,1783573048867,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +83,1783573049393,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +84,1783573049916,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +85,1783573050445,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +86,1783573050965,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +87,1783573051488,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +88,1783573052011,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +89,1783573052541,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +90,1783573053064,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +91,1783573053588,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +92,1783573054111,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +93,1783573054634,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +94,1783573055161,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +95,1783573055687,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +96,1783573056201,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +97,1783573056734,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +98,1783573057252,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +99,1783573057780,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +100,1783573058303,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +101,1783573058827,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +102,1783573059351,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +103,1783573059876,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +104,1783573060509,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +105,1783573061051,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +106,1783573061583,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +107,1783573062098,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +108,1783573062636,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +109,1783573063148,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +110,1783573063679,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +111,1783573064205,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +112,1783573064721,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +113,1783573065251,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +114,1783573065780,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +115,1783573066294,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +116,1783573066823,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +117,1783573067339,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +118,1783573067874,1783573047988,62387.30,62387.20,62387.20,False,False,False,False,True +119,1783573068397,1783573068429,62381.70,62381.60,62381.60,True,True,True,True,True +120,1783573068911,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +121,1783573069457,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +122,1783573069966,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +123,1783573070493,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +124,1783573071017,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +125,1783573071539,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +126,1783573072064,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +127,1783573072588,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +128,1783573073113,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +129,1783573073634,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +130,1783573074158,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +131,1783573074685,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +132,1783573075214,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +133,1783573075730,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +134,1783573076254,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +135,1783573076785,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +136,1783573077302,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +137,1783573077830,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +138,1783573078368,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +139,1783573079404,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +140,1783573079925,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +141,1783573080450,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +142,1783573080982,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +143,1783573081496,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +144,1783573082024,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +145,1783573082556,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +146,1783573083070,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +147,1783573083595,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +148,1783573084120,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +149,1783573084791,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +150,1783573085276,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +151,1783573085752,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +152,1783573086228,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +153,1783573086742,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +154,1783573087267,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +155,1783573087798,1783573068429,62381.70,62381.60,62381.60,False,False,False,False,True +156,1783573088309,1783573088341,62382.45,62382.35,62382.35,True,True,True,True,True +157,1783573088850,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +158,1783573089373,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +159,1783573089882,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +160,1783573090435,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +161,1783573090939,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +162,1783573091465,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +163,1783573091986,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +164,1783573092512,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +165,1783573093038,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +166,1783573093563,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +167,1783573094083,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +168,1783573094608,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +169,1783573095129,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +170,1783573095650,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +171,1783573096177,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +172,1783573096703,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +173,1783573097226,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +174,1783573097752,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +175,1783573098290,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +176,1783573098805,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +177,1783573099331,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +178,1783573099853,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +179,1783573100376,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +180,1783573100898,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +181,1783573101420,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +182,1783573101955,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +183,1783573102469,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +184,1783573102995,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +185,1783573103515,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +186,1783573104044,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +187,1783573104571,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +188,1783573105089,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +189,1783573105620,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +190,1783573106142,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +191,1783573106663,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +192,1783573107189,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +193,1783573107711,1783573088341,62382.45,62382.35,62382.35,False,False,False,False,True +194,1783573108250,1783573108276,62383.15,62383.05,62383.05,True,True,True,True,True +195,1783573108765,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +196,1783573109286,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +197,1783573109815,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +198,1783573110334,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +199,1783573110863,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +200,1783573111381,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +201,1783573111910,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +202,1783573112435,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +203,1783573112959,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +204,1783573113481,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +205,1783573114007,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +206,1783573115112,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +207,1783573115633,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +208,1783573116109,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +209,1783573116622,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +210,1783573117153,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +211,1783573117678,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +212,1783573118198,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +213,1783573118723,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +214,1783573119254,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +215,1783573119775,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +216,1783573120303,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +217,1783573120817,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +218,1783573121347,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +219,1783573121875,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +220,1783573122391,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +221,1783573122920,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +222,1783573123476,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +223,1783573123964,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +224,1783573124490,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +225,1783573125019,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +226,1783573125542,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +227,1783573126063,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +228,1783573126589,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +229,1783573127116,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +230,1783573127642,1783573108276,62383.15,62383.05,62383.05,False,False,False,False,True +231,1783573128167,1783573128197,62381.15,62381.05,62381.05,True,True,True,True,True +232,1783573128679,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +233,1783573129206,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +234,1783573129736,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +235,1783573130263,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +236,1783573130782,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +237,1783573131306,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +238,1783573131834,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +239,1783573132364,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +240,1783573132879,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +241,1783573133406,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +242,1783573133928,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +243,1783573134460,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +244,1783573134977,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +245,1783573135498,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +246,1783573136030,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +247,1783573136548,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +248,1783573137079,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +249,1783573137600,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +250,1783573138117,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +251,1783573138670,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +252,1783573139168,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +253,1783573139694,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +254,1783573140224,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +255,1783573140748,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +256,1783573141265,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +257,1783573141794,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +258,1783573142319,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +259,1783573142843,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +260,1783573143359,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +261,1783573143887,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +262,1783573144539,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +263,1783573145015,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +264,1783573145728,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +265,1783573146248,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +266,1783573146772,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +267,1783573147297,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +268,1783573147821,1783573128197,62381.15,62381.05,62381.05,False,False,False,False,True +269,1783573148347,1783573148322,62378.30,62378.20,62378.20,True,True,True,True,True +270,1783573148872,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +271,1783573149989,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +272,1783573150569,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +273,1783573151112,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +274,1783573151634,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +275,1783573152150,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +276,1783573152683,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +277,1783573153198,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +278,1783573153732,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +279,1783573154247,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +280,1783573154772,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +281,1783573155301,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +282,1783573155818,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +283,1783573156344,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +284,1783573156868,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +285,1783573157401,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +286,1783573157916,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +287,1783573158443,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +288,1783573158966,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +289,1783573159494,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +290,1783573160022,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +291,1783573160540,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +292,1783573161072,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +293,1783573161603,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +294,1783573162113,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +295,1783573162641,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +296,1783573163160,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +297,1783573163688,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +298,1783573164216,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +299,1783573164734,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True +300,1783573165266,1783573148322,62378.30,62378.20,62378.20,False,False,False,False,True diff --git a/app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD.json b/app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD.json new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD_LEVERAGE.json b/app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD_LEVERAGE.json new file mode 100644 index 0000000..c91aeef --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/aggTrades/BTC_USD_LEVERAGE.json @@ -0,0 +1,142 @@ +[ + { + "T": 1783537892620, + "a": 1822810536, + "m": false, + "p": "62061.5", + "q": "0.0397" + }, + { + "T": 1783537892620, + "a": 1822810535, + "m": false, + "p": "62061.5", + "q": "0.0343" + }, + { + "T": 1783537892620, + "a": 1822810534, + "m": false, + "p": "62061.5", + "q": "0.0754" + }, + { + "T": 1783537622558, + "a": 1822804397, + "m": true, + "p": "62104", + "q": "1.0E-4" + }, + { + "T": 1783537513393, + "a": 1822801716, + "m": true, + "p": "62112", + "q": "0.0754" + }, + { + "T": 1783537513255, + "a": 1822801711, + "m": true, + "p": "62112", + "q": "0.1" + }, + { + "T": 1783537370617, + "a": 1822798425, + "m": false, + "p": "62121.3", + "q": "0.0016" + }, + { + "T": 1783537276165, + "a": 1822796047, + "m": false, + "p": "62184.1", + "q": "0.0277" + }, + { + "T": 1783537139900, + "a": 1822792790, + "m": true, + "p": "62110.5", + "q": "0.0319" + }, + { + "T": 1783536987519, + "a": 1822789817, + "m": true, + "p": "62112.8", + "q": "0.08" + }, + { + "T": 1783536987519, + "a": 1822789816, + "m": true, + "p": "62112.8", + "q": "0.08" + }, + { + "T": 1783536987519, + "a": 1822789815, + "m": true, + "p": "62112.8", + "q": "0.08" + }, + { + "T": 1783536638208, + "a": 1822781814, + "m": true, + "p": "62153.5", + "q": "0.021" + }, + { + "T": 1783536600688, + "a": 1822780882, + "m": true, + "p": "62136.5", + "q": "1.0" + }, + { + "T": 1783536553807, + "a": 1822779904, + "m": false, + "p": "62116.9", + "q": "0.6253" + }, + { + "T": 1783536482604, + "a": 1822778210, + "m": true, + "p": "62075.6", + "q": "0.6253" + }, + { + "T": 1783535795368, + "a": 1822762667, + "m": true, + "p": "61987.5", + "q": "1.0E-4" + }, + { + "T": 1783535791631, + "a": 1822762573, + "m": true, + "p": "61980", + "q": "0.3047" + }, + { + "T": 1783535791631, + "a": 1822762572, + "m": true, + "p": "61980", + "q": "1.0E-4" + }, + { + "T": 1783535721173, + "a": 1822760997, + "m": false, + "p": "62009.4", + "q": "1.0E-4" + } +] diff --git a/app/tools/dzengi_probe/runtime_samples/rest/depth/BTC_USD_LEVERAGE.json b/app/tools/dzengi_probe/runtime_samples/rest/depth/BTC_USD_LEVERAGE.json new file mode 100644 index 0000000..b8ab0e6 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/depth/BTC_USD_LEVERAGE.json @@ -0,0 +1,15 @@ +{ + "asks": [ + [ + 62055.9, + 5.0 + ] + ], + "bids": [ + [ + 62055.8, + 5.0 + ] + ], + "lastUpdateId": 0 +} diff --git a/app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json b/app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json new file mode 100644 index 0000000..d134f9e --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json @@ -0,0 +1,2313 @@ +{ + "exchangeFilters": [], + "rateLimits": [ + { + "interval": "MINUTE", + "intervalNum": 1, + "limit": 1200, + "rateLimitType": "REQUEST_WEIGHT" + }, + { + "interval": "SECOND", + "intervalNum": 1, + "limit": 10, + "rateLimitType": "ORDERS" + }, + { + "interval": "DAY", + "intervalNum": 1, + "limit": 864000, + "rateLimitType": "ORDERS" + } + ], + "serverTime": 1783537921471, + "symbols": [ + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "ETH", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.001", + "stepSize": "0.001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "2" + } + ], + "industry": "", + "longRate": -0.01, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "ETH/EUR", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 480, + "symbol": "ETH/EUR_LEVERAGE", + "tickSize": 0.01, + "tickValue": 18.3415, + "tradingFee": 0.06, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "ETH", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.001", + "stepSize": "0.001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "3" + } + ], + "industry": "", + "longRate": -0.01, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "ETH/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 480, + "symbol": "ETH/USD_LEVERAGE", + "tickSize": 0.01, + "tickValue": 21.32095, + "tradingFee": 0.06, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "EUR", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.11623801" + } + ], + "industry": "", + "longRate": -0.0088046, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "EUR/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": 0.0005846, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "EUR/USD_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 1.162e-05, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "BCH", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "2500", + "minQty": "0.001", + "stepSize": "0.001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00000456" + } + ], + "industry": "", + "longRate": -0.06, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "BCH/BTC", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "BTC", + "quoteAssetId": "BTC_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.01, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "BCH/BTC_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 5e-08, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "EQUITY", + "baseAsset": "TUI1.", + "baseAssetPrecision": 3, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "33000", + "minQty": "0.1", + "stepSize": "0.1" + } + ], + "industry": "Leisure & Recreation", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "TUI - EUR", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Cyclical Consumer Goods & Services", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "TUI1.", + "tickSize": 0.005, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "EQUITY", + "baseAsset": "DBK.", + "baseAssetPrecision": 3, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "130000", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "29" + } + ], + "industry": "Banks", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Deutsche Bank", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Financials", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "DBK.", + "tickSize": 0.005, + "tickValue": 0.1443375, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "INDEX", + "baseAsset": "EU50.", + "baseAssetPrecision": 2, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + } + ], + "industry": "", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "EU Stocks 50", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "EU50.", + "tickSize": 0.1, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00; Sun 22:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "GBP", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.13461800" + } + ], + "industry": "", + "longRate": -0.0039629, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "GBP/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0042571, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "GBP/USD_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 1.346e-05, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "INDEX", + "baseAsset": "IT40.", + "baseAssetPrecision": 2, + "country": "IT", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "100", + "minQty": "0.01", + "stepSize": "0.01" + } + ], + "industry": "", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Italy 40", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "IT40.", + "tickSize": 1, + "tradingFee": 0, + "tradingHours": "UTC; Mon 06:00 - 20:00; Tue 06:00 - 20:00; Wed 06:00 - 20:00; Thu 06:00 - 20:00; Fri 06:00 - 20:00" + }, + { + "assetType": "INDEX", + "baseAsset": "US100.", + "baseAssetPrecision": 2, + "country": "US", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "300" + } + ], + "industry": "", + "longRate": -0.0247624, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "US Tech 100", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": 0.0025401, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "US100.", + "tickSize": 0.1, + "tickValue": 2999.275, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 22:01 -; Tue - 21:00, 22:01 -; Wed - 21:00, 22:01 -; Thu - 21:00, 22:01 -; Fri - 21:00; Sun 22:01 -" + }, + { + "assetType": "INDEX", + "baseAsset": "DE40.", + "baseAssetPrecision": 2, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "253" + } + ], + "industry": "", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Germany 40", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "DE40.", + "tickSize": 0.1, + "tickValue": 2526.95, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00; Sun 22:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "AUD", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.07169900" + } + ], + "industry": "", + "longRate": -0.0023868, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "AUD/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0058332, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "AUD/USD_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 7.17e-06, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "NZD", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "10" + } + ], + "industry": "", + "longRate": 0.0005909, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "NZD/JPY", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "JPY", + "quoteAssetId": "JPY_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": -0.0088109, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "NZD/JPY_LEVERAGE", + "tickSize": 0.001, + "tickValue": 0.0930675, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "GBP", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.10569501" + } + ], + "industry": "", + "longRate": 0.0068146, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "GBP/CHF", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "CHF", + "quoteAssetId": "CHF_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0150346, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "GBP/CHF_LEVERAGE", + "tickSize": 5e-05, + "tickValue": 5.285e-05, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "INDEX", + "baseAsset": "US30.", + "baseAssetPrecision": 2, + "country": "US", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "100", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "507" + } + ], + "industry": "", + "longRate": -0.0214013, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "US Wall Street 30", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0008209, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "US30.", + "tickSize": 1, + "tickValue": 50665.5, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 22:01 -; Tue - 21:00, 22:01 -; Wed - 21:00, 22:01 -; Thu - 21:00, 22:01 -; Fri - 21:00; Sun 22:01 -" + }, + { + "assetType": "EQUITY", + "baseAsset": "ADS.DE.", + "baseAssetPrecision": 2, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "6500", + "minQty": "1", + "stepSize": "1" + } + ], + "industry": "Footwear", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Adidas", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "Cyclical Consumer Goods & Services", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "ADS.DE.", + "tickSize": 0.05, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "EQUITY", + "baseAsset": "A2A.", + "baseAssetPrecision": 3, + "country": "IT", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "345000", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "3" + } + ], + "industry": "Electric Utilities", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "A2A", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Utilities", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "A2A.", + "tickSize": 0.001, + "tickValue": 0.0023185, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "INDEX", + "baseAsset": "FR40.", + "baseAssetPrecision": 2, + "country": "FR", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "83" + } + ], + "industry": "", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "France 40", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "FR40.", + "tickSize": 0.1, + "tickValue": 820.69, + "tradingFee": 0, + "tradingHours": "UTC; Mon 00:15 - 20:00; Tue 00:15 - 20:00; Wed 00:15 - 20:00; Thu 00:15 - 20:00; Fri 00:15 - 20:00" + }, + { + "assetType": "CURRENCY", + "baseAsset": "GBP", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.18775801" + } + ], + "industry": "", + "longRate": -0.005853, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "GBP/AUD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "AUD", + "quoteAssetId": "AUD_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.002367, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "GBP/AUD_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 1.878e-05, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "USD", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.07851251" + } + ], + "industry": "", + "longRate": 0.0067006, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "USD/CHF", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "CHF", + "quoteAssetId": "CHF_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0149206, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "USD/CHF_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 7.85e-06, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "EUR", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.09126150" + } + ], + "industry": "", + "longRate": 0.0020053, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "EUR/CHF", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "CHF", + "quoteAssetId": "CHF_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0102253, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "EUR/CHF_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 9.13e-06, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Gold.", + "baseAssetPrecision": 2, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "46" + } + ], + "industry": "", + "longRate": -0.0152567, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Gold", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": 0.0070367, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "Gold.", + "tickSize": 0.01, + "tickValue": 45.1313, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59, 22:00 -; Tue - 20:59, 22:00 -; Wed - 20:59, 22:00 -; Thu - 20:59, 22:00 -; Fri - 20:59; Sun 22:00 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Silver.", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "100000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "8" + } + ], + "industry": "", + "longRate": -0.0151374, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Silver", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.0069174, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "Silver.", + "tickSize": 0.001, + "tickValue": 0.076189, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59, 22:00 -; Tue - 20:59, 22:00 -; Wed - 20:59, 22:00 -; Thu - 20:59, 22:00 -; Fri - 20:59; Sun 22:00 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "BTC", + "baseAssetPrecision": 4, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "100", + "minQty": "0.0001", + "stepSize": "0.0001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "7" + } + ], + "industry": "", + "longRate": -0.01, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "BTC/EUR", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 4, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 480, + "symbol": "BTC/EUR_LEVERAGE", + "tickSize": 0.05, + "tickValue": 3336.44625, + "tradingFee": 0.06, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "ETH", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.001", + "stepSize": "0.001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00002748" + } + ], + "industry": "", + "longRate": -0.06, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "ETH/BTC", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "BTC", + "quoteAssetId": "BTC_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.01, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "ETH/BTC_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 2.7e-07, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "BTC", + "baseAssetPrecision": 4, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.0001", + "stepSize": "0.0001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "8" + } + ], + "industry": "", + "longRate": -0.01, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "BTC/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 4, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 480, + "symbol": "BTC/USD_LEVERAGE", + "tickSize": 0.05, + "tickValue": 3878.86, + "tradingFee": 0.06, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "BCH", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "2500", + "minQty": "0.001", + "stepSize": "0.001" + } + ], + "industry": "", + "longRate": -0.06, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "BCH/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "BCH/USD_LEVERAGE", + "tickSize": 0.05, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "INDEX", + "baseAsset": "US500.", + "baseAssetPrecision": 2, + "country": "US", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "76" + } + ], + "industry": "", + "longRate": -0.0214013, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "US 500", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0008209, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "US500.", + "tickSize": 0.1, + "tickValue": 754.18, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 22:01 -; Tue - 21:00, 22:01 -; Wed - 21:00, 22:01 -; Thu - 21:00, 22:01 -; Fri - 21:00; Sun 22:01 -" + }, + { + "assetType": "INDEX", + "baseAsset": "SP35.", + "baseAssetPrecision": 2, + "country": "ES", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "185" + } + ], + "industry": "", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Spain 35", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "SP35.", + "tickSize": 0.1, + "tickValue": 1841.57, + "tradingFee": 0, + "tradingHours": "UTC; Mon 06:01 - 17:55; Tue 06:01 - 17:55; Wed 06:01 - 17:55; Thu 06:01 - 17:55; Fri 06:01 - 17:55" + }, + { + "assetType": "EQUITY", + "baseAsset": "VOW3.", + "baseAssetPrecision": 2, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "8500", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "91" + } + ], + "industry": "Auto & Truck Manufacturers", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Volkswagen AG (Pfd)", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "Cyclical Consumer Goods & Services", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "VOW3.", + "tickSize": 0.05, + "tickValue": 4.5425, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "EQUITY", + "baseAsset": "UN0.", + "baseAssetPrecision": 3, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "18500", + "minQty": "1", + "stepSize": "1" + } + ], + "industry": "Electric Utilities", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Uniper", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Utilities", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "UN0.", + "tickSize": 0.005, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "LTC", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.001", + "stepSize": "0.001" + } + ], + "industry": "", + "longRate": -0.06, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "LTC/EUR", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "LTC/EUR_LEVERAGE", + "tickSize": 0.01, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00; Sun 21:05 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "LTC", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.001", + "stepSize": "0.001" + } + ], + "industry": "", + "longRate": -0.06, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "LTC/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "LTC/USD_LEVERAGE", + "tickSize": 0.01, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "CNH", + "baseAssetPrecision": 4, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1500000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "3" + } + ], + "industry": "", + "longRate": -0.0028201, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "CNH/JPY", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "JPY", + "quoteAssetId": "JPY_LEVERAGE", + "quotePrecision": 4, + "sector": "", + "shortRate": -0.0053999, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "CNH/JPY_LEVERAGE", + "tickSize": 0.0001, + "tickValue": 0.00234713, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "EQUITY", + "baseAsset": "LHA.", + "baseAssetPrecision": 3, + "country": "DE", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "65000", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "9" + } + ], + "industry": "Airlines", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Lufthansa", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Industrials", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "LHA.", + "tickSize": 0.005, + "tickValue": 0.04075, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "EQUITY", + "baseAsset": "OHL.", + "baseAssetPrecision": 4, + "country": "ES", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "85975", + "minQty": "1", + "stepSize": "1" + } + ], + "industry": "Construction & Engineering", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 20.0, + "maxTPGap": 20.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "OHL", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 4, + "sector": "Industrials", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "OHL.", + "tickSize": 0.0005, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "INDEX", + "baseAsset": "NL25.", + "baseAssetPrecision": 2, + "country": "NL", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "11" + } + ], + "industry": "", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Netherlands 25", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "NL25.", + "tickSize": 0.01, + "tickValue": 10.47275, + "tradingFee": 0, + "tradingHours": "UTC; Mon 06:00 - 20:00; Tue 06:00 - 20:00; Wed 06:00 - 20:00; Thu 06:00 - 20:00; Fri 06:00 - 20:00" + }, + { + "assetType": "CURRENCY", + "baseAsset": "NZD", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.05843601" + } + ], + "industry": "", + "longRate": -0.0079041, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "NZD/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0003159, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "NZD/USD_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 5.84e-06, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Platinum.", + "baseAssetPrecision": 2, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "196" + } + ], + "industry": "", + "longRate": -0.0129182, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Platinum", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": 0.0046982, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "Platinum.", + "tickSize": 0.01, + "tickValue": 19.5139, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 22:05 -; Tue - 21:00, 22:05 -; Wed - 21:00, 22:05 -; Thu - 21:00, 22:05 -; Fri - 21:00; Sun 22:05 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Palladium.", + "baseAssetPrecision": 2, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.01", + "stepSize": "0.01" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "14" + } + ], + "industry": "", + "longRate": -0.013855, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Palladium", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": 0.005635, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "Palladium.", + "tickSize": 0.01, + "tickValue": 13.861, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 22:05 -; Tue - 21:00, 22:05 -; Wed - 21:00, 22:05 -; Thu - 21:00, 22:05 -; Fri - 21:00; Sun 22:05 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Oil - Crude.", + "baseAssetPrecision": 2, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "100000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "10" + } + ], + "industry": "", + "longRate": 0.1389493, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Crude Oil", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.1608693, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "Oil - Crude.", + "tickSize": 0.01, + "tickValue": 0.92875, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59, 22:00 -; Tue - 20:59, 22:00 -; Wed - 20:59, 22:00 -; Thu - 20:59, 22:00 -; Fri - 20:59; Sun 22:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "GBP", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "22" + } + ], + "industry": "", + "longRate": 0.0043436, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "GBP/JPY", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "JPY", + "quoteAssetId": "JPY_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": -0.0125636, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "GBP/JPY_LEVERAGE", + "tickSize": 0.001, + "tickValue": 0.214402, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Oil - Brent.", + "baseAssetPrecision": 2, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "100000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "10" + } + ], + "industry": "", + "longRate": 0.1291556, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Brent Oil", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.1510756, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "Oil - Brent.", + "tickSize": 0.01, + "tickValue": 0.9702, + "tradingFee": 0, + "tradingHours": "UTC; Sun 22:00 -; Mon - 22:00; Tue 00:00 - 22:00; Wed 00:00 - 22:00; Thu 00:00 - 22:00; Fri 00:00 - 21:00" + }, + { + "assetType": "EQUITY", + "baseAsset": "AFp.", + "baseAssetPrecision": 3, + "country": "FR", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "90000", + "minQty": "0.1", + "stepSize": "0.1" + } + ], + "industry": "Airlines", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Air France", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Industrials", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "AFp.", + "tickSize": 0.005, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "XLM", + "baseAssetPrecision": 5, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.15069000" + } + ], + "industry": "", + "longRate": -0.0684932, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "XLM/USD", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 5, + "sector": "", + "shortRate": -0.0684932, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "XLM/USD_LEVERAGE", + "tickSize": 1e-05, + "tickValue": 1.51e-06, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 21:05 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "COMMODITY", + "baseAsset": "Natural Gas.", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000000", + "minQty": "0.1", + "stepSize": "0.1" + } + ], + "industry": "", + "longRate": -0.01096, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Natural Gas", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": -0.01096, + "status": "HALT", + "swapChargeInterval": 1440, + "symbol": "Natural Gas.", + "tickSize": 0.001, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 21:00, 22:05 -; Tue - 21:00, 22:05 -; Wed - 21:00, 22:05 -; Thu - 21:00, 22:05 -; Fri - 21:00; Sun 22:05 -" + }, + { + "assetType": "CRYPTOCURRENCY", + "baseAsset": "LTC", + "baseAssetPrecision": 8, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000", + "minQty": "0.001", + "stepSize": "0.001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00000069" + } + ], + "industry": "", + "longRate": -0.06, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "LTC/BTC", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "BTC", + "quoteAssetId": "BTC_LEVERAGE", + "quotePrecision": 8, + "sector": "", + "shortRate": -0.01, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "LTC/BTC_LEVERAGE", + "tickSize": 1e-08, + "tickValue": 0, + "tradingFee": 0.075, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "USD", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "16" + } + ], + "industry": "", + "longRate": 0.0042521, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "USD/JPY", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "JPY", + "quoteAssetId": "JPY_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": -0.0124721, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "USD/JPY_LEVERAGE", + "tickSize": 0.001, + "tickValue": 0.159269, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "CURRENCY", + "baseAsset": "EUR", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "10000000", + "minQty": "0.1", + "stepSize": "0.1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "19" + } + ], + "industry": "", + "longRate": -0.000433, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "EUR/JPY", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "JPY", + "quoteAssetId": "JPY_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": -0.007787, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "EUR/JPY_LEVERAGE", + "tickSize": 0.001, + "tickValue": 0.185132, + "tradingFee": 0, + "tradingHours": "UTC; Mon - 20:59:50, 21:05 -; Tue - 20:59:50, 21:05 -; Wed - 20:59:50, 21:05 -; Thu - 20:59:50, 21:05 -; Fri - 20:59:50; Sun 21:00 -" + }, + { + "assetType": "INDEX", + "baseAsset": "CN50.", + "baseAssetPrecision": 2, + "country": "CN", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.01", + "stepSize": "0.01" + } + ], + "industry": "", + "longRate": -0.01497, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 30.0, + "maxTPGap": 30.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "China A50", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "USD", + "quoteAssetId": "USD_LEVERAGE", + "quotePrecision": 2, + "sector": "", + "shortRate": -0.00725, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "CN50.", + "tickSize": 1, + "tradingFee": 0, + "tradingHours": "UTC; Mon 01:00 - 08:30, 09:00 - 21:00; Tue 01:00 - 08:30, 09:00 - 21:00; Wed 01:00 - 08:30, 09:00 - 21:00; Thu 01:00 - 08:30, 09:00 - 21:00; Fri 01:00 - 08:30, 09:00 - 21:00" + }, + { + "assetType": "EQUITY", + "baseAsset": "PHAR.", + "baseAssetPrecision": 3, + "country": "NL", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "110880", + "minQty": "1", + "stepSize": "1" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "2" + } + ], + "industry": "Biotechnology & Medical Research", + "longRate": -0.0165933, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 20.0, + "maxTPGap": 20.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "Pharming Group", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "Healthcare", + "shortRate": -0.0056289, + "status": "TRADING", + "swapChargeInterval": 1440, + "symbol": "PHAR.", + "tickSize": 0.001, + "tickValue": 0.0011745, + "tradingFee": 0, + "tradingHours": "UTC; Mon 07:00 - 15:30; Tue 07:00 - 15:30; Wed 07:00 - 15:30; Thu 07:00 - 15:30; Fri 07:00 - 15:30" + } + ], + "timezone": "UTC" +} diff --git a/app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_1m.json b/app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_1m.json new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_LEVERAGE_1m.json b/app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_LEVERAGE_1m.json new file mode 100644 index 0000000..24797f6 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/klines/BTC_USD_LEVERAGE_1m.json @@ -0,0 +1,82 @@ +[ + [ + 1783537320000, + "62156.40", + "62160.70", + "62118.35", + "62144.60", + 200 + ], + [ + 1783537380000, + "62148.50", + "62183.05", + "62123.55", + "62157.45", + 246 + ], + [ + 1783537440000, + "62154.40", + "62155.00", + "62122.10", + "62128.10", + 153 + ], + [ + 1783537500000, + "62125.05", + "62129.10", + "62085.00", + "62113.70", + 178 + ], + [ + 1783537560000, + "62111.80", + "62117.60", + "62085.20", + "62099.00", + 115 + ], + [ + 1783537620000, + "62101.70", + "62130.80", + "62101.70", + "62111.10", + 121 + ], + [ + 1783537680000, + "62109.90", + "62114.70", + "62066.20", + "62085.10", + 219 + ], + [ + 1783537740000, + "62082.90", + "62106.65", + "62065.00", + "62103.90", + 244 + ], + [ + 1783537800000, + "62102.60", + "62104.50", + "62064.65", + "62078.20", + 119 + ], + [ + 1783537860000, + "62076.90", + "62077.70", + "62035.10", + "62041.60", + 78 + ] +] diff --git a/app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/BTC_USD_LEVERAGE.json b/app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/BTC_USD_LEVERAGE.json new file mode 100644 index 0000000..68a0c44 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/BTC_USD_LEVERAGE.json @@ -0,0 +1,18 @@ +{ + "askPrice": "62039.05", + "bidPrice": "62038.95", + "closeTime": 1783537908139, + "highPrice": "63727.2", + "lastPrice": "62038.95", + "lastQty": "5.0", + "lowPrice": "61479", + "openPrice": "63334.10", + "openTime": 1783468800000, + "prevClosePrice": "63334.10", + "priceChange": "-1292.15", + "priceChangePercent": "-2.040309", + "quoteVolume": "1480205.506845", + "symbol": "BTC/USD_LEVERAGE", + "volume": "23.8199", + "weightedAvgPrice": "62039.00" +} diff --git a/app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/README.md b/app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/README.md new file mode 100644 index 0000000..fcc6409 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/ticker24hr/README.md @@ -0,0 +1,11 @@ +# ticker24hr + +Источник: +GET /api/v1/ticker/24hr + +Назначение: +Исследование структуры ответа endpoint. + +Файлы: + +- BTC_USD_LEVERAGE.json \ No newline at end of file diff --git a/app/tools/dzengi_probe/runtime_samples/rest/time/response.json b/app/tools/dzengi_probe/runtime_samples/rest/time/response.json new file mode 100644 index 0000000..0d459ee --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/rest/time/response.json @@ -0,0 +1,3 @@ +{ + "serverTime": 1783537921216 +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/depth/BTC_USD_LEVERAGE.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/depth/BTC_USD_LEVERAGE.jsonl new file mode 100644 index 0000000..9397d0f --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/depth/BTC_USD_LEVERAGE.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-depth-request", "destination": "/api/v1/depth", "payload": {"asks": [[62055.05, 5.0]], "bids": [[62054.95, 5.0]], "lastUpdateId": 0}, "status": "OK"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/depthMarketData.subscribe/BTC_USD.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/depthMarketData.subscribe/BTC_USD.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.jsonl new file mode 100644 index 0000000..29f37e9 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-market-data-empty-symbols", "destination": "marketData.subscribe", "payload": {"code": -1128, "msg": "Mandatory parameter 'symbols' was not sent, was empty/null, or malformed."}, "status": "ERROR"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.request.json new file mode 100644 index 0000000..3609a60 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.empty-symbols.request.json @@ -0,0 +1,7 @@ +{ + "correlationId": "probe-market-data-empty-symbols", + "destination": "marketData.subscribe", + "payload": { + "symbols": [] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.jsonl new file mode 100644 index 0000000..bf87072 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-market-data-invalid-destination", "payload": {"errorCode": "BAD_REQUEST"}, "status": "ERROR"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.request.json new file mode 100644 index 0000000..bf5ea0b --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-destination.request.json @@ -0,0 +1,9 @@ +{ + "correlationId": "probe-market-data-invalid-destination", + "destination": "invalid.subscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE" + ] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.jsonl new file mode 100644 index 0000000..d6c8d81 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-market-data-invalid-symbol", "destination": "marketData.subscribe", "payload": {"subscriptions": {"INVALID/SYMBOL": "ERROR: INVALID/SYMBOL not found"}}, "status": "OK"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.request.json new file mode 100644 index 0000000..e6e12c3 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.invalid-symbol.request.json @@ -0,0 +1,9 @@ +{ + "correlationId": "probe-market-data-invalid-symbol", + "destination": "marketData.subscribe", + "payload": { + "symbols": [ + "INVALID/SYMBOL" + ] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.jsonl new file mode 100644 index 0000000..285b926 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.jsonl @@ -0,0 +1,854 @@ +{"correlationId": "probe-market-data-subscribe", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "PROCESSED"}}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62662.55, "bidQty": 5.0, "ofr": 62662.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579287}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62659.35, "bidQty": 5.0, "ofr": 62659.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579317}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62655.2, "bidQty": 5.0, "ofr": 62655.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579358}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62655.1, "bidQty": 5.0, "ofr": 62655.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579522}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62652.8, "bidQty": 5.0, "ofr": 62652.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579531}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62656.3, "bidQty": 5.0, "ofr": 62656.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579632}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62661.25, "bidQty": 5.0, "ofr": 62661.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579752}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62661.8, "bidQty": 5.0, "ofr": 62661.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579762}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.15, "bidQty": 5.0, "ofr": 62663.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579802}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62660.45, "bidQty": 5.0, "ofr": 62660.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579882}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62654.45, "bidQty": 5.0, "ofr": 62654.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579902}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62654.4, "bidQty": 5.0, "ofr": 62654.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594579962}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62660.85, "bidQty": 5.0, "ofr": 62660.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580002}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62662.25, "bidQty": 5.0, "ofr": 62662.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580083}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.5, "bidQty": 5.0, "ofr": 62663.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580193}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.3, "bidQty": 5.0, "ofr": 62663.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580203}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.6, "bidQty": 5.0, "ofr": 62663.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580253}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.8, "bidQty": 5.0, "ofr": 62663.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580385}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62661.7, "bidQty": 5.0, "ofr": 62661.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580633}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62662.85, "bidQty": 5.0, "ofr": 62662.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594580914}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62664.55, "bidQty": 5.0, "ofr": 62664.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594581334}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.7, "bidQty": 5.0, "ofr": 62663.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594581414}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.3, "bidQty": 5.0, "ofr": 62663.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594581861}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.1, "bidQty": 5.0, "ofr": 62663.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594581921}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.55, "bidQty": 5.0, "ofr": 62663.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594582253}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62665.35, "bidQty": 5.0, "ofr": 62665.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594582423}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62667.85, "bidQty": 5.0, "ofr": 62667.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594582453}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62668.05, "bidQty": 5.0, "ofr": 62668.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594582494}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62667.35, "bidQty": 5.0, "ofr": 62667.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594582595}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62667.25, "bidQty": 5.0, "ofr": 62667.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594582713}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62665.95, "bidQty": 5.0, "ofr": 62666.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594583144}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62662.75, "bidQty": 5.0, "ofr": 62662.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594583591}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62664.75, "bidQty": 5.0, "ofr": 62664.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594583751}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62662.55, "bidQty": 5.0, "ofr": 62662.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594583791}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62660.05, "bidQty": 5.0, "ofr": 62660.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594583872}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.2, "bidQty": 5.0, "ofr": 62663.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594583982}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62662.65, "bidQty": 5.0, "ofr": 62662.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594584605}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.7, "bidQty": 5.0, "ofr": 62663.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594584938}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62663.5, "bidQty": 5.0, "ofr": 62663.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594586481}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62666.45, "bidQty": 5.0, "ofr": 62666.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594586585}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62668.15, "bidQty": 5.0, "ofr": 62668.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594586634}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62668.6, "bidQty": 5.0, "ofr": 62668.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594586696}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62665.5, "bidQty": 5.0, "ofr": 62665.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594586717}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62668.1, "bidQty": 5.0, "ofr": 62668.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594586899}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62670.2, "bidQty": 5.0, "ofr": 62670.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594587541}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62675.8, "bidQty": 5.0, "ofr": 62675.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594587611}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62676.6, "bidQty": 5.0, "ofr": 62676.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594588007}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62672.55, "bidQty": 5.0, "ofr": 62672.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594588240}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62671.7, "bidQty": 5.0, "ofr": 62671.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594588889}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.05, "bidQty": 5.0, "ofr": 62673.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589009}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.85, "bidQty": 5.0, "ofr": 62673.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589079}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.2, "bidQty": 5.0, "ofr": 62673.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589140}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62670.45, "bidQty": 5.0, "ofr": 62670.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589270}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.05, "bidQty": 5.0, "ofr": 62673.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589299}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.45, "bidQty": 5.0, "ofr": 62673.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589376}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62674.75, "bidQty": 5.0, "ofr": 62674.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594589436}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62676.85, "bidQty": 5.0, "ofr": 62676.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594590048}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.95, "bidQty": 5.0, "ofr": 62674.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594590515}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62674.85, "bidQty": 5.0, "ofr": 62674.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594590545}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.95, "bidQty": 5.0, "ofr": 62674.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594590615}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62676.65, "bidQty": 5.0, "ofr": 62676.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594591042}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62674.25, "bidQty": 5.0, "ofr": 62674.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594591214}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62674.35, "bidQty": 5.0, "ofr": 62674.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594591816}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.55, "bidQty": 5.0, "ofr": 62673.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594591826}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62677.35, "bidQty": 5.0, "ofr": 62677.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594591876}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62676.05, "bidQty": 5.0, "ofr": 62676.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594591937}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62672.9, "bidQty": 5.0, "ofr": 62673.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594592020}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62673.45, "bidQty": 5.0, "ofr": 62673.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594592060}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62677.35, "bidQty": 5.0, "ofr": 62677.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594592226}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62675.8, "bidQty": 5.0, "ofr": 62675.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594594116}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62675.45, "bidQty": 5.0, "ofr": 62675.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594594146}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62676.95, "bidQty": 5.0, "ofr": 62677.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594594274}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62681.8, "bidQty": 5.0, "ofr": 62681.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596314}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62688.5, "bidQty": 5.0, "ofr": 62688.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596325}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62696.75, "bidQty": 5.0, "ofr": 62696.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596504}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.1, "bidQty": 5.0, "ofr": 62695.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596524}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62691.45, "bidQty": 5.0, "ofr": 62691.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596606}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.6, "bidQty": 5.0, "ofr": 62693.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596716}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.7, "bidQty": 5.0, "ofr": 62695.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596887}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.8, "bidQty": 5.0, "ofr": 62694.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596938}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62691.75, "bidQty": 5.0, "ofr": 62691.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594596957}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62691.25, "bidQty": 5.0, "ofr": 62691.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597067}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62687.85, "bidQty": 5.0, "ofr": 62687.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597077}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62687.35, "bidQty": 5.0, "ofr": 62687.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597117}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62687.95, "bidQty": 5.0, "ofr": 62688.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597177}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62683.45, "bidQty": 5.0, "ofr": 62683.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597187}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62688.75, "bidQty": 5.0, "ofr": 62688.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597491}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62689.45, "bidQty": 5.0, "ofr": 62689.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597588}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62686.85, "bidQty": 5.0, "ofr": 62686.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597764}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62689.6, "bidQty": 5.0, "ofr": 62689.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597915}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62686.85, "bidQty": 5.0, "ofr": 62686.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594597965}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62684.85, "bidQty": 5.0, "ofr": 62684.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598015}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62685.0, "bidQty": 5.0, "ofr": 62685.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598086}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62686.45, "bidQty": 5.0, "ofr": 62686.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598115}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62686.85, "bidQty": 5.0, "ofr": 62686.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598125}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62688.55, "bidQty": 5.0, "ofr": 62688.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598196}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62689.6, "bidQty": 5.0, "ofr": 62689.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598431}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62686.3, "bidQty": 5.0, "ofr": 62686.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594598692}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62689.25, "bidQty": 5.0, "ofr": 62689.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594599820}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62688.85, "bidQty": 5.0, "ofr": 62688.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594605046}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62689.15, "bidQty": 5.0, "ofr": 62689.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594605087}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62691.95, "bidQty": 5.0, "ofr": 62692.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594605097}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.7, "bidQty": 5.0, "ofr": 62694.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594605988}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.8, "bidQty": 5.0, "ofr": 62694.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594605997}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62696.05, "bidQty": 5.0, "ofr": 62696.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606007}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62698.15, "bidQty": 5.0, "ofr": 62698.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606027}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62699.05, "bidQty": 5.0, "ofr": 62699.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606047}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.55, "bidQty": 5.0, "ofr": 62694.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606107}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.95, "bidQty": 5.0, "ofr": 62696.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606471}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.95, "bidQty": 5.0, "ofr": 62695.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606575}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62697.5, "bidQty": 5.0, "ofr": 62697.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606804}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62698.55, "bidQty": 5.0, "ofr": 62698.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606875}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62699.15, "bidQty": 5.0, "ofr": 62699.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594606945}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62696.65, "bidQty": 5.0, "ofr": 62696.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594607150}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.9, "bidQty": 5.0, "ofr": 62694.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594607261}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.25, "bidQty": 5.0, "ofr": 62694.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594607312}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.85, "bidQty": 5.0, "ofr": 62694.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594607381}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62696.75, "bidQty": 5.0, "ofr": 62696.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594607464}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.55, "bidQty": 5.0, "ofr": 62694.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594608021}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62688.7, "bidQty": 5.0, "ofr": 62688.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594608031}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62687.9, "bidQty": 5.0, "ofr": 62688.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594608203}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62690.35, "bidQty": 5.0, "ofr": 62690.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594608982}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62690.55, "bidQty": 5.0, "ofr": 62690.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594608992}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.95, "bidQty": 5.0, "ofr": 62693.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594609122}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.7, "bidQty": 5.0, "ofr": 62692.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594609163}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.3, "bidQty": 5.0, "ofr": 62694.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594609213}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.15, "bidQty": 5.0, "ofr": 62693.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594609375}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.4, "bidQty": 5.0, "ofr": 62693.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594609607}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.55, "bidQty": 5.0, "ofr": 62695.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594609736}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.3, "bidQty": 5.0, "ofr": 62694.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594611129}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62690.5, "bidQty": 5.0, "ofr": 62690.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594611180}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.25, "bidQty": 5.0, "ofr": 62692.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594613769}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.7, "bidQty": 5.0, "ofr": 62692.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594613799}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.25, "bidQty": 5.0, "ofr": 62692.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594613829}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.8, "bidQty": 5.0, "ofr": 62693.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594614040}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.1, "bidQty": 5.0, "ofr": 62692.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594614292}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62691.2, "bidQty": 5.0, "ofr": 62691.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594614404}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62692.2, "bidQty": 5.0, "ofr": 62692.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594615769}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.05, "bidQty": 5.0, "ofr": 62693.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594615821}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.9, "bidQty": 5.0, "ofr": 62694.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594617086}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.55, "bidQty": 5.0, "ofr": 62693.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594617293}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62694.45, "bidQty": 5.0, "ofr": 62694.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594619891}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62693.85, "bidQty": 5.0, "ofr": 62693.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594620439}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.7, "bidQty": 5.0, "ofr": 62695.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594622115}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.9, "bidQty": 5.0, "ofr": 62696.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594622165}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.1, "bidQty": 5.0, "ofr": 62701.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594622785}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.25, "bidQty": 5.0, "ofr": 62704.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594622935}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.5, "bidQty": 5.0, "ofr": 62703.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594622946}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.15, "bidQty": 5.0, "ofr": 62705.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594623055}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.05, "bidQty": 5.0, "ofr": 62708.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594623075}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.45, "bidQty": 5.0, "ofr": 62708.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594623125}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.75, "bidQty": 5.0, "ofr": 62708.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594623135}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.05, "bidQty": 5.0, "ofr": 62705.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594623250}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.85, "bidQty": 5.0, "ofr": 62704.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594625154}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.75, "bidQty": 5.0, "ofr": 62704.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594625700}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.85, "bidQty": 5.0, "ofr": 62704.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594626033}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.75, "bidQty": 5.0, "ofr": 62704.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594626297}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.6, "bidQty": 5.0, "ofr": 62702.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594626456}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.0, "bidQty": 5.0, "ofr": 62702.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594626466}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.1, "bidQty": 5.0, "ofr": 62704.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594626626}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62700.55, "bidQty": 5.0, "ofr": 62700.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594626807}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.5, "bidQty": 5.0, "ofr": 62703.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594627502}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.15, "bidQty": 5.0, "ofr": 62704.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594627573}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.55, "bidQty": 5.0, "ofr": 62705.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594628967}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.7, "bidQty": 5.0, "ofr": 62701.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594629027}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.25, "bidQty": 5.0, "ofr": 62705.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594629261}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.55, "bidQty": 5.0, "ofr": 62705.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594629353}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.8, "bidQty": 5.0, "ofr": 62702.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594629374}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.4, "bidQty": 5.0, "ofr": 62704.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594632957}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.35, "bidQty": 5.0, "ofr": 62702.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633110}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.25, "bidQty": 5.0, "ofr": 62702.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633161}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.1, "bidQty": 5.0, "ofr": 62701.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633193}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62700.05, "bidQty": 5.0, "ofr": 62700.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633203}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.65, "bidQty": 5.0, "ofr": 62702.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633274}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.1, "bidQty": 5.0, "ofr": 62703.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633356}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.35, "bidQty": 5.0, "ofr": 62703.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633449}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.95, "bidQty": 5.0, "ofr": 62704.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594633542}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.85, "bidQty": 5.0, "ofr": 62701.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594636517}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.45, "bidQty": 5.0, "ofr": 62704.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594636711}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.85, "bidQty": 5.0, "ofr": 62701.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594637134}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62700.45, "bidQty": 5.0, "ofr": 62700.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594637212}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.8, "bidQty": 5.0, "ofr": 62702.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594637343}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.75, "bidQty": 5.0, "ofr": 62703.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594637393}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.35, "bidQty": 5.0, "ofr": 62707.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594647766}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.1, "bidQty": 5.0, "ofr": 62708.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594647877}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.5, "bidQty": 5.0, "ofr": 62709.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594647957}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.1, "bidQty": 5.0, "ofr": 62709.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594648321}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.7, "bidQty": 5.0, "ofr": 62713.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594648391}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.2, "bidQty": 5.0, "ofr": 62712.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594648562}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.05, "bidQty": 5.0, "ofr": 62714.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594649720}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.5, "bidQty": 5.0, "ofr": 62714.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594649790}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.35, "bidQty": 5.0, "ofr": 62714.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594649880}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.75, "bidQty": 5.0, "ofr": 62713.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594650249}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.6, "bidQty": 5.0, "ofr": 62711.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594650257}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.95, "bidQty": 5.0, "ofr": 62712.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594650379}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.25, "bidQty": 5.0, "ofr": 62713.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594650431}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.85, "bidQty": 5.0, "ofr": 62713.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594650698}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.75, "bidQty": 5.0, "ofr": 62712.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594651488}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.25, "bidQty": 5.0, "ofr": 62711.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594654072}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.4, "bidQty": 5.0, "ofr": 62712.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594654286}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.5, "bidQty": 5.0, "ofr": 62709.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594656133}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.25, "bidQty": 5.0, "ofr": 62710.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594656336}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.6, "bidQty": 5.0, "ofr": 62711.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594656428}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.25, "bidQty": 5.0, "ofr": 62710.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594656701}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.5, "bidQty": 5.0, "ofr": 62706.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594657087}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62695.7, "bidQty": 5.0, "ofr": 62695.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594662379}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62698.75, "bidQty": 5.0, "ofr": 62698.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594662607}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62700.15, "bidQty": 5.0, "ofr": 62700.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594662698}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.05, "bidQty": 5.0, "ofr": 62702.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594662789}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62699.2, "bidQty": 5.0, "ofr": 62699.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594662799}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62697.9, "bidQty": 5.0, "ofr": 62698.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594665313}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62698.15, "bidQty": 5.0, "ofr": 62698.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594665424}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.25, "bidQty": 5.0, "ofr": 62701.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594666014}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62700.55, "bidQty": 5.0, "ofr": 62700.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594666670}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.05, "bidQty": 5.0, "ofr": 62702.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594666842}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.2, "bidQty": 5.0, "ofr": 62701.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594666911}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.05, "bidQty": 5.0, "ofr": 62702.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594667108}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.0, "bidQty": 5.0, "ofr": 62703.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594667139}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.3, "bidQty": 5.0, "ofr": 62701.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594667651}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62698.9, "bidQty": 5.0, "ofr": 62699.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594670723}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62700.95, "bidQty": 5.0, "ofr": 62701.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594670979}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.0, "bidQty": 5.0, "ofr": 62702.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594671483}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.4, "bidQty": 5.0, "ofr": 62704.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594671818}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.95, "bidQty": 5.0, "ofr": 62705.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594671828}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.6, "bidQty": 5.0, "ofr": 62705.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594671888}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.05, "bidQty": 5.0, "ofr": 62707.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672048}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.75, "bidQty": 5.0, "ofr": 62713.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672058}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.65, "bidQty": 5.0, "ofr": 62711.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672118}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.8, "bidQty": 5.0, "ofr": 62707.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672174}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.25, "bidQty": 5.0, "ofr": 62712.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672394}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.75, "bidQty": 5.0, "ofr": 62712.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672457}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.8, "bidQty": 5.0, "ofr": 62709.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672507}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.7, "bidQty": 5.0, "ofr": 62710.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594672608}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.5, "bidQty": 5.0, "ofr": 62710.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594673574}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.3, "bidQty": 5.0, "ofr": 62708.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594674783}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.35, "bidQty": 5.0, "ofr": 62707.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594674829}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.85, "bidQty": 5.0, "ofr": 62707.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594674940}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.4, "bidQty": 5.0, "ofr": 62709.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594675196}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.3, "bidQty": 5.0, "ofr": 62705.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594675578}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.95, "bidQty": 5.0, "ofr": 62709.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594675764}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.45, "bidQty": 5.0, "ofr": 62709.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594675774}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.4, "bidQty": 5.0, "ofr": 62710.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594675819}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.3, "bidQty": 5.0, "ofr": 62708.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594675918}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.05, "bidQty": 5.0, "ofr": 62708.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594677000}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.9, "bidQty": 5.0, "ofr": 62709.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594677071}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.5, "bidQty": 5.0, "ofr": 62708.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594677111}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.05, "bidQty": 5.0, "ofr": 62708.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594677121}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.95, "bidQty": 5.0, "ofr": 62708.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594677222}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.55, "bidQty": 5.0, "ofr": 62707.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594678953}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.7, "bidQty": 5.0, "ofr": 62708.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594679200}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.4, "bidQty": 5.0, "ofr": 62709.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594679394}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.8, "bidQty": 5.0, "ofr": 62708.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680391}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.7, "bidQty": 5.0, "ofr": 62706.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680401}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.15, "bidQty": 5.0, "ofr": 62703.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680477}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.6, "bidQty": 5.0, "ofr": 62705.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680497}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.5, "bidQty": 5.0, "ofr": 62706.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680589}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.5, "bidQty": 5.0, "ofr": 62707.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680639}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.35, "bidQty": 5.0, "ofr": 62708.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680695}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.55, "bidQty": 5.0, "ofr": 62708.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594680847}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.5, "bidQty": 5.0, "ofr": 62707.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594681024}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.75, "bidQty": 5.0, "ofr": 62707.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594681145}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.65, "bidQty": 5.0, "ofr": 62708.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594681429}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.9, "bidQty": 5.0, "ofr": 62708.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594682418}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.35, "bidQty": 5.0, "ofr": 62708.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594682636}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.9, "bidQty": 5.0, "ofr": 62708.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594682710}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.3, "bidQty": 5.0, "ofr": 62705.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594683279}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.05, "bidQty": 5.0, "ofr": 62708.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594683446}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.8, "bidQty": 5.0, "ofr": 62708.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594684891}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.0, "bidQty": 5.0, "ofr": 62708.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594684902}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.65, "bidQty": 5.0, "ofr": 62707.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594684920}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.75, "bidQty": 5.0, "ofr": 62705.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594684950}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.5, "bidQty": 5.0, "ofr": 62705.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594684971}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.7, "bidQty": 5.0, "ofr": 62709.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594684991}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.35, "bidQty": 5.0, "ofr": 62709.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594685031}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.85, "bidQty": 5.0, "ofr": 62707.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594685142}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.1, "bidQty": 5.0, "ofr": 62711.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594685286}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.3, "bidQty": 5.0, "ofr": 62712.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594685378}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.55, "bidQty": 5.0, "ofr": 62710.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594685960}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.5, "bidQty": 5.0, "ofr": 62712.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594687599}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.75, "bidQty": 5.0, "ofr": 62710.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594687864}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.95, "bidQty": 5.0, "ofr": 62710.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594687942}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.75, "bidQty": 5.0, "ofr": 62713.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594688035}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.55, "bidQty": 5.0, "ofr": 62718.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594688087}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.25, "bidQty": 5.0, "ofr": 62717.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594688137}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.0, "bidQty": 5.0, "ofr": 62715.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594688157}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.95, "bidQty": 5.0, "ofr": 62716.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594688353}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.05, "bidQty": 5.0, "ofr": 62715.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689460}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.9, "bidQty": 5.0, "ofr": 62711.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689494}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.8, "bidQty": 5.0, "ofr": 62710.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689505}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.65, "bidQty": 5.0, "ofr": 62710.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689514}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.4, "bidQty": 5.0, "ofr": 62710.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689574}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.3, "bidQty": 5.0, "ofr": 62714.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689757}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.65, "bidQty": 5.0, "ofr": 62713.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689948}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.5, "bidQty": 5.0, "ofr": 62711.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594689958}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.8, "bidQty": 5.0, "ofr": 62711.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594690068}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.65, "bidQty": 5.0, "ofr": 62713.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594690178}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.1, "bidQty": 5.0, "ofr": 62714.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594691429}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.9, "bidQty": 5.0, "ofr": 62713.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594691635}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.25, "bidQty": 5.0, "ofr": 62709.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594691675}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.75, "bidQty": 5.0, "ofr": 62710.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594691793}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.85, "bidQty": 5.0, "ofr": 62710.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594691844}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.4, "bidQty": 5.0, "ofr": 62711.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594692434}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.75, "bidQty": 5.0, "ofr": 62711.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594694418}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.85, "bidQty": 5.0, "ofr": 62713.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594695165}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.25, "bidQty": 5.0, "ofr": 62714.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594695236}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.95, "bidQty": 5.0, "ofr": 62713.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594695909}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.85, "bidQty": 5.0, "ofr": 62712.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594695949}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.7, "bidQty": 5.0, "ofr": 62712.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594695989}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.25, "bidQty": 5.0, "ofr": 62713.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594696079}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.55, "bidQty": 5.0, "ofr": 62713.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594696604}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.35, "bidQty": 5.0, "ofr": 62714.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594697483}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.0, "bidQty": 5.0, "ofr": 62715.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594697971}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.85, "bidQty": 5.0, "ofr": 62714.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594698631}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.25, "bidQty": 5.0, "ofr": 62717.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594698794}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.6, "bidQty": 5.0, "ofr": 62717.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594698874}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.8, "bidQty": 5.0, "ofr": 62717.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594698932}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.35, "bidQty": 5.0, "ofr": 62716.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594698963}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.5, "bidQty": 5.0, "ofr": 62715.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594698972}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.15, "bidQty": 5.0, "ofr": 62715.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594699039}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.35, "bidQty": 5.0, "ofr": 62715.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594699136}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.75, "bidQty": 5.0, "ofr": 62715.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594699188}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.85, "bidQty": 5.0, "ofr": 62713.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594699198}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.15, "bidQty": 5.0, "ofr": 62715.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594699378}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.85, "bidQty": 5.0, "ofr": 62713.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594701454}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.85, "bidQty": 5.0, "ofr": 62714.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594701698}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.75, "bidQty": 5.0, "ofr": 62719.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594703090}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.55, "bidQty": 5.0, "ofr": 62720.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594703135}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.8, "bidQty": 5.0, "ofr": 62721.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594703177}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.5, "bidQty": 5.0, "ofr": 62719.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594703673}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.75, "bidQty": 5.0, "ofr": 62718.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594703734}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.05, "bidQty": 5.0, "ofr": 62716.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594704491}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.45, "bidQty": 5.0, "ofr": 62716.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594704624}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.15, "bidQty": 5.0, "ofr": 62718.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594704718}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.25, "bidQty": 5.0, "ofr": 62718.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594704850}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.8, "bidQty": 5.0, "ofr": 62719.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594704891}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.05, "bidQty": 5.0, "ofr": 62719.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594705031}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.5, "bidQty": 5.0, "ofr": 62719.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594705551}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.05, "bidQty": 5.0, "ofr": 62719.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594705677}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.4, "bidQty": 5.0, "ofr": 62716.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594705898}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.7, "bidQty": 5.0, "ofr": 62718.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594706160}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.8, "bidQty": 5.0, "ofr": 62719.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594706517}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.25, "bidQty": 5.0, "ofr": 62720.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594710280}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.05, "bidQty": 5.0, "ofr": 62720.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594710291}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.9, "bidQty": 5.0, "ofr": 62722.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594710520}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.15, "bidQty": 5.0, "ofr": 62722.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594710611}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.25, "bidQty": 5.0, "ofr": 62722.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594711296}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.65, "bidQty": 5.0, "ofr": 62720.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594711317}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.75, "bidQty": 5.0, "ofr": 62722.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594711460}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.65, "bidQty": 5.0, "ofr": 62722.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594711877}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.6, "bidQty": 5.0, "ofr": 62720.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594712062}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.85, "bidQty": 5.0, "ofr": 62721.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594712369}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.5, "bidQty": 5.0, "ofr": 62720.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594712378}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.15, "bidQty": 5.0, "ofr": 62720.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594712428}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.3, "bidQty": 5.0, "ofr": 62720.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594712578}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.55, "bidQty": 5.0, "ofr": 62718.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594714100}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.65, "bidQty": 5.0, "ofr": 62719.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594714372}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.95, "bidQty": 5.0, "ofr": 62720.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594714433}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.55, "bidQty": 5.0, "ofr": 62718.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594714482}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.4, "bidQty": 5.0, "ofr": 62719.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594714678}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.95, "bidQty": 5.0, "ofr": 62720.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716302}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.45, "bidQty": 5.0, "ofr": 62720.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716342}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.65, "bidQty": 5.0, "ofr": 62720.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716362}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.05, "bidQty": 5.0, "ofr": 62722.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716392}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.15, "bidQty": 5.0, "ofr": 62722.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716455}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.95, "bidQty": 5.0, "ofr": 62719.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716750}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.35, "bidQty": 5.0, "ofr": 62718.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716820}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.95, "bidQty": 5.0, "ofr": 62719.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716830}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.35, "bidQty": 5.0, "ofr": 62721.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716851}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.8, "bidQty": 5.0, "ofr": 62721.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716918}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.65, "bidQty": 5.0, "ofr": 62722.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594716989}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.2, "bidQty": 5.0, "ofr": 62720.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594717909}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.25, "bidQty": 5.0, "ofr": 62722.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594718316}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.0, "bidQty": 5.0, "ofr": 62720.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594718688}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.6, "bidQty": 5.0, "ofr": 62718.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594718779}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.35, "bidQty": 5.0, "ofr": 62720.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594718890}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.95, "bidQty": 5.0, "ofr": 62721.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594718970}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.25, "bidQty": 5.0, "ofr": 62721.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594719715}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.95, "bidQty": 5.0, "ofr": 62721.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594720397}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.25, "bidQty": 5.0, "ofr": 62721.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594720469}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.65, "bidQty": 5.0, "ofr": 62720.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594721158}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.6, "bidQty": 5.0, "ofr": 62718.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594721196}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.85, "bidQty": 5.0, "ofr": 62719.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594721405}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.2, "bidQty": 5.0, "ofr": 62720.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594721543}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.75, "bidQty": 5.0, "ofr": 62714.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594723636}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.3, "bidQty": 5.0, "ofr": 62712.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594723646}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.6, "bidQty": 5.0, "ofr": 62710.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594723929}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.95, "bidQty": 5.0, "ofr": 62711.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594723980}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.95, "bidQty": 5.0, "ofr": 62715.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724030}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.55, "bidQty": 5.0, "ofr": 62715.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724071}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.35, "bidQty": 5.0, "ofr": 62717.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724221}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.15, "bidQty": 5.0, "ofr": 62720.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724232}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.1, "bidQty": 5.0, "ofr": 62717.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724416}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.85, "bidQty": 5.0, "ofr": 62713.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724466}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.75, "bidQty": 5.0, "ofr": 62718.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724526}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.65, "bidQty": 5.0, "ofr": 62721.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724636}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.65, "bidQty": 5.0, "ofr": 62720.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724951}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.05, "bidQty": 5.0, "ofr": 62721.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594724962}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.15, "bidQty": 5.0, "ofr": 62721.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594725220}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.4, "bidQty": 5.0, "ofr": 62719.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594725976}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.05, "bidQty": 5.0, "ofr": 62721.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594726215}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.45, "bidQty": 5.0, "ofr": 62719.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594726735}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.45, "bidQty": 5.0, "ofr": 62716.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594726917}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.1, "bidQty": 5.0, "ofr": 62713.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594726927}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.65, "bidQty": 5.0, "ofr": 62714.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594727019}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.9, "bidQty": 5.0, "ofr": 62717.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594727069}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.75, "bidQty": 5.0, "ofr": 62717.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594727176}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.8, "bidQty": 5.0, "ofr": 62718.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594727835}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.65, "bidQty": 5.0, "ofr": 62718.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594732636}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.95, "bidQty": 5.0, "ofr": 62718.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594735822}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.25, "bidQty": 5.0, "ofr": 62716.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594735833}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.75, "bidQty": 5.0, "ofr": 62716.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594735936}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.0, "bidQty": 5.0, "ofr": 62718.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594736030}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.35, "bidQty": 5.0, "ofr": 62718.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594736081}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.1, "bidQty": 5.0, "ofr": 62719.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594737432}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.35, "bidQty": 5.0, "ofr": 62719.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594737567}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.25, "bidQty": 5.0, "ofr": 62717.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738423}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.75, "bidQty": 5.0, "ofr": 62718.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738453}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.95, "bidQty": 5.0, "ofr": 62718.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738512}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.5, "bidQty": 5.0, "ofr": 62723.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738522}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.1, "bidQty": 5.0, "ofr": 62730.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738562}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.25, "bidQty": 5.0, "ofr": 62733.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738612}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.65, "bidQty": 5.0, "ofr": 62731.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738653}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.75, "bidQty": 5.0, "ofr": 62730.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738754}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.6, "bidQty": 5.0, "ofr": 62731.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738853}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.95, "bidQty": 5.0, "ofr": 62732.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738883}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.15, "bidQty": 5.0, "ofr": 62735.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738903}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.65, "bidQty": 5.0, "ofr": 62735.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594738964}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.55, "bidQty": 5.0, "ofr": 62735.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739004}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.85, "bidQty": 5.0, "ofr": 62733.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739054}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.95, "bidQty": 5.0, "ofr": 62732.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739094}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.0, "bidQty": 5.0, "ofr": 62731.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739278}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.6, "bidQty": 5.0, "ofr": 62727.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739368}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.95, "bidQty": 5.0, "ofr": 62730.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739438}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.4, "bidQty": 5.0, "ofr": 62729.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739593}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.65, "bidQty": 5.0, "ofr": 62730.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739633}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.25, "bidQty": 5.0, "ofr": 62731.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739768}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.85, "bidQty": 5.0, "ofr": 62728.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594739868}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.95, "bidQty": 5.0, "ofr": 62730.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594740095}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.6, "bidQty": 5.0, "ofr": 62727.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594740104}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.25, "bidQty": 5.0, "ofr": 62729.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594740255}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.65, "bidQty": 5.0, "ofr": 62729.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594740306}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.5, "bidQty": 5.0, "ofr": 62728.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594742625}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.05, "bidQty": 5.0, "ofr": 62728.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743128}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.1, "bidQty": 5.0, "ofr": 62726.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743138}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.15, "bidQty": 5.0, "ofr": 62724.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743168}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.05, "bidQty": 5.0, "ofr": 62724.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743198}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.65, "bidQty": 5.0, "ofr": 62724.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743314}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.15, "bidQty": 5.0, "ofr": 62726.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743361}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.65, "bidQty": 5.0, "ofr": 62724.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743682}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.45, "bidQty": 5.0, "ofr": 62724.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743733}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.0, "bidQty": 5.0, "ofr": 62725.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594743904}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.15, "bidQty": 5.0, "ofr": 62722.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594744015}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.6, "bidQty": 5.0, "ofr": 62722.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594744215}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.75, "bidQty": 5.0, "ofr": 62722.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594744296}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.35, "bidQty": 5.0, "ofr": 62723.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594744619}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.15, "bidQty": 5.0, "ofr": 62722.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594744704}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.5, "bidQty": 5.0, "ofr": 62721.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594745279}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.95, "bidQty": 5.0, "ofr": 62723.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594745526}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.45, "bidQty": 5.0, "ofr": 62725.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594745618}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.35, "bidQty": 5.0, "ofr": 62723.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594746094}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.2, "bidQty": 5.0, "ofr": 62728.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594746273}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.6, "bidQty": 5.0, "ofr": 62726.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594746374}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.35, "bidQty": 5.0, "ofr": 62730.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594746684}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.65, "bidQty": 5.0, "ofr": 62726.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594746867}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.5, "bidQty": 5.0, "ofr": 62725.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594746878}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.45, "bidQty": 5.0, "ofr": 62731.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594747245}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.15, "bidQty": 5.0, "ofr": 62733.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594747406}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.95, "bidQty": 5.0, "ofr": 62731.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594747619}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.35, "bidQty": 5.0, "ofr": 62729.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594747816}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.5, "bidQty": 5.0, "ofr": 62729.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594748371}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.8, "bidQty": 5.0, "ofr": 62723.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594748453}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.2, "bidQty": 5.0, "ofr": 62729.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594748472}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.05, "bidQty": 5.0, "ofr": 62731.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594748712}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.5, "bidQty": 5.0, "ofr": 62731.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594749332}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.05, "bidQty": 5.0, "ofr": 62734.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594749392}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.95, "bidQty": 5.0, "ofr": 62732.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594749512}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.6, "bidQty": 5.0, "ofr": 62730.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594749532}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.4, "bidQty": 5.0, "ofr": 62731.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751413}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.2, "bidQty": 5.0, "ofr": 62732.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751509}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.95, "bidQty": 5.0, "ofr": 62736.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751650}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.55, "bidQty": 5.0, "ofr": 62735.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751699}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.95, "bidQty": 5.0, "ofr": 62736.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751719}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.95, "bidQty": 5.0, "ofr": 62733.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751740}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.2, "bidQty": 5.0, "ofr": 62735.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594751870}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.25, "bidQty": 5.0, "ofr": 62736.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752031}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.35, "bidQty": 5.0, "ofr": 62736.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752091}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.6, "bidQty": 5.0, "ofr": 62735.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752181}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.8, "bidQty": 5.0, "ofr": 62733.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752222}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.45, "bidQty": 5.0, "ofr": 62734.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752371}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.45, "bidQty": 5.0, "ofr": 62736.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752433}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.85, "bidQty": 5.0, "ofr": 62736.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752512}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.95, "bidQty": 5.0, "ofr": 62738.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752710}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.65, "bidQty": 5.0, "ofr": 62736.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752772}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.4, "bidQty": 5.0, "ofr": 62737.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752813}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.6, "bidQty": 5.0, "ofr": 62736.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752823}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.85, "bidQty": 5.0, "ofr": 62737.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594752968}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.55, "bidQty": 5.0, "ofr": 62738.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594753038}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62742.6, "bidQty": 5.0, "ofr": 62742.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594753246}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62740.35, "bidQty": 5.0, "ofr": 62740.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594753912}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.95, "bidQty": 5.0, "ofr": 62739.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594753990}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62740.15, "bidQty": 5.0, "ofr": 62740.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594755389}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.25, "bidQty": 5.0, "ofr": 62738.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594755569}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.95, "bidQty": 5.0, "ofr": 62739.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594755777}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.75, "bidQty": 5.0, "ofr": 62736.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594756263}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.65, "bidQty": 5.0, "ofr": 62738.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594756425}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.05, "bidQty": 5.0, "ofr": 62738.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594757114}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.15, "bidQty": 5.0, "ofr": 62735.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594757125}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.95, "bidQty": 5.0, "ofr": 62736.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594757426}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.75, "bidQty": 5.0, "ofr": 62737.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594757489}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.3, "bidQty": 5.0, "ofr": 62736.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594758693}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.05, "bidQty": 5.0, "ofr": 62737.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594758814}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.55, "bidQty": 5.0, "ofr": 62739.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594758879}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.65, "bidQty": 5.0, "ofr": 62739.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594759036}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.15, "bidQty": 5.0, "ofr": 62738.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594760510}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.85, "bidQty": 5.0, "ofr": 62735.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594760531}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.35, "bidQty": 5.0, "ofr": 62733.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594760577}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.2, "bidQty": 5.0, "ofr": 62737.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594760792}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.55, "bidQty": 5.0, "ofr": 62737.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594760914}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.3, "bidQty": 5.0, "ofr": 62737.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594762075}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.65, "bidQty": 5.0, "ofr": 62736.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594766055}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.05, "bidQty": 5.0, "ofr": 62737.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594766265}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.45, "bidQty": 5.0, "ofr": 62735.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594766275}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.8, "bidQty": 5.0, "ofr": 62735.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594766480}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.1, "bidQty": 5.0, "ofr": 62735.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594766837}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.65, "bidQty": 5.0, "ofr": 62735.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594767599}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.2, "bidQty": 5.0, "ofr": 62736.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594768301}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.55, "bidQty": 5.0, "ofr": 62735.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594769138}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.1, "bidQty": 5.0, "ofr": 62736.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594770269}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.05, "bidQty": 5.0, "ofr": 62737.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594773141}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.4, "bidQty": 5.0, "ofr": 62736.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594773809}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.5, "bidQty": 5.0, "ofr": 62737.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774018}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.55, "bidQty": 5.0, "ofr": 62739.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774080}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.0, "bidQty": 5.0, "ofr": 62737.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774445}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.55, "bidQty": 5.0, "ofr": 62738.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774545}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62740.95, "bidQty": 5.0, "ofr": 62741.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774595}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62741.15, "bidQty": 5.0, "ofr": 62741.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774655}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.05, "bidQty": 5.0, "ofr": 62739.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594774893}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.7, "bidQty": 5.0, "ofr": 62736.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594775857}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.05, "bidQty": 5.0, "ofr": 62738.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594776034}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.7, "bidQty": 5.0, "ofr": 62736.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594779713}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.55, "bidQty": 5.0, "ofr": 62737.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594780030}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.6, "bidQty": 5.0, "ofr": 62736.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782313}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.85, "bidQty": 5.0, "ofr": 62733.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782393}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.35, "bidQty": 5.0, "ofr": 62732.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782403}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.35, "bidQty": 5.0, "ofr": 62734.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782463}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.25, "bidQty": 5.0, "ofr": 62734.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782503}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.55, "bidQty": 5.0, "ofr": 62733.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782533}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.05, "bidQty": 5.0, "ofr": 62734.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782693}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.05, "bidQty": 5.0, "ofr": 62733.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782724}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.45, "bidQty": 5.0, "ofr": 62733.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594782997}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.6, "bidQty": 5.0, "ofr": 62730.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784183}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.85, "bidQty": 5.0, "ofr": 62732.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784334}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.95, "bidQty": 5.0, "ofr": 62732.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784344}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.85, "bidQty": 5.0, "ofr": 62729.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784364}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.8, "bidQty": 5.0, "ofr": 62730.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784385}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.0, "bidQty": 5.0, "ofr": 62730.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784394}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.75, "bidQty": 5.0, "ofr": 62729.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784446}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.55, "bidQty": 5.0, "ofr": 62730.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784519}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.05, "bidQty": 5.0, "ofr": 62730.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784589}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.55, "bidQty": 5.0, "ofr": 62726.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784777}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.4, "bidQty": 5.0, "ofr": 62725.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784849}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.3, "bidQty": 5.0, "ofr": 62727.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594784918}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.0, "bidQty": 5.0, "ofr": 62728.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594785018}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.95, "bidQty": 5.0, "ofr": 62727.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594785082}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.1, "bidQty": 5.0, "ofr": 62727.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594787661}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.85, "bidQty": 5.0, "ofr": 62725.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594787753}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.6, "bidQty": 5.0, "ofr": 62728.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594787861}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.9, "bidQty": 5.0, "ofr": 62729.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594788014}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.85, "bidQty": 5.0, "ofr": 62724.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594788964}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.5, "bidQty": 5.0, "ofr": 62724.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594788975}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.65, "bidQty": 5.0, "ofr": 62723.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594789034}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.1, "bidQty": 5.0, "ofr": 62725.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594789076}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.9, "bidQty": 5.0, "ofr": 62727.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594789136}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.6, "bidQty": 5.0, "ofr": 62727.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594789187}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.45, "bidQty": 5.0, "ofr": 62728.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594789253}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.15, "bidQty": 5.0, "ofr": 62727.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791039}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.95, "bidQty": 5.0, "ofr": 62729.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791150}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.6, "bidQty": 5.0, "ofr": 62731.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791262}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.9, "bidQty": 5.0, "ofr": 62732.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791540}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.65, "bidQty": 5.0, "ofr": 62729.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791699}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.95, "bidQty": 5.0, "ofr": 62730.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791832}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.95, "bidQty": 5.0, "ofr": 62729.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594791842}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.05, "bidQty": 5.0, "ofr": 62730.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594792031}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.8, "bidQty": 5.0, "ofr": 62733.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594792474}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.45, "bidQty": 5.0, "ofr": 62732.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594792555}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.0, "bidQty": 5.0, "ofr": 62729.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594792564}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.1, "bidQty": 5.0, "ofr": 62731.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594792995}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.2, "bidQty": 5.0, "ofr": 62728.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594793044}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.7, "bidQty": 5.0, "ofr": 62728.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594793232}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.95, "bidQty": 5.0, "ofr": 62728.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594793305}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.4, "bidQty": 5.0, "ofr": 62728.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594793491}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.0, "bidQty": 5.0, "ofr": 62726.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594796333}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.15, "bidQty": 5.0, "ofr": 62728.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594796637}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.0, "bidQty": 5.0, "ofr": 62726.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594797128}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.55, "bidQty": 5.0, "ofr": 62728.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594797453}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.45, "bidQty": 5.0, "ofr": 62728.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594800142}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.5, "bidQty": 5.0, "ofr": 62729.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594800191}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.55, "bidQty": 5.0, "ofr": 62726.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594800584}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.4, "bidQty": 5.0, "ofr": 62729.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594800764}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.05, "bidQty": 5.0, "ofr": 62728.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594801147}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62729.9, "bidQty": 5.0, "ofr": 62730.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594801259}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.45, "bidQty": 5.0, "ofr": 62725.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594801688}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.15, "bidQty": 5.0, "ofr": 62728.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594801851}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.65, "bidQty": 5.0, "ofr": 62724.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802140}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.45, "bidQty": 5.0, "ofr": 62722.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802161}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.15, "bidQty": 5.0, "ofr": 62722.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802226}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.35, "bidQty": 5.0, "ofr": 62721.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802318}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.95, "bidQty": 5.0, "ofr": 62722.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802494}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.35, "bidQty": 5.0, "ofr": 62722.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802616}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.75, "bidQty": 5.0, "ofr": 62722.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802712}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.95, "bidQty": 5.0, "ofr": 62723.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594802812}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.65, "bidQty": 5.0, "ofr": 62723.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803011}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.05, "bidQty": 5.0, "ofr": 62721.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803051}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.1, "bidQty": 5.0, "ofr": 62720.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803141}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.55, "bidQty": 5.0, "ofr": 62719.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803191}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.45, "bidQty": 5.0, "ofr": 62721.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803212}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.55, "bidQty": 5.0, "ofr": 62721.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803274}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.8, "bidQty": 5.0, "ofr": 62720.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803314}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.45, "bidQty": 5.0, "ofr": 62722.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803380}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.05, "bidQty": 5.0, "ofr": 62723.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803608}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.65, "bidQty": 5.0, "ofr": 62722.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803961}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.85, "bidQty": 5.0, "ofr": 62720.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594803991}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.5, "bidQty": 5.0, "ofr": 62721.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594804164}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.85, "bidQty": 5.0, "ofr": 62720.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594804171}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.25, "bidQty": 5.0, "ofr": 62721.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594804334}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.6, "bidQty": 5.0, "ofr": 62721.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594806773}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.15, "bidQty": 5.0, "ofr": 62723.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594806853}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.85, "bidQty": 5.0, "ofr": 62724.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594806978}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.15, "bidQty": 5.0, "ofr": 62723.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807450}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.2, "bidQty": 5.0, "ofr": 62724.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807551}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.95, "bidQty": 5.0, "ofr": 62725.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807591}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.7, "bidQty": 5.0, "ofr": 62726.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807677}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.5, "bidQty": 5.0, "ofr": 62725.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807707}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.4, "bidQty": 5.0, "ofr": 62724.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807718}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62725.9, "bidQty": 5.0, "ofr": 62726.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594807986}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.55, "bidQty": 5.0, "ofr": 62724.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594810344}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.5, "bidQty": 5.0, "ofr": 62722.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594810364}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.0, "bidQty": 5.0, "ofr": 62722.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594810424}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.4, "bidQty": 5.0, "ofr": 62722.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594810465}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.45, "bidQty": 5.0, "ofr": 62724.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594810532}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.9, "bidQty": 5.0, "ofr": 62725.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594810611}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.85, "bidQty": 5.0, "ofr": 62722.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811306}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.8, "bidQty": 5.0, "ofr": 62721.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811383}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.95, "bidQty": 5.0, "ofr": 62721.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811393}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.45, "bidQty": 5.0, "ofr": 62720.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811453}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.5, "bidQty": 5.0, "ofr": 62719.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811514}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.85, "bidQty": 5.0, "ofr": 62719.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811595}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.85, "bidQty": 5.0, "ofr": 62720.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811647}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.85, "bidQty": 5.0, "ofr": 62719.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811707}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.7, "bidQty": 5.0, "ofr": 62715.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811718}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.15, "bidQty": 5.0, "ofr": 62718.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811868}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.35, "bidQty": 5.0, "ofr": 62719.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594811977}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.05, "bidQty": 5.0, "ofr": 62718.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812319}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62716.85, "bidQty": 5.0, "ofr": 62716.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812572}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.65, "bidQty": 5.0, "ofr": 62713.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812602}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.05, "bidQty": 5.0, "ofr": 62713.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812694}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.45, "bidQty": 5.0, "ofr": 62712.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812744}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.35, "bidQty": 5.0, "ofr": 62712.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812794}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.65, "bidQty": 5.0, "ofr": 62712.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594812844}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.2, "bidQty": 5.0, "ofr": 62710.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594814230}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.7, "bidQty": 5.0, "ofr": 62708.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594814360}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.85, "bidQty": 5.0, "ofr": 62708.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594814401}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.7, "bidQty": 5.0, "ofr": 62709.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594814473}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.1, "bidQty": 5.0, "ofr": 62709.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594814912}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.2, "bidQty": 5.0, "ofr": 62706.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815126}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.1, "bidQty": 5.0, "ofr": 62707.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815290}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.0, "bidQty": 5.0, "ofr": 62709.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815420}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.75, "bidQty": 5.0, "ofr": 62709.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815553}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.15, "bidQty": 5.0, "ofr": 62709.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815564}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.6, "bidQty": 5.0, "ofr": 62712.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815715}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.15, "bidQty": 5.0, "ofr": 62711.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815854}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.4, "bidQty": 5.0, "ofr": 62710.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815863}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.9, "bidQty": 5.0, "ofr": 62710.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815903}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.25, "bidQty": 5.0, "ofr": 62708.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594815913}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.8, "bidQty": 5.0, "ofr": 62710.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594816111}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.15, "bidQty": 5.0, "ofr": 62707.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594817084}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.55, "bidQty": 5.0, "ofr": 62705.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594817104}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.1, "bidQty": 5.0, "ofr": 62708.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594817215}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.95, "bidQty": 5.0, "ofr": 62710.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594817284}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.6, "bidQty": 5.0, "ofr": 62706.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594819806}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.55, "bidQty": 5.0, "ofr": 62709.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594820030}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.1, "bidQty": 5.0, "ofr": 62709.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594821604}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.4, "bidQty": 5.0, "ofr": 62709.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594821633}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.5, "bidQty": 5.0, "ofr": 62708.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594821693}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62712.45, "bidQty": 5.0, "ofr": 62712.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594821704}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.7, "bidQty": 5.0, "ofr": 62714.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594821804}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.45, "bidQty": 5.0, "ofr": 62714.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594821854}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62713.8, "bidQty": 5.0, "ofr": 62713.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594822905}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.25, "bidQty": 5.0, "ofr": 62709.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594822915}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.9, "bidQty": 5.0, "ofr": 62705.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594822924}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.95, "bidQty": 5.0, "ofr": 62707.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823015}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.35, "bidQty": 5.0, "ofr": 62707.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823125}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62701.5, "bidQty": 5.0, "ofr": 62701.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823259}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.6, "bidQty": 5.0, "ofr": 62703.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823312}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.8, "bidQty": 5.0, "ofr": 62707.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823372}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.55, "bidQty": 5.0, "ofr": 62709.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823423}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.45, "bidQty": 5.0, "ofr": 62711.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823483}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.8, "bidQty": 5.0, "ofr": 62711.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823558}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62711.15, "bidQty": 5.0, "ofr": 62711.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823730}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.8, "bidQty": 5.0, "ofr": 62710.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823780}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.9, "bidQty": 5.0, "ofr": 62709.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823812}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.55, "bidQty": 5.0, "ofr": 62708.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823932}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.9, "bidQty": 5.0, "ofr": 62709.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594823995}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.45, "bidQty": 5.0, "ofr": 62708.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594824974}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.75, "bidQty": 5.0, "ofr": 62708.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825332}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.25, "bidQty": 5.0, "ofr": 62707.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825537}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.7, "bidQty": 5.0, "ofr": 62706.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825548}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.15, "bidQty": 5.0, "ofr": 62705.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825557}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.95, "bidQty": 5.0, "ofr": 62705.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825577}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.25, "bidQty": 5.0, "ofr": 62703.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825587}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.15, "bidQty": 5.0, "ofr": 62704.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825710}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.5, "bidQty": 5.0, "ofr": 62704.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594825774}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62702.5, "bidQty": 5.0, "ofr": 62702.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826449}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.5, "bidQty": 5.0, "ofr": 62705.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826556}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.5, "bidQty": 5.0, "ofr": 62707.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826674}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.5, "bidQty": 5.0, "ofr": 62705.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826748}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.05, "bidQty": 5.0, "ofr": 62707.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826930}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.55, "bidQty": 5.0, "ofr": 62705.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826960}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62703.6, "bidQty": 5.0, "ofr": 62703.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594826972}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.0, "bidQty": 5.0, "ofr": 62704.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594827219}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.65, "bidQty": 5.0, "ofr": 62704.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594827356}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.3, "bidQty": 5.0, "ofr": 62705.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594827861}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.7, "bidQty": 5.0, "ofr": 62704.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594828748}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.9, "bidQty": 5.0, "ofr": 62706.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594828880}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.75, "bidQty": 5.0, "ofr": 62707.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594828929}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62706.55, "bidQty": 5.0, "ofr": 62706.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830381}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62708.95, "bidQty": 5.0, "ofr": 62709.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830402}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.5, "bidQty": 5.0, "ofr": 62707.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830463}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62707.0, "bidQty": 5.0, "ofr": 62707.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830472}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62710.35, "bidQty": 5.0, "ofr": 62710.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830493}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62704.35, "bidQty": 5.0, "ofr": 62704.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830513}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62705.5, "bidQty": 5.0, "ofr": 62705.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830592}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62709.25, "bidQty": 5.0, "ofr": 62709.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594830613}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.15, "bidQty": 5.0, "ofr": 62714.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594838663}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.85, "bidQty": 5.0, "ofr": 62718.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594838724}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62721.05, "bidQty": 5.0, "ofr": 62721.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594838753}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62728.65, "bidQty": 5.0, "ofr": 62728.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594838814}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62727.55, "bidQty": 5.0, "ofr": 62727.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594838833}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.45, "bidQty": 5.0, "ofr": 62726.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594838864}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.75, "bidQty": 5.0, "ofr": 62722.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594839295}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62723.65, "bidQty": 5.0, "ofr": 62723.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594839524}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62722.9, "bidQty": 5.0, "ofr": 62723.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594840750}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.55, "bidQty": 5.0, "ofr": 62720.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594840779}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.45, "bidQty": 5.0, "ofr": 62720.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594840812}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62718.25, "bidQty": 5.0, "ofr": 62718.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594840893}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62714.2, "bidQty": 5.0, "ofr": 62714.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594840903}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62715.6, "bidQty": 5.0, "ofr": 62715.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594840954}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62717.8, "bidQty": 5.0, "ofr": 62717.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594841083}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.15, "bidQty": 5.0, "ofr": 62720.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594842671}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.8, "bidQty": 5.0, "ofr": 62720.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594842762}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.9, "bidQty": 5.0, "ofr": 62720.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594842782}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62719.7, "bidQty": 5.0, "ofr": 62719.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594849144}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.5, "bidQty": 5.0, "ofr": 62720.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594849294}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62720.45, "bidQty": 5.0, "ofr": 62720.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594852674}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62724.6, "bidQty": 5.0, "ofr": 62724.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594852775}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.45, "bidQty": 5.0, "ofr": 62726.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594852995}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62726.7, "bidQty": 5.0, "ofr": 62726.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594853433}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.5, "bidQty": 5.0, "ofr": 62735.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594853568}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.55, "bidQty": 5.0, "ofr": 62734.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594853588}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.2, "bidQty": 5.0, "ofr": 62736.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594853668}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62741.1, "bidQty": 5.0, "ofr": 62741.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594853945}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.35, "bidQty": 5.0, "ofr": 62738.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594854060}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.9, "bidQty": 5.0, "ofr": 62738.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594854401}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.45, "bidQty": 5.0, "ofr": 62739.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594855229}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.8, "bidQty": 5.0, "ofr": 62736.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594855979}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.85, "bidQty": 5.0, "ofr": 62733.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856020}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.95, "bidQty": 5.0, "ofr": 62733.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856080}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62730.05, "bidQty": 5.0, "ofr": 62730.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856130}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62732.05, "bidQty": 5.0, "ofr": 62732.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856395}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.45, "bidQty": 5.0, "ofr": 62733.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856455}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62731.95, "bidQty": 5.0, "ofr": 62732.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856654}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62733.65, "bidQty": 5.0, "ofr": 62733.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856893}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.35, "bidQty": 5.0, "ofr": 62734.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594856954}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.45, "bidQty": 5.0, "ofr": 62734.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857034}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62734.95, "bidQty": 5.0, "ofr": 62735.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857123}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62735.25, "bidQty": 5.0, "ofr": 62735.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857133}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.1, "bidQty": 5.0, "ofr": 62736.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857194}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.75, "bidQty": 5.0, "ofr": 62736.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857401}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62737.0, "bidQty": 5.0, "ofr": 62737.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857452}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.8, "bidQty": 5.0, "ofr": 62736.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857579}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62736.25, "bidQty": 5.0, "ofr": 62736.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594857987}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62740.05, "bidQty": 5.0, "ofr": 62740.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594858078}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.95, "bidQty": 5.0, "ofr": 62739.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594858185}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.3, "bidQty": 5.0, "ofr": 62739.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594858195}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62740.5, "bidQty": 5.0, "ofr": 62740.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594858428}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62738.95, "bidQty": 5.0, "ofr": 62739.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594858438}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62739.85, "bidQty": 5.0, "ofr": 62739.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594859196}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62741.8, "bidQty": 5.0, "ofr": 62741.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594859236}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62752.5, "bidQty": 5.0, "ofr": 62752.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594859780}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62750.85, "bidQty": 5.0, "ofr": 62750.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594859911}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62749.05, "bidQty": 5.0, "ofr": 62749.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860014}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62750.55, "bidQty": 5.0, "ofr": 62750.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860024}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62752.05, "bidQty": 5.0, "ofr": 62752.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860085}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62760.65, "bidQty": 5.0, "ofr": 62760.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860391}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62760.75, "bidQty": 5.0, "ofr": 62760.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860433}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62758.55, "bidQty": 5.0, "ofr": 62758.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860453}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62754.45, "bidQty": 5.0, "ofr": 62754.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860773}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62754.9, "bidQty": 5.0, "ofr": 62755.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594860832}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62754.35, "bidQty": 5.0, "ofr": 62754.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594861214}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62753.7, "bidQty": 5.0, "ofr": 62753.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594861244}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62757.9, "bidQty": 5.0, "ofr": 62758.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594861284}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62757.55, "bidQty": 5.0, "ofr": 62757.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594861546}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62758.25, "bidQty": 5.0, "ofr": 62758.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594861596}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62757.65, "bidQty": 5.0, "ofr": 62757.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594862138}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.9, "bidQty": 5.0, "ofr": 62756.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594862807}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.25, "bidQty": 5.0, "ofr": 62755.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594862857}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62753.95, "bidQty": 5.0, "ofr": 62754.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594862957}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62754.6, "bidQty": 5.0, "ofr": 62754.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594864241}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62754.95, "bidQty": 5.0, "ofr": 62755.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865350}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.75, "bidQty": 5.0, "ofr": 62755.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865399}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.95, "bidQty": 5.0, "ofr": 62756.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865499}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62756.85, "bidQty": 5.0, "ofr": 62756.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865509}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.4, "bidQty": 5.0, "ofr": 62755.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865778}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.25, "bidQty": 5.0, "ofr": 62755.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865828}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.05, "bidQty": 5.0, "ofr": 62755.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865889}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.35, "bidQty": 5.0, "ofr": 62755.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594865975}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62755.65, "bidQty": 5.0, "ofr": 62755.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594866036}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62757.05, "bidQty": 5.0, "ofr": 62757.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594866630}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62756.2, "bidQty": 5.0, "ofr": 62756.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867176}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62754.4, "bidQty": 5.0, "ofr": 62754.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867196}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62750.5, "bidQty": 5.0, "ofr": 62750.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867206}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62746.45, "bidQty": 5.0, "ofr": 62746.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867596}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62745.85, "bidQty": 5.0, "ofr": 62745.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867630}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62747.45, "bidQty": 5.0, "ofr": 62747.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867742}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62748.7, "bidQty": 5.0, "ofr": 62748.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594867792}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62752.45, "bidQty": 5.0, "ofr": 62752.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594868646}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62751.15, "bidQty": 5.0, "ofr": 62751.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594868697}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62750.45, "bidQty": 5.0, "ofr": 62750.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594868768}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62748.35, "bidQty": 5.0, "ofr": 62748.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594871894}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62744.2, "bidQty": 5.0, "ofr": 62744.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594871967}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62746.65, "bidQty": 5.0, "ofr": 62746.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594874498}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62746.95, "bidQty": 5.0, "ofr": 62747.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594874508}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62747.25, "bidQty": 5.0, "ofr": 62747.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594874559}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62745.05, "bidQty": 5.0, "ofr": 62745.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594874674}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62747.25, "bidQty": 5.0, "ofr": 62747.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594875756}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62742.45, "bidQty": 5.0, "ofr": 62742.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594876832}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62743.9, "bidQty": 5.0, "ofr": 62744.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594876983}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62742.75, "bidQty": 5.0, "ofr": 62742.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877032}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62742.9, "bidQty": 5.0, "ofr": 62743.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877072}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62743.2, "bidQty": 5.0, "ofr": 62743.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877132}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62743.1, "bidQty": 5.0, "ofr": 62743.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877167}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62744.2, "bidQty": 5.0, "ofr": 62744.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877186}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62743.05, "bidQty": 5.0, "ofr": 62743.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877490}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62744.05, "bidQty": 5.0, "ofr": 62744.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594877652}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62746.6, "bidQty": 5.0, "ofr": 62746.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878188}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62748.25, "bidQty": 5.0, "ofr": 62748.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878370}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62745.25, "bidQty": 5.0, "ofr": 62745.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878471}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62747.55, "bidQty": 5.0, "ofr": 62747.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878639}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62747.05, "bidQty": 5.0, "ofr": 62747.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878678}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62744.65, "bidQty": 5.0, "ofr": 62744.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878699}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62747.75, "bidQty": 5.0, "ofr": 62747.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878782}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62748.2, "bidQty": 5.0, "ofr": 62748.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594878792}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62751.15, "bidQty": 5.0, "ofr": 62751.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783594880981}, "status": "OK"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.jsonl new file mode 100644 index 0000000..ecc3b0f --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-market-data-missing-payload", "destination": "marketData.subscribe", "payload": {"code": -1128, "msg": "Mandatory parameter 'symbols' was not sent, was empty/null, or malformed."}, "status": "ERROR"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.request.json new file mode 100644 index 0000000..63e1469 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-payload.request.json @@ -0,0 +1,4 @@ +{ + "correlationId": "probe-market-data-missing-payload", + "destination": "marketData.subscribe" +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.jsonl new file mode 100644 index 0000000..00a07eb --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-market-data-missing-symbols", "destination": "marketData.subscribe", "payload": {"code": -1128, "msg": "Mandatory parameter 'symbols' was not sent, was empty/null, or malformed."}, "status": "ERROR"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.request.json new file mode 100644 index 0000000..dc5e2ba --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.missing-symbols.request.json @@ -0,0 +1,5 @@ +{ + "correlationId": "probe-market-data-missing-symbols", + "destination": "marketData.subscribe", + "payload": {} +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.jsonl new file mode 100644 index 0000000..f3d4d69 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.jsonl @@ -0,0 +1,148 @@ +{"correlationId": "probe-market-data-multi-symbol-1", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "PROCESSED", "ETH/USD_LEVERAGE": "PROCESSED"}}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62659.95, "bidQty": 5.0, "ofr": 62660.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616094717}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62653.8, "bidQty": 5.0, "ofr": 62653.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616094727}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62654.7, "bidQty": 5.0, "ofr": 62654.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616094829}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62656.9, "bidQty": 5.0, "ofr": 62657.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616095021}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62654.7, "bidQty": 5.0, "ofr": 62654.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616095571}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62656.9, "bidQty": 5.0, "ofr": 62657.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616095857}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62655.65, "bidQty": 5.0, "ofr": 62655.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616096813}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62654.3, "bidQty": 5.0, "ofr": 62654.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616097433}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62653.3, "bidQty": 5.0, "ofr": 62653.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616100105}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62650.65, "bidQty": 5.0, "ofr": 62650.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616100279}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62648.75, "bidQty": 5.0, "ofr": 62648.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616100289}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1737.35, "bidQty": 30.0, "ofr": 1737.41, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616100289}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1737.25, "bidQty": 30.0, "ofr": 1737.3, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616100300}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1737.01, "bidQty": 30.0, "ofr": 1737.07, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616100590}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.86, "bidQty": 30.0, "ofr": 1736.92, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616101712}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.88, "bidQty": 30.0, "ofr": 1736.93, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616101722}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62641.25, "bidQty": 5.0, "ofr": 62641.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616105630}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62636.95, "bidQty": 5.0, "ofr": 62637.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616105640}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62641.2, "bidQty": 5.0, "ofr": 62641.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616105853}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62636.9, "bidQty": 5.0, "ofr": 62637.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616108921}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.63, "bidQty": 30.0, "ofr": 1736.68, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616108991}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.64, "bidQty": 30.0, "ofr": 1736.7, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616109082}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62638.15, "bidQty": 5.0, "ofr": 62638.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616109162}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62638.25, "bidQty": 5.0, "ofr": 62638.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616109244}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62638.35, "bidQty": 5.0, "ofr": 62638.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616109305}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62638.1, "bidQty": 5.0, "ofr": 62638.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616109705}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62636.55, "bidQty": 5.0, "ofr": 62636.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616109945}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62637.3, "bidQty": 5.0, "ofr": 62637.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616110116}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62636.35, "bidQty": 5.0, "ofr": 62636.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616110305}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62635.85, "bidQty": 5.0, "ofr": 62635.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616110428}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.1, "bidQty": 5.0, "ofr": 62629.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616111388}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.5, "bidQty": 5.0, "ofr": 62630.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616111548}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62635.9, "bidQty": 5.0, "ofr": 62636.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616111818}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62635.7, "bidQty": 5.0, "ofr": 62635.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616111867}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62635.45, "bidQty": 5.0, "ofr": 62635.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616112084}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62634.0, "bidQty": 5.0, "ofr": 62634.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616113345}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.05, "bidQty": 5.0, "ofr": 62630.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616113434}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.45, "bidQty": 5.0, "ofr": 62630.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616113473}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62631.0, "bidQty": 5.0, "ofr": 62631.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616113563}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.45, "bidQty": 5.0, "ofr": 62630.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616113897}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.5, "bidQty": 5.0, "ofr": 62628.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114343}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62627.85, "bidQty": 5.0, "ofr": 62627.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114384}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.0, "bidQty": 5.0, "ofr": 62624.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114413}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62626.85, "bidQty": 5.0, "ofr": 62626.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114443}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.45, "bidQty": 5.0, "ofr": 62624.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114735}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.75, "bidQty": 5.0, "ofr": 62624.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114755}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.05, "bidQty": 5.0, "ofr": 62625.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616114816}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.9, "bidQty": 5.0, "ofr": 62626.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616115008}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.3, "bidQty": 5.0, "ofr": 62623.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616116420}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.35, "bidQty": 5.0, "ofr": 62622.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616116469}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62621.55, "bidQty": 5.0, "ofr": 62621.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616116614}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.15, "bidQty": 5.0, "ofr": 62622.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616116746}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.5, "bidQty": 5.0, "ofr": 62622.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616117274}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62634.45, "bidQty": 5.0, "ofr": 62634.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616117506}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.65, "bidQty": 30.0, "ofr": 1736.7, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616117525}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62633.95, "bidQty": 5.0, "ofr": 62634.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616117666}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62634.95, "bidQty": 5.0, "ofr": 62635.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616117715}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62633.95, "bidQty": 5.0, "ofr": 62634.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616119083}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.25, "bidQty": 5.0, "ofr": 62629.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616119093}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62633.45, "bidQty": 5.0, "ofr": 62633.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616119263}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.45, "bidQty": 5.0, "ofr": 62629.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616121051}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.2, "bidQty": 5.0, "ofr": 62629.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616121060}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.45, "bidQty": 5.0, "ofr": 62628.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616121131}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.7, "bidQty": 5.0, "ofr": 62628.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616121248}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.25, "bidQty": 5.0, "ofr": 62629.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616121319}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.0, "bidQty": 5.0, "ofr": 62630.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616121972}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.57, "bidQty": 30.0, "ofr": 1736.63, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616122165}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.6, "bidQty": 5.0, "ofr": 62629.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616122951}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.45, "bidQty": 5.0, "ofr": 62629.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616123137}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.6, "bidQty": 5.0, "ofr": 62629.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616123435}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.7, "bidQty": 5.0, "ofr": 62629.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616124591}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.05, "bidQty": 5.0, "ofr": 62630.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616124610}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62630.2, "bidQty": 5.0, "ofr": 62630.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616124640}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62632.25, "bidQty": 5.0, "ofr": 62632.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616124690}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62633.45, "bidQty": 5.0, "ofr": 62633.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616124701}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.95, "bidQty": 5.0, "ofr": 62623.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616125943}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.3, "bidQty": 5.0, "ofr": 62628.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616126017}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.35, "bidQty": 5.0, "ofr": 62629.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616126161}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.75, "bidQty": 5.0, "ofr": 62629.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616126221}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.85, "bidQty": 5.0, "ofr": 62629.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616126428}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.4, "bidQty": 5.0, "ofr": 62629.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616128726}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.3, "bidQty": 5.0, "ofr": 62628.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616128766}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.75, "bidQty": 5.0, "ofr": 62624.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616128776}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62626.25, "bidQty": 5.0, "ofr": 62626.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616128888}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.0, "bidQty": 5.0, "ofr": 62628.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616128976}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62627.65, "bidQty": 5.0, "ofr": 62627.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616129597}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.05, "bidQty": 5.0, "ofr": 62628.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616129766}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.05, "bidQty": 5.0, "ofr": 62629.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616129820}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62631.6, "bidQty": 5.0, "ofr": 62631.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616131457}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62619.6, "bidQty": 5.0, "ofr": 62619.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616133968}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62619.2, "bidQty": 5.0, "ofr": 62619.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616134027}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62618.65, "bidQty": 5.0, "ofr": 62618.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616134077}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62629.75, "bidQty": 5.0, "ofr": 62629.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616137324}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62627.35, "bidQty": 5.0, "ofr": 62627.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616137344}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.6, "bidQty": 5.0, "ofr": 62628.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616137434}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62627.35, "bidQty": 5.0, "ofr": 62627.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616137484}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.55, "bidQty": 5.0, "ofr": 62628.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616137716}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62627.35, "bidQty": 5.0, "ofr": 62627.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616140697}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.6, "bidQty": 5.0, "ofr": 62625.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616140741}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.2, "bidQty": 5.0, "ofr": 62624.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616140812}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.52, "bidQty": 30.0, "ofr": 1736.58, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616140832}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.45, "bidQty": 5.0, "ofr": 62625.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616140883}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.35, "bidQty": 5.0, "ofr": 62623.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616140892}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.05, "bidQty": 5.0, "ofr": 62624.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616140943}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62626.15, "bidQty": 5.0, "ofr": 62626.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616141005}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62626.7, "bidQty": 5.0, "ofr": 62626.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616141080}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.57, "bidQty": 30.0, "ofr": 1736.62, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616141341}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.53, "bidQty": 30.0, "ofr": 1736.58, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616141850}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.51, "bidQty": 30.0, "ofr": 1736.57, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616141859}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.25, "bidQty": 5.0, "ofr": 62625.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616141860}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.5, "bidQty": 5.0, "ofr": 62622.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616141951}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.45, "bidQty": 5.0, "ofr": 62623.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142030}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.95, "bidQty": 5.0, "ofr": 62624.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142080}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.75, "bidQty": 5.0, "ofr": 62622.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142120}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.55, "bidQty": 5.0, "ofr": 62623.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142180}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.95, "bidQty": 5.0, "ofr": 62624.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142323}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.8, "bidQty": 5.0, "ofr": 62623.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142400}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62620.35, "bidQty": 5.0, "ofr": 62620.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142733}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62619.3, "bidQty": 5.0, "ofr": 62619.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142743}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62620.9, "bidQty": 5.0, "ofr": 62621.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142855}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62621.85, "bidQty": 5.0, "ofr": 62621.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616142911}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.25, "bidQty": 5.0, "ofr": 62622.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616143018}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62622.4, "bidQty": 5.0, "ofr": 62622.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616143078}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62619.85, "bidQty": 5.0, "ofr": 62619.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616143863}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62619.15, "bidQty": 5.0, "ofr": 62619.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616144369}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62616.2, "bidQty": 5.0, "ofr": 62616.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616144380}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62615.95, "bidQty": 5.0, "ofr": 62616.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616144419}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62616.85, "bidQty": 5.0, "ofr": 62616.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616144429}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62614.45, "bidQty": 5.0, "ofr": 62614.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616144459}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62620.95, "bidQty": 5.0, "ofr": 62621.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616145440}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.55, "bidQty": 5.0, "ofr": 62623.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616147026}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62626.7, "bidQty": 5.0, "ofr": 62626.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616147096}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.5, "bidQty": 5.0, "ofr": 62625.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616147125}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.65, "bidQty": 5.0, "ofr": 62625.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616147136}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.15, "bidQty": 5.0, "ofr": 62628.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616147175}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.85, "bidQty": 5.0, "ofr": 62628.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616147451}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.5, "bidQty": 5.0, "ofr": 62628.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616149134}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.35, "bidQty": 5.0, "ofr": 62628.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616149412}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62628.5, "bidQty": 5.0, "ofr": 62628.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616149688}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.3, "bidQty": 5.0, "ofr": 62625.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616149831}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.45, "bidQty": 5.0, "ofr": 62625.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616150432}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 1736.46, "bidQty": 30.0, "ofr": 1736.52, "ofrQty": 30.0, "symbolName": "ETH/USD_LEVERAGE", "timestamp": 1783616150587}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62625.35, "bidQty": 5.0, "ofr": 62625.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616151192}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.1, "bidQty": 5.0, "ofr": 62624.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616151491}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.5, "bidQty": 5.0, "ofr": 62623.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616151553}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62624.4, "bidQty": 5.0, "ofr": 62624.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616151859}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62623.05, "bidQty": 5.0, "ofr": 62623.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783616154380}, "status": "OK"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.request.json new file mode 100644 index 0000000..22e36f0 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.multi-symbol.request.json @@ -0,0 +1,10 @@ +{ + "correlationId": "probe-market-data-multi-symbol", + "destination": "marketData.subscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE" + ] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.request.json new file mode 100644 index 0000000..564d5db --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.request.json @@ -0,0 +1,9 @@ +{ + "correlationId": "probe-market-data-subscribe", + "destination": "marketData.subscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE" + ] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.jsonl new file mode 100644 index 0000000..7f90fe0 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.jsonl @@ -0,0 +1 @@ +{"correlationId": "probe-market-data-unsubscribe-1", "payload": {"errorCode": "BAD_REQUEST"}, "status": "ERROR"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.request.json new file mode 100644 index 0000000..5d4b7bb --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.unsubscribe.request.json @@ -0,0 +1,9 @@ +{ + "correlationId": "probe-market-data-unsubscribe", + "destination": "marketData.unsubscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE" + ] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.jsonl new file mode 100644 index 0000000..7dee079 --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.jsonl @@ -0,0 +1,171 @@ +{"correlationId": "probe-market-data-subscribe-1", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "PROCESSED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-2", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-4", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-3", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-5", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-8", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-10", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-9", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-7", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-6", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-11", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-14", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-13", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-12", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-16", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-15", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-17", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-20", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-19", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"correlationId": "probe-market-data-subscribe-18", "destination": "marketData.subscribe", "payload": {"subscriptions": {"BTC/USD_LEVERAGE": "ALREADY_SUBSCRIBED"}}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.0, "bidQty": 5.0, "ofr": 62916.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613094329}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.75, "bidQty": 5.0, "ofr": 62914.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613094340}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62913.85, "bidQty": 5.0, "ofr": 62913.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613094380}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62913.95, "bidQty": 5.0, "ofr": 62914.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613094390}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62915.3, "bidQty": 5.0, "ofr": 62915.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613094474}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.65, "bidQty": 5.0, "ofr": 62916.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613094482}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.0, "bidQty": 5.0, "ofr": 62914.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613096181}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.25, "bidQty": 5.0, "ofr": 62914.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613096242}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.45, "bidQty": 5.0, "ofr": 62916.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613096262}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62912.3, "bidQty": 5.0, "ofr": 62912.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613096321}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.45, "bidQty": 5.0, "ofr": 62916.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613096351}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62911.8, "bidQty": 5.0, "ofr": 62911.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613096391}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62911.55, "bidQty": 5.0, "ofr": 62911.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613098328}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62910.75, "bidQty": 5.0, "ofr": 62910.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613098357}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62910.0, "bidQty": 5.0, "ofr": 62910.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613098408}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62900.2, "bidQty": 5.0, "ofr": 62900.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613101146}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62896.85, "bidQty": 5.0, "ofr": 62896.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613102736}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62896.65, "bidQty": 5.0, "ofr": 62896.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613102755}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.45, "bidQty": 5.0, "ofr": 62898.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613102765}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62896.95, "bidQty": 5.0, "ofr": 62897.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613102943}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62897.75, "bidQty": 5.0, "ofr": 62897.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103327}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.85, "bidQty": 5.0, "ofr": 62898.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103336}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.65, "bidQty": 5.0, "ofr": 62898.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103366}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62897.95, "bidQty": 5.0, "ofr": 62898.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103417}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62897.25, "bidQty": 5.0, "ofr": 62897.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103498}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62900.2, "bidQty": 5.0, "ofr": 62900.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103765}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.85, "bidQty": 5.0, "ofr": 62898.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613103928}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.25, "bidQty": 5.0, "ofr": 62898.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613105344}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.05, "bidQty": 5.0, "ofr": 62898.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613105479}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62898.95, "bidQty": 5.0, "ofr": 62899.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613105735}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62901.35, "bidQty": 5.0, "ofr": 62901.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613107016}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62903.1, "bidQty": 5.0, "ofr": 62903.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613107056}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62905.45, "bidQty": 5.0, "ofr": 62905.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613107108}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.25, "bidQty": 5.0, "ofr": 62914.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613107590}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.15, "bidQty": 5.0, "ofr": 62916.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613108258}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62915.45, "bidQty": 5.0, "ofr": 62915.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613108440}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.75, "bidQty": 5.0, "ofr": 62914.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613109059}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.95, "bidQty": 5.0, "ofr": 62915.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613109732}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.05, "bidQty": 5.0, "ofr": 62916.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613110034}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.6, "bidQty": 5.0, "ofr": 62914.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613110937}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.0, "bidQty": 5.0, "ofr": 62914.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613112184}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62915.2, "bidQty": 5.0, "ofr": 62915.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613114886}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.85, "bidQty": 5.0, "ofr": 62914.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613116438}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62911.85, "bidQty": 5.0, "ofr": 62911.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122559}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.05, "bidQty": 5.0, "ofr": 62914.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122610}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.65, "bidQty": 5.0, "ofr": 62922.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122660}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62924.45, "bidQty": 5.0, "ofr": 62924.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122820}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.8, "bidQty": 5.0, "ofr": 62926.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122850}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62929.2, "bidQty": 5.0, "ofr": 62929.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122870}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62930.95, "bidQty": 5.0, "ofr": 62931.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122942}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62929.25, "bidQty": 5.0, "ofr": 62929.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613122982}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.75, "bidQty": 5.0, "ofr": 62926.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123042}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.85, "bidQty": 5.0, "ofr": 62926.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123112}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62925.95, "bidQty": 5.0, "ofr": 62926.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123142}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.45, "bidQty": 5.0, "ofr": 62926.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123163}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.15, "bidQty": 5.0, "ofr": 62926.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123183}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62925.65, "bidQty": 5.0, "ofr": 62925.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123233}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62920.95, "bidQty": 5.0, "ofr": 62921.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123442}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.6, "bidQty": 5.0, "ofr": 62922.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123534}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62929.45, "bidQty": 5.0, "ofr": 62929.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123665}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62925.8, "bidQty": 5.0, "ofr": 62925.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613123795}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.2, "bidQty": 5.0, "ofr": 62923.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124489}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.05, "bidQty": 5.0, "ofr": 62922.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124588}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.85, "bidQty": 5.0, "ofr": 62923.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124598}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.55, "bidQty": 5.0, "ofr": 62922.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124618}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.85, "bidQty": 5.0, "ofr": 62923.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124658}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62924.05, "bidQty": 5.0, "ofr": 62924.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124698}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.15, "bidQty": 5.0, "ofr": 62923.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613124769}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.15, "bidQty": 5.0, "ofr": 62922.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613125105}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.85, "bidQty": 5.0, "ofr": 62926.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613126312}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62925.55, "bidQty": 5.0, "ofr": 62925.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613126343}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62925.45, "bidQty": 5.0, "ofr": 62925.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613126403}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62924.75, "bidQty": 5.0, "ofr": 62924.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613126523}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62924.5, "bidQty": 5.0, "ofr": 62924.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613127246}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62925.15, "bidQty": 5.0, "ofr": 62925.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613127435}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.6, "bidQty": 5.0, "ofr": 62923.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613128236}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.95, "bidQty": 5.0, "ofr": 62923.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613128569}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62920.1, "bidQty": 5.0, "ofr": 62920.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613130447}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62912.7, "bidQty": 5.0, "ofr": 62912.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613130617}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62921.15, "bidQty": 5.0, "ofr": 62921.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613131968}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62921.55, "bidQty": 5.0, "ofr": 62921.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613132017}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62922.2, "bidQty": 5.0, "ofr": 62922.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613132140}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62919.15, "bidQty": 5.0, "ofr": 62919.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613133026}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62919.55, "bidQty": 5.0, "ofr": 62919.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613133103}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62919.65, "bidQty": 5.0, "ofr": 62919.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613133396}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62920.5, "bidQty": 5.0, "ofr": 62920.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613133406}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.1, "bidQty": 5.0, "ofr": 62923.2, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613133934}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62934.05, "bidQty": 5.0, "ofr": 62934.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135135}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62932.25, "bidQty": 5.0, "ofr": 62932.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135315}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.55, "bidQty": 5.0, "ofr": 62931.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135346}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.7, "bidQty": 5.0, "ofr": 62931.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135426}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.55, "bidQty": 5.0, "ofr": 62931.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135467}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.75, "bidQty": 5.0, "ofr": 62931.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135520}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.85, "bidQty": 5.0, "ofr": 62931.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135601}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62930.85, "bidQty": 5.0, "ofr": 62930.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135812}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.05, "bidQty": 5.0, "ofr": 62931.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135865}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62934.7, "bidQty": 5.0, "ofr": 62934.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135875}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62934.75, "bidQty": 5.0, "ofr": 62934.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135895}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62937.5, "bidQty": 5.0, "ofr": 62937.6, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613135955}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62939.2, "bidQty": 5.0, "ofr": 62939.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613136007}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62937.15, "bidQty": 5.0, "ofr": 62937.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613136077}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62938.6, "bidQty": 5.0, "ofr": 62938.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613138343}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62938.75, "bidQty": 5.0, "ofr": 62938.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613139713}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62939.3, "bidQty": 5.0, "ofr": 62939.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613139768}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62939.85, "bidQty": 5.0, "ofr": 62939.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613139842}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62941.9, "bidQty": 5.0, "ofr": 62942.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613139892}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.15, "bidQty": 5.0, "ofr": 62947.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613143087}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62946.3, "bidQty": 5.0, "ofr": 62946.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613143243}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.45, "bidQty": 5.0, "ofr": 62947.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613143871}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.65, "bidQty": 5.0, "ofr": 62947.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613144338}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.55, "bidQty": 5.0, "ofr": 62947.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613144605}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.65, "bidQty": 5.0, "ofr": 62947.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613144766}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.45, "bidQty": 5.0, "ofr": 62947.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613145038}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.65, "bidQty": 5.0, "ofr": 62947.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613145177}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.45, "bidQty": 5.0, "ofr": 62947.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613145461}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.65, "bidQty": 5.0, "ofr": 62947.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613145598}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.45, "bidQty": 5.0, "ofr": 62947.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613145864}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62947.65, "bidQty": 5.0, "ofr": 62947.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613146016}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62946.35, "bidQty": 5.0, "ofr": 62946.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613146105}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62945.75, "bidQty": 5.0, "ofr": 62945.85, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613146367}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62945.95, "bidQty": 5.0, "ofr": 62946.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613147041}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62948.05, "bidQty": 5.0, "ofr": 62948.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613147419}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62945.55, "bidQty": 5.0, "ofr": 62945.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148493}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62941.6, "bidQty": 5.0, "ofr": 62941.7, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148603}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62936.7, "bidQty": 5.0, "ofr": 62936.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148734}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62939.2, "bidQty": 5.0, "ofr": 62939.3, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148784}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62942.55, "bidQty": 5.0, "ofr": 62942.65, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148825}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62944.35, "bidQty": 5.0, "ofr": 62944.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148916}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62941.25, "bidQty": 5.0, "ofr": 62941.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613148946}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62932.05, "bidQty": 5.0, "ofr": 62932.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613149157}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.35, "bidQty": 5.0, "ofr": 62931.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613149166}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62931.65, "bidQty": 5.0, "ofr": 62931.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613149187}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62927.4, "bidQty": 5.0, "ofr": 62927.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613149477}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62932.15, "bidQty": 5.0, "ofr": 62932.25, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613149958}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62930.8, "bidQty": 5.0, "ofr": 62930.9, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150105}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62934.05, "bidQty": 5.0, "ofr": 62934.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150216}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62933.05, "bidQty": 5.0, "ofr": 62933.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150276}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62932.25, "bidQty": 5.0, "ofr": 62932.35, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150377}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62929.3, "bidQty": 5.0, "ofr": 62929.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150397}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62923.4, "bidQty": 5.0, "ofr": 62923.5, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150606}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62926.35, "bidQty": 5.0, "ofr": 62926.45, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613150816}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62927.45, "bidQty": 5.0, "ofr": 62927.55, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613151315}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62924.95, "bidQty": 5.0, "ofr": 62925.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613151415}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62914.0, "bidQty": 5.0, "ofr": 62914.1, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613151426}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.85, "bidQty": 5.0, "ofr": 62916.95, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613151536}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62917.95, "bidQty": 5.0, "ofr": 62918.05, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613151565}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62920.05, "bidQty": 5.0, "ofr": 62920.15, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613151952}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62919.3, "bidQty": 5.0, "ofr": 62919.4, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613152717}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62916.65, "bidQty": 5.0, "ofr": 62916.75, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613152787}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62915.9, "bidQty": 5.0, "ofr": 62916.0, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613152987}, "status": "OK"} +{"destination": "internal.quote", "payload": {"bid": 62915.7, "bidQty": 5.0, "ofr": 62915.8, "ofrQty": 5.0, "symbolName": "BTC/USD_LEVERAGE", "timestamp": 1783613153189}, "status": "OK"} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.request.json b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.request.json new file mode 100644 index 0000000..564d5db --- /dev/null +++ b/app/tools/dzengi_probe/runtime_samples/websocket/marketData.subscribe/BTC_USD_LEVERAGE.valid.request.json @@ -0,0 +1,9 @@ +{ + "correlationId": "probe-market-data-subscribe", + "destination": "marketData.subscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE" + ] + } +} diff --git a/app/tools/dzengi_probe/runtime_samples/websocket/trades.subscribe/BTC_USD.jsonl b/app/tools/dzengi_probe/runtime_samples/websocket/trades.subscribe/BTC_USD.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/dzengi_probe/stream_depth_market_data_probe.py b/app/tools/dzengi_probe/stream_depth_market_data_probe.py new file mode 100644 index 0000000..bb109cd --- /dev/null +++ b/app/tools/dzengi_probe/stream_depth_market_data_probe.py @@ -0,0 +1,2 @@ +# app/tools/dzengi_probe/stream_depth_market_data_probe.py + diff --git a/app/tools/dzengi_probe/stream_market_data_probe.py b/app/tools/dzengi_probe/stream_market_data_probe.py new file mode 100644 index 0000000..b10da09 --- /dev/null +++ b/app/tools/dzengi_probe/stream_market_data_probe.py @@ -0,0 +1,216 @@ +# app/tools/dzengi_probe/stream_market_data_probe.py + +from __future__ import annotations + +import asyncio +import json +from dataclasses import dataclass +from typing import Any + +import websockets +from websockets.typing import Subprotocol + +from app.tools.dzengi_probe.config import DzengiProbeConfig +from app.tools.dzengi_probe.response_store import append_jsonl, save_json + + +MarketDataScenario = str + + +@dataclass(frozen=True) +class StreamProbeResult: + stream: str + output_file: str + ok: bool + messages_saved: int + error: str | None = None + + +class MarketDataStreamProbe: + def __init__(self, config: DzengiProbeConfig) -> None: + self.config = config + + async def run( + self, + *, + max_messages: int | None = 10, + duration_seconds: int | None = None, + scenario: MarketDataScenario = "valid", + repeat: int = 1, + ) -> StreamProbeResult: + output_file = ( + f"websocket/marketData.subscribe/" + f"{self.config.symbol_file_name}.{scenario}.jsonl" + ) + + request_file = ( + f"websocket/marketData.subscribe/" + f"{self.config.symbol_file_name}.{scenario}.request.json" + ) + + subscribe_message = self._build_subscribe_message(scenario) + + save_json(self.config.output_dir / request_file, subscribe_message) + + return await self._subscribe_and_save( + subscribe_message=subscribe_message, + output_file=output_file, + max_messages=max_messages, + duration_seconds=duration_seconds, + repeat=repeat, + ) + + def _build_subscribe_message( + self, + scenario: MarketDataScenario, + ) -> dict[str, Any]: + if scenario == "valid": + return { + "correlationId": "probe-market-data-subscribe", + "destination": "marketData.subscribe", + "payload": { + "symbols": [self.config.symbol], + }, + } + + if scenario == "invalid-symbol": + return { + "correlationId": "probe-market-data-invalid-symbol", + "destination": "marketData.subscribe", + "payload": { + "symbols": ["INVALID/SYMBOL"], + }, + } + + if scenario == "empty-symbols": + return { + "correlationId": "probe-market-data-empty-symbols", + "destination": "marketData.subscribe", + "payload": { + "symbols": [], + }, + } + + if scenario == "missing-symbols": + return { + "correlationId": "probe-market-data-missing-symbols", + "destination": "marketData.subscribe", + "payload": {}, + } + + if scenario == "missing-payload": + return { + "correlationId": "probe-market-data-missing-payload", + "destination": "marketData.subscribe", + } + + if scenario == "invalid-destination": + return { + "correlationId": "probe-market-data-invalid-destination", + "destination": "invalid.subscribe", + "payload": { + "symbols": [self.config.symbol], + }, + } + + if scenario == "unsubscribe": + return { + "correlationId": "probe-market-data-unsubscribe", + "destination": "marketData.unsubscribe", + "payload": { + "symbols": [self.config.symbol], + }, + } + + if scenario == "multi-symbol": + return { + "correlationId": "probe-market-data-multi-symbol", + "destination": "marketData.subscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE", + "ETH/USD_LEVERAGE", + ], + }, + } + + raise ValueError(f"Unknown marketData.subscribe scenario: {scenario}") + + async def _subscribe_and_save( + self, + *, + subscribe_message: dict[str, Any], + output_file: str, + max_messages: int | None, + duration_seconds: int | None, + repeat: int, + ) -> StreamProbeResult: + output_path = self.config.output_dir / output_file + messages_saved = 0 + + if output_path.exists(): + output_path.unlink() + + try: + async with websockets.connect( + self.config.ws_url, + extra_headers={ + "Origin": self.config.base_url, + "Content-Type": "application/json", + }, + subprotocols=[Subprotocol("json")], + ping_interval=20, + ping_timeout=20, + close_timeout=5, + ) as websocket: + for index in range(repeat): + message = dict(subscribe_message) + message["correlationId"] = f"{subscribe_message.get('correlationId')}-{index + 1}" + await websocket.send(json.dumps(message)) + + started_at = asyncio.get_running_loop().time() + + while True: + if max_messages is not None and messages_saved >= max_messages: + break + + if duration_seconds is not None: + elapsed = asyncio.get_running_loop().time() - started_at + if elapsed >= duration_seconds: + break + + try: + raw_message = await asyncio.wait_for( + websocket.recv(), + timeout=self.config.timeout_seconds, + ) + except asyncio.TimeoutError: + if duration_seconds is not None: + continue + + raise + + try: + payload = json.loads(raw_message) + except json.JSONDecodeError: + payload = {"raw": raw_message} + + print(payload) + append_jsonl(output_path, payload) + messages_saved += 1 + + return StreamProbeResult( + stream="marketData.subscribe", + output_file=str(output_path), + ok=True, + messages_saved=messages_saved, + ) + + except Exception as exc: + return StreamProbeResult( + stream="marketData.subscribe", + output_file=str(output_path), + ok=False, + messages_saved=messages_saved, + error=str(exc), + ) \ No newline at end of file diff --git a/app/tools/dzengi_probe/stream_ohlc_market_data_probe.py b/app/tools/dzengi_probe/stream_ohlc_market_data_probe.py new file mode 100644 index 0000000..848a17c --- /dev/null +++ b/app/tools/dzengi_probe/stream_ohlc_market_data_probe.py @@ -0,0 +1,2 @@ +# app/tools/dzengi_probe/stream_ohlc_market_data_probe.py + diff --git a/app/tools/dzengi_probe/stream_trades_probe.py b/app/tools/dzengi_probe/stream_trades_probe.py new file mode 100644 index 0000000..c7bd73a --- /dev/null +++ b/app/tools/dzengi_probe/stream_trades_probe.py @@ -0,0 +1,2 @@ +# app/tools/dzengi_probe/stream_trades_probe.py + diff --git a/app/tools/dzengi_probe/ticker24hr_update_probe.py b/app/tools/dzengi_probe/ticker24hr_update_probe.py new file mode 100644 index 0000000..3fdba57 --- /dev/null +++ b/app/tools/dzengi_probe/ticker24hr_update_probe.py @@ -0,0 +1,135 @@ +# app/tools/dzengi_probe/ticker24hr_update_probe.py + +from __future__ import annotations + +import asyncio +import csv +import json +import time +from dataclasses import dataclass +from decimal import Decimal +from pathlib import Path +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from app.tools.dzengi_probe.config import DzengiProbeConfig + + +@dataclass(frozen=True) +class Ticker24hrSample: + sample: int + local_time_ms: int + ask_price: str | None + bid_price: str | None + last_price: str | None + close_time: int | None + + +class Ticker24hrUpdateProbe: + def __init__(self, config: DzengiProbeConfig) -> None: + self.config = config + + async def run( + self, + *, + samples: int = 300, + interval_seconds: float = 0.2, + ) -> Path: + output_path = ( + self.config.output_dir + / "reports" + / f"ticker24hr_update_{self.config.symbol_file_name}.csv" + ) + output_path.parent.mkdir(parents=True, exist_ok=True) + + rows: list[Ticker24hrSample] = [] + + for index in range(1, samples + 1): + payload = self._get_ticker24hr() + + rows.append( + Ticker24hrSample( + sample=index, + local_time_ms=int(time.time() * 1000), + ask_price=self._decimal_str(payload.get("askPrice")), + bid_price=self._decimal_str(payload.get("bidPrice")), + last_price=self._decimal_str(payload.get("lastPrice")), + close_time=self._int_or_none(payload.get("closeTime")), + ) + ) + + await asyncio.sleep(interval_seconds) + + self._save_csv(output_path, rows) + return output_path + + def _get_ticker24hr(self) -> dict: + query = urlencode({"symbol": self.config.symbol}) + url = ( + f"{self.config.base_url}" + f"/api/{self.config.api_version}/ticker/24hr" + f"?{query}" + ) + + request = Request( + url=url, + method="GET", + headers={ + "Accept": "application/json", + "User-Agent": "dzentra-dzengi-probe/1.0", + }, + ) + + with urlopen(request, timeout=self.config.timeout_seconds) as response: + return json.loads(response.read().decode("utf-8")) + + def _decimal_str(self, value: object) -> str | None: + if value is None: + return None + return str(Decimal(str(value))) + + def _int_or_none(self, value: object) -> int | None: + if value is None: + return None + + return int(str(value)) + + def _save_csv(self, path: Path, rows: list[Ticker24hrSample]) -> None: + with path.open("w", encoding="utf-8", newline="") as file: + writer = csv.writer(file) + writer.writerow( + [ + "sample", + "local_time_ms", + "close_time", + "ask_price", + "bid_price", + "last_price", + "ask_changed", + "bid_changed", + "last_changed", + "close_time_changed", + "last_equals_bid", + ] + ) + + previous: Ticker24hrSample | None = None + + for row in rows: + writer.writerow( + [ + row.sample, + row.local_time_ms, + row.close_time, + row.ask_price, + row.bid_price, + row.last_price, + previous is not None and row.ask_price != previous.ask_price, + previous is not None and row.bid_price != previous.bid_price, + previous is not None and row.last_price != previous.last_price, + previous is not None and row.close_time != previous.close_time, + row.last_price == row.bid_price, + ] + ) + + previous = row \ No newline at end of file diff --git a/app/tools/dzengi_probe/websocket_probe.py b/app/tools/dzengi_probe/websocket_probe.py new file mode 100644 index 0000000..09e47d5 --- /dev/null +++ b/app/tools/dzengi_probe/websocket_probe.py @@ -0,0 +1,134 @@ +# app/tools/dzengi_probe/websocket_probe.py + +from __future__ import annotations + +import asyncio +import json +from dataclasses import dataclass +from typing import Any + +import websockets +from websockets.typing import Subprotocol + +from app.tools.dzengi_probe.config import DzengiProbeConfig +from app.tools.dzengi_probe.response_store import append_jsonl, save_json + + +@dataclass(frozen=True) +class WebSocketProbeResult: + stream: str + output_file: str + ok: bool + messages_saved: int + error: str | None = None + + +class DzengiWebSocketProbe: + def __init__(self, config: DzengiProbeConfig) -> None: + self.config = config + + async def probe_market_data(self, *, max_messages: int = 10) -> WebSocketProbeResult: + output_file = ( + f"websocket/marketData.subscribe/" + f"{self.config.symbol_file_name}.jsonl" + ) + + request_file = ( + f"websocket/marketData.subscribe/" + f"{self.config.symbol_file_name}.request.json" + ) + + subscribe_message: dict[str, Any] = { + "correlationId": "probe-market-data-subscribe", + "destination": "marketData.subscribe", + "payload": { + "symbols": [self.config.symbol], + }, + } + + save_json(self.config.output_dir / request_file, subscribe_message) + + return await self._subscribe_and_save( + stream="marketData.subscribe", + subscribe_message=subscribe_message, + output_file=output_file, + max_messages=max_messages, + ) + + async def _subscribe_and_save( + self, + *, + stream: str, + subscribe_message: dict[str, Any], + output_file: str, + max_messages: int, + ) -> WebSocketProbeResult: + output_path = self.config.output_dir / output_file + messages_saved = 0 + + if output_path.exists(): + output_path.unlink() + + try: + async with websockets.connect( + self.config.ws_url, + extra_headers={ + "Origin": self.config.base_url, + "Content-Type": "application/json", + }, + subprotocols=[Subprotocol("json")], + ping_interval=20, + ping_timeout=20, + close_timeout=5, + ) as websocket: + await websocket.send(json.dumps(subscribe_message)) + + while messages_saved < max_messages: + raw_message = await asyncio.wait_for( + websocket.recv(), + timeout=self.config.timeout_seconds, + ) + + try: + payload = json.loads(raw_message) + except json.JSONDecodeError: + payload = {"raw": raw_message} + + print(payload) + append_jsonl(output_path, payload) + messages_saved += 1 + + return WebSocketProbeResult( + stream=stream, + output_file=str(output_path), + ok=True, + messages_saved=messages_saved, + ) + + except Exception as exc: + return WebSocketProbeResult( + stream=stream, + output_file=str(output_path), + ok=False, + messages_saved=messages_saved, + error=str(exc), + ) + + async def probe_depth_request(self) -> WebSocketProbeResult: + output_file = f"websocket/depth/{self.config.symbol_file_name}.jsonl" + + request_message: dict[str, Any] = { + "correlationId": "probe-depth-request", + "destination": f"/api/{self.config.api_version}/depth", + "payload": { + "symbol": self.config.symbol, + "limit": int(self.config.depth_limit), + }, + } + + return await self._subscribe_and_save( + stream="wss:/api/v1/depth", + subscribe_message=request_message, + output_file=output_file, + max_messages=1, + ) \ No newline at end of file diff --git a/docs/market_intelligence/README.md b/docs/market_intelligence/README.md new file mode 100644 index 0000000..7501928 --- /dev/null +++ b/docs/market_intelligence/README.md @@ -0,0 +1,115 @@ +# Market Intelligence + +Документация подсистемы **Market Intelligence** проекта **Dzentra**. + +--- + +# Назначение + +Market Intelligence является аналитическим уровнем платформы. + +Его задача — понимать текущее поведение рынка. + +Market Intelligence не открывает сделки, не закрывает позиции и не управляет ордерами. + +Он отвечает только на вопрос: + +> **Что сейчас происходит с рынком?** + +Все торговые решения принимаются верхними уровнями архитектуры платформы. + +--- + +# Главная цель + +Перевести Dzentra от модели + +```text +бот принимает решения по индикаторам +``` + +к модели + +```text +профессиональная автономная торговая платформа, +состоящая из специализированных аналитических движков +``` + +--- + +# Структура документации + +| Документ | Назначение | +|----------|------------| +| `architecture_principles.md` | Архитектурные принципы Market Intelligence | +| `runtime_contract.md` | Контракт работы всех Engine | +| `build_history.md` | Общая история Build | +| `builds/` | Полная документация каждого Build | +| `decisions/` | Архитектурные решения | +| `reviews/` | Compile Check, Architecture Review и Domain Review | +| `common/` | Документация общего слоя | +| `engines/` | Документация отдельных Engine | +| `diagrams/` | Архитектурные схемы | +| `glossary/` | Словарь терминов | +| `roadmap/` | План развития платформы | + +--- + +# Текущий фокус + +Сейчас реализуется фундамент: + +```text +app/src/trading/market_intelligence/common/ +``` + +Этот слой определяет единый язык для всех будущих Engine. + +--- + +# Статус Common + +| Файл | Статус | +|------|---------| +| enums.py | ✅ Accepted | +| types.py | ✅ Accepted | +| constants.py | ✅ Accepted | +| reasons.py | ✅ Accepted | +| scores.py | ✅ Accepted | +| models.py | ✅ Accepted | +| validation.py | ⏳ Planned | +| checks.py | ⏳ Planned | +| payloads.py | ⏳ Planned | +| snapshots.py | ⏳ Planned | +| events.py | ⏳ Planned | +| timeframes.py | ⏳ Planned | + +--- + +# Жизненный цикл разработки + +```text +Architecture Design + ↓ +Implementation + ↓ +Compile Check + ↓ +Architecture Review + ↓ +Domain Review + ↓ +Documentation Update + ↓ +User Confirmation + ↓ +Build Closed +``` + +--- + +# Долгосрочная цель + +Создать профессиональную аналитическую платформу, которую можно развивать годами без деградации архитектуры. + +Добавление нового Engine не должно требовать переписывания уже существующих Engine. \ No newline at end of file diff --git a/docs/market_intelligence/architecture_principles.md b/docs/market_intelligence/architecture_principles.md new file mode 100644 index 0000000..1ff7af8 --- /dev/null +++ b/docs/market_intelligence/architecture_principles.md @@ -0,0 +1,1610 @@ +# Dzentra Market Intelligence Architecture Principles + +**Engineering Standard Release** + +--- + +## Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Документ | Dzentra Market Intelligence Architecture Principles | +| Тип документа | Engineering Standard | +| Версия | 2.0 | +| Статус | **Release** | +| Подсистема | Market Intelligence | +| Проект | Dzentra | +| Владелец стандарта | Dzentra Architecture | +| Язык | Русский | +| Применяется к | Архитектуре подсистемы Market Intelligence | + +--- + +## Оглавление + +### Общие положения + +- Контроль документа +- Статус документа +- Управление стандартом +- Соответствие стандарту +- Нормативная терминология +- Официальная терминология +- Назначение +- Область применения +- Главная архитектурная идея +- Архитектурная философия +- Общие положения + +### Part I. Фундаментальные архитектурные принципы + +- Архитектурные принципы +- Взаимосвязь архитектурных принципов +- Непрерывность развития архитектуры +- Централизованный жизненный цикл Engine + +### Part II. Архитектурная модель платформы + +- Общая архитектурная модель +- Архитектурные слои +- Архитектурные компоненты +- Модель ответственности +- Модель зависимостей +- Поток данных +- Поток управления + +### Part III. Архитектурные ограничения + +- Общие ограничения +- Ограничения Common Layer +- Ограничения Runtime Layer +- Ограничения Engine Layer +- Ограничения Coordinator Layer +- Ограничения взаимодействия +- Запрещённые архитектурные решения + +### Part IV. Архитектурные инварианты + +- Инварианты платформы +- Инварианты Common Layer +- Инварианты Runtime Layer +- Инварианты Engine Layer +- Инварианты Coordinator Layer +- Инварианты контрактов +- Инварианты развития + +### Part V. Эволюция архитектуры + +- Общие принципы развития +- Расширение архитектуры +- Изменение архитектуры +- Архитектурная стабильность + +### Part VI. Архитектурная согласованность + +- Архитектурная целостность +- Архитектурная простота +- Архитектурная сопровождаемость +- Архитектурная масштабируемость + +### Part VII. Заключительные положения + +- Соответствие стандарту +- Развитие стандарта +- Архитектурная аксиома +- Заключительные положения + +### Приложения + +- Приложение А. Архитектурный глоссарий +- Приложение Б. Архитектурные инварианты +- Приложение В. Каталог архитектурных диаграмм +- Приложение Г. Архитектурные шаблоны +- Приложение Д. Примеры допустимых архитектурных зависимостей +- Приложение Е. Примеры недопустимых архитектурных решений + +--- + +## Статус документа + +Настоящий документ имеет статус **Release**. + +Документ является официальным архитектурным стандартом подсистемы **Market Intelligence** проекта **Dzentra**. + +Настоящий стандарт определяет фундаментальные законы построения архитектуры платформы и устанавливает обязательные требования к организации её архитектурных компонентов, архитектурных связей и долгосрочного развития. + +Настоящий документ не определяет инженерный процесс разработки. + +Порядок разработки, выполнения Build, проведения инженерных проверок и сопровождения документации определяется документом **Development Process**. + +Изменение настоящего стандарта допускается исключительно посредством выпуска новой официальной версии документа. + +--- + +## Управление стандартом + +Настоящий документ определяет фундаментальные архитектурные принципы платформы **Market Intelligence**. + +Все архитектурные решения должны соответствовать требованиям настоящего стандарта независимо от: + +- используемого языка программирования; +- способа реализации компонентов; +- стадии развития платформы; +- количества участников разработки; +- используемых инженерных инструментов. + +Настоящий стандарт определяет исключительно архитектурные требования. + +Инженерный процесс разработки, порядок выполнения Build и проведение Engineering Review регулируются документом **Development Process**. + +Включение нового архитектурного принципа допускается только при одновременном выполнении следующих условий: + +- принцип обладает долгосрочной архитектурной ценностью; +- принцип подтверждён практикой разработки; +- принцип уменьшает архитектурную сложность платформы; +- принцип повышает сопровождаемость архитектуры; +- принцип не противоречит существующей архитектурной модели. + +Локальные архитектурные решения отдельных Build не могут изменять фундаментальные требования настоящего стандарта. + +При возникновении противоречий приоритет имеет настоящий документ. + +--- + +## Соответствие стандарту + +Архитектура платформы считается соответствующей настоящему стандарту только при соблюдении всех обязательных архитектурных требований, определённых данным документом. + +Нарушение любого обязательного архитектурного принципа рассматривается как архитектурный дефект независимо от корректности реализации отдельных компонентов. + +Рекомендации настоящего стандарта не являются обязательными, однако рассматриваются как предпочтительная архитектурная практика. + +Проверка соответствия архитектуры выполняется посредством процедур Architecture Review, определённых документом **Development Process**. + +--- + +## Нормативная терминология + +Во всём тексте настоящего документа используются следующие нормативные формулировки. + +| Формулировка | Значение | +|--------------|----------| +| **Должен** | Обязательное архитектурное требование. | +| **Не должен** | Архитектурное действие запрещено. | +| **Обязан** | Обязательное действие архитектурного компонента или участника разработки. | +| **Не допускается** | Полный запрет соответствующего архитектурного решения. | +| **Следует** | Предпочтительная архитектурная практика, допускающая обоснованные исключения. | +| **Рекомендуется** | Наиболее предпочтительный способ построения архитектуры. | +| **Может** | Допустимое архитектурное решение при соблюдении остальных требований настоящего стандарта. | + +Если явно не указано иное, все перечисленные формулировки используются исключительно в приведённых выше значениях. + +--- + +## Официальная терминология + +В целях единообразного понимания настоящего стандарта используются следующие определения. + +| Термин | Определение | +|---------|-------------| +| **Архитектура** | Совокупность архитектурных компонентов, их ответственности, взаимосвязей и ограничений. | +| **Архитектурный принцип** | Фундаментальное правило построения архитектуры платформы. | +| **Архитектурный компонент** | Самостоятельная архитектурная сущность с определённой областью ответственности. | +| **Layer** | Архитектурный уровень платформы. | +| **Common Layer** | Наиболее стабильный архитектурный уровень, содержащий общие модели, типы, перечисления и базовые архитектурные сущности. | +| **Runtime Layer** | Архитектурный уровень, определяющий состояние платформы и официальные контракты взаимодействия компонентов. | +| **Engine** | Независимый аналитический компонент, реализующий один вид анализа рынка. | +| **Coordinator** | Архитектурный компонент, координирующий работу нескольких Engine без реализации собственной аналитической логики. | +| **Contract** | Официальный контракт взаимодействия между архитектурными компонентами. | +| **Инвариант** | Архитектурное свойство, которое обязано сохраняться независимо от развития платформы. | + +Все перечисленные термины используются исключительно в приведённых выше значениях. + +--- + +## Назначение + +Настоящий документ определяет фундаментальные архитектурные принципы построения подсистемы **Market Intelligence** проекта **Dzentra**. + +Стандарт устанавливает обязательные требования к: + +- архитектурной модели платформы; +- организации архитектурных компонентов; +- распределению архитектурной ответственности; +- архитектурным зависимостям; +- архитектурным ограничениям; +- долгосрочному развитию архитектуры. + +Настоящий документ определяет архитектурные законы платформы и не регламентирует инженерный процесс разработки. + +Все вопросы, связанные с жизненным циклом Build, Engineering Review, сопровождением документации и организацией разработки, регулируются документом **Development Process**. + +--- + +## Область применения + +Настоящий стандарт распространяется на все архитектурные компоненты подсистемы **Market Intelligence**, включая: + +- Common Layer; +- Runtime Layer; +- Runtime Contract; +- Engine Layer; +- Coordinator Layer; +- архитектурные модели; +- архитектурные контракты; +- взаимодействие архитектурных компонентов; +- развитие архитектуры платформы. + +Требования настоящего стандарта обязательны для всех существующих и будущих архитектурных компонентов подсистемы **Market Intelligence** независимо от способа реализации и времени их создания. + +--- + +## Связанные документы + +Настоящий стандарт применяется совместно со следующими инженерными документами. + +| Документ | Назначение | +|----------|------------| +| Development Process | Определяет инженерный процесс разработки платформы. | +| Runtime Contract | Определяет официальные контракты взаимодействия компонентов. | +| Architecture Decision Records | Фиксируют долгосрочные архитектурные решения. | +| Build Documentation | Фиксирует историю архитектурного развития платформы. | +| Engineering Reviews | Подтверждают соответствие архитектуры требованиям стандартов. | + +Настоящий документ не дублирует содержание указанных документов и определяет исключительно фундаментальные архитектурные требования. + +--- + +## Главная архитектурная идея + +Подсистема **Market Intelligence** создаётся не как совокупность технических индикаторов и не как механизм генерации торговых сигналов. + +Market Intelligence представляет собой самостоятельную аналитическую подсистему, предназначенную для формирования объективной модели текущего состояния рынка. + +Главная задача платформы заключается не в вычислении отдельных технических показателей, а в построении целостного понимания поведения рынка посредством совместной работы специализированных аналитических компонентов. + +Главный вопрос, на который должна отвечать архитектура платформы, формулируется следующим образом. + +> **Что происходит с рынком в настоящий момент?** + +Ответ на данный вопрос является результатом согласованной работы независимых архитектурных компонентов, каждый из которых отвечает только за собственную область анализа. + +--- + +## Архитектурная философия + +Архитектура является главным инженерным активом платформы. + +Исходный код представляет собой реализацию архитектурных решений, а не источник архитектуры. + +Архитектурные решения принимаются с расчётом на многолетнее развитие платформы. + +Основной целью архитектуры является создание системы, способной непрерывно расширяться без накопления архитектурной сложности, архитектурного долга и потери сопровождаемости. + +Каждый новый компонент должен естественным образом интегрироваться в существующую архитектуру без нарушения её фундаментальных принципов. + +Архитектура должна оставаться понятной, последовательной и объяснимой независимо от размера платформы. + +--- + +## Общие положения + +Настоящий документ определяет фундаментальные законы построения архитектуры подсистемы **Market Intelligence**. + +Все архитектурные решения обязаны соответствовать данным законам независимо от используемых технологий и особенностей реализации. + +Настоящий документ определяет: + +- фундаментальные архитектурные принципы; +- архитектурную модель платформы; +- архитектурные ограничения; +- архитектурные инварианты; +- правила долгосрочного развития архитектуры. + +Настоящий документ не регламентирует: + +- процесс разработки; +- жизненный цикл Build; +- инженерные проверки; +- сопровождение инженерной документации; +- организацию совместной разработки. + +Указанные вопросы регулируются документом **Development Process**. + +Архитектурные принципы настоящего документа являются фундаментом всей архитектуры подсистемы **Market Intelligence**. + +Ни одно архитектурное решение не должно противоречить требованиям настоящего стандарта. + +--- + +## Part I. Фундаментальные архитектурные принципы + +### Архитектурные принципы + +Настоящий раздел определяет фундаментальные принципы построения архитектуры подсистемы **Market Intelligence**. + +Каждый принцип представляет собой самостоятельное обязательное архитектурное требование. + +Все принципы применяются совместно и образуют единую архитектурную модель платформы. + +Нарушение любого из указанных принципов рассматривается как нарушение архитектурной целостности платформы. + +--- + +#### Принцип 1. Architecture First + +Архитектура всегда предшествует реализации. + +До начала разработки любого архитектурного компонента должны быть определены: + +- назначение компонента; +- область архитектурной ответственности; +- место компонента в общей архитектуре; +- архитектурные зависимости; +- публичные контракты взаимодействия; +- ограничения компонента. + +Исходный код рассматривается исключительно как реализация заранее определённой архитектуры. + +Архитектура не должна формироваться под влиянием существующей реализации. + +--- + +#### Принцип 2. Stable Foundation + +Архитектура развивается последовательно — от наиболее стабильных компонентов к наиболее изменяемым. + +Фундаментальные архитектурные слои должны изменяться значительно реже прикладных компонентов. + +Каждый следующий уровень архитектуры может строиться только на полностью сформированном предыдущем уровне. + +Изменение фундаментальных компонентов допускается исключительно при наличии подтверждённой архитектурной необходимости. + +--- + +#### Принцип 3. One Source of Truth + +Каждая архитектурная сущность должна иметь единственный официальный источник определения. + +К таким сущностям относятся: + +- модели; +- перечисления; +- контракты; +- типы; +- константы; +- диагностические коды; +- архитектурные правила. + +Повторное определение одной и той же сущности не допускается. + +Дублирование архитектурных знаний рассматривается как архитектурный дефект. + +--- + +#### Принцип 4. Single Responsibility + +Каждый архитектурный компонент отвечает только за одну область ответственности. + +Ответственность компонента должна быть: + +- очевидной; +- изолированной; +- объяснимой; +- документированной. + +Если компонент начинает выполнять несколько независимых функций, архитектура должна быть переработана посредством разделения компонента. + +--- + +#### Принцип 5. Layer Isolation + +Архитектура платформы строится как система независимых архитектурных слоёв. + +Каждый слой обладает собственной областью ответственности. + +Внутренняя реализация слоя не должна использоваться другими слоями напрямую. + +Взаимодействие между слоями допускается исключительно посредством официальных архитектурных контрактов. + +Изоляция слоёв является обязательным условием сопровождаемости платформы. + +--- + +#### Принцип 6. Component Independence + +Каждый архитектурный компонент должен быть максимально независимым. + +Компонент не должен: + +- использовать внутреннюю реализацию других компонентов; +- создавать скрытые зависимости; +- зависеть от деталей реализации соседних компонентов; +- формировать циклические зависимости. + +Компоненты взаимодействуют исключительно посредством официальных контрактов. + +--- + +#### Принцип 7. Explainable Architecture + +Любое архитектурное решение должно быть объяснимым. + +Для каждого архитектурного компонента должна существовать возможность объяснить: + +- зачем он существует; +- какую задачу решает; +- почему расположен именно в данном архитектурном слое; +- почему выбран именно такой способ взаимодействия. + +Необъяснимые архитектурные решения считаются потенциальными источниками архитектурного долга. + +--- + +#### Принцип 8. Runtime Neutrality + +Runtime не должен содержать предметной, аналитической или торговой логики. + +Runtime отвечает исключительно за: + +- описание состояния; +- архитектурные контракты; +- модели взаимодействия; +- события Runtime. + +Любая логика анализа должна располагаться за пределами Runtime Layer. + +--- + +#### Принцип 9. Engine Independence + +Каждый аналитический Engine является полностью самостоятельным архитектурным компонентом. + +Engine не должен зависеть от внутренней реализации других Engine. + +Обмен результатами анализа осуществляется только посредством официальных контрактов Runtime либо Coordinator. + +Это обеспечивает независимое развитие аналитических движков. + +--- + +#### Принцип 10. Centralized Engine Lifecycle + +Экземпляры аналитических Engine создаются исключительно компонентом `RuntimeRunner`. + +Ни один другой компонент платформы не имеет права самостоятельно создавать экземпляры Engine. + +Данное правило обеспечивает: + +- единый жизненный цикл Engine; +- централизованный контроль выполнения; +- возможность профилирования; +- поддержку параллельного выполнения; +- поддержку отмены выполнения; +- централизованную обработку ошибок; +- минимизацию связанности компонентов Runtime. + +Запрещается создание экземпляров Engine внутри: + +- RuntimeRegistry; +- Coordinator; +- AutoTrade; +- Telegram; +- других Engine; +- любых компонентов, не являющихся RuntimeRunner. + +RuntimeRunner является единственной точкой создания экземпляров Engine. + +Все Engine обязаны иметь конструктор без пользовательских параметров. + +Всё изменяемое состояние анализа передаётся исключительно через `EngineContext`. + +--- + +#### Принцип 11. No Duplicate Logic + +Каждый алгоритм должен существовать только в одном месте архитектуры. + +Перед созданием новой реализации необходимо определить: + +- существует ли аналогичный алгоритм; +- возможно ли повторное использование; +- требуется ли расширение существующего решения. + +Повторная реализация существующей логики запрещается. + +--- + +#### Принцип 12. Documentation Is Architecture + +Архитектурная документация рассматривается как часть архитектуры платформы. + +Архитектурное решение считается завершённым только после его документирования. + +Изменение архитектуры автоматически означает необходимость актуализации соответствующей документации. + +Документация должна описывать архитектурные решения, а не детали реализации. + +--- + +#### Принцип 13. Long-Term Maintainability + +Все архитектурные решения принимаются с расчётом на долгосрочное развитие платформы. + +При выборе между несколькими допустимыми вариантами предпочтение должно отдаваться решению, которое: + +- уменьшает архитектурную сложность; +- повышает сопровождаемость; +- улучшает масштабируемость; +- облегчает повторное использование компонентов; +- минимизирует вероятность возникновения архитектурного долга. + +Краткосрочное упрощение реализации не должно ухудшать долгосрочное качество архитектуры. + +--- + +#### Принцип 14. Evolution Without Degradation + +Архитектура должна обеспечивать возможность непрерывного расширения платформы без ухудшения её качества. + +Добавление новых компонентов не должно: + +- увеличивать связанность системы; +- нарушать архитектурные границы; +- изменять фундаментальные принципы; +- усложнять понимание архитектуры. + +Развитие платформы должно происходить посредством последовательного расширения существующей архитектуры. + +--- + +#### Принцип 15. Architectural Simplicity + +Архитектурная простота рассматривается как самостоятельная архитектурная ценность. + +При наличии нескольких архитектурно корректных решений предпочтение должно отдаваться наиболее простому. + +Под архитектурной простотой понимаются: + +- минимальное количество зависимостей; +- отсутствие скрытого поведения; +- минимальное количество уровней взаимодействия; +- высокая объяснимость архитектуры; +- очевидность распределения ответственности. + +Простота не должна достигаться ценой потери архитектурной целостности. + +--- + +### Взаимосвязь архитектурных принципов + +Фундаментальные архитектурные принципы образуют единую взаимосвязанную систему. + +Ни один принцип не должен рассматриваться отдельно от остальных. + +Каждый архитектурный принцип усиливает действие других принципов и применяется совместно с ними. + +При наличии нескольких допустимых архитектурных решений предпочтение должно отдаваться варианту, который в наибольшей степени соответствует совокупности принципов настоящего стандарта. + +Фундаментальные архитектурные принципы являются неизменяемой основой всей архитектуры платформы **Market Intelligence**. + +--- + +### Непрерывность развития архитектуры + +Архитектура платформы рассматривается как непрерывно развивающаяся система. + +Каждое новое архитектурное решение должно быть совместимо с фундаментальными принципами настоящего стандарта. + +Развитие платформы должно происходить посредством последовательного расширения существующей архитектуры, а не посредством её периодического перепроектирования. + +Каждый новый компонент должен: + +- сохранять архитектурную целостность; +- поддерживать архитектурную согласованность; +- уменьшать сложность дальнейшего развития; +- обеспечивать возможность безопасного масштабирования платформы. + +Фундаментальные архитектурные принципы остаются неизменной основой архитектуры независимо от количества Build, реализованных компонентов и этапов развития проекта. + +--- + +## Part II. Архитектурная модель платформы + +Настоящая часть определяет официальную архитектурную модель подсистемы **Market Intelligence**. + +Архитектурная модель устанавливает: + +- состав архитектурных слоёв; +- состав архитектурных компонентов; +- распределение ответственности; +- правила взаимодействия; +- допустимые направления зависимостей; +- движение данных внутри платформы. + +Все существующие и будущие компоненты платформы обязаны соответствовать данной архитектурной модели. + +--- + +### Общая архитектурная модель + +Подсистема **Market Intelligence** представляет собой самостоятельную аналитическую платформу. + +Архитектура платформы строится как последовательность независимых архитектурных слоёв. + +Каждый слой имеет собственную область ответственности и взаимодействует с другими слоями исключительно посредством официальных архитектурных контрактов. + +Общая архитектурная модель имеет следующий вид. + +```text + External Market Data + │ + ▼ + Market Data Provider + │ + ▼ + Common Layer + │ + ▼ + Runtime Contract + │ + ▼ + Runtime Layer + │ + ▼ + Engine Layer + │ + ▼ + Coordinator Layer + │ + ▼ + Market Intelligence Result +``` + +Архитектура строится снизу вверх. + +Зависимости направлены только сверху вниз. + +Нижележащие компоненты не должны зависеть от вышележащих. + +--- + +### Архитектурные слои + +Архитектура платформы состоит из нескольких независимых слоёв. + +Каждый слой имеет единственную область ответственности. + +--- + +#### Common Layer + +Common Layer является фундаментом всей платформы. + +Данный слой содержит наиболее стабильные архитектурные сущности. + +К Common Layer относятся: + +- перечисления; +- типы; +- модели; +- константы; +- диагностические коды; +- общие архитектурные структуры. + +Common Layer не зависит ни от одного другого слоя платформы. + +--- + +#### Runtime Contract + +Runtime Contract определяет официальные контракты взаимодействия компонентов. + +Runtime Contract является единственным допустимым способом обмена информацией между архитектурными компонентами. + +Изменение Runtime Contract рассматривается как архитектурное изменение. + +--- + +#### Runtime Layer + +Runtime Layer представляет текущее состояние аналитической платформы. + +Runtime Layer отвечает исключительно за: + +- хранение состояния; +- модели состояния; +- события Runtime; +- жизненный цикл Runtime. + +Runtime Layer не содержит аналитической логики. + +--- + +#### Engine Layer + +Engine Layer реализует аналитическую обработку рынка. + +Каждый Engine представляет самостоятельный архитектурный компонент. + +Каждый Engine реализует только один независимый аспект анализа рынка. + +Например: + +- Trend Engine; +- Wave Engine; +- Liquidity Engine; +- Volatility Engine; +- Market Cycle Engine. + +Количество Engine не ограничивается. + +--- + +#### Coordinator Layer + +Coordinator Layer координирует работу аналитических Engine. + +Coordinator: + +- определяет последовательность выполнения Engine; +- объединяет результаты анализа; +- управляет зависимостями между вычислениями; +- публикует итоговый аналитический результат. + +Coordinator не реализует собственную аналитику. + +--- + +### Архитектурные компоненты + +Каждый архитектурный компонент платформы является самостоятельной архитектурной единицей. + +Компонент обязан иметь: + +- единственную область ответственности; +- официальный публичный контракт; +- определённые архитектурные границы; +- документированное назначение; +- возможность независимого сопровождения. + +Компоненты взаимодействуют исключительно посредством официальных архитектурных контрактов. + +Использование внутренних структур других компонентов не допускается. + +--- + +### Модель ответственности + +Ответственность архитектурных компонентов распределяется следующим образом. + +| Компонент | Основная ответственность | +|------------|--------------------------| +| Common Layer | Общие архитектурные сущности | +| Runtime Contract | Контракты взаимодействия | +| Runtime Layer | Представление состояния | +| Engine | Анализ одного аспекта рынка | +| Coordinator | Координация аналитических движков | + +Пересечение областей ответственности между компонентами не допускается. + +Если компонент начинает выполнять функции другого компонента, архитектура должна быть пересмотрена. + +--- + +### Модель зависимостей + +Архитектура платформы использует однонаправленную модель зависимостей. + +Допустимое направление зависимостей имеет следующий вид. + +```text +Coordinator + │ + ▼ +Engine + │ + ▼ +Runtime + │ + ▼ +Common +``` + +Обратные зависимости запрещаются. + +Компонент не должен зависеть от слоя, расположенного выше него. + +Все зависимости должны быть: + +- явными; +- объяснимыми; +- документированными; +- архитектурно обоснованными. + +Циклические зависимости запрещаются. + +--- + +### Поток данных + +Архитектура определяет единый поток движения данных. + +```text +Market Data + │ + ▼ +Runtime Context + │ + ▼ +Engine + │ + ▼ +Engine Result + │ + ▼ +Coordinator + │ + ▼ +Market Intelligence Result +``` + +Каждый этап обработки использует исключительно официальные архитектурные контракты. + +Изменение данных предыдущих этапов обработки не допускается. + +Каждый следующий этап получает результат предыдущего исключительно посредством публичного контракта. + +--- + +### Поток управления + +Архитектура определяет единый поток управления аналитической системой. + +```text +Coordinator + │ + ▼ +Engine + │ + ▼ +Runtime +``` + +Coordinator управляет выполнением аналитических компонентов. + +Engine не управляют друг другом. + +Runtime не управляет Engine. + +Common Layer не управляет никакими компонентами. + +Управление всегда осуществляется сверху вниз. + +Это обеспечивает независимость компонентов и предотвращает появление скрытых архитектурных зависимостей. + +--- + +### Завершение архитектурной модели + +Архитектурная модель, определённая настоящей частью, является официальной моделью построения подсистемы **Market Intelligence**. + +Все существующие и будущие архитектурные компоненты обязаны соответствовать данной модели. + +Любое изменение архитектурной модели допускается исключительно посредством выпуска новой версии настоящего стандарта. + +--- + +## Part III. Архитектурные ограничения + +Настоящая часть определяет обязательные архитектурные ограничения, действующие для всех компонентов подсистемы **Market Intelligence**. + +Архитектурные ограничения конкретизируют применение фундаментальных принципов при проектировании, реализации и развитии платформы. + +Нарушение любого из указанных ограничений рассматривается как нарушение архитектурной целостности платформы. + +--- + +### Общие ограничения + +Все архитектурные компоненты платформы обязаны соблюдать следующие ограничения. + +Не допускается: + +- нарушение архитектурной иерархии; +- нарушение границ ответственности компонентов; +- появление скрытых зависимостей; +- использование внутренних структур соседних компонентов; +- дублирование архитектурных сущностей; +- обход официальных архитектурных контрактов. + +Архитектурные ограничения являются обязательными независимо от особенностей реализации. + +--- + +### Ограничения Common Layer + +Common Layer является фундаментом архитектуры платформы. + +Common Layer обязан содержать только архитектурно нейтральные сущности. + +Допускается размещение: + +- типов; +- моделей; +- перечислений; +- констант; +- общих утилитарных структур; +- диагностических кодов. + +Не допускается размещение: + +- аналитической логики; +- торговой логики; +- Runtime; +- Engine; +- Coordinator; +- зависимостей от вышележащих архитектурных слоёв. + +Common Layer не должен зависеть ни от одного другого слоя платформы. + +--- + +### Ограничения Runtime Layer + +Runtime Layer отвечает исключительно за описание состояния аналитической платформы. + +Runtime Layer не должен: + +- выполнять анализ рынка; +- принимать аналитические решения; +- принимать торговые решения; +- выполнять вычисления Engine; +- управлять последовательностью выполнения компонентов; +- взаимодействовать с пользовательским интерфейсом. + +Runtime Layer представляет исключительно состояние платформы. + +Изменение Runtime Contract допускается только посредством официального архитектурного процесса. + +--- + +### Ограничения Engine Layer + +Engine Layer предназначен исключительно для аналитической обработки рыночной информации. + +Каждый Engine обязан реализовывать только один самостоятельный аспект анализа. + +Engine не должен: + +- открывать или закрывать позиции; +- рассчитывать размер позиции; +- взаимодействовать с биржей; +- изменять Runtime; +- импортировать соседние Engine; +- содержать пользовательский интерфейс; +- управлять другими Engine. + +Engine публикует исключительно результат собственного анализа. + +--- + +### Ограничения Coordinator Layer + +Coordinator Layer предназначен исключительно для координации аналитических компонентов. + +Coordinator может: + +- запускать Engine; +- определять порядок вычислений; +- агрегировать результаты; +- формировать итоговый аналитический вывод. + +Coordinator не должен: + +- реализовывать собственные аналитические алгоритмы; +- выполнять торговые операции; +- изменять внутреннюю реализацию Engine; +- изменять Runtime Contract; +- использовать внутренние структуры Engine. + +Coordinator отвечает только за организацию совместной работы аналитических компонентов. + +--- + +### Ограничения взаимодействия компонентов + +Все архитектурные компоненты взаимодействуют исключительно посредством официальных контрактов. + +Не допускается: + +- использование внутренних структур другого компонента; +- обращение к приватной реализации; +- скрытая передача данных; +- зависимость от внутреннего состояния соседнего компонента; +- обход Runtime Contract. + +Каждый компонент должен быть способен функционировать независимо при условии соблюдения официальных контрактов взаимодействия. + +--- + +### Запрещённые архитектурные решения + +Настоящий стандарт запрещает следующие архитектурные решения. + +Не допускается: + +- импорт одного Engine другим Engine; +- циклические архитектурные зависимости; +- наличие нескольких источников истины для одной сущности; +- размещение торговой логики внутри Market Intelligence; +- размещение аналитической логики внутри Runtime; +- размещение бизнес-логики в Common Layer; +- нарушение установленной архитектурной иерархии; +- создание компонентов без определённой области ответственности; +- создание компонентов исключительно "на будущее"; +- изменение Runtime Contract без архитектурного анализа; +- использование временных архитектурных решений в качестве постоянной архитектуры. + +Каждое из перечисленных решений рассматривается как нарушение настоящего стандарта и должно быть устранено до завершения Build. + +--- + +### Завершение архитектурных ограничений + +Архитектурные ограничения являются обязательной частью архитектурной модели платформы. + +При возникновении противоречия между удобством реализации и установленными архитектурными ограничениями приоритет всегда имеют требования настоящего стандарта. + +Архитектурные ограничения сохраняют свою силу независимо от развития платформы, появления новых компонентов или изменения технологий реализации. + +--- + +## Part IV. Архитектурные инварианты + +Настоящая часть определяет фундаментальные архитектурные свойства, которые должны сохраняться независимо от развития платформы. + +Архитектурный инвариант представляет собой свойство архитектуры, нарушение которого означает нарушение архитектурной модели платформы. + +Инварианты сохраняются независимо от: + +- количества реализованных Build; +- появления новых Engine; +- изменения Runtime; +- расширения функциональности; +- изменения технологий реализации. + +Изменение любого архитектурного инварианта допускается исключительно посредством выпуска новой редакции настоящего стандарта. + +--- + +### Инварианты платформы + +Следующие свойства являются обязательными для всей платформы. + +Платформа всегда должна сохранять: + +- единую архитектурную модель; +- однонаправленные зависимости; +- архитектурную изоляцию компонентов; +- единственный источник истины для каждой сущности; +- единые публичные контракты взаимодействия; +- отсутствие архитектурного дублирования. + +Эти свойства являются фундаментальными характеристиками архитектуры **Market Intelligence**. + +--- + +### Инварианты Common Layer + +Common Layer всегда остаётся наиболее стабильным архитектурным слоем платформы. + +Common Layer обязан содержать исключительно общие архитектурные сущности. + +Инвариантами Common Layer являются: + +- отсутствие зависимостей от вышележащих слоёв; +- отсутствие аналитической логики; +- отсутствие торговой логики; +- единое определение базовых моделей; +- единое определение типов; +- единое определение перечислений; +- единое определение констант. + +Common Layer всегда остаётся фундаментом всей архитектуры платформы. + +--- + +### Инварианты Runtime Layer + +Runtime Layer всегда представляет исключительно состояние платформы. + +Runtime никогда не принимает аналитических решений. + +Runtime никогда не принимает торговых решений. + +Экземпляры Engine всегда создаются исключительно компонентом `RuntimeRunner`. + +`RuntimeRegistry` хранит только регистрацию Engine и не создаёт экземпляры Engine. + +Жизненный цикл экземпляра Engine ограничен одним запуском анализа. + +Инвариантами Runtime являются: + +- описание состояния; +- официальные Runtime Contract; +- Runtime Events; +- Runtime Models; +- отсутствие аналитической логики; +- отсутствие логики координации. + +Runtime остаётся нейтральным архитектурным слоем. + +--- + +### Инварианты Engine Layer + +Каждый Engine является самостоятельным аналитическим компонентом. + +Каждый Engine обязан: + +- иметь единственную область ответственности; +- выполнять один вид анализа; +- использовать официальные архитектурные контракты; +- возвращать единый тип результата; +- быть пригодным для независимого тестирования; +- быть независимым от реализации других Engine. + +Engine никогда не содержит: + +- торговой логики; +- пользовательского интерфейса; +- управления Coordinator; +- изменения Runtime. + +Все Engine остаются независимыми архитектурными компонентами. + +--- + +### Инварианты Coordinator Layer + +Coordinator всегда выполняет исключительно координацию аналитических компонентов. + +Coordinator обязан: + +- управлять последовательностью выполнения Engine; +- агрегировать результаты анализа; +- использовать официальные архитектурные контракты; +- публиковать итоговый аналитический результат. + +Coordinator никогда не: + +- реализует собственный анализ; +- заменяет Engine; +- содержит торговую логику; +- нарушает архитектурные границы компонентов. + +Coordinator остаётся исключительно координирующим компонентом платформы. + +--- + +### Инварианты архитектурных контрактов + +Все архитектурные взаимодействия осуществляются только посредством официальных контрактов. + +Инвариантами контрактной модели являются: + +- единый Runtime Contract; +- единые модели обмена данными; +- отсутствие скрытых контрактов; +- отсутствие прямого использования внутренней реализации компонентов; +- совместимость контрактов между архитектурными слоями. + +Архитектурные контракты являются единственным допустимым способом взаимодействия компонентов. + +--- + +### Инварианты развития архитектуры + +Развитие платформы должно сохранять архитектурную целостность. + +Каждый новый компонент обязан: + +- соответствовать архитектурной модели; +- соблюдать архитектурные ограничения; +- использовать существующие контракты либо официально расширять их; +- уменьшать либо не увеличивать архитектурную сложность платформы; +- сохранять возможность дальнейшего масштабирования. + +Расширение функциональности не должно нарушать фундаментальные свойства архитектуры. + +--- + +### Архитектурная неизменяемость инвариантов + +Архитектурные инварианты определяют фундаментальную идентичность платформы. + +Изменение инвариантов означает изменение самой архитектурной модели **Market Intelligence**. + +По этой причине инварианты могут быть изменены исключительно посредством официального пересмотра настоящего стандарта. + +Любое архитектурное решение, противоречащее одному или нескольким инвариантам, считается несовместимым с архитектурой платформы. + +--- + +## Part V. Эволюция архитектуры + +Настоящая часть определяет правила долгосрочного развития архитектуры подсистемы **Market Intelligence**. + +Архитектура платформы рассматривается как непрерывно развивающаяся система. + +Развитие архитектуры допускается только при сохранении фундаментальных принципов, архитектурных ограничений и архитектурных инвариантов, определённых настоящим стандартом. + +Цель эволюции архитектуры заключается в последовательном расширении функциональности платформы без увеличения архитектурной сложности. + +--- + +### Общие принципы развития + +Архитектура развивается посредством последовательного расширения существующей модели. + +Каждый новый архитектурный компонент должен естественным образом интегрироваться в существующую архитектуру. + +При развитии платформы должны сохраняться: + +- архитектурная целостность; +- архитектурная согласованность; +- понятность структуры; +- независимость компонентов; +- стабильность архитектурных контрактов. + +Изменение архитектуры не должно ухудшать её сопровождаемость. + +--- + +### Расширение архитектуры + +Расширение архитектуры является предпочтительным способом развития платформы. + +Новые возможности должны реализовываться посредством добавления новых компонентов, а не изменения фундаментальной структуры существующих. + +При расширении архитектуры должны соблюдаться следующие требования: + +- новый компонент имеет единственную область ответственности; +- место компонента в архитектуре определено заранее; +- архитектурные зависимости соответствуют архитектурной модели; +- используются существующие архитектурные контракты либо их официальное расширение; +- отсутствует дублирование существующей функциональности. + +Расширение архитектуры не должно нарушать существующие архитектурные инварианты. + +--- + +### Изменение архитектуры + +Изменение существующей архитектуры допускается исключительно при наличии подтверждённой архитектурной необходимости. + +К таким случаям относятся: + +- устранение архитектурного дефекта; +- упрощение архитектурной модели; +- повышение сопровождаемости; +- повышение масштабируемости; +- устранение архитектурного долга; +- развитие архитектурных контрактов. + +Изменение архитектуры не должно выполняться исключительно ради изменения структуры или используемых технологий. + +--- + +### Архитектурный рефакторинг + +Архитектурный рефакторинг рассматривается как один из основных механизмов развития платформы. + +Цель архитектурного рефакторинга заключается в улучшении архитектуры без изменения её фундаментальной модели. + +Архитектурный рефакторинг должен: + +- уменьшать связанность компонентов; +- повышать понятность архитектуры; +- устранять архитектурное дублирование; +- упрощать развитие платформы; +- улучшать распределение ответственности. + +После завершения рефакторинга архитектура должна стать проще для понимания и сопровождения. + +--- + +### Развитие архитектурных контрактов + +Архитектурные контракты могут развиваться одновременно с развитием платформы. + +Любое изменение архитектурного контракта должно: + +- сохранять архитектурную целостность; +- быть совместимым с существующей архитектурой либо сопровождаться официальным изменением стандарта; +- иметь архитектурное обоснование; +- быть отражено в инженерной документации. + +Контракты являются частью фундаментальной архитектуры платформы и требуют особой осторожности при изменении. + +--- + +### Архитектурная стабильность + +Архитектура платформы должна сохранять устойчивость независимо от количества реализованных Build. + +При развитии платформы наиболее стабильными остаются: + +- Common Layer; +- Runtime Contract; +- Runtime Layer; +- архитектурные модели. + +Наиболее изменяемыми являются: + +- аналитические Engine; +- Coordinator; +- прикладные сценарии использования. + +Чем ближе архитектурный компонент расположен к фундаменту платформы, тем более обоснованными должны быть его изменения. + +--- + +### Критерии качественной эволюции + +Архитектурное развитие считается успешным только при одновременном выполнении следующих условий: + +- функциональные возможности платформы расширяются; +- архитектурная сложность не увеличивается; +- сопровождаемость улучшается либо сохраняется; +- повторное использование компонентов возрастает; +- архитектурные инварианты сохраняются. + +Если развитие функциональности сопровождается деградацией архитектуры, принятое решение должно быть пересмотрено. + +--- + +### Непрерывность архитектурного развития + +Развитие архитектуры рассматривается как непрерывный процесс. + +Каждый завершённый Build должен оставлять платформу в состоянии, при котором: + +- архитектура остаётся целостной; +- все архитектурные ограничения соблюдены; +- инварианты сохраняются; +- архитектурные контракты актуальны; +- следующий этап развития может быть начат без пересмотра фундаментальной архитектуры. + +Архитектурная эволюция рассматривается как последовательность небольших согласованных изменений, каждое из которых делает платформу функционально богаче и архитектурно зрелее. + +--- + +## Part VI. Архитектурная согласованность + +Настоящая часть определяет критерии, которым должна соответствовать архитектура подсистемы **Market Intelligence** независимо от этапа её развития. + +Архитектурная согласованность рассматривается как обязательное условие долгосрочной стабильности платформы. + +Любое архитектурное решение должно способствовать повышению согласованности архитектуры либо, как минимум, не ухудшать её. + +--- + +### Архитектурная целостность + +Архитектура платформы должна восприниматься как единая система. + +Все архитектурные компоненты обязаны естественным образом дополнять друг друга. + +При развитии платформы должны сохраняться: + +- единая архитектурная модель; +- единая терминология; +- единые архитектурные контракты; +- единая система ответственности; +- единая модель взаимодействия компонентов. + +Архитектурная целостность является обязательным свойством платформы. + +--- + +### Архитектурная простота + +Архитектура должна оставаться максимально простой при сохранении необходимой функциональности. + +Архитектурная простота достигается посредством: + +- минимального количества зависимостей; +- отсутствия скрытого поведения; +- отсутствия необоснованных уровней абстракции; +- понятного распределения ответственности; +- использования единых архитектурных решений. + +Простота архитектуры рассматривается как долгосрочная инженерная ценность. + +--- + +### Архитектурная сопровождаемость + +Архитектура должна обеспечивать возможность безопасного сопровождения платформы на протяжении всего жизненного цикла. + +Архитектурная сопровождаемость предполагает: + +- локальность изменений; +- минимизацию влияния изменений на соседние компоненты; +- понятную структуру каталогов; +- очевидные архитектурные зависимости; +- наличие актуальной инженерной документации. + +Изменение одного компонента не должно требовать массового изменения остальных компонентов платформы. + +--- + +### Архитектурная масштабируемость + +Архитектура должна поддерживать последовательное расширение платформы. + +Добавление новых компонентов не должно требовать изменения фундаментальной архитектурной модели. + +Архитектурная масштабируемость достигается посредством: + +- независимости компонентов; +- использования официальных контрактов; +- изоляции архитектурных слоёв; +- отсутствия скрытых зависимостей; +- последовательного распределения ответственности. + +Архитектура должна поддерживать возможность роста платформы без ухудшения её качества. + +--- + +### Архитектурная объяснимость + +Каждый архитектурный компонент должен иметь понятное место в общей архитектуре платформы. + +Для любого компонента должна существовать возможность ответить на следующие вопросы: + +- зачем существует данный компонент; +- почему он расположен именно в этом архитектурном слое; +- почему его ответственность определена именно таким образом; +- каким образом он взаимодействует с другими компонентами; +- почему архитектура выбрана именно в таком виде. + +Архитектура считается зрелой только в том случае, если её структура объяснима без обращения к деталям реализации. + +--- + +### Архитектурная устойчивость + +Архитектура должна сохранять свои фундаментальные свойства независимо от количества реализованных Build. + +Новые функциональные возможности не должны: + +- нарушать архитектурные принципы; +- изменять фундаментальную архитектурную модель; +- разрушать архитектурные инварианты; +- увеличивать связанность компонентов; +- усложнять архитектуру платформы. + +Архитектурная устойчивость является обязательным условием долгосрочного развития проекта. + +--- + +### Архитектурная зрелость + +Архитектура платформы считается зрелой при одновременном выполнении следующих условий: + +- соблюдаются фундаментальные архитектурные принципы; +- соблюдаются архитектурные ограничения; +- сохраняются архитектурные инварианты; +- архитектура допускает безопасное расширение; +- отсутствуют архитектурные противоречия; +- отсутствует архитектурный долг; +- архитектурная документация соответствует текущему состоянию платформы. + +Архитектурная зрелость является результатом последовательного развития платформы и соблюдения требований настоящего стандарта. + +--- + +## Part VII. Заключительные положения + +Настоящая часть завершает настоящий архитектурный стандарт и определяет порядок его применения, развития и сопровождения. + +Все положения настоящего документа являются обязательными для архитектуры подсистемы **Market Intelligence** проекта **Dzentra**. + +--- + +### Соответствие настоящему стандарту + +Все архитектурные компоненты платформы обязаны соответствовать требованиям настоящего стандарта. + +Соответствие подтверждается посредством архитектурных проверок, предусмотренных документом **Development Process**. + +Архитектурное решение считается соответствующим настоящему стандарту только при одновременном выполнении следующих условий: + +- соблюдены фундаментальные архитектурные принципы; +- соблюдены архитектурные ограничения; +- сохранены архитектурные инварианты; +- обеспечена архитектурная согласованность; +- архитектурная документация соответствует текущему состоянию платформы. + +Несоответствие любому обязательному требованию настоящего стандарта рассматривается как архитектурный дефект. + +--- + +### Развитие стандарта + +Настоящий документ рассматривается как долгосрочный архитектурный стандарт. + +Развитие настоящего стандарта допускается только посредством выпуска новой версии документа. + +Каждая новая редакция должна: + +- сохранять фундаментальную архитектурную модель платформы либо официально фиксировать её изменение; +- устранять неоднозначности; +- повышать согласованность архитектурных требований; +- улучшать сопровождаемость архитектуры; +- уменьшать сложность архитектурной модели. + +Изменение настоящего стандарта не должно приводить к ухудшению архитектурного качества платформы. + +--- + +### Приоритет настоящего стандарта + +Настоящий документ является официальным источником фундаментальных архитектурных принципов подсистемы **Market Intelligence**. + +При возникновении противоречий между настоящим документом и другими архитектурными материалами приоритет имеют требования настоящего стандарта. + +Документы более низкого уровня должны развивать положения настоящего стандарта и не могут изменять его фундаментальные требования. + +--- + +### Архитектурная аксиома Dzentra + +Фундаментальной архитектурной аксиомой платформы **Dzentra** является следующее утверждение. + +> **Каждый новый архитектурный компонент обязан делать платформу функционально богаче, но архитектурно проще.** + +Данная аксиома является итоговым выражением всех принципов настоящего стандарта. + +Любое архитектурное решение должно оцениваться прежде всего с точки зрения соответствия этой аксиоме. + +Если после реализации нового компонента архитектура становится менее понятной, менее согласованной или менее сопровождаемой, принятое решение должно быть пересмотрено независимо от достигнутой функциональности. + +--- + +### Заключительные положения + +Настоящий документ определяет фундаментальные архитектурные законы подсистемы **Market Intelligence** проекта **Dzentra**. + +Все архитектурные решения, независимо от времени их принятия, обязаны соответствовать требованиям настоящего стандарта. + +Фундаментальные архитектурные принципы являются основой: + +- Development Process; +- Runtime Contract; +- Architecture Decision Records; +- Engineering Reviews; +- Build Documentation; +- всех последующих архитектурных решений платформы. + +Настоящий документ остаётся единым официальным источником фундаментальных архитектурных требований подсистемы **Market Intelligence**. + +--- + +## Приложения + +Приложения являются справочной частью настоящего стандарта. + +Они не изменяют обязательные требования документа, но используются для унификации архитектурной практики проекта. + +В последующих версиях стандарта могут быть подготовлены отдельные приложения. + +- **Приложение А.** Архитектурный глоссарий. +- **Приложение Б.** Каталог архитектурных инвариантов. +- **Приложение В.** Каталог архитектурных диаграмм. +- **Приложение Г.** Архитектурные шаблоны компонентов. +- **Приложение Д.** Примеры допустимых архитектурных зависимостей. +- **Приложение Е.** Примеры недопустимых архитектурных решений. \ No newline at end of file diff --git a/docs/market_intelligence/build_history.md b/docs/market_intelligence/build_history.md new file mode 100644 index 0000000..f67918a --- /dev/null +++ b/docs/market_intelligence/build_history.md @@ -0,0 +1,296 @@ +# Build History + +Данный документ является общей историей разработки подсистемы **Market Intelligence**. + +Каждый Build представляет собой логически завершённый этап развития архитектуры. + +Подробное описание каждого Build хранится в каталоге: + +```text +docs/market_intelligence/builds/ +``` + +--- + +# Статусы Build + +| Статус | Значение | +|--------|----------| +| ⏳ Planned | Build ещё не начат | +| 🚧 In Progress | Build находится в разработке | +| ✅ Accepted | Build полностью завершён | +| 🔄 Replaced | Build заменён более новой реализацией | +| ❌ Rejected | Build отклонён | + +--- + +# История Build + +| Build | Компонент | Статус | Документ | +|------:|-----------|:------:|----------| +| 001 | Common / Enums | ✅ | `build-001-common-enums.md` | +| 002 | Common / Types | ✅ | `build-002-common-types.md` | +| 003 | Common / Constants | ✅ | `build-003-common-constants.md` | +| 004 | Common / Reasons | ✅ | `build-004-common-reasons.md` | +| 005 | Common / Scores | ✅ | `build-005-common-scores.md` | +| 006 | Common / Models | ✅ | `build-006-common-models.md` | +| 006.1 | Common / Models Extension (EngineMetadata) | ✅ | `build-006-1-common-models-engine-metadata.md` | +| 007 | Common / Validation | ✅ | `build-007-common-validation.md` | +| 008 | Common / Checks | ✅ | `build-008-common-checks.md` | +| 009 | Common / Payloads | ✅ | `build-009-common-payloads.md` | +| 010 | Common / Snapshots | ✅ | `build-010-common-snapshots.md` | +| 011 | Common / Events | ✅ | `build-011-common-events.md` | +| 012 | Common / Timeframes | ✅ | `build-012-common-timeframes.md` | +| 013 | Engine Layer Architecture | ✅ | `build-013-engine-layer-architecture.md` | +| 014.1 | Engine Base Contracts / Engine Protocol | ✅ | `build-014-1-engine-protocol.md` | +| 014.2 | Engine Base / BaseEngine | ✅ | `build-014-2-engine-base.md` | +| 014.3 | Engine Exceptions | ✅ | `build-014-3-engine-exceptions.md` | +| 015.1 | Common Models Extension / RuntimeResult | ✅ | `build-015-1-common-models-runtime-result.md` | +| 015.2 | Runtime Layer / Runtime Protocol | ✅ | `build-015-2-runtime-protocol.md` | +| 015.3 | Common Models Extension / EngineRegistration | ✅ | `build-015-3-common-models-engine-registration.md` | +| 015.4 | Runtime Layer / Runtime Registry | ✅ | `build-015-4-runtime-registry.md` | +| 015.5 | Runtime Layer / Runtime Core | ✅ | `build-015-5-runtime-core.md` | +| 015.6 | Runtime Layer / Runtime Dependencies | ✅ | `build-015-6-runtime-dependencies.md` | +| 015.7 | Runtime Layer / Runtime Validation | ✅ | `build-015-7-runtime-validation.md` | +| 015.8 | Runtime Layer / Runtime Service | ✅ | `build-015-8-runtime-service.md` | +| 016 | Coordinator Layer Architecture | ✅ | `build-016-coordinator-layer-architecture.md` | +| 016.1 | Common Models Extension / CoordinatorResult | ✅ | `build-016-1-common-models-coordinator-result.md` | +| 016.2 | Coordinator Layer / Coordinator Protocol | ✅ | `build-016-2-coordinator-protocol.md` | +| 016.3 | Coordinator Layer / Coordinator Exceptions | ✅ | `build-016-3-coordinator-exceptions.md` | +| 016.4 | Coordinator Layer / Coordinator Validation | ✅ | `build-016-4-coordinator-validation.md` | +| 016.5 | Coordinator Layer / Coordinator Rules | ✅ | `build-016-5-coordinator-rules.md` | +| 016.6 | Coordinator Layer / Coordinator Service | ✅ | `build-016-6-coordinator-service.md` | +| 017 | Trading Layer Architecture | ✅ | `build-017-trading-layer-architecture.md` | +| 017.1 | Trading Layer / Trading Models | ✅ | `build-017-1-trading-models.md` | +| 017.2 | Trading Layer / Trading Protocol | ✅ | `build-017-2-trading-protocol.md` | +| 017.3 | Trading Layer / Trading Exceptions | ✅ | `build-017-3-trading-exceptions.md` | +| 017.4 | Trading Layer / Trading Validation | ✅ | `build-017-4-trading-validation.md` | +| 017.5 | Trading Layer / Trading Rules | ✅ | `build-017-5-trading-rules.md` | +| 017.6 | Trading Layer / Trading Service | ✅ | `build-017-6-trading-service.md` | +| 017.7 | Trading Boundary Correction | ✅ | `build-017-7-trading-boundary-correction.md` | + +--- + +# Текущий прогресс Common + +| Компонент | Готовность | +|-----------|-----------:| +| Enums | 100% | +| Types | 100% | +| Constants | 100% | +| Reasons | 100% | +| Scores | 100% | +| Models | 100% | +| Validation | 100% | +| Checks | 100% | +| Payloads | 100% | +| Snapshots | 100% | +| Events | 100% | +| Timeframes | 100% | + +> Build 015.1 расширил Common Layer новой моделью RuntimeResult без изменения его архитектурной структуры. +> Build 015.3 расширил Common Layer новой моделью `EngineRegistration`, необходимой для реализации Runtime Registry, без изменения архитектуры Common Layer. + +**Common Layer завершён полностью.** + +--- + +# Текущий прогресс Runtime + +| Компонент | Готовность | +|-----------|-----------:| +| Runtime Architecture | 100% | +| Runtime Models | ✅ Completed | +| Protocol | ✅ Completed | +| Registry | ✅ Completed | +| Runner | ✅ Completed | +| Runtime Exceptions | ✅ Completed | +| Dependencies | ✅ Completed | +| Runtime Validation | ✅ Completed | +| Runtime Service | ✅ Completed | + +> Build 015.5 реализовал `RuntimeRunner` — компонент Runtime Layer, отвечающий исключительно за создание экземпляра зарегистрированного Engine и вызов его метода `analyze()`. +> +> Во время Code Review Build 015.5 обнаружено частичное дублирование контрактов `EngineProtocol` и `EngineTypeProtocol`. Для обеспечения корректной статической типизации в `EngineTypeProtocol` добавлен метод `analyze()`. Архитектурное объединение контрактов не входит в Build 015.5 и может быть рассмотрено отдельным Build после завершения Runtime Layer. +> +> Build 015.6 реализовал `RuntimeDependencies`, отвечающий за проверку обязательных зависимостей Engine и построение порядка их выполнения посредством топологической сортировки. Компонент не выполняет Engine и не содержит аналитической логики. + +> Build 015.7 реализовал `RuntimeValidation` — централизованный компонент проверки готовности Runtime Layer. Компонент валидирует Registry и Metadata зарегистрированных Engine, а проверку зависимостей делегирует `RuntimeDependencies`, сохраняя принцип единственной ответственности. +> +> Во время Code Review подтверждено, что проверка повторной регистрации Engine в текущей реализации `RuntimeRegistry` практически недостижима из-за хранения регистраций в словаре. Тем не менее данная проверка сохранена как часть публичного контракта `RuntimeValidation` для обеспечения устойчивости архитектуры при возможном изменении внутренней реализации Registry. + +--- + +# Текущий прогресс Coordinator + +| Компонент | Готовность | +|-----------|-----------:| +| Coordinator Architecture | ✅ Completed | +| Coordinator Models | ✅ Completed | +| Protocol | ✅ Completed | +| Exceptions | ✅ Completed | +| Validation | ✅ Completed | +| Rules | ✅ Completed | +| Service | ✅ Completed | + + +> Build 016 утвердил архитектуру Coordinator Layer и определил его место между Runtime Layer и будущим Trading Layer. +> Build не содержит реализации компонентов и фиксирует только архитектурные решения. + +> Build 016.1 расширил Common Layer новой межслойной моделью `CoordinatorResult`, а также моделями `CoordinatorDiagnostics` и `CoordinatorEvaluationMeta`. Coordinator Layer получил официальный выходной контракт без нарушения архитектурных границ Common Layer. + +> Build 016.2 добавил официальный публичный контракт `CoordinatorProtocol`, завершив формирование базового интерфейса Coordinator Layer. Контракт определяет единственную точку входа Coordinator и отделяет внешний API слоя от его будущей реализации. + +> Build 016.3 добавил собственную иерархию исключений Coordinator Layer (`CoordinatorError`, `InvalidCoordinatorResultError`, `CoordinatorValidationError`, `CoordinatorExecutionError`). Coordinator окончательно отделён от Runtime Layer в части обработки ошибок и получил собственное пространство исключений. + +> Build 016.4 добавил компонент `CoordinatorValidation`, выполняющий предварительную проверку входного `RuntimeResult`. Validation отделён от правил согласования и обеспечивает корректность входных данных до начала работы Coordinator Layer. + +> Build 016.5 реализовал компонент `CoordinatorRules`, отвечающий за преобразование `RuntimeResult` в `CoordinatorResult`. На текущем этапе создан базовый каркас согласования результатов Engine без реализации сложных механизмов весов, разрешения конфликтов и вероятностных моделей. Архитектура подготовлена к постепенному развитию правил согласования по мере появления специализированных Engine. + +> Build 016.6 завершил построение Coordinator Foundation, реализовав `CoordinatorService` — единую публичную точку входа Coordinator Layer. Service инкапсулирует `CoordinatorValidation` и `CoordinatorRules`, обеспечивая единый жизненный цикл обработки `RuntimeResult` и формирование итогового `CoordinatorResult`. + +--- + +# Текущий прогресс Decision + +| Компонент | Готовность | +|-----------|-----------:| +| Decision Architecture | ✅ Completed | +| Models | ✅ Completed | +| Protocol | ✅ Completed | +| Exceptions | ✅ Completed | +| Validation | ✅ Completed | +| Rules | ✅ Completed | +| Service | ✅ Completed | + +> Build 017 утвердил архитектуру Trading Layer как самостоятельной подсистемы платформы Dzentra. Trading Layer использует `CoordinatorResult` как единственный аналитический вход и формирует `TradingDecision` — независимый объект, описывающий итоговое торговое решение. Реализация компонентов Trading Layer начинается с Build 017.1. + +> Build 017.1 расширил Common Layer моделями Trading Layer. Добавлены `TradingDiagnostics`, `TradingEvaluationMeta` и `TradingDecision`. `TradingDecision` становится официальным выходным контрактом Trading Layer и одновременно входным контрактом для будущих слоёв Execution, Risk и Portfolio. Модель намеренно содержит только общие характеристики торгового решения без конкретных торговых действий, размеров позиции или планов исполнения. + +> Build 017.2 реализовал `TradingProtocol` — официальный публичный контракт Trading Layer. Trading Layer теперь имеет единый интерфейс взаимодействия: принимает `CoordinatorResult` и возвращает `TradingDecision`. Контракт полностью изолирован от Runtime, Coordinator internals, биржевой инфраструктуры и содержит исключительно описание публичного API без реализации. + +> Build 017.3 сформировал собственое пространство исключений Trading Layer. Добавлены `TradingError`, `InvalidTradingDecisionError`, `TradingValidationError` и `TradingExecutionError`. Иерархия полностью изолирована от Runtime Layer и Coordinator Layer и станет единым механизмом обработки ошибок для будущих компонентов Validation, Rules и Service. + +> Build 017.4 реализовал `TradingValidation` — компонент предварительной проверки входного `CoordinatorResult`. Validation использует только публичный контракт Coordinator (`is_usable` и `has_errors`), не зависит от внутренних статусов Coordinator и отделяет проверку входных данных от бизнес-логики Trading Layer. Это стало первым архитектурным улучшением по сравнению с Coordinator Foundation, усилив слабую связанность между слоями. + +> Build 017.5 реализовал `TradingRules` — центральный компонент Trading Layer, отвечающий за преобразование `CoordinatorResult` в `TradingDecision`. На этапе Foundation Rules формирует только базовый объект `TradingDecision` без реализации торговых стратегий, расчёта риска, управления позицией или исполнения сделок. Это обеспечивает стабильный публичный контракт и позволяет в дальнейшем расширять бизнес-логику без изменения архитектуры слоя. + +> Build 017.6 завершил построение Trading Foundation. Реализован `TradingService` — единая публичная точка входа Trading Layer, объединяющая `TradingValidation` и `TradingRules`. Внешние компоненты платформы взаимодействуют с Trading Layer исключительно через `TradingProtocol`, что завершает формирование единого архитектурного стандарта для слоя. + +--- + +## Архитектурная коррекция + +| Build | Описание | Статус | +|-------:|----------|:------:| +| 017.7 | Перенос Trading Foundation из `market_intelligence` в самостоятельный слой `decision` | ✅ | + +--- + +# Общий прогресс архитектуры Dzentra + +| Layer | Состояние | +|--------|----------| +| Common Foundation | ✅ Completed | +| Engine Foundation | ✅ Completed | +| Runtime Foundation | ✅ Completed | +| Coordinator Foundation | ✅ Completed | +| Decision Foundation | ✅ Completed | +| Execution Architecture | ⏳ Planned | + +> Build 017 сформировал фундамент будущего Decision Layer. Build 017.7 завершил архитектурную декомпозицию, выделив Decision в самостоятельную подсистему, полностью отделённую от Market Intelligence. + +> Начиная с Build 017.4 в Dzentra закрепляется принцип взаимодействия между слоями исключительно через публичные контракты. Trading Layer использует свойства `CoordinatorResult.is_usable` и `CoordinatorResult.has_errors`, не анализируя внутренние статусы Coordinator. Это снижает связанность подсистем и позволяет изменять внутреннюю реализацию Coordinator без изменения Trading Layer. + +> Build 017.6 завершил формирование Trading Foundation. Платформа Dzentra теперь имеет три полностью реализованных базовых слоя: Runtime Foundation, Coordinator Foundation и Trading Foundation. Все они построены по единому архитектурному шаблону (`Protocol → Validation → Rules → Service`), что обеспечивает единообразие разработки, тестирования и дальнейшего развития системы. + +> Build 017.7 завершил архитектурную декомпозицию платформы. Trading Foundation был выделен в самостоятельный слой `src/trading/decision`, полностью отделённый от `market_intelligence`. Это восстановило принцип единственной ответственности: Market Intelligence отвечает исключительно за анализ рынка, а Decision Layer — за принятие торгового решения. + +--- + +# Итоговая схема платформы + +После Build 017.7 верхнеуровневая архитектура Dzentra выглядит следующим образом: + +```text +Market Data + │ + ▼ +Market Intelligence + │ + ├── Common + ├── Engine + ├── Runtime + └── Coordinator + │ + ▼ +Decision + │ + ├── Models + ├── Protocol + ├── Validation + ├── Rules + └── Service + │ + ▼ +Execution + │ + ▼ +Exchange +``` + +Данная схема отражает окончательное разделение ответственности между анализом рынка, принятием торгового решения и будущим слоем исполнения. + +--- + +# Архитектурная веха + +Build 017.7 завершает первый крупный этап разработки Dzentra. + +Полностью реализованы фундаментальные слои платформы: + +• Common Foundation +• Engine Foundation +• Runtime Foundation +• Coordinator Foundation +• Decision Foundation + +Во всех слоях используется единый архитектурный стандарт: + +Protocol +↓ +Validation +↓ +Rules +↓ +Service + +Это означает, что дальнейшие подсистемы (Execution, Risk, Portfolio и другие) будут строиться по уже утверждённому шаблону, без изменения существующих публичных контрактов. + +Следующий этап развития платформы — построение Execution Layer, который станет первым потребителем `TradingDecision` и будет отвечать за преобразование торгового решения в конкретные действия по исполнению. + +--- + +# Следующий Build + +```text +Build 018 + +Execution Layer Architecture +``` + +--- + +# Правило сопровождения + +После завершения каждого Build обязательно выполняются следующие этапы: + +1. Compile Check (если есть исходный код). +2. Architecture Review. +3. Domain Review. +4. Documentation Review. +5. Обновление документации Build. +6. Обновление Build History. +7. Подтверждение пользователем завершения Build. + +Только после завершения полного жизненного цикла допускается переход к следующему Build. \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-001-common-enums.md b/docs/market_intelligence/builds/build-001-common-enums.md new file mode 100644 index 0000000..1b7f7a7 --- /dev/null +++ b/docs/market_intelligence/builds/build-001-common-enums.md @@ -0,0 +1,77 @@ +# Build №001 — Common Enums + +## Файл + +```text +app/src/trading/market_intelligence/common/enums.py +``` + +## Назначение + +Создание единого набора перечислений, используемых всеми аналитическими движками Market Intelligence. + +Файл определяет общий язык состояний, статусов, уровней уверенности, ролей таймфреймов и этапов обработки. + +--- + +## Реализовано + +Добавлены перечисления: + +- `MarketDirection` +- `MarketBias` +- `MarketPhase` +- `MarketRegime` +- `MarketQuality` +- `EngineStatus` +- `ConfidenceLevel` +- `SignalFreshness` +- `RiskLevel` +- `TimeframeRole` +- `CheckStatus` +- `ProcessingStage` + +--- + +## Архитектурные решения + +- перечисления не содержат торговой логики; +- перечисления не принимают торговых решений; +- значения используются только для описания состояния рынка и работы движков; +- проверки движков описываются через смысловые этапы обработки, а не через имена файлов; +- добавлен `ConfidenceLevel` для человекочитаемой интерпретации числовой уверенности; +- добавлен `SKIPPED` для корректного описания намеренно пропущенных проверок. + +--- + +## Compile Check + +```text +PASSED +``` + +## Architecture Review + +```text +PASSED +``` + +## Domain Review + +```text +PASSED +``` + +## Обязательные замечания + +Нет. + +## Рекомендации + +В будущем `ProcessingStage` можно расширить, если появится реальная необходимость. До этого перечисление не расширяется заранее. + +## Статус + +```text +ACCEPTED +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-002-common-types.md b/docs/market_intelligence/builds/build-002-common-types.md new file mode 100644 index 0000000..d2bb0ed --- /dev/null +++ b/docs/market_intelligence/builds/build-002-common-types.md @@ -0,0 +1,96 @@ +# Build №002 — Common Types + +## Файл + +```text +app/src/trading/market_intelligence/common/types.py +``` + +## Назначение + +Создание общего набора типовых алиасов для подсистемы Market Intelligence. + +Файл определяет единый типовой контракт для будущих Engine, payload, диагностики и результатов анализа. + +--- + +## Реализовано + +Добавлены типы: + +- `SymbolName` +- `TimeframeName` +- `EngineName` +- `EngineVersion` +- `ReasonCode` +- `ReasonText` +- `ScoreValue` +- `ConfidenceValue` +- `ProbabilityValue` +- `WeightValue` +- `AgeSeconds` +- `DurationMs` +- `MetricsDict` +- `PayloadDict` +- `ContextDict` +- `MarketData` +- `DependencyResults` +- `DiagnosticMessages` +- `DiagnosticValue` + +Повторно используются существующие типы проекта: + +- `JsonDict` +- `JsonList` + +из: + +```text +src.core.types +``` + +--- + +## Архитектурные решения + +- используется существующий `core.types` как единый источник истины; +- не создаются дубли `JsonDict` и `JsonList`; +- файл не зависит от runtime, execution, Telegram, Journal, EventBus или Exchange; +- файл не содержит торговой логики; +- комментарии объясняют назначение типов, а не синтаксис Python. + +--- + +## Compile Check + +```text +PASSED +``` + +## Architecture Review + +```text +PASSED +``` + +## Domain Review + +```text +PASSED +``` + +## Обязательные замечания + +Нет. + +## Рекомендации + +`DependencyResults` временно использует `Any`. После появления `EngineResult` в `common/models.py` рекомендуется заменить значение словаря на специализированный тип результата движка. + +В будущем допускается переход от универсальных словарей `PayloadDict`, `MetricsDict`, `ContextDict` к специализированным `TypedDict`, если это потребуется для усиления типизации. + +## Статус + +```text +ACCEPTED +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-003-common-constants.md b/docs/market_intelligence/builds/build-003-common-constants.md new file mode 100644 index 0000000..b4b9c7e --- /dev/null +++ b/docs/market_intelligence/builds/build-003-common-constants.md @@ -0,0 +1,126 @@ +# Build №003 — Common Constants + +## Файл + +```text +app/src/trading/market_intelligence/common/constants.py +``` + +## Назначение + +Создание общего набора архитектурных констант Market Intelligence. + +Файл определяет безопасные диапазоны значений, ограничения диагностики, базовые настройки свежести данных и защитные ограничения аналитического слоя. + +--- + +## Реализовано + +Добавлены группы констант: + +- диапазоны `score`; +- диапазоны `confidence`; +- диапазоны `probability`; +- диапазоны `weight`; +- значения по умолчанию; +- пороги человекочитаемой оценки; +- время устаревания аналитического результата; +- время жизни аналитического сигнала; +- ограничения количества зависимостей Engine; +- ограничения количества метрик; +- ограничения количества предупреждений и ошибок; +- базовая конфигурация таймфреймов; +- запрещённые торговые поля. + +--- + +## Архитектурные решения + +- `common/constants.py` содержит только архитектурные ограничения платформы; +- файл не содержит параметров технических индикаторов; +- файл не содержит параметров торговых стратегий; +- файл не содержит настроек открытия или закрытия сделок; +- константы не зависят от конкретного Engine; +- `FORBIDDEN_TRADING_FIELDS` вводит защиту границ между аналитикой и торговыми действиями. + +--- + +## Compile Check + +```text +PASSED +``` + +## Architecture Review + +```text +PASSED +``` + +## Domain Review + +```text +PASSED +``` + +## Обязательные замечания + +Нет. + +## Post Review Notes + +### Решение №001 + +Архитектурные константы должны содержать только ограничения платформы. + +В `common/constants.py` запрещается размещать: + +- параметры технических индикаторов; +- параметры торговых стратегий; +- настройки открытия и закрытия сделок; +- параметры биржи; +- параметры управления позицией. + +Такие константы должны размещаться внутри соответствующих Engine. + +### Решение №002 + +Константы должны быть сгруппированы по смысловым разделам: + +```text +Score +Confidence +Probability +Runtime +Diagnostics +Architecture +Timeframes +Safety +``` + +### Решение №003 + +Таймфреймы являются частью конфигурации платформы, а не жёстким ограничением архитектуры. + +`DEFAULT_TIMEFRAMES` описывает базовую конфигурацию первого этапа Market Intelligence. + +### Решение №004 + +Возраст результата анализа и возраст торгового сигнала являются разными понятиями. + +- `DEFAULT_STALE_AFTER_SECONDS` — когда аналитический результат считается устаревшим. +- `DEFAULT_SIGNAL_TTL_SECONDS` — когда влияние аналитического сигнала начинает уменьшаться. + +### Решение №005 + +Константы, относящиеся только к одному Engine, не должны находиться в `common/constants.py`. + +Они должны переноситься в каталог соответствующего Engine. + +--- + +## Статус + +```text +ACCEPTED +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-004-common-reasons.md b/docs/market_intelligence/builds/build-004-common-reasons.md new file mode 100644 index 0000000..5115856 --- /dev/null +++ b/docs/market_intelligence/builds/build-004-common-reasons.md @@ -0,0 +1,101 @@ +# Build №004 — Common Reasons + +## Файл + +```text +app/src/trading/market_intelligence/common/reasons.py +``` + +## Назначение + +Создание единого реестра машинных кодов причин `ReasonCode`. + +Файл определяет стандартный словарь причин, который используется будущими Engine для диагностики, payload, snapshot, событий и журналирования. + +--- + +## Реализовано + +Добавлен класс: + +```text +ReasonCode +``` + +Он содержит группы причин для следующих областей: + +- Common; +- Data; +- Engine Runtime; +- Validation; +- Market State; +- Structure; +- Trend; +- Momentum; +- Volatility; +- Wave; +- Cycle; +- Liquidity; +- Regime; +- Confidence; +- Signal Aging; +- Timeframe. + +--- + +## Архитектурные решения + +- движки публикуют стандартизированные коды причин; +- движки не формируют человекочитаемый текст; +- причины отделены от пользовательского интерфейса; +- причины не содержат торговых действий; +- единый словарь причин используется всеми Engine платформы; +- причины описывают состояние рынка или состояние работы движка. + +--- + +## Compile Check + +```text +PASSED +``` + +## Architecture Review + +```text +PASSED +``` + +## Domain Review + +```text +PASSED +``` + +## Обязательные замечания + +Нет. + +## Рекомендации + +После завершения базовых моделей рекомендуется реализовать отдельный слой формирования человекочитаемых объяснений. + +Возможное имя файла: + +```text +common/reason_texts.py +``` + +или: + +```text +common/explanations.py +``` + +Этот слой будет преобразовывать `ReasonCode` в понятные сообщения для журнала, интерфейса и отчётов. + +## Статус + +```text +ACCEPTED +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-005-common-scores.md b/docs/market_intelligence/builds/build-005-common-scores.md new file mode 100644 index 0000000..a20e2d0 --- /dev/null +++ b/docs/market_intelligence/builds/build-005-common-scores.md @@ -0,0 +1,88 @@ +# Build №005 — Common Scores + +## Файл + +```text +app/src/trading/market_intelligence/common/scores.py +``` + +## Назначение + +Создание единого слоя оценок для Market Intelligence. + +Файл определяет общий способ работы с: + +- score; +- confidence; +- probability; +- weight; +- weighted score; +- score breakdown. + +--- + +## Реализовано + +Добавлены функции: + +- `clamp_score` +- `clamp_confidence` +- `clamp_probability` +- `clamp_weight` +- `classify_score_quality` +- `classify_confidence_level` + +Добавлены модели: + +- `EngineScore` +- `EngineConfidence` +- `ProbabilityScore` +- `WeightedScore` +- `ScoreBreakdown` + +--- + +## Архитектурные решения + +- все числовые оценки приводятся к безопасным диапазонам; +- score всегда работает в диапазоне `0...100`; +- confidence всегда работает в диапазоне `0...1`; +- probability всегда работает в диапазоне `0...100`; +- weight всегда работает в диапазоне `0...1`; +- числовые значения получают человекочитаемую интерпретацию; +- итоговая оценка может объясняться через `ScoreBreakdown`; +- файл не содержит торговых решений. + +--- + +## Compile Check + +```text +PASSED +``` + +## Architecture Review + +```text +PASSED +``` + +## Domain Review + +```text +PASSED +``` + +## Обязательные замечания + +Нет. + +## Рекомендации + +После появления `common/validation.py` часть проверок диапазонов может быть повторно использована в механизме валидации EngineResult и Payload. + +## Статус + +```text +ACCEPTED +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-006-1-common-models-engine-metadata.md b/docs/market_intelligence/builds/build-006-1-common-models-engine-metadata.md new file mode 100644 index 0000000..6d78776 --- /dev/null +++ b/docs/market_intelligence/builds/build-006-1-common-models-engine-metadata.md @@ -0,0 +1,268 @@ +# Build 006.1 — Common Models Extension / EngineMetadata + +**Engineering Build Document** + +--- + +## Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 006.1 | +| Название | Common Models Extension / EngineMetadata | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Common | +| Тип | Architecture Extension | +| Версия | 1.0 | +| Язык | Русский | + +--- + +## Причина появления Build + +Во время проектирования **Engine Layer** было обнаружено, что базовый контракт Engine требует наличия модели, описывающей сам Engine как архитектурный компонент. + +Первоначально предполагалось, что данная модель относится к Engine Layer. + +Однако архитектурный анализ показал, что она используется значительно шире. + +Модель необходима для: + +- Engine Layer; +- Runtime Layer; +- будущего Registry; +- будущего Coordinator; +- построения графа зависимостей; +- диагностики; +- документации. + +Следовательно, данная модель является частью **Common Layer**, а не Engine Layer. + +Для сохранения чистоты архитектуры было принято решение выпустить отдельный Build 006.1 вместо изменения уже принятого Build 006. + +--- + +## Цель Build + +Добавить в Common Layer новую базовую модель: + +```text +EngineMetadata +``` + +которая описывает Engine как компонент платформы, а не результат его работы. + +--- + +## Архитектурное решение + +Модель размещается в: + +```text +app/src/trading/market_intelligence/common/models.py +``` + +а не в: + +```text +app/src/trading/market_intelligence/engine/ +``` + +поскольку является общим контрактом платформы. + +--- + +## Architecture Decision (ADR) + +### Решение + +Добавить модель `EngineMetadata` в Common Layer. + +### Статус + +**Accepted** + +### Обоснование + +Во время проектирования Engine Layer было установлено, что описание Engine используется не только самим Engine, но и Runtime, Registry, Coordinator и другими архитектурными компонентами. + +Следовательно, `EngineMetadata` является общим контрактом платформы и должна располагаться в `common.models`. + +Размещение модели в Engine Layer привело бы к неправильному направлению архитектурных зависимостей и нарушило бы принцип повторного использования общих моделей. + +### Последствия + +После принятия решения: + +- Engine Layer использует `EngineMetadata` из Common Layer; +- Runtime зависит только от общих контрактов; +- Coordinator сможет получать описание Engine без знания его реализации; +- архитектурные зависимости остаются однонаправленными. + +--- + +## Состав модели + +```python +@dataclass(frozen=True, slots=True) +class EngineMetadata: + # Описание Engine как компонента платформы. + # Metadata не содержит аналитической логики и не является результатом анализа. + # Она используется Runtime, Registry, Coordinator и документацией. + name: EngineName + version: EngineVersion + description: str = "" + supported_timeframes: tuple[TimeframeName, ...] = () + required_dependencies: tuple[EngineName, ...] = () + optional_dependencies: tuple[EngineName, ...] = () + enabled_by_default: bool = True +``` + +--- + +## Обоснование полей + +### name + +Уникальное имя Engine. + +Используется Registry, Coordinator и журналированием. + +--- + +### version + +Версия реализации Engine. + +Позволяет определять, какая именно версия логики сформировала результат анализа. + +--- + +### description + +Краткое описание назначения Engine. + +Используется документацией, диагностикой и интерфейсами разработчика. + +--- + +### supported_timeframes + +Перечень поддерживаемых таймфреймов. + +Позволяет Coordinator определить применимость Engine для конкретного анализа. + +--- + +### required_dependencies + +Перечень обязательных зависимостей. + +Если хотя бы один требуемый Engine недоступен, выполнение данного Engine невозможно. + +--- + +### optional_dependencies + +Перечень необязательных зависимостей. + +Их отсутствие не запрещает выполнение Engine, но может снизить качество анализа. + +--- + +### enabled_by_default + +Признак регистрации Engine по умолчанию. + +Позволяет включать или отключать Engine без изменения Runtime. + +--- + +## Что Build НЕ добавляет + +Build сознательно не включает: + +- Runtime; +- Registry; +- Coordinator; +- Dependency Graph; +- порядок запуска Engine; +- приоритеты выполнения; +- настройки Runtime; +- timeout; +- retry; +- cache policy; +- аналитическую логику. + +Эти возможности относятся к следующим Build. + +--- + +## Архитектурные последствия + +После появления `EngineMetadata` становится возможным построение базового контракта Engine Layer. + +Новая зависимость архитектуры выглядит следующим образом: + +```text +Common Layer +│ +├── EngineContext +├── EngineResult +├── EngineDependencyResult +└── EngineMetadata + + ▲ + +Engine Layer +│ +├── EngineProtocol +└── Engine Implementation + + ▲ + +Runtime Layer + + ▲ + +Coordinator Layer +``` + +--- + +## Итоги Build + +В результате Build 006.1: + +- Common Layer получил новый общий контракт платформы; +- устранён архитектурный пробел, обнаруженный при проектировании Engine Layer; +- подготовлена основа для реализации `EngineProtocol`; +- Runtime сможет использовать единый контракт Engine без знания конкретных реализаций. + +--- + +## Acceptance + +Build считается завершённым после выполнения: + +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +--- + +## Следующий Build + +```text +Build 014 + +Engine Base Contracts + +engine/protocol.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-006-common-models.md b/docs/market_intelligence/builds/build-006-common-models.md new file mode 100644 index 0000000..c49d313 --- /dev/null +++ b/docs/market_intelligence/builds/build-006-common-models.md @@ -0,0 +1,263 @@ +# Build №006 — Common Models + +## Файл + +```text +app/src/trading/market_intelligence/common/models.py +``` + +--- + +# Назначение + +Создание единого архитектурного контракта результатов работы всех аналитических движков платформы. + +Данный файл определяет общий формат обмена данными между Engine и является центральной моделью подсистемы **Market Intelligence**. + +Любой аналитический движок платформы должен использовать данный контракт независимо от своей предметной области. + +--- + +# Реализовано + +Созданы базовые модели: + +- `EngineMetric` +- `EngineDiagnostics` +- `EngineEvaluationMeta` +- `EngineDependencyResult` +- `EngineContext` +- `EngineResult` + +Все модели реализованы как неизменяемые (`frozen=True`) dataclass с использованием `slots=True`. + +--- + +# Назначение моделей + +## EngineMetric + +Описывает одну измеряемую характеристику, рассчитанную движком. + +Примеры: + +- сила движения; +- процент волатильности; +- ширина спреда; +- длительность волны. + +Метрика не является торговым решением. + +--- + +## EngineDiagnostics + +Хранит техническую диагностику выполнения движка. + +Используется для: + +- журналирования; +- диагностики; +- проверки качества работы; +- анализа ошибок. + +Диагностика полностью отделена от пользовательского интерфейса. + +--- + +## EngineEvaluationMeta + +Содержит служебную информацию о расчёте. + +Например: + +- название движка; +- версия движка; +- время выполнения; +- длительность расчёта; +- возраст входных данных. + +--- + +## EngineDependencyResult + +Представляет результат другого аналитического движка в компактной форме. + +Используется для построения зависимостей между Engine без прямых импортов. + +--- + +## EngineContext + +Определяет единый входной контракт любого аналитического движка. + +Контекст содержит только данные, необходимые для анализа рынка. + +Контекст не содержит информации о: + +- позициях; +- балансе; +- исполнении; +- торговых приказах; +- пользовательском интерфейсе. + +--- + +## EngineResult + +Является единым результатом работы любого Engine. + +Содержит: + +- состояние выполнения; +- оценки; +- уровень уверенности; +- направление рынка; +- режим рынка; +- фазу рынка; +- диагностическую информацию; +- рассчитанные метрики; +- служебные сведения. + +EngineResult описывает только результат анализа рынка. + +EngineResult не содержит торговых решений. + +--- + +# Архитектурные решения + +Во время реализации приняты следующие решения. + +## Единый контракт + +Все аналитические движки используют один и тот же тип результата. + +Это позволяет Coordinator работать с любым Engine одинаковым образом. + +--- + +## Разделение ответственности + +Контракт разделён на независимые части: + +- входные данные (`EngineContext`); +- результат анализа (`EngineResult`); +- диагностика (`EngineDiagnostics`); +- служебные сведения (`EngineEvaluationMeta`); +- зависимости (`EngineDependencyResult`); +- отдельные измеряемые показатели (`EngineMetric`). + +--- + +## Независимость от торговли + +Контракт не содержит: + +- открытия позиции; +- закрытия позиции; +- управления ордерами; +- расчёта размера позиции; +- информации о балансе; +- информации о бирже. + +Market Intelligence остаётся исключительно аналитическим уровнем платформы. + +--- + +## Независимость движков + +Ни один Engine не импортирует другой Engine напрямую. + +Передача результатов между движками осуществляется через `EngineDependencyResult`. + +Это исключает циклические зависимости и упрощает масштабирование платформы. + +--- + +## Минимальный контекст + +Engine получает только необходимые входные данные. + +Контекст не превращается в универсальное хранилище состояния платформы. + +Это позволяет каждому Engine работать независимо. + +--- + +## Иммутабельность + +Все модели объявлены как неизменяемые (`frozen=True`). + +После формирования результата он больше не изменяется. + +Это обеспечивает: + +- предсказуемость; +- безопасную передачу между компонентами; +- стабильное журналирование; +- корректное сравнение результатов. + +--- + +# Compile Check + +```text +PASSED +``` + +--- + +# Architecture Review + +```text +PASSED +``` + +--- + +# Domain Review + +```text +PASSED +``` + +--- + +# Обязательные замечания + +Нет. + +--- + +# Рекомендации + +В дальнейшем рекомендуется дополнить архитектуру следующими сущностями после появления соответствующей практической необходимости: + +- идентификатор результата (`result_id`); +- источник рыночных данных (`data_source`); +- время окончания актуальности результата (`expires_at`). + +До появления реальной потребности данные поля не добавляются. + +Это соответствует принципу **No Premature Abstractions**. + +--- + +# Статус + +```text +ACCEPTED +``` + +--- + +# Итоги Build + +Build №006 завершил проектирование единого контракта аналитических движков. + +Начиная с данного Build все новые Engine должны возвращать результат исключительно через `EngineResult`. + +Создание собственных моделей результатов внутри отдельных Engine запрещается. + +`common/models.py` становится единым источником истины для архитектуры результатов Market Intelligence. \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-007-common-validation.md b/docs/market_intelligence/builds/build-007-common-validation.md new file mode 100644 index 0000000..c1ae1dd --- /dev/null +++ b/docs/market_intelligence/builds/build-007-common-validation.md @@ -0,0 +1,248 @@ +# Build 007 — Common Validation Layer + +**Build ID:** 007 + +**Component:** `common/validation.py` + +**Stage:** Stage 08.2 — Common Foundation + +**Status:** **ACCEPTED** + +--- + +# Цель Build + +Создать единый слой архитектурной проверки (**Validation Layer**) для компонентов Market Intelligence. + +Validation Layer отвечает за проверку соблюдения Runtime Contract и архитектурных ограничений Common Layer. + +Данный слой не анализирует рынок и не принимает торговых решений. + +--- + +# Контекст + +К моменту начала Build уже существовали: + +- единые перечисления (`enums.py`); +- общие типы (`types.py`); +- архитектурные константы (`constants.py`); +- единый словарь причин (`reasons.py`); +- модели оценок (`scores.py`); +- единые Runtime-модели (`models.py`). + +Следующим необходимым компонентом стала централизованная проверка корректности этих моделей. + +--- + +# Реализовано + +Создан файл: + +```text +app/src/trading/market_intelligence/common/validation.py +``` + +Добавлены модели: + +- ValidationIssue +- ValidationResult + +Добавлены проверки: + +- validate_engine_context() +- validate_engine_result() +- validate_payload_has_no_trading_fields() + +--- + +# Назначение Validation Layer + +Validation Layer выполняет исключительно архитектурную проверку. + +Он отвечает за: + +- проверку обязательных полей Runtime Contract; +- проверку ограничений Common Layer; +- проверку запрещённых торговых полей; +- формирование диагностического результата проверки. + +Validation Layer не анализирует состояние рынка. + +--- + +# Архитектурные решения + +Во время Build были подтверждены следующие решения. + +## Validation не принимает торговых решений + +Validation Layer не выполняет: + +- анализ рынка; +- расчёт сигналов; +- принятие торговых решений; +- взаимодействие с биржей. + +Он проверяет только соблюдение архитектурного контракта. + +--- + +## Validation возвращает результат проверки + +Проверка не использует исключения как основной механизм обработки. + +Результатом проверки всегда является объект: + +```text +ValidationResult +``` + +Это позволяет Coordinator и Runtime безопасно обрабатывать ошибки без остановки всей платформы. + +--- + +## Validation использует единые модели Common + +Validation Layer повторно использует: + +- EngineContext +- EngineResult +- ReasonCode +- CheckStatus +- EngineStatus + +Новые дублирующие модели не создаются. + +--- + +## Validation защищает границы Market Intelligence + +Добавлена централизованная проверка запрещённых торговых полей. + +Используется архитектурная константа: + +```text +FORBIDDEN_TRADING_FIELDS +``` + +Это предотвращает случайное проникновение торговой логики в аналитический слой. + +--- + +## Validation не зависит от конкретных Engine + +Validation Layer не содержит: + +- знаний о Trend Engine; +- знаний о Wave Engine; +- знаний о Liquidity Engine; +- знаний о Strategy; +- знаний о Execution. + +Он одинаково применим для любого аналитического движка платформы. + +--- + +# Compile Check + +Статус: + +**PASSED** + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence/common/validation.py +``` + +Компиляция завершилась успешно. + +--- + +# Architecture Review + +Статус: + +**PASSED** + +Проверено: + +- отсутствие циклических зависимостей; +- соблюдение Layer Isolation; +- повторное использование моделей Common; +- соблюдение Runtime Contract; +- отсутствие нарушения зон ответственности. + +Обязательные замечания отсутствуют. + +--- + +# Domain Review + +Статус: + +**PASSED** + +Подтверждено: + +- отсутствует торговая логика; +- отсутствуют торговые решения; +- отсутствует взаимодействие с биржей; +- отсутствует работа с позициями; +- Validation выполняет только проверку архитектурного контракта. + +--- + +# Engineering Review + +Подтверждено формирование законченного архитектурного фундамента Common Layer. + +На текущем этапе сформирована следующая последовательность компонентов: + +```text +types + ↓ +enums + ↓ +constants + ↓ +reasons + ↓ +scores + ↓ +models + ↓ +validation +``` + +Validation Layer завершает базовый уровень проверки архитектурного контракта. + +--- + +# Рекомендации + +В дальнейшем допускается развитие Validation Layer следующими возможностями: + +- специализированные проверки отдельных Engine; +- расширенная проверка Runtime Contract; +- интеграция с будущим Review Layer; +- автоматическая генерация диагностических отчётов. + +Данные возможности не входят в текущий Build. + +--- + +# Итог + +Build №007 завершает создание базового Validation Layer подсистемы Market Intelligence. + +Компонент полностью соответствует: + +- Development Process v2.0; +- Architecture Principles; +- Runtime Contract. + +Build получает статус: + +**ACCEPTED** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-008-common-checks.md b/docs/market_intelligence/builds/build-008-common-checks.md new file mode 100644 index 0000000..c9b1243 --- /dev/null +++ b/docs/market_intelligence/builds/build-008-common-checks.md @@ -0,0 +1,248 @@ +# Build 008 — Common Checks Layer + +**Build ID:** 008 + +**Component:** `common/checks.py` + +**Stage:** Stage 08.2 — Common Foundation + +**Status:** **ACCEPTED** + +--- + +# Цель Build + +Создать единый слой инженерных проверок (**Checks Layer**) для аналитических движков подсистемы Market Intelligence. + +Checks Layer предназначен для фиксации прохождения отдельных этапов обработки Engine и формирования единого инженерного отчёта о ходе выполнения анализа. + +Данный слой не выполняет проверку корректности моделей и не анализирует состояние рынка. + +--- + +# Контекст + +К началу Build уже существовали: + +- единые перечисления (`enums.py`); +- общие типы (`types.py`); +- архитектурные константы (`constants.py`); +- единый словарь причин (`reasons.py`); +- модели оценок (`scores.py`); +- Runtime-модели (`models.py`); +- Validation Layer (`validation.py`). + +Следующим этапом стало создание отдельного слоя инженерных проверок выполнения Engine. + +--- + +# Реализовано + +Создан файл: + +```text +app/src/trading/market_intelligence/common/checks.py +``` + +Добавлены модели: + +- EngineCheck +- EngineCheckReport + +Добавлены функции: + +- build_check() +- build_check_report() + +--- + +# Назначение Checks Layer + +Checks Layer используется для фиксации результатов внутренних этапов обработки аналитического движка. + +Он позволяет: + +- описывать прохождение отдельных этапов Runtime; +- объединять проверки в единый отчёт; +- отделить инженерные проверки выполнения от проверки архитектурного контракта. + +Checks Layer не анализирует рынок и не проверяет корректность входных данных. + +--- + +# Архитектурные решения + +Во время Build были подтверждены следующие решения. + +## Checks Layer не заменяет Validation Layer + +Validation Layer и Checks Layer имеют разные области ответственности. + +Validation Layer отвечает на вопрос: + +> **Корректен ли Runtime Contract?** + +Checks Layer отвечает на вопрос: + +> **Какие этапы обработки выполнил Engine?** + +Таким образом оба слоя взаимно дополняют друг друга, не дублируя функциональность. + +--- + +## Проверка выполняется по этапам обработки + +Каждая проверка относится к одному значению `ProcessingStage`. + +Например: + +- INPUT +- CALCULATION +- VALIDATION +- PAYLOAD +- SNAPSHOT +- RESULT +- EVENT + +Это позволяет анализировать работу Engine поэтапно. + +--- + +## Отчёт агрегирует независимые проверки + +Engine может сформировать несколько отдельных проверок. + +Они объединяются в объект: + +```text +EngineCheckReport +``` + +Отчёт определяет общий инженерный статус выполнения без повторной проверки отдельных этапов. + +--- + +## Checks Layer не содержит предметной логики + +Checks Layer не знает: + +- что такое тренд; +- что такое волна; +- что такое ликвидность; +- что такое фаза рынка. + +Он работает исключительно с инженерными этапами выполнения Runtime. + +--- + +## Checks Layer не зависит от конкретных Engine + +Файл не содержит ссылок на: + +- Trend Engine; +- Wave Engine; +- Liquidity Engine; +- Coordinator; +- Strategy; +- Execution. + +Любой аналитический движок может использовать данный слой без изменений. + +--- + +# Compile Check + +Статус: + +**PASSED** + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence/common/checks.py +``` + +Компиляция завершилась успешно. + +--- + +# Architecture Review + +Статус: + +**PASSED** + +Подтверждено: + +- отсутствие циклических зависимостей; +- корректное разделение Validation Layer и Checks Layer; +- соблюдение Layer Isolation; +- повторное использование моделей Common; +- масштабируемость архитектуры. + +Обязательные замечания отсутствуют. + +--- + +# Domain Review + +Статус: + +**PASSED** + +Подтверждено: + +- отсутствует торговая логика; +- отсутствует анализ рынка; +- отсутствует взаимодействие с биржей; +- отсутствует работа с позициями; +- Checks Layer выполняет исключительно инженерную диагностику выполнения Engine. + +--- + +# Engineering Review + +После завершения Build №008 фундамент Common Layer получил два независимых уровня контроля качества: + +```text +Runtime Contract + ↓ +Validation Layer + ↓ +Checks Layer +``` + +Validation Layer отвечает за корректность архитектурного контракта. + +Checks Layer отвечает за фиксацию прохождения этапов обработки. + +Такое разделение обеспечивает независимое развитие обоих компонентов и предотвращает смешивание архитектурных обязанностей. + +--- + +# Рекомендации + +В дальнейшем допускается развитие Checks Layer следующими возможностями: + +- группировка проверок по Engine; +- сохранение времени выполнения отдельных этапов; +- интеграция с Runtime Review; +- автоматическое формирование инженерных отчётов. + +Данные возможности не входят в текущий Build. + +--- + +# Итог + +Build №008 завершает создание базового Checks Layer подсистемы Market Intelligence. + +Компонент полностью соответствует: + +- Development Process v2.0; +- Architecture Principles; +- Runtime Contract. + +Build получает статус: + +**ACCEPTED** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-009-common-payloads.md b/docs/market_intelligence/builds/build-009-common-payloads.md new file mode 100644 index 0000000..5989be1 --- /dev/null +++ b/docs/market_intelligence/builds/build-009-common-payloads.md @@ -0,0 +1,187 @@ +# Build 009 — Common Payloads Layer + +**Build ID:** 009 + +**Компонент:** `common/payloads.py` + +**Статус:** **Accepted** + +--- + +# Цель Build + +Реализовать единый механизм преобразования результатов работы Market Intelligence в стандартный диагностический Payload. + +Payload является официальным способом передачи результатов аналитических движков между слоями платформы, а также используется для: + +- журналирования; +- диагностики; +- Snapshot; +- Runtime; +- Event; +- последующей сериализации. + +Данный Build не реализует торговую логику и не принимает торговых решений. + +--- + +# Реализованные компоненты + +Создан файл: + +```text +market_intelligence/common/payloads.py +``` + +Реализованы функции: + +- `value_to_payload()` +- `mapping_to_payload()` +- `engine_score_to_payload()` +- `engine_confidence_to_payload()` +- `engine_metric_to_payload()` +- `engine_diagnostics_to_payload()` +- `engine_dependency_to_payload()` +- `engine_result_to_payload()` + +--- + +# Архитектурная задача + +До появления данного Build каждый Engine мог потенциально формировать собственный формат диагностических данных. + +После реализации общего Payload Layer все Engine используют единый механизм сериализации. + +Таким образом достигаются: + +- единый формат Runtime; +- единый формат Snapshot; +- единый формат Diagnostics; +- единый формат Event; +- единый формат журналирования. + +--- + +# Архитектурные решения + +В ходе реализации приняты следующие решения. + +## Единая сериализация + +Все значения проходят единый процесс преобразования. + +Поддерживаются: + +- dataclass; +- Enum; +- Mapping; +- tuple; +- list; +- простые типы Python. + +Payload всегда содержит только сериализуемые структуры. + +--- + +## Payload не принимает решений + +Payload является исключительно транспортным представлением результата. + +Он не содержит: + +- торговых команд; +- расчётов позиции; +- рекомендаций на открытие сделки; +- изменений Runtime. + +--- + +## Автоматическая проверка архитектурных ограничений + +Перед возвратом итогового Payload выполняется: + +```text +validate_payload_has_no_trading_fields() +``` + +Если обнаружены запрещённые торговые поля, информация сохраняется в диагностическом разделе Payload. + +Нарушение не скрывается и становится доступным для анализа во время Runtime и Engineering Review. + +--- + +## Независимость от Engine + +Payload Layer не знает: + +- конкретные Engine; +- Strategy; +- AutoTrade; +- Exchange; +- Telegram UI. + +Компонент работает исключительно с общими моделями слоя Common. + +--- + +# Compile Check + +Выполнена проверка: + +```bash +python -m compileall \ +src/trading/market_intelligence/common/payloads.py +``` + +Результат: + +```text +Compile successful +``` + +--- + +# Architecture Review + +Результат: **PASSED** + +Проверено: + +- соблюдение ответственности слоя; +- отсутствие торговой логики; +- независимость от Runtime; +- независимость от UI; +- независимость от AutoTrade; +- единый механизм сериализации. + +Архитектурных замечаний не выявлено. + +--- + +# Domain Review + +Результат: **PASSED** + +Проверено: + +- отсутствие торговых решений; +- объяснимость структуры Payload; +- пригодность для журналирования; +- пригодность для диагностики; +- соответствие Runtime Contract. + +Замечаний не выявлено. + +--- + +# Итог Build + +Build успешно завершил создание общего слоя сериализации результатов Market Intelligence. + +Payload становится официальным форматом передачи аналитических результатов между компонентами платформы. + +--- + +# Build Status + +**Accepted** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-010-common-snapshots.md b/docs/market_intelligence/builds/build-010-common-snapshots.md new file mode 100644 index 0000000..7e1a083 --- /dev/null +++ b/docs/market_intelligence/builds/build-010-common-snapshots.md @@ -0,0 +1,193 @@ +# Build 010 — Common Snapshots Layer + +**Build ID:** 010 + +**Компонент:** `common/snapshots.py` + +**Статус:** **Accepted** + +--- + +# Цель Build + +Реализовать единый слой Snapshot для подсистемы Market Intelligence. + +Snapshot представляет собой неизменяемый снимок результата работы аналитического движка в определённый момент времени. + +Данный Build создаёт единый формат хранения результатов анализа без привязки к конкретному Engine. + +--- + +# Реализованные компоненты + +Создан файл: + +```text +market_intelligence/common/snapshots.py +``` + +Реализованы: + +- `EngineSnapshot` +- `build_engine_snapshot()` +- `engine_snapshot_to_payload()` + +--- + +# Архитектурная задача + +До появления данного Build существовал единый Runtime Result (`EngineResult`), но отсутствовал стандартный механизм фиксации его состояния. + +После реализации Snapshot Layer каждый результат анализа может быть сохранён в неизменяемом виде. + +Snapshot становится стандартным объектом для: + +- журналирования; +- диагностики; +- формирования Event; +- хранения истории анализа; +- последующего сравнения состояний рынка. + +--- + +# Архитектурные решения + +В ходе реализации приняты следующие решения. + +## Snapshot является неизменяемым + +Используется + +```python +@dataclass(frozen=True, slots=True) +``` + +После создания Snapshot его содержимое больше не изменяется. + +Это гарантирует воспроизводимость результатов анализа. + +--- + +## Snapshot строится только из EngineResult + +Snapshot никогда не вычисляет данные самостоятельно. + +Он всегда строится через: + +```text +EngineResult + ↓ +engine_result_to_payload() + ↓ +EngineSnapshot +``` + +Таким образом существует единая точка формирования результата анализа. + +--- + +## Snapshot не содержит торговой логики + +Snapshot не имеет права хранить: + +- команды открытия позиции; +- команды закрытия позиции; +- расчёт размера позиции; +- состояние AutoTrade; +- состояние Exchange; +- Runtime Position. + +Snapshot фиксирует исключительно аналитический результат. + +--- + +## Payload является частью Snapshot + +Snapshot не копирует отдельные поля EngineResult. + +Вместо этого используется единый Payload Layer. + +Это исключает дублирование логики сериализации. + +--- + +## Независимость от Runtime + +Snapshot Layer не зависит от: + +- AutoTrade; +- Exchange; +- Telegram; +- Journal; +- конкретных Engine. + +Компонент использует исключительно сущности Common Layer. + +--- + +# Compile Check + +Выполнена проверка: + +```bash +python -m compileall \ +src/trading/market_intelligence/common/snapshots.py +``` + +Результат: + +```text +Compile successful +``` + +--- + +# Architecture Review + +Результат: + +**PASSED** + +Проверено: + +- соблюдение Single Responsibility; +- неизменяемость Snapshot; +- отсутствие торговой логики; +- использование единого Payload Layer; +- отсутствие нарушения архитектурных границ. + +Замечаний не выявлено. + +--- + +# Domain Review + +Результат: + +**PASSED** + +Проверено: + +- Snapshot отражает исключительно состояние анализа; +- отсутствуют торговые решения; +- структура пригодна для журналирования; +- структура пригодна для Runtime; +- структура соответствует Runtime Contract. + +Замечаний не выявлено. + +--- + +# Итог Build + +Build завершил создание общего Snapshot Layer. + +Все аналитические движки платформы получили единый механизм фиксации собственного состояния. + +Snapshot становится стандартным архитектурным объектом Market Intelligence. + +--- + +# Build Status + +**Accepted** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-011-common-events.md b/docs/market_intelligence/builds/build-011-common-events.md new file mode 100644 index 0000000..ff9e8d0 --- /dev/null +++ b/docs/market_intelligence/builds/build-011-common-events.md @@ -0,0 +1,205 @@ +# Build 011 — Common Events Layer + +**Build ID:** 011 + +**Компонент:** `common/events.py` + +**Статус:** **Accepted** + +--- + +# Цель Build + +Реализовать единый слой представления событий подсистемы Market Intelligence. + +Events Layer определяет стандартный формат аналитических событий, которые в дальнейшем смогут использоваться Runtime, журналом, системой диагностики и EventBus. + +Данный Build не реализует публикацию событий и не зависит от конкретного механизма доставки. + +--- + +# Реализованные компоненты + +Создан файл: + +```text +market_intelligence/common/events.py +``` + +Реализованы: + +- `MarketIntelligenceEvent` +- `build_engine_result_event()` +- `build_engine_snapshot_event()` +- `build_result_snapshot_event()` +- `market_intelligence_event_to_payload()` + +--- + +# Архитектурная задача + +До появления данного Build существовали: + +```text +EngineResult + +↓ + +Payload + +↓ + +Snapshot +``` + +Однако отсутствовало единое архитектурное представление аналитического события. + +После реализации Events Layer любое завершение работы Engine может быть представлено в виде стандартного события Market Intelligence. + +--- + +# Архитектурные решения + +В ходе реализации приняты следующие решения. + +## Event является архитектурной моделью + +MarketIntelligenceEvent описывает событие анализа рынка. + +Сам объект события не занимается публикацией. + +Таким образом разделяются: + +```text +Event Model + +↓ + +Event Transport +``` + +Это позволяет использовать любые механизмы доставки без изменения модели события. + +--- + +## Event строится из существующих моделей + +События создаются исключительно через существующие архитектурные объекты. + +Поддерживаются цепочки: + +```text +EngineResult + ↓ +Event +``` + +и + +```text +EngineResult + ↓ +Snapshot + ↓ +Event +``` + +Никаких дополнительных расчётов внутри Event Layer не выполняется. + +--- + +## Payload не дублируется + +Events Layer не сериализует данные самостоятельно. + +Используются уже существующие компоненты: + +- Payload Layer; +- Snapshot Layer. + +Это сохраняет принцип единственного источника истины. + +--- + +## Независимость от EventBus + +Common Layer определяет только структуру события. + +Он ничего не знает о: + +- EventBus; +- Runtime; +- Journal; +- AutoTrade; +- Telegram; +- Exchange. + +Таким образом Event Layer остаётся полностью независимым. + +--- + +# Compile Check + +Выполнена проверка: + +```bash +python -m compileall \ +src/trading/market_intelligence/common/events.py +``` + +Результат: + +```text +Compile successful +``` + +--- + +# Architecture Review + +Результат: + +**PASSED** + +Проверено: + +- соблюдение Single Responsibility; +- отсутствие публикации событий; +- отсутствие торговой логики; +- использование существующих моделей; +- независимость от Runtime; +- отсутствие циклических зависимостей. + +Архитектурных замечаний не выявлено. + +--- + +# Domain Review + +Результат: + +**PASSED** + +Проверено: + +- событие отражает только факт завершения анализа; +- отсутствуют торговые действия; +- отсутствуют Runtime-решения; +- структура пригодна для журналирования; +- структура соответствует Runtime Contract. + +Замечаний не выявлено. + +--- + +# Итог Build + +Build завершил создание общего Events Layer. + +Market Intelligence получил единый формат аналитических событий, который может использоваться любыми внешними компонентами платформы. + +--- + +# Build Status + +**Accepted** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-012-common-timeframes.md b/docs/market_intelligence/builds/build-012-common-timeframes.md new file mode 100644 index 0000000..213bf99 --- /dev/null +++ b/docs/market_intelligence/builds/build-012-common-timeframes.md @@ -0,0 +1,226 @@ +# Build 012 — Common Timeframes + +**Build ID:** 012 + +**Component:** `common/timeframes.py` + +**Status:** ✅ Accepted + +**Development Process:** v2.0 + +--- + +# Цель Build + +Создать единый архитектурный слой описания таймфреймов, который будет использоваться всеми будущими Engine подсистемы Market Intelligence. + +До данного Build информация о таймфреймах существовала только внутри отдельных модулей старой системы анализа рынка. + +Это затрудняло повторное использование логики и не позволяло создать единый контракт для многоуровневого анализа. + +Настоящий Build переносит описание таймфреймов в слой Common и делает его независимым от реализации конкретных аналитических движков. + +--- + +# Реализованные компоненты + +Добавлен новый файл: + +```text +common/timeframes.py +``` + +В рамках Build реализованы: + +- модель `Timeframe`; +- единый реестр поддерживаемых таймфреймов; +- архитектурные роли таймфреймов; +- карта переходов к старшему таймфрейму; +- функции поиска и проверки таймфреймов; +- преобразование модели в диагностический payload. + +--- + +# Архитектурные решения + +## Единая модель Timeframe + +Каждый временной интервал представлен неизменяемым объектом `Timeframe`. + +Модель содержит только архитектурное описание интервала: + +- имя; +- длительность; +- роль; +- описание. + +Модель не содержит торговой логики. + +--- + +## Централизованный реестр + +Все поддерживаемые интервалы собраны в одном месте. + +На текущем этапе поддерживаются: + +```text +1m +5m +15m +1h +4h +1d +1w +``` + +Это создаёт единый источник истины для всей платформы. + +--- + +## Архитектурные роли + +Каждому таймфрейму назначается роль: + +- LOWER; +- PRIMARY; +- CONFIRMATION; +- HIGHER. + +Роль используется будущими движками при построении многоуровневого анализа. + +--- + +## Карта старших таймфреймов + +Добавлена централизованная карта переходов: + +```text +1m → 5m +5m → 1h +15m → 1h +1h → 4h +4h → 1d +1d → 1w +``` + +На текущем этапе она полностью совместима с существующей реализацией `MarketAnalysisService`, где рабочий интервал `5m` использует старший `1h`. + +--- + +## Независимость от существующего анализа рынка + +Файл не использует: + +- `MarketAnalysisService`; +- `ExchangeService`; +- старые модели; +- AutoTrade. + +Таким образом создаётся самостоятельный фундамент для новой подсистемы Market Intelligence без изменения существующего поведения бота. + +--- + +# Реализованные функции + +Добавлены следующие функции: + +- `get_timeframe()` +- `require_timeframe()` +- `is_supported_timeframe()` +- `get_higher_timeframe()` +- `get_timeframes_by_role()` +- `timeframe_to_payload()` + +Все функции являются детерминированными и не имеют побочных эффектов. + +--- + +# Compile Check + +Статус: + +```text +PASSED +``` + +Проверка: + +```bash +python -m compileall src/trading/market_intelligence/common/timeframes.py +``` + +Результат: + +```text +Compiling 'src/trading/market_intelligence/common/timeframes.py'... +``` + +Ошибок компиляции не обнаружено. + +--- + +# Architecture Review + +**Статус:** ✅ Passed + +Проверено: + +- отсутствие циклических зависимостей; +- отсутствие торговой логики; +- независимость от Runtime; +- независимость от Exchange; +- независимость от AutoTrade; +- соответствие архитектуре Common Layer. + +Замечаний нет. + +--- + +# Domain Review + +**Статус:** ✅ Passed + +Проверено: + +- корректность терминологии; +- соответствие философии Market Intelligence; +- отсутствие принятия торговых решений; +- корректное разделение понятий "таймфрейм" и "анализ рынка". + +Замечаний нет. + +--- + +# Engineering Review + +Положительные результаты Build: + +- создан единый источник истины для таймфреймов; +- устранено дублирование будущих определений; +- подготовлен фундамент для Multi-Timeframe Engine; +- сохранена совместимость с существующей системой анализа рынка. + +Build не оказывает влияния на работу действующего торгового бота. + +--- + +# Документация + +В рамках Build подготовлены: + +- `common/timeframes.py`; +- `build-012-common-timeframes.md`; +- обновление `build_history.md`. + +--- + +# Итог + +Build успешно завершил создание единого слоя описания таймфреймов. + +Следующие аналитические движки смогут использовать единый контракт работы с временными интервалами без зависимости от устаревшей реализации `market_analysis`. + +--- + +**Итоговый статус Build:** ✅ **Accepted** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-013-runtime-architecture.md b/docs/market_intelligence/builds/build-013-runtime-architecture.md new file mode 100644 index 0000000..a8de5dc --- /dev/null +++ b/docs/market_intelligence/builds/build-013-runtime-architecture.md @@ -0,0 +1,215 @@ +# Build 013 — Runtime Architecture + +**Build ID:** 013 + +**Компонент:** Runtime Layer + +**Статус:** ✅ Accepted + +**Тип Build:** Architecture Build + +--- + +# Цель Build + +Спроектировать архитектуру слоя Runtime подсистемы **Market Intelligence**. + +До начала реализации первого аналитического Engine необходимо определить единый механизм выполнения движков. + +В рамках Build проектируется инфраструктурный слой Runtime, который станет основой для всех последующих Engine. + +Исходный код в рамках данного Build не создаётся. + +--- + +# Причина появления Runtime + +После завершения Common Layer платформа получила единый набор: + +- моделей; +- типов; +- диагностических структур; +- проверки корректности; +- snapshot; +- payload; +- событий. + +Следующим логическим уровнем архитектуры является Runtime. + +Без Runtime каждый Engine был бы вынужден самостоятельно решать вопросы: + +- запуска; +- обработки ошибок; +- проверки результата; +- формирования fallback; +- взаимодействия с Coordinator. + +Это неизбежно привело бы к дублированию логики. + +--- + +# Архитектурное решение + +Введён новый архитектурный уровень: + +```text +Common Layer + ↓ +Runtime Layer + ↓ +Engine Layer + ↓ +Coordinator Layer +``` + +Runtime становится единственной инфраструктурой выполнения Engine. + +--- + +# Спроектированная структура Runtime + +Определена следующая структура каталога. + +```text +runtime/ + +├── __init__.py +├── protocol.py +├── base.py +├── runner.py +├── registry.py +├── dependencies.py +└── validation.py +``` + +Каждый файл получил единственную область ответственности. + +--- + +# Основные обязанности Runtime + +Runtime отвечает исключительно за инфраструктуру выполнения аналитических Engine. + +В область ответственности Runtime входят: + +- единый контракт Engine; +- единый жизненный цикл выполнения; +- безопасный запуск Engine; +- обработка исключений; +- Runtime Validation; +- регистрация Engine; +- управление зависимостями; +- формирование безопасного EngineResult. + +Runtime не содержит аналитической логики. + +--- + +# Основные архитектурные принципы + +Во время проектирования подтверждены следующие правила. + +- Runtime не анализирует рынок. +- Runtime не принимает торговых решений. +- Runtime не взаимодействует с биржей. +- Runtime не знает об AutoTrade. +- Runtime использует Common как единственный источник базовых моделей. +- Engine взаимодействуют только через Runtime. + +--- + +# Документация + +В рамках Build создан новый раздел документации. + +```text +docs/market_intelligence/runtime/ +``` + +Созданы документы: + +```text +runtime/README.md + +runtime/architecture.md +``` + +Теперь Runtime имеет собственную архитектурную документацию, независимую от Build History. + +--- + +# Реализованные изменения + +Исходный код не создавался. + +Выполнено архитектурное проектирование Runtime Layer. + +--- + +# Compile Check + +Не требуется. + +Build содержит исключительно архитектурную документацию. + +--- + +# Architecture Review + +**Статус:** PASSED + +Подтверждено: + +- правильное положение Runtime в архитектуре; +- отсутствие нарушения слоёв; +- независимость Runtime от Engine; +- независимость Runtime от AutoTrade; +- отсутствие циклических зависимостей. + +--- + +# Domain Review + +**Статус:** PASSED + +Runtime остаётся инфраструктурным уровнем. + +Торговая логика отсутствует. + +Аналитическая логика отсутствует. + +--- + +# Documentation Review + +Созданы новые документы Runtime. + +Build History подлежит обновлению. + +README Runtime создан. + +Архитектурная спецификация Runtime создана. + +--- + +# Следующий Build + +```text +Build 014 + +runtime/protocol.py +``` + +--- + +# Итог + +Build успешно завершил проектирование Runtime Layer. + +Данный Build создаёт архитектурный фундамент для всех будущих аналитических Engine. + +Статус Build: + +```text +ACCEPTED +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-014-1-engine-protocol.md b/docs/market_intelligence/builds/build-014-1-engine-protocol.md new file mode 100644 index 0000000..31c5fc6 --- /dev/null +++ b/docs/market_intelligence/builds/build-014-1-engine-protocol.md @@ -0,0 +1,243 @@ +# Build 014.1 — Engine Base Contracts / Engine Protocol + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 014.1 | +| Название | Engine Base Contracts / Engine Protocol | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Engine | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После завершения проектирования **Engine Layer Architecture** началась разработка Runtime Layer. + +Во время проектирования было выявлено, что Runtime не может зависеть от конкретных аналитических движков. + +Для обеспечения независимости Runtime требуется единый официальный контракт любого Engine. + +Настоящий Build вводит данный контракт. + +--- + +# Цель Build + +Создать минимальный контракт любого аналитического движка платформы **Market Intelligence**. + +Контракт должен позволять Runtime взаимодействовать с любым Engine без знания его внутренней реализации. + +--- + +# Architecture + +Каждый Engine обязан предоставлять два обязательных элемента: + +```text +EngineProtocol +├── get_metadata() +└── analyze(context) +``` + +Назначение методов: + +- `get_metadata()` — предоставляет описание Engine без создания экземпляра. +- `analyze()` — выполняет анализ рыночного контекста и возвращает единый результат. + +Engine не содержит Runtime-инфраструктуры и не определяет порядок выполнения других Engine. + +--- + +# Architecture Review + +Проверка показала соответствие архитектурным принципам проекта. + +Подтверждено: + +- Engine остаются независимыми друг от друга. +- Metadata полностью отделена от аналитической логики. +- Runtime сможет работать исключительно через контракт. +- Coordinator не зависит от реализации Engine. +- AutoTrade не затрагивается. + +Build признан архитектурно корректным. + +--- + +# Architecture Decision (ADR) + +## Решение + +Ввести единый контракт: + +```text +EngineProtocol +``` + +## Статус + +**Accepted** + +## Обоснование + +Runtime и будущий Coordinator должны работать с любым аналитическим движком посредством единого интерфейса. + +Описание Engine должно быть доступно без создания экземпляра. + +Поэтому контракт включает: + +- `get_metadata()`; +- `analyze(context)`. + +## Последствия + +После принятия решения: + +- Runtime зависит только от `EngineProtocol`; +- новые Engine могут свободно добавляться в платформу; +- архитектурные зависимости остаются однонаправленными; +- Engine Layer получает единый официальный контракт. + +--- + +# Build Design + +Создан файл: + +```text +app/src/trading/market_intelligence/engine/protocol.py +``` + +Файл содержит исключительно Protocol. + +Он не содержит: + +- реализации; +- аналитической логики; +- Runtime; +- Coordinator; +- Registry; +- инфраструктуры исполнения. + +--- + +# Implementation + +Реализован следующий контракт: + +```python +class EngineProtocol(Protocol): + + @classmethod + def get_metadata(cls) -> EngineMetadata: + ... + + async def analyze( + self, + context: EngineContext, + ) -> EngineResult: + ... +``` + +Контракт использует только общие модели Common Layer: + +- EngineContext; +- EngineMetadata; +- EngineResult. + +--- + +# Compile Check + +Проверено: + +- файл успешно импортируется; +- отсутствуют циклические зависимости; +- используются только модели Common Layer; +- контракт не зависит от Runtime; +- контракт не зависит от Coordinator. + +Build успешно проходит Compile Check. + +--- + +# Domain Review + +Проверено соответствие предметной области. + +EngineProtocol: + +- описывает исключительно контракт аналитического Engine; +- не содержит аналитической логики; +- не содержит Runtime; +- не содержит Coordinator; +- не содержит торговой логики; +- не зависит от конкретных Engine. + +Контракт признан соответствующим архитектуре платформы. + +--- + +# Documentation + +В рамках Build обновлены: + +- Build History; +- Runtime Contract; +- Engine Layer Architecture; +- Common Models Documentation. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 014.1: + +- создан официальный контракт Engine Layer; +- Runtime получил стабильную точку зависимости; +- завершён фундамент для создания BaseEngine; +- архитектура стала полностью соответствовать принципу Dependency Inversion. + +--- + +# Следующий Build + +```text +Build 014.2 + +Engine Base + +engine/base.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-014-2-engine-base.md b/docs/market_intelligence/builds/build-014-2-engine-base.md new file mode 100644 index 0000000..28a7066 --- /dev/null +++ b/docs/market_intelligence/builds/build-014-2-engine-base.md @@ -0,0 +1,272 @@ +# Build 014.2 — Engine Base / BaseEngine + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 014.2 | +| Название | Engine Base / BaseEngine | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Engine | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После введения официального контракта `EngineProtocol` возникла необходимость определить единый жизненный цикл выполнения всех аналитических движков. + +Без общего базового класса каждый Engine был бы вынужден самостоятельно реализовывать: + +- обработку ошибок; +- проверку входного контекста; +- формирование служебной metadata; +- единый жизненный цикл анализа. + +Это неизбежно привело бы к дублированию инфраструктурного кода и постепенному расхождению поведения различных Engine. + +--- + +# Цель Build + +Создать инфраструктурный базовый класс `BaseEngine`, который определяет единый жизненный цикл выполнения любого Engine, не включая аналитическую логику. + +--- + +# Architecture + +`BaseEngine` представляет собой инфраструктурный шаблон выполнения. + +Он отвечает исключительно за организацию жизненного цикла Engine. + +Общая модель выглядит следующим образом: + +```text +analyze(context) + │ + ▼ +_validate_context(context) + │ + ▼ +_analyze_impl(context) + │ + ▼ +_normalize_result(result) + │ + ▼ +EngineResult +``` + +Публичной точкой входа является только метод: + +```text +analyze(context) +``` + +Вся предметная аналитика выполняется исключительно внутри: + +```text +_analyze_impl(context) +``` + +--- + +# Architecture Review + +В ходе проверки подтверждено: + +- BaseEngine не содержит аналитической логики; +- Runtime не переносится внутрь Engine; +- Coordinator не зависит от реализации Engine; +- Engine продолжают оставаться полностью независимыми; +- Metadata остаётся отделённой от аналитической логики; +- Runtime сможет использовать единый жизненный цикл всех Engine. + +Архитектурное решение признано корректным. + +--- + +# Architecture Decision (ADR) + +## Решение + +Принято использовать инфраструктурный базовый класс: + +```text +BaseEngine +``` + +вместо минимального абстрактного класса. + +## Статус + +**Accepted** + +## Обоснование + +Платформа проектируется как система, включающая большое количество специализированных аналитических движков. + +Общая инфраструктура должна быть реализована один раз и использоваться всеми Engine. + +Это позволяет: + +- устранить дублирование кода; +- унифицировать жизненный цикл выполнения; +- обеспечить одинаковое поведение всех Engine; +- уменьшить вероятность архитектурных расхождений. + +## Последствия + +После принятия решения: + +- все Engine наследуются от BaseEngine; +- Runtime работает через единый жизненный цикл; +- аналитическая логика полностью остаётся в конкретных Engine; +- изменение инфраструктуры выполняется централизованно. + +--- + +# Build Design + +Создан файл: + +```text +app/src/trading/market_intelligence/engine/base.py +``` + +Класс содержит только инфраструктурные механизмы. + +В его состав входят: + +- получение EngineMetadata; +- единый метод `analyze()`; +- базовая проверка входного контекста; +- защищённый метод `_analyze_impl()`; +- нормализация результата; +- безопасное формирование результата при ошибке. + +--- + +# Implementation + +Реализованы следующие элементы: + +```text +BaseEngine +├── ENGINE_METADATA +├── get_metadata() +├── analyze() +├── _validate_context() +├── _analyze_impl() +├── _normalize_result() +└── _build_error_result() +``` + +Все методы снабжены комментариями на русском языке в соответствии со стандартом проекта. + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `engine/base.py`; +- успешно скомпилирован `engine/protocol.py`; +- отсутствуют синтаксические ошибки; +- отсутствуют циклические импорты; +- успешно компилируется весь пакет `market_intelligence`. + +Дополнительно после Code Review внесено улучшение: +`EngineEvaluationMeta.calculated_at` теперь заполняется при нормализации результата и при формировании error-result. + +Повторный Compile Check выполнен успешно. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +`BaseEngine`: + +- не содержит аналитической логики; +- не принимает торговых решений; +- не взаимодействует с Runtime; +- не взаимодействует с Coordinator; +- не зависит от AutoTrade; +- не обращается к другим Engine; +- не содержит инфраструктуры Telegram, БД или API. + +Класс полностью соответствует архитектурной роли инфраструктурной основы Engine Layer. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build обновлены: + +- Build History; +- Runtime Contract; +- Engine Layer Architecture. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 014.2: + +- создан инфраструктурный базовый класс Engine Layer; +- определён единый жизненный цикл выполнения Engine; +- устранено дублирование общей инфраструктуры; +- подготовлена основа для реализации всех специализированных аналитических движков платформы. + +--- + +# Следующий Build + +```text +Build 014.3 + +Engine Exceptions + +engine/exceptions.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-014-3-engine-exceptions.md b/docs/market_intelligence/builds/build-014-3-engine-exceptions.md new file mode 100644 index 0000000..c6b7820 --- /dev/null +++ b/docs/market_intelligence/builds/build-014-3-engine-exceptions.md @@ -0,0 +1,235 @@ +# Build 014.3 — Engine Exceptions + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 014.3 | +| Название | Engine Exceptions | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Engine | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После реализации `EngineProtocol` и `BaseEngine` возникла необходимость стандартизировать обработку внутренних ошибок Engine Layer. + +Использование стандартных исключений Python (`ValueError`, `NotImplementedError` и других) не отражало архитектурную модель платформы и затрудняло разделение инфраструктурных ошибок от штатных аналитических состояний. + +Настоящий Build вводит единый набор внутренних исключений Engine Layer и интегрирует их в `BaseEngine`. + +--- + +# Цель Build + +Создать единый механизм внутренних исключений Engine Layer. + +Исключения предназначены исключительно для обнаружения нарушений инфраструктурного жизненного цикла Engine и не являются частью внешнего Runtime Contract. + +--- + +# Architecture + +В Engine Layer вводится единая иерархия исключений. + +```text +EngineError +│ +├── EngineConfigurationError +├── EngineContractError +├── EngineContextError +└── EngineExecutionError +``` + +Все исключения используются исключительно внутри Engine Layer. + +За пределы Engine Layer передаются только объекты `EngineResult`. + +--- + +# Architecture Review + +Проверка подтвердила соответствие архитектурным принципам проекта. + +Подтверждено: + +- Runtime не зависит от исключений Engine; +- Coordinator работает только с `EngineResult`; +- штатные аналитические состояния не оформляются через исключения; +- исключения используются исключительно для ошибок инфраструктуры Engine Layer; +- архитектурные зависимости остаются однонаправленными. + +Архитектурное решение признано корректным. + +--- + +# Architecture Decision (ADR) + +## Решение + +Ввести единый набор внутренних исключений Engine Layer. + +## Статус + +**Accepted** + +## Обоснование + +Исключения необходимы для описания нарушений жизненного цикла Engine: + +- ошибок конфигурации; +- ошибок входного контекста; +- нарушений архитектурного контракта; +- внутренних ошибок реализации. + +При этом исключения не являются способом передачи аналитического результата. + +Внешним контрактом платформы остаётся исключительно `EngineResult`. + +## Последствия + +После принятия решения: + +- BaseEngine использует специализированные исключения Engine Layer; +- Runtime не зависит от механизма исключений; +- Coordinator продолжает работать только через официальный Runtime Contract; +- архитектура сохраняет слабую связанность компонентов. + +--- + +# Build Design + +Создан файл: + +```text +app/src/trading/market_intelligence/engine/exceptions.py +``` + +Также обновлён файл: + +```text +app/src/trading/market_intelligence/engine/base.py +``` + +BaseEngine переведён на использование новых исключений. + +--- + +# Implementation + +Реализованы классы: + +```text +EngineError +├── EngineConfigurationError +├── EngineContractError +├── EngineContextError +└── EngineExecutionError +``` + +В `BaseEngine` выполнены следующие изменения: + +- отсутствие `ENGINE_METADATA` приводит к `EngineConfigurationError`; +- отсутствие обязательных полей `EngineContext` приводит к `EngineContextError`; +- возврат объекта, отличного от `EngineResult`, приводит к `EngineContractError`. + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `engine/exceptions.py`; +- успешно скомпилирован обновлённый `engine/base.py`; +- отсутствуют синтаксические ошибки; +- отсутствуют циклические зависимости. + +После интеграции исключений выполнена повторная проверка компиляции. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +Подтверждено: + +- исключения используются только внутри Engine Layer; +- внешний контракт остаётся `EngineResult`; +- штатные аналитические состояния не оформляются через исключения; +- ошибки контекста отделены от ошибок конфигурации и нарушений контракта; +- Runtime и Coordinator полностью изолированы от внутреннего механизма исключений. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build обновлены: + +- Build History; +- Engine Layer Architecture; +- документация Engine Base. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 014.3: + +- создан единый механизм внутренних исключений Engine Layer; +- `BaseEngine` переведён на использование специализированных исключений; +- внешний Runtime Contract не изменился; +- архитектура стала более согласованной и подготовлена к появлению первых аналитических Engine. + +--- + +# Следующий Build + +```text +Build 015 + +Runtime Layer + +runtime/protocol.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-1-common-models-runtime-result.md b/docs/market_intelligence/builds/build-015-1-common-models-runtime-result.md new file mode 100644 index 0000000..4503a2f --- /dev/null +++ b/docs/market_intelligence/builds/build-015-1-common-models-runtime-result.md @@ -0,0 +1,237 @@ +# Build 015.1 — Common Models Extension / RuntimeResult + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 015.1 | +| Название | Common Models Extension / RuntimeResult | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Common | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +Во время проектирования `RuntimeProtocol` было установлено, что Runtime не может возвращать `EngineResult`. + +`EngineResult` описывает результат выполнения одного Engine. + +Runtime агрегирует результаты нескольких Engine, поэтому ему нужен собственный официальный результат выполнения. + +--- + +# Цель Build + +Добавить в Common Layer модель: + +```text +RuntimeResult +``` + +`RuntimeResult` является единым контрактом результата выполнения Runtime Layer. + +--- + +# Architecture + +`RuntimeResult` агрегирует результаты нескольких Engine. + +Модель не содержит аналитики, не принимает торговых решений и не зависит от конкретных Engine. + +Основной источник истины: + +```text +engine_results +``` + +Все производные данные вычисляются из него. + +--- + +# Architecture Review + +Проверка подтвердила: + +- модель относится к Common Layer; +- RuntimeResult нужен Runtime, Coordinator, стратегиям, диагностике и тестам; +- производные данные не должны храниться отдельно; +- RuntimeResult не должен зависеть от Runtime-реализации. + +Архитектура Build признана корректной. + +--- + +# Architecture Decision (ADR) + +## Решение + +Добавить `RuntimeResult` в `common.models`. + +## Статус + +**Accepted** + +## Обоснование + +Runtime выполняет несколько Engine и должен возвращать агрегированный результат. + +Размещение `RuntimeResult` в Common Layer сохраняет однонаправленные зависимости и позволяет использовать модель разными слоями платформы. + +## Последствия + +После принятия решения: + +- Runtime сможет возвращать единый результат выполнения; +- Coordinator сможет работать с агрегированным контрактом; +- исключается дублирование списков выполненных и ошибочных Engine; +- RuntimeProtocol можно проектировать без временных моделей. + +--- + +# Build Design + +Модель добавлена в файл: + +```text +app/src/trading/market_intelligence/common/models.py +``` + +Расположение: + +```text +EngineResult +↓ +RuntimeResult +``` + +--- + +# Implementation + +Реализованы хранимые поля: + +```text +engine_results +diagnostics +started_at +finished_at +duration_ms +metadata +``` + +Реализованы вычисляемые свойства: + +```text +successful_results +failed_results +partial_results +stale_results +executed_engines +failed_engines +successful_engines +has_errors +is_successful +total_engines +successful_count +``` + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `common/models.py`; +- синтаксические ошибки отсутствуют; +- циклические зависимости отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +`RuntimeResult`: + +- агрегирует результаты нескольких `EngineResult`; +- не содержит аналитической логики; +- не принимает торговых решений; +- не зависит от Runtime-реализации; +- не зависит от Coordinator; +- не зависит от конкретных Engine; +- производные данные вычисляются из `engine_results`. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build должны быть обновлены: + +- Build History; +- Runtime Contract; +- Common Models Documentation; +- будущая документация Runtime Layer. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 015.1: + +- Common Layer получил модель `RuntimeResult`; +- Runtime Layer получил будущий выходной контракт; +- Coordinator сможет работать с агрегированным результатом Runtime; +- подготовлена основа для Build 015.2 — `runtime/protocol.py`. + +--- + +# Следующий Build + +```text +Build 015.2 + +Runtime Protocol + +runtime/protocol.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-2-runtime-protocol.md b/docs/market_intelligence/builds/build-015-2-runtime-protocol.md new file mode 100644 index 0000000..6082946 --- /dev/null +++ b/docs/market_intelligence/builds/build-015-2-runtime-protocol.md @@ -0,0 +1,284 @@ +# Build 015.2 — Runtime Protocol + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 015.2 | +| Название | Runtime Protocol | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Runtime | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После появления `RuntimeResult` стало возможно определить официальный контракт Runtime Layer. + +Runtime должен взаимодействовать с аналитическими Engine через единый стабильный интерфейс и не зависеть от конкретных реализаций Engine. + +--- + +# Цель Build + +Создать файл: + +```text +app/src/trading/market_intelligence/runtime/protocol.py +``` + +и определить в нём официальный контракт Runtime Layer. + +--- + +# Architecture + +`RuntimeProtocol` описывает только внешний интерфейс Runtime. + +Runtime отвечает за: + +- регистрацию Engine; +- удаление Engine; +- получение зарегистрированного Engine; +- выполнение анализа; +- возврат агрегированного `RuntimeResult`. + +Runtime не отвечает за: + +- аналитику; +- торговые решения; +- порядок стратегий; +- пользовательский интерфейс; +- работу с биржей; +- работу с БД; +- журналирование. + +--- + +# Architecture Review + +Проверка подтвердила: + +- Runtime работает через `EngineProtocol`; +- Runtime регистрирует классы Engine, а не экземпляры; +- Runtime возвращает `RuntimeResult`; +- Runtime не знает конкретных Engine; +- Runtime не содержит аналитики; +- Runtime не содержит Coordinator-логики; +- AutoTrade не изменяется. + +Архитектура Build признана корректной. + +--- + +# Architecture Decision (ADR) + +## Решение + +Runtime Layer регистрирует классы Engine, а не экземпляры Engine. + +Официальный контракт: + +```python +register_engine( + engine_type: type[EngineProtocol], +) -> None +``` + +## Статус + +**Accepted** + +## Обоснование + +`EngineMetadata` доступна без создания экземпляра Engine через `get_metadata()`. + +Поэтому Runtime не обязан создавать объект Engine на этапе регистрации. + +Регистрация класса позволяет Runtime самостоятельно управлять жизненным циклом экземпляров Engine. + +## Обязательное правило + +Все Engine обязаны иметь конструктор без пользовательских параметров. + +Всё изменяемое состояние и конфигурация анализа передаются через: + +```text +EngineContext +``` + +## Запрещено + +```python +register_engine(engine: EngineProtocol) +``` + +Регистрация экземпляров Engine запрещена. + +## Последствия + +После принятия решения: + +- Runtime контролирует жизненный цикл Engine; +- Engine не сохраняют состояние между запусками; +- Runtime готов к параллельному выполнению; +- Registry сможет хранить типы Engine; +- добавление новых Engine не требует изменения Runtime. + +--- + +# Build Design + +Реализуется контракт: + +```text +RuntimeProtocol +├── register_engine(engine_type) +├── unregister_engine(engine_name) +├── get_engine(engine_name) +└── analyze(context) +``` + +Файл не содержит реализации Runtime. + +--- + +# Implementation + +Реализован файл: + +```text +app/src/trading/market_intelligence/runtime/protocol.py +``` + +Содержимое контракта: + +```python +class RuntimeProtocol(Protocol): + def register_engine( + self, + engine_type: type[EngineProtocol], + ) -> None: + ... + + def unregister_engine( + self, + engine_name: EngineName, + ) -> None: + ... + + def get_engine( + self, + engine_name: EngineName, + ) -> type[EngineProtocol] | None: + ... + + async def analyze( + self, + context: EngineContext, + ) -> RuntimeResult: + ... +``` + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `runtime/protocol.py`; +- синтаксические ошибки отсутствуют; +- циклические зависимости отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +`RuntimeProtocol`: + +- работает через `EngineProtocol`; +- регистрирует классы Engine; +- возвращает `RuntimeResult`; +- не знает конкретных Engine; +- не содержит аналитики; +- не содержит Coordinator-логики; +- не зависит от AutoTrade; +- не обращается к бирже, БД, Telegram или EventBus; +- получает изменяемые данные анализа только через `EngineContext`. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build должны быть обновлены: + +- Build History; +- Runtime Contract; +- Engine Layer Architecture; +- Runtime Architecture. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 015.2: + +- создан официальный контракт Runtime Layer; +- Runtime получил стабильную зависимость от `EngineProtocol`; +- закреплена регистрация классов Engine вместо экземпляров; +- Runtime возвращает агрегированный `RuntimeResult`; +- подготовлена основа для реализации Runtime Registry. + +--- + +# Следующий Build + +```text +Build 015.3 + +Runtime Registry + +runtime/registry.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-3-common-models-engine-registration.md b/docs/market_intelligence/builds/build-015-3-common-models-engine-registration.md new file mode 100644 index 0000000..b04b8b7 --- /dev/null +++ b/docs/market_intelligence/builds/build-015-3-common-models-engine-registration.md @@ -0,0 +1,296 @@ +# Build 015.3 — Common Models Extension / EngineRegistration + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 015.3 | +| Название | Common Models Extension / EngineRegistration | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Common | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +Во время проектирования `RuntimeRegistry` было установлено, что хранение только класса Engine недостаточно. + +Runtime Registry постоянно работает с двумя сущностями: + +- типом Engine; +- его Metadata. + +Постоянный вызов `Engine.get_metadata()` привёл бы к дублированию логики и усложнил бы реализацию Registry. + +Для устранения этой проблемы введена единая модель регистрации Engine. + +--- + +# Цель Build + +Добавить в Common Layer новую фундаментальную модель: + +```text +EngineRegistration +``` + +Модель описывает зарегистрированный аналитический движок и является единицей хранения Runtime Registry. + +--- + +# Architecture + +`EngineRegistration` представляет собой атомарную запись регистрации Engine. + +Модель не является: + +- Engine; +- Runtime; +- Registry; +- Runtime Contract. + +Это исключительно модель Common Layer. + +--- + +# Architecture Review + +Проверка подтвердила: + +- модель относится к Common Layer; +- Runtime Registry использует одну запись регистрации вместо двух независимых сущностей; +- отсутствует дублирование Metadata; +- сохраняется разделение Metadata и Logic; +- сохраняется однонаправленная архитектура зависимостей. + +Архитектура Build признана корректной. + +--- + +# Architecture Decision (ADR) + +## Решение + +Добавить модель: + +```text +EngineRegistration +``` + +в `common.models`. + +## Статус + +**Accepted** + +## Обоснование + +Во время проектирования Runtime Registry подтверждено, что регистрация Engine является самостоятельной архитектурной сущностью. + +Модель объединяет: + +- тип Engine; +- его Metadata. + +При этом сама Metadata остаётся единственным источником информации об Engine. + +## Главное правило + +`EngineRegistration` не должен дублировать информацию, уже содержащуюся в `EngineMetadata`. + +Запрещается добавлять отдельные поля: + +```text +engine_name +engine_version +dependencies +priority +enabled_by_default +registered_at +``` + +## Последствия + +После принятия решения: + +- Runtime Registry сможет хранить атомарные записи регистрации; +- Metadata будет доступна без обращения к классу Engine; +- Runtime сохранит контроль над жизненным циклом Engine; +- модель регистрации останется независимой от Runtime-состояния. + +--- + +# Build Design + +В файл + +```text +app/src/trading/market_intelligence/common/models.py +``` + +добавлены: + +```text +EngineTypeProtocol +EngineRegistration +``` + +`EngineTypeProtocol` используется как минимальный контракт класса Engine и позволяет избежать запрещённой зависимости: + +```text +Common → Engine +``` + +--- + +# Implementation + +Реализованы: + +```text +EngineTypeProtocol +``` + +```text +EngineRegistration +``` + +Состав модели: + +```text +EngineRegistration +├── engine_type +└── metadata +``` + +Модель не содержит дополнительных полей. + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `common/models.py`; +- синтаксические ошибки отсутствуют; +- циклические зависимости отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +Подтверждено: + +- `EngineRegistration` описывает регистрацию Engine, а не сам Engine; +- модель не содержит аналитической логики; +- модель не содержит Runtime-состояния; +- `EngineRegistration` не дублирует данные из `EngineMetadata`; +- Common Layer не импортирует `EngineProtocol`; +- Runtime Registry сможет использовать `EngineRegistration` как единую запись регистрации. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build должны быть обновлены: + +- Build History; +- Runtime Architecture; +- Engine Layer Architecture; +- Common Models Documentation. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 015.3: + +- Common Layer получил модель `EngineRegistration`; +- Runtime Registry получил единицу хранения зарегистрированных Engine; +- устранено дублирование вызовов `Engine.get_metadata()`; +- сохранён принцип единственного источника истины; +- сохранена однонаправленная архитектура зависимостей между слоями. + +--- + +# Архитектурные принципы, закреплённые Build + +В рамках Build официально закреплены следующие правила. + +### EngineRegistration не дублирует Metadata + +Если информация уже содержится в `EngineMetadata`, она не должна повторяться в `EngineRegistration`. + +### Один источник истины + +Описание Engine хранится исключительно в `EngineMetadata`. + +`EngineRegistration` только связывает тип Engine с его Metadata. + +### Common Layer не зависит от Engine Layer + +Для хранения типа Engine используется минимальный локальный протокол: + +```text +EngineTypeProtocol +``` + +Это позволяет избежать запрещённой зависимости: + +```text +Common → Engine +``` + +--- + +# Следующий Build + +```text +Build 015.4 + +Runtime Registry + +runtime/registry.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-4-runtime-registry.md b/docs/market_intelligence/builds/build-015-4-runtime-registry.md new file mode 100644 index 0000000..36866e3 --- /dev/null +++ b/docs/market_intelligence/builds/build-015-4-runtime-registry.md @@ -0,0 +1,360 @@ +# Build 015.4 — Runtime Registry + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 015.4 | +| Название | Runtime Registry | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Runtime | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После завершения Build: + +- 015.1 (`RuntimeResult`); +- 015.2 (`RuntimeProtocol`); +- 015.3 (`EngineRegistration`); + +Runtime получил все необходимые фундаментальные модели для реализации первого инфраструктурного компонента Runtime Layer. + +Таким компонентом стал `RuntimeRegistry`. + +--- + +# Цель Build + +Создать каталог зарегистрированных аналитических Engine. + +Registry является единым источником информации о зарегистрированных Engine и предоставляет минимальный API регистрации. + +--- + +# Architecture + +`RuntimeRegistry` отвечает исключительно за регистрацию аналитических Engine. + +Registry не выполняет анализ рынка. + +Registry не создаёт `EngineContext`. + +Registry не запускает Engine. + +Registry не агрегирует результаты. + +Registry не управляет Coordinator. + +Единственная ответственность Registry — управление зарегистрированными Engine. + +--- + +# Architecture Review + +Проверка подтвердила: + +- Registry соответствует принципу Single Responsibility; +- Registry использует `EngineRegistration`; +- Registry не содержит аналитики; +- Registry не зависит от Coordinator; +- Registry не зависит от AutoTrade; +- Registry не создаёт `EngineContext`; +- Registry не запускает Engine. + +Архитектура Build признана корректной. + +--- + +# Architecture Decision (ADR) + +## Решение + +Добавить компонент: + +```text +RuntimeRegistry +``` + +в Runtime Layer. + +## Статус + +**Accepted** + +## Обоснование + +Runtime должен иметь единый каталог зарегистрированных Engine. + +Registry хранит записи регистрации и предоставляет к ним доступ другим компонентам Runtime. + +Для хранения используется модель: + +```text +EngineRegistration +``` + +## Утверждённая модель хранения + +```python +dict[ + EngineName, + EngineRegistration, +] +``` + +## Публичный API + +```text +register() +unregister() +get() +contains() +list() +clear() +``` + +## Запрещено + +Registry не должен содержать: + +```text +analyze() +run() +execute() +resolve_dependencies() +validate() +build_graph() +sort() +``` + +## Последствия + +После принятия решения: + +- Runtime получил единый каталог Engine; +- Registry остаётся независимым от аналитики; +- RuntimeRunner сможет использовать Registry; +- DependencyResolver сможет использовать Registry; +- добавление новых Engine не требует изменения Runtime. + +--- + +# Build Design + +Создан файл: + +```text +app/src/trading/market_intelligence/runtime/registry.py +``` + +Внутренний каталог Registry: + +```python +self._engines: dict[ + EngineName, + EngineRegistration, +] +``` + +При регистрации Registry самостоятельно создаёт: + +```text +EngineRegistration +``` + +через: + +```python +engine_type.get_metadata() +``` + +Дополнительная фабрика регистрации не используется. + +--- + +# Implementation + +Реализован компонент: + +```text +RuntimeRegistry +``` + +Публичный интерфейс: + +```text +register() +unregister() +get() +contains() +list() +clear() +``` + +Registry использует: + +```text +EngineRegistration +``` + +как единственную единицу хранения зарегистрированных Engine. + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `runtime/registry.py`; +- синтаксические ошибки отсутствуют; +- циклические зависимости отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +Подтверждено: + +- Registry хранит только зарегистрированные Engine; +- Registry использует `EngineRegistration`; +- Registry не содержит аналитики; +- Registry не управляет выполнением Engine; +- Registry не создаёт `EngineContext`; +- Registry не формирует `RuntimeResult`; +- Registry не знает Coordinator; +- Registry не зависит от AutoTrade; +- Registry не обращается к бирже, БД, Telegram или EventBus. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build должны быть обновлены: + +- Build History; +- Runtime Architecture; +- Runtime Contract; +- Runtime Layer Documentation. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 015.4: + +- Runtime Layer получил первый инфраструктурный компонент; +- появился единый каталог зарегистрированных Engine; +- Runtime получил единый механизм регистрации аналитических движков; +- Registry использует `EngineRegistration` как атомарную запись; +- подготовлена основа для реализации Runtime Runner. + +--- + +# Архитектурные принципы, закреплённые Build + +## Registry отвечает только за регистрацию + +`RuntimeRegistry` не выполняет анализ и не управляет выполнением Engine. + +Единственная ответственность Registry — управление каталогом зарегистрированных Engine. + +--- + +## EngineRegistration является единицей хранения + +Внутри Registry хранится исключительно: + +```text +EngineRegistration +``` + +Хранение отдельных `EngineType` или `EngineMetadata` запрещено. + +--- + +## Runtime самостоятельно создаёт регистрацию + +Во время регистрации Registry самостоятельно получает Metadata: + +```python +metadata = engine_type.get_metadata() +``` + +и создаёт: + +```text +EngineRegistration +``` + +Отдельная фабрика регистрации не вводится. + +--- + +## Registry не содержит эксплуатационной логики + +Registry не выполняет: + +- анализ; +- запуск Engine; +- сортировку; +- разрешение зависимостей; +- построение графов; +- валидацию. + +Все перечисленные обязанности относятся к другим компонентам Runtime Layer. + +--- + +# Следующий Build + +```text +Build 015.5 + +Runtime Runner + +runtime/runner.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-5-runtime-core.md b/docs/market_intelligence/builds/build-015-5-runtime-core.md new file mode 100644 index 0000000..2f6b5fa --- /dev/null +++ b/docs/market_intelligence/builds/build-015-5-runtime-core.md @@ -0,0 +1,275 @@ +# Build 015.5 — Runtime Core + +**Build Documentation** + +--- + +# Контроль Build + +| Свойство | Значение | +|----------|-----------| +| Build | 015.5 | +| Название | Runtime Core | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Проект | Dzentra | +| Тип | Architecture Build | +| Версия | 1.0 | + +--- + +# Цель Build + +Завершить формирование базовой инфраструктуры Runtime Layer, реализовав компоненты, необходимые для регистрации аналитических Engine, их безопасного выполнения и обработки внутренних ошибок Runtime. + +Build завершает минимальное ядро Runtime перед переходом к разработке Coordinator Layer. + +--- + +# Архитектурная задача + +До начала Build Runtime уже содержал: + +- Runtime Protocol; +- Runtime Models; +- Runtime Registry. + +Отсутствовали компоненты, отвечающие за непосредственное выполнение зарегистрированного Engine и унифицированную модель внутренних исключений Runtime. + +Настоящий Build реализует данные компоненты без изменения существующих Runtime Contract. + +--- + +# Архитектурное решение + +В рамках Build реализованы два самостоятельных компонента Runtime Layer. + +## Runtime Runner + +`RuntimeRunner` отвечает исключительно за выполнение одного зарегистрированного Engine. + +Ответственность Runner ограничивается: + +- созданием экземпляра Engine; +- передачей Runtime Context; +- получением Engine Result. + +Runner не содержит: + +- логики анализа рынка; +- управления последовательностью выполнения Engine; +- агрегации результатов; +- обработки зависимостей; +- координации Runtime. + +--- + +## Runtime Exceptions + +Создан единый набор специализированных исключений Runtime. + +Исключения предназначены исключительно для внутренней инфраструктуры Runtime Layer. + +Реализованы: + +- RuntimeError; +- EngineNotRegisteredError; +- InvalidRuntimeContextError; +- EngineExecutionError. + +Наличие собственного пространства исключений исключает использование необобщённых Exception внутри Runtime. + +--- + +# Реализованные файлы + +Добавлены: + +```text +runtime/exceptions.py +runtime/runner.py +``` + +Использован существующий компонент: + +```text +runtime/registry.py +``` + +Изменён: + +```text +common/models.py +``` + +В `EngineTypeProtocol` добавлен метод: + +```python +analyze(context) -> EngineResult +``` + +Данное изменение устраняет расхождение между Runtime Registry и Runtime Runner и обеспечивает корректную статическую типизацию без изменения архитектурного контракта Runtime. + +--- + +# Архитектурные зависимости + +Build не изменяет существующую модель зависимостей. + +Итоговая структура Runtime Layer: + +```text +Runtime Layer + +├── protocol.py +├── registry.py +├── runner.py +└── exceptions.py +``` + +Зависимости остаются однонаправленными. + +```text +RuntimeRunner + │ + ▼ +EngineRegistration + │ + ▼ +EngineProtocol +``` + +Runtime по-прежнему не зависит от Coordinator и не содержит предметной логики. + +--- + +# Runtime Contract + +Build не изменяет: + +- Runtime Context; +- Engine Result; +- Runtime Status; +- Runtime Events; +- Runtime Models; +- Runtime Contract. + +Все существующие Runtime Contract остаются полностью совместимыми. + +--- + +# Engineering Review + +## Architecture Review + +Passed. + +Компоненты имеют единственную область ответственности. + +--- + +## Dependency Review + +Passed. + +Циклические зависимости отсутствуют. + +Направление зависимостей соответствует утверждённой архитектуре. + +--- + +## Runtime Review + +Passed. + +Runtime остаётся инфраструктурным уровнем. + +Runtime не содержит аналитической или торговой логики. + +--- + +## Code Review + +Passed. + +Код соответствует принципам: + +- Single Responsibility Principle; +- One Source of Truth; +- Stable Foundation; +- Architecture First. + +Во время проверки обнаружено частичное дублирование контрактов `EngineProtocol` и `EngineTypeProtocol`. + +Для обеспечения корректной статической типизации в `EngineTypeProtocol` добавлен метод `analyze()`. + +Архитектурное объединение контрактов признано отдельной задачей и не входит в настоящий Build. + +--- + +## Compile Review + +Passed. + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Ошибок компиляции не обнаружено. + +--- + +# Изменённые документы + +Обновление Runtime Contract не потребовалось. + +Настоящий Build документирует исключительно развитие Runtime Layer. + +--- + +# Итоговое состояние Runtime + +После завершения Build Runtime Layer имеет следующий состав. + +```text +runtime/ + +├── protocol.py +├── registry.py +├── runner.py +├── exceptions.py +``` + +Runtime Layer полностью готов к реализации Coordinator Layer. + +--- + +# Следующий Build + +Следующим этапом развития платформы является: + +> **Build 016 — Coordinator Core** + +Coordinator станет первым компонентом, использующим Runtime Registry и Runtime Runner для выполнения полного набора зарегистрированных аналитических Engine. + +--- + +# Итог + +Build 015.5 полностью завершает формирование базовой инфраструктуры Runtime Layer. + +Все поставленные архитектурные задачи выполнены. + +Build успешно прошёл: + +- Architecture Review; +- Dependency Review; +- Runtime Review; +- Code Review; +- Compile Review. + +Build получает статус: + +> **Accepted** \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-6-runtime-dependencies.md b/docs/market_intelligence/builds/build-015-6-runtime-dependencies.md new file mode 100644 index 0000000..f928e01 --- /dev/null +++ b/docs/market_intelligence/builds/build-015-6-runtime-dependencies.md @@ -0,0 +1,265 @@ +# Build 015.6 — Runtime Dependencies + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 015.6 | +| Название | Runtime Dependencies | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Runtime | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После реализации `RuntimeRegistry` и `RuntimeRunner` Runtime Layer получил возможность хранить и выполнять зарегистрированные Engine. + +Однако порядок выполнения Engine должен учитывать зависимости, объявленные в `EngineMetadata`. + +Для этого необходим отдельный компонент Runtime Layer, отвечающий только за разрешение зависимостей. + +--- + +# Цель Build + +Создать компонент: + +```text +RuntimeDependencies +``` + +который определяет корректный порядок выполнения зарегистрированных Engine на основании их обязательных зависимостей. + +--- + +# Architecture + +`RuntimeDependencies` получает: + +```text +tuple[EngineRegistration, ...] +``` + +и возвращает: + +```text +tuple[EngineRegistration, ...] +``` + +в порядке выполнения. + +Компонент не создаёт Engine, не запускает Engine, не формирует результаты и не зависит от Coordinator. + +--- + +# Architecture Review + +Проверка подтвердила: + +- Engine остаются независимыми; +- зависимости объявляются через `EngineMetadata`; +- Runtime не содержит аналитики; +- Runner не смешивается с Dependency Resolver; +- Registry не строит граф зависимостей; +- Coordinator пока не реализуется. + +**Статус:** ✅ Passed + +--- + +# Architecture Decision (ADR) + +## Решение + +В Runtime Layer вводится компонент: + +```text +RuntimeDependencies +``` + +## Статус + +**Accepted** + +## Обоснование + +Engine могут объявлять обязательные зависимости через: + +```text +EngineMetadata.required_dependencies +``` + +Runtime должен определить порядок выполнения Engine до запуска анализа. + +Эта ответственность не относится к Registry, Runner, Coordinator или конкретным Engine. + +--- + +# Build Design + +Создан файл: + +```text +app/src/trading/market_intelligence/runtime/dependencies.py +``` + +Обновлён файл: + +```text +app/src/trading/market_intelligence/runtime/exceptions.py +``` + +--- + +# Implementation + +Реализован класс: + +```text +RuntimeDependencies +``` + +Публичный API: + +```text +resolve(registrations) +``` + +Внутренние этапы: + +```text +_build_registration_map() +_build_graph() +_validate_dependencies() +_topological_sort() +_visit() +``` + +Добавлены Runtime-исключения: + +```text +MissingEngineDependencyError +CircularEngineDependencyError +``` + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `runtime/dependencies.py`; +- успешно скомпилирован `runtime/exceptions.py`; +- синтаксические ошибки отсутствуют; +- циклические зависимости отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +`RuntimeDependencies`: + +- работает только с зарегистрированными Engine; +- берёт зависимости исключительно из `EngineMetadata.required_dependencies`; +- не создаёт экземпляры Engine; +- не запускает Engine; +- не формирует `EngineResult` или `RuntimeResult`; +- не зависит от Coordinator; +- не добавляет аналитической логики. + +**Статус:** ✅ Passed + +--- + +# Code Review / Runtime Review + +Проверка пройдена. + +Подтверждено: + +- компонент имеет одну ответственность; +- отсутствующие обязательные зависимости обрабатываются отдельной ошибкой; +- циклические зависимости обрабатываются отдельной ошибкой; +- топологическая сортировка возвращает корректный порядок выполнения; +- зависимости между слоями не нарушены. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build должны быть обновлены: + +- Build History; +- Runtime Architecture; +- Runtime Layer Documentation. + +Runtime Contract не изменяется. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Architecture +- ✅ Architecture Review +- ✅ Architecture Decision +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Runtime Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 015.6: + +- Runtime Layer получил механизм разрешения зависимостей; +- порядок выполнения Engine теперь может быть построен из `EngineMetadata`; +- Engine остаются независимыми и не обращаются друг к другу напрямую; +- Registry, Runner и Dependency Resolver имеют разные зоны ответственности; +- Runtime Layer стал готов к следующему этапу — Runtime Validation или Coordinator Core. + +--- + +# Следующий Build + +```text +Build 015.7 + +Runtime Validation + +runtime/validation.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-7-runtime-validation.md b/docs/market_intelligence/builds/build-015-7-runtime-validation.md new file mode 100644 index 0000000..f83e695 --- /dev/null +++ b/docs/market_intelligence/builds/build-015-7-runtime-validation.md @@ -0,0 +1,280 @@ +# Build 015.7 — Runtime Validation + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 015.7 | +| Название | Runtime Validation | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Runtime | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Причина появления Build + +После реализации компонентов: + +- Runtime Registry; +- Runtime Runner; +- Runtime Dependencies; + +Runtime Layer получил возможность хранить зарегистрированные Engine, определять порядок их выполнения и запускать анализ. + +Однако отсутствовала единая точка проверки корректности Runtime перед началом выполнения. + +Проверка конфигурации Runtime должна выполняться централизованно и до запуска любого Engine. + +--- + +# Цель Build + +Создать компонент: + +```text +RuntimeValidation +``` + +который подтверждает готовность Runtime Layer к выполнению анализа. + +--- + +# Architecture + +`RuntimeValidation` получает: + +```text +RuntimeRegistry +``` + +и выполняет проверку корректности зарегистрированных Engine. + +Компонент: + +- не создаёт Engine; +- не запускает Engine; +- не изменяет Registry; +- не выполняет анализ рынка; +- не строит граф зависимостей самостоятельно. + +Проверка зависимостей делегируется компоненту: + +```text +RuntimeDependencies +``` + +--- + +# Architecture Review + +Подтверждено: + +- RuntimeValidation имеет единственную ответственность; +- проверки отделены от Registry и Runner; +- RuntimeDependencies используется повторно без дублирования логики; +- Coordinator не участвует в проверке Runtime; +- архитектурные зависимости соответствуют утверждённой модели Runtime Layer. + +**Статус:** ✅ Passed + +--- + +# Architecture Decision (ADR) + +## Решение + +В Runtime Layer вводится новый инфраструктурный компонент: + +```text +RuntimeValidation +``` + +## Статус + +**Accepted** + +## Обоснование + +Runtime должен гарантировать собственную корректность до запуска первого Engine. + +Для этого необходим отдельный компонент, который централизует все проверки конфигурации Runtime. + +Проверка зависимостей выполняется посредством использования уже существующего `RuntimeDependencies`. + +Это сохраняет принцип единственной ответственности и предотвращает дублирование логики. + +--- + +# Build Design + +Создан файл: + +```text +app/src/trading/market_intelligence/runtime/validation.py +``` + +Обновлён файл: + +```text +app/src/trading/market_intelligence/runtime/exceptions.py +``` + +--- + +# Implementation + +Реализован класс: + +```text +RuntimeValidation +``` + +Публичный интерфейс: + +```text +validate(registry) +``` + +Во время проверки выполняются следующие этапы: + +```text +_validate_registry() +_validate_metadata() +_validate_duplicates() +RuntimeDependencies.resolve() +``` + +Добавлены новые исключения Runtime Validation: + +```text +RuntimeValidationError +EmptyRuntimeRegistryError +DuplicateEngineRegistrationError +InvalidEngineMetadataError +``` + +--- + +# Compile Check + +Проверка выполнена командой: + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +- успешно скомпилирован `runtime/validation.py`; +- успешно обновлён `runtime/exceptions.py`; +- синтаксические ошибки отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Domain Review + +Проверено соответствие предметной области. + +Подтверждено: + +- RuntimeValidation проверяет только готовность Runtime; +- Engine не создаются; +- Engine не запускаются; +- Registry не изменяется; +- RuntimeResult не формируется; +- аналитическая логика отсутствует; +- проверка зависимостей делегирована RuntimeDependencies. + +**Статус:** ✅ Passed + +--- + +# Code Review / Runtime Review + +Проверка успешно завершена. + +Подтверждено: + +- компонент имеет единственную ответственность; +- отсутствует дублирование проверки зависимостей; +- используется существующий RuntimeDependencies; +- корректно проверяются обязательные поля Metadata; +- иерархия Runtime-исключений расширена без нарушения существующей структуры. + +Во время Code Review также подтверждено, что проверка повторной регистрации Engine в текущей реализации практически недостижима из-за использования словаря в `RuntimeRegistry`. Тем не менее данная проверка сохранена как часть публичного контракта `RuntimeValidation`, что обеспечивает устойчивость архитектуры при возможном изменении внутренней реализации Registry. + +**Статус:** ✅ Passed + +--- + +# Documentation + +В рамках Build должны быть обновлены: + +- Build History; +- Runtime Architecture; +- Runtime Layer Documentation. + +Runtime Contract не изменяется. + +--- + +# Acceptance + +Build считается завершённым. + +Выполнены: + +- ✅ Development Strategy +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ Architecture Decision +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Runtime Review +- ✅ Documentation +- ✅ Acceptance + +Статус Build: + +**Accepted** + +--- + +# Итоги Build + +В результате Build 015.7 Runtime Layer получил централизованный механизм проверки собственной корректности. + +Теперь Runtime способен: + +- проверять наличие зарегистрированных Engine; +- проверять обязательные поля Metadata; +- использовать RuntimeDependencies для проверки зависимостей; +- подтверждать готовность Runtime до начала выполнения анализа. + +После завершения Build 015.7 базовая инфраструктура Runtime Layer сформирована полностью. + +--- + +# Следующий Build + +```text +Build 015.8 + +Runtime Service + +runtime/service.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-015-8-runtime-service.md b/docs/market_intelligence/builds/build-015-8-runtime-service.md new file mode 100644 index 0000000..b7cacd7 --- /dev/null +++ b/docs/market_intelligence/builds/build-015-8-runtime-service.md @@ -0,0 +1,226 @@ +# Build 015.8 — Runtime Layer / Runtime Service + +**Статус:** ✅ Accepted + +--- + +# Цель Build + +Завершить построение базовой инфраструктуры Runtime Layer путём введения единой публичной точки входа — `RuntimeService`. + +Build объединяет ранее реализованные компоненты Runtime в единый жизненный цикл выполнения, не нарушая принцип единственной ответственности. + +--- + +# Архитектурная задача + +До Build 015.8 Runtime Layer уже содержал: + +- Runtime Registry; +- Runtime Validation; +- Runtime Dependencies; +- Runtime Runner. + +Однако отсутствовал компонент, объединяющий их в единый процесс выполнения. + +Build 015.8 завершает базовую архитектуру Runtime Layer введением Runtime Service. + +--- + +# Реализованные компоненты + +```text +runtime/ + + protocol.py + registry.py + validation.py + dependencies.py + runner.py + service.py + exceptions.py +``` + +--- + +# Реализовано + +Создан новый компонент: + +```text +RuntimeService +``` + +Runtime Service предоставляет единый публичный API Runtime Layer. + +Поддерживаются операции: + +- регистрация Engine; +- удаление Engine; +- получение регистрации Engine; +- выполнение полного Runtime Pipeline. + +--- + +# Архитектура выполнения + +Во время выполнения Runtime Service использует существующие компоненты Runtime. + +```text +RuntimeRegistry + ↓ +RuntimeValidation + ↓ +RuntimeDependencies + ↓ +RuntimeRunner + ↓ +RuntimeResult +``` + +Каждый компонент сохраняет собственную область ответственности. + +--- + +# Ответственность Runtime Service + +Runtime Service отвечает исключительно за координацию жизненного цикла Runtime. + +Он: + +- не содержит аналитики; +- не реализует алгоритмы Engine; +- не принимает торговых решений; +- не интерпретирует результаты анализа; +- не содержит собственной логики проверки зависимостей; +- не содержит собственной логики запуска Engine. + +Все специализированные задачи делегируются соответствующим Runtime-компонентам. + +--- + +# Публичный API + +Реализован следующий публичный интерфейс: + +```python +register_engine(...) +unregister_engine(...) +get_engine(...) +analyze(...) +``` + +Метод `analyze()` возвращает единый объект `RuntimeResult`. + +--- + +# Runtime Pipeline + +Во время анализа Runtime выполняет следующие этапы: + +```text +Registry + ↓ +Validation + ↓ +Dependency Resolution + ↓ +Engine Runner + ↓ +RuntimeResult +``` + +Таким образом Runtime Layer получил полностью определённый жизненный цикл выполнения. + +--- + +# Архитектурные результаты + +Build завершил построение публичного фасада Runtime Layer. + +После реализации: + +- Coordinator больше не должен использовать внутренние Runtime-компоненты напрямую; +- Runtime Layer имеет единственную официальную точку входа; +- детали реализации Runtime полностью инкапсулированы; +- дальнейшее развитие Runtime возможно без изменения внешнего контракта. + +--- + +# Engineering Review + +## Architecture Review + +Passed. + +Runtime Service реализует исключительно координацию компонентов Runtime и не нарушает архитектурные границы. + +--- + +## Domain Review + +Passed. + +Runtime остаётся инфраструктурным слоем и не содержит аналитической либо торговой логики. + +--- + +## Code Review + +Passed. + +Выявлено: + +- отсутствует дублирование логики; +- отсутствуют циклические зависимости; +- соблюдён принцип единственной ответственности; +- все существующие Runtime-компоненты используются повторно. + +--- + +## Runtime Review + +Passed. + +Жизненный цикл Runtime полностью соответствует ранее утверждённому Runtime Protocol. + +--- + +# ADR + +В рамках Build принято следующее архитектурное решение. + +> Runtime Service является единственной публичной точкой входа Runtime Layer. + +Внутренние компоненты Runtime (`Registry`, `Validation`, `Dependencies`, `Runner`) рассматриваются как детали реализации и не используются внешними слоями напрямую. + +--- + +# Compile Check + +```text +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Итог + +Build 015.8 завершил построение базового Runtime Layer. + +После завершения Build Runtime включает: + +- Runtime Protocol; +- Runtime Registry; +- Runtime Runner; +- Runtime Dependencies; +- Runtime Validation; +- Runtime Service. + +Runtime Layer готов к переходу к следующему этапу развития архитектуры — Coordinator Layer. \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-1-common-models-coordinator-result.md b/docs/market_intelligence/builds/build-016-1-common-models-coordinator-result.md new file mode 100644 index 0000000..1c02c28 --- /dev/null +++ b/docs/market_intelligence/builds/build-016-1-common-models-coordinator-result.md @@ -0,0 +1,192 @@ +# Build 016.1 — Coordinator Models / CoordinatorResult + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 016.1 | +| Название | Coordinator Models / CoordinatorResult | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Common / Coordinator Contract | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Добавить базовые модели результата Coordinator Layer. + +Основной результат Build: + +```text +CoordinatorResult +``` + +--- + +# Причина появления Build + +После утверждения архитектуры Coordinator Layer потребовался официальный выходной контракт Coordinator. + +`CoordinatorResult` является межслойной моделью: + +```text +RuntimeResult + ↓ +Coordinator + ↓ +CoordinatorResult + ↓ +Trading Layer +``` + +Поэтому модель размещена в Common Layer. + +--- + +# Реализованные модели + +В файл: + +```text +app/src/trading/market_intelligence/common/models.py +``` + +добавлены: + +```text +CoordinatorDiagnostics +CoordinatorEvaluationMeta +CoordinatorResult +``` + +--- + +# Architecture Review + +Проверка подтвердила: + +- модели корректно размещены в Common Layer; +- `CoordinatorResult` не является торговым решением; +- `CoordinatorResult` не зависит от реализации Coordinator; +- `RuntimeResult` остаётся входом Coordinator; +- `EngineResult` и `RuntimeResult` не изменяются. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение добавить модели результата Coordinator в Common Layer, так как `CoordinatorResult` является межслойным контрактом между Coordinator Layer и будущим Trading Layer. + +**Статус:** Accepted + +--- + +# Implementation + +Добавлены модели: + +```text +CoordinatorDiagnostics +CoordinatorEvaluationMeta +CoordinatorResult +``` + +`CoordinatorResult` содержит: + +- `runtime_result`; +- `diagnostics`; +- `meta`; +- `payload`; +- `status`; +- `score`; +- `confidence`; +- `reason`; +- `direction`; +- `bias`; +- `phase`; +- `regime`; +- `risk_level`. + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- `CoordinatorResult` является итоговой аналитической интерпретацией `RuntimeResult`; +- модель не содержит торгового решения; +- модель не содержит логики запуска Engine; +- модель не содержит правил Coordinator; +- будущий Trading Layer сможет использовать `CoordinatorResult` как контракт. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Замечаний, блокирующих Build, нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 016.2 + +Coordinator Protocol + +coordinator/protocol.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-2-coordinator-protocol.md b/docs/market_intelligence/builds/build-016-2-coordinator-protocol.md new file mode 100644 index 0000000..9191386 --- /dev/null +++ b/docs/market_intelligence/builds/build-016-2-coordinator-protocol.md @@ -0,0 +1,189 @@ +# Build 016.2 — Coordinator Protocol + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 016.2 | +| Название | Coordinator Protocol | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Coordinator | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать официальный публичный контракт Coordinator Layer. + +--- + +# Причина появления Build + +После появления модели `CoordinatorResult` Coordinator Layer должен получить стабильный интерфейс взаимодействия. + +Контракт Coordinator определяет единственную модель взаимодействия: + +```text +RuntimeResult + ↓ +Coordinator + ↓ +CoordinatorResult +``` + +--- + +# Architecture + +`CoordinatorProtocol` описывает только внешний интерфейс Coordinator. + +Он не содержит: + +- реализации; +- правил согласования; +- валидации; +- исключений; +- торговой логики; +- зависимостей от конкретных Engine; +- зависимостей от внутренних компонентов Runtime. + +--- + +# Architecture Review + +Проверка подтвердила: + +- Coordinator получает только `RuntimeResult`; +- Coordinator возвращает только `CoordinatorResult`; +- контракт не содержит реализации; +- контракт не зависит от внутренних компонентов Runtime; +- контракт не зависит от конкретных Engine; +- контракт не содержит торговой логики. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение добавить официальный контракт: + +```text +CoordinatorProtocol +``` + +Утверждённый метод: + +```python +async def coordinate( + self, + runtime_result: RuntimeResult, +) -> CoordinatorResult: + ... +``` + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/coordinator/protocol.py +``` + +Содержимое контракта: + +```python +class CoordinatorProtocol(Protocol): + async def coordinate( + self, + runtime_result: RuntimeResult, + ) -> CoordinatorResult: + ... +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- Coordinator принимает исключительно `RuntimeResult`; +- Coordinator возвращает исключительно `CoordinatorResult`; +- протокол не содержит аналитической логики; +- протокол не содержит торговой логики; +- протокол не зависит от конкретных Engine; +- протокол не зависит от реализации Runtime Layer. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 016.3 + +Coordinator Exceptions + +coordinator/exceptions.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-3-coordinator-exceptions.md b/docs/market_intelligence/builds/build-016-3-coordinator-exceptions.md new file mode 100644 index 0000000..21d8f62 --- /dev/null +++ b/docs/market_intelligence/builds/build-016-3-coordinator-exceptions.md @@ -0,0 +1,168 @@ +# Build 016.3 — Coordinator Exceptions + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 016.3 | +| Название | Coordinator Exceptions | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Coordinator | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать единую иерархию исключений Coordinator Layer. + +--- + +# Причина появления Build + +Coordinator Layer является самостоятельным архитектурным уровнем. + +Следовательно, он должен иметь собственное пространство ошибок и не использовать исключения Runtime Layer. + +--- + +# Architecture + +Создана минимальная иерархия исключений: + +```text +CoordinatorError +├── InvalidCoordinatorResultError +├── CoordinatorValidationError +└── CoordinatorExecutionError +``` + +--- + +# Architecture Review + +Подтверждено: + +- Coordinator Layer имеет собственную иерархию исключений; +- исключения не зависят от Runtime Exceptions; +- файл не содержит логики Coordinator; +- файл не зависит от Engine; +- дополнительные исключения не добавлены преждевременно. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение ввести собственное пространство ошибок Coordinator Layer. + +Это обеспечивает: + +- независимость Coordinator Layer; +- явные границы между Runtime и Coordinator; +- локальную обработку ошибок Coordinator; +- возможность расширения без изменения Runtime. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/coordinator/exceptions.py +``` + +Реализованы: + +```text +CoordinatorError +InvalidCoordinatorResultError +CoordinatorValidationError +CoordinatorExecutionError +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- ошибки относятся исключительно к ответственности Coordinator; +- исключения не пересекаются с Runtime Layer; +- отсутствует торговая логика; +- отсутствует аналитическая логика; +- отсутствуют зависимости от Engine; +- отсутствуют зависимости от Runtime internals. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 016.4 + +Coordinator Validation + +coordinator/validation.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-4-coordinator-validation.md b/docs/market_intelligence/builds/build-016-4-coordinator-validation.md new file mode 100644 index 0000000..b34e224 --- /dev/null +++ b/docs/market_intelligence/builds/build-016-4-coordinator-validation.md @@ -0,0 +1,212 @@ +# Build 016.4 — Coordinator Validation + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 016.4 | +| Название | Coordinator Validation | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Coordinator | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать компонент проверки входного `RuntimeResult` перед началом работы Coordinator Layer. + +--- + +# Причина появления Build + +Coordinator является следующим архитектурным уровнем после Runtime Layer. + +Перед тем как Coordinator начнёт согласовывать результаты аналитических Engine, необходимо убедиться, что входной `RuntimeResult` корректен. + +Validation выделяется в самостоятельный компонент, чтобы отделить проверку входных данных от правил согласования и логики Coordinator. + +--- + +# Architecture + +Создан компонент: + +```text +CoordinatorValidation +``` + +с единственным публичным методом: + +```python +validate( + runtime_result: RuntimeResult, +) -> None +``` + +Компонент выполняет только предварительную проверку входного результата Runtime. + +--- + +# Проверяемые условия + +В Build реализованы следующие проверки: + +- RuntimeResult передан; +- RuntimeResult содержит результаты Engine; +- RuntimeResult успешно завершён и не содержит критических ошибок Runtime. + +Все остальные проверки будут реализованы в последующих Build Coordinator Rules. + +--- + +# Architecture Review + +Подтверждено: + +- Validation проверяет только входные данные; +- Validation не изменяет `RuntimeResult`; +- Validation не формирует `CoordinatorResult`; +- Validation не содержит аналитической логики; +- Validation не зависит от внутренних компонентов Runtime; +- Validation использует только публичный контракт `RuntimeResult`. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение выделить предварительную проверку входного `RuntimeResult` в отдельный компонент Coordinator Layer. + +Validation выполняется до запуска правил согласования и обеспечивает корректность входных данных. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/coordinator/validation.py +``` + +Реализован класс: + +```text +CoordinatorValidation +``` + +Публичный метод: + +```python +validate( + runtime_result: RuntimeResult, +) +``` + +Внутренние проверки: + +```text +_validate_runtime_result_exists() +_validate_runtime_has_results() +_validate_runtime_successful() +``` + +При обнаружении ошибки используется исключение: + +```text +CoordinatorValidationError +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- компонент проверяет только входной `RuntimeResult`; +- компонент не изменяет `RuntimeResult`; +- компонент не формирует `CoordinatorResult`; +- компонент не согласует результаты Engine; +- компонент не анализирует рынок; +- компонент не принимает торговых решений; +- компонент использует только публичные свойства `RuntimeResult`. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- компонент минимален и соответствует принципу Single Responsibility; +- отсутствуют лишние зависимости; +- отсутствует логика согласования результатов; +- используется собственная иерархия исключений Coordinator Layer. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 016.5 + +Coordinator Rules + +coordinator/rules.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-5-coordinator-rules.md b/docs/market_intelligence/builds/build-016-5-coordinator-rules.md new file mode 100644 index 0000000..af57819 --- /dev/null +++ b/docs/market_intelligence/builds/build-016-5-coordinator-rules.md @@ -0,0 +1,199 @@ +# Build 016.5 — Coordinator Rules + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 016.5 | +| Название | Coordinator Rules | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Coordinator | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать компонент базового согласования результатов Engine. + +--- + +# Причина появления Build + +После реализации `CoordinatorValidation` Coordinator Layer получил механизм проверки входного `RuntimeResult`. + +Следующим шагом стал компонент, который преобразует: + +```text +RuntimeResult +``` + +в: + +```text +CoordinatorResult +``` + +--- + +# Architecture + +Создан компонент: + +```text +CoordinatorRules +``` + +Компонент отвечает за применение правил согласования результатов Engine. + +В Build 016.5 реализован минимальный базовый каркас правил без сложных механизмов весов, конфликтов и голосования. + +--- + +# Architecture Review + +Подтверждено: + +- Rules не запускает Engine; +- Rules не обращается к RuntimeService; +- Rules не выполняет Validation; +- Rules не зависит от CoordinatorService; +- Rules не принимает торговых решений; +- Rules работает только с `RuntimeResult` и формирует `CoordinatorResult`. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение выделить правила согласования результатов Engine в отдельный компонент Coordinator Layer. + +`CoordinatorRules` отвечает за формирование итогового `CoordinatorResult` из `RuntimeResult`. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/coordinator/rules.py +``` + +Реализован класс: + +```text +CoordinatorRules +``` + +Публичный метод: + +```python +coordinate(runtime_result) +``` + +Внутренние методы: + +```text +_resolve_status() +_resolve_score() +_resolve_confidence() +_resolve_direction() +_resolve_bias() +_resolve_phase() +_resolve_regime() +_resolve_risk_level() +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- компонент преобразует `RuntimeResult` в `CoordinatorResult`; +- компонент не запускает Engine; +- компонент не обращается к Runtime internals; +- компонент не выполняет Validation; +- компонент не принимает торговых решений; +- компонент пока реализует только базовые правила согласования; +- сложные механизмы весов, конфликтов и голосования не добавлены преждевременно. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- компонент минимален; +- публичный метод только один; +- Validation не дублируется; +- торговая логика отсутствует; +- лишние зависимости отсутствуют; +- сложные правила не добавлены преждевременно. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 016.6 + +Coordinator Service + +coordinator/service.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-6-coordinator-service.md b/docs/market_intelligence/builds/build-016-6-coordinator-service.md new file mode 100644 index 0000000..9e43091 --- /dev/null +++ b/docs/market_intelligence/builds/build-016-6-coordinator-service.md @@ -0,0 +1,252 @@ +# Build 016.6 — Coordinator Service + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 016.6 | +| Название | Coordinator Service | +| Статус | **Accepted** | +| Подсистема | Market Intelligence | +| Layer | Coordinator | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать единую публичную точку входа Coordinator Layer. + +--- + +# Причина появления Build + +После реализации компонентов: + +- `CoordinatorProtocol`; +- `CoordinatorValidation`; +- `CoordinatorRules`; + +Coordinator Layer получил все необходимые внутренние элементы, однако отсутствовал компонент, объединяющий их в единый рабочий процесс. + +Эту роль выполняет `CoordinatorService`. + +--- + +# Architecture + +Создан компонент: + +```text +CoordinatorService +``` + +Он реализует официальный контракт: + +```text +CoordinatorProtocol +``` + +и обеспечивает последовательность выполнения: + +```text +RuntimeResult + │ + ▼ +CoordinatorValidation + │ + ▼ +CoordinatorRules + │ + ▼ +CoordinatorResult +``` + +--- + +# Architecture Review + +Подтверждено: + +- Service является единственной публичной точкой входа Coordinator Layer; +- Service принимает только `RuntimeResult`; +- Service возвращает только `CoordinatorResult`; +- Service реализует `CoordinatorProtocol`; +- Service делегирует проверку `CoordinatorValidation`; +- Service делегирует согласование `CoordinatorRules`; +- Service не содержит собственной бизнес-логики; +- Service не зависит от внутренних компонентов Runtime Layer. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение объединить внутренние компоненты Coordinator Layer через единый сервис. + +`CoordinatorService` становится единственной точкой взаимодействия внешних слоёв с Coordinator. + +Все внутренние компоненты Coordinator инкапсулируются внутри Service. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/coordinator/service.py +``` + +Реализован класс: + +```text +CoordinatorService +``` + +Публичный метод: + +```python +coordinate( + runtime_result: RuntimeResult, +) -> CoordinatorResult +``` + +Внутренние зависимости: + +```text +CoordinatorValidation +CoordinatorRules +``` + +Последовательность работы: + +```text +Validation + ↓ +Rules + ↓ +CoordinatorResult +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- компонент является публичной точкой входа Coordinator Layer; +- принимает только `RuntimeResult`; +- возвращает только `CoordinatorResult`; +- не содержит правил согласования; +- не изменяет `RuntimeResult`; +- не запускает Engine; +- не принимает торговых решений; +- не обращается к Runtime internals. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- `CoordinatorService` реализует `CoordinatorProtocol`; +- публичный метод только один; +- Validation и Rules не дублируются; +- отсутствуют лишние зависимости; +- отсутствует торговая логика. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Итог Build + +Build 016.6 завершает построение **Coordinator Foundation**. + +Coordinator Layer полностью сформирован и включает: + +```text +Coordinator + +├── models +├── protocol +├── exceptions +├── validation +├── rules +└── service +``` + +Теперь Coordinator Layer обладает: + +- единым публичным API; +- собственными моделями; +- собственным пространством исключений; +- механизмом предварительной проверки входных данных; +- базовыми правилами согласования; +- сервисом-оркестратором. + +Архитектурный фундамент Coordinator завершён. + +--- + +# Следующий Build + +```text +Build 017 + +Trading Layer Architecture +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-016-coordinator-layer-architecture.md b/docs/market_intelligence/builds/build-016-coordinator-layer-architecture.md new file mode 100644 index 0000000..ea3fc4e --- /dev/null +++ b/docs/market_intelligence/builds/build-016-coordinator-layer-architecture.md @@ -0,0 +1,363 @@ +# Build 016 — Coordinator Layer Architecture + +**Статус:** ✅ Accepted + +--- + +# Цель Build + +Определить архитектуру нового слоя **Coordinator Layer**, отвечающего за объединение результатов аналитических Engine в единое представление состояния рынка. + +Build не содержит реализации компонентов и фиксирует исключительно архитектурные решения, необходимые для дальнейшего развития платформы. + +--- + +# Архитектурная задача + +После завершения Runtime Layer платформа способна: + +- зарегистрировать Engine; +- проверить корректность Runtime; +- определить порядок выполнения Engine; +- выполнить все зарегистрированные Engine; +- собрать результаты в единый `RuntimeResult`. + +Однако `RuntimeResult` представляет собой лишь совокупность независимых результатов Engine. + +Для получения единой аналитической картины рынка необходим отдельный архитектурный слой. + +--- + +# Причина появления Coordinator Layer + +Каждый Engine анализирует только собственную область ответственности. + +Например: + +```text +Trend Engine + ↓ +UP + +Structure Engine + ↓ +BULLISH + +Wave Engine + ↓ +IMPULSE + +Liquidity Engine + ↓ +LOW +``` + +Каждый вывод корректен в рамках своего Engine. + +Однако платформа должна получить единый ответ на вопрос: + +> **Каково текущее состояние рынка с учётом всех аналитических компонентов одновременно?** + +Эта ответственность не относится к Runtime Layer. + +--- + +# Назначение Coordinator Layer + +Coordinator Layer становится следующим архитектурным уровнем после Runtime Layer. + +Он получает: + +```text +RuntimeResult +``` + +и формирует: + +```text +CoordinatorResult +``` + +Coordinator отвечает исключительно за согласование результатов аналитических Engine. + +--- + +# Ответственность Coordinator + +Coordinator обязан: + +- принимать `RuntimeResult`; +- анализировать результаты всех Engine; +- учитывать статус каждого Engine; +- учитывать confidence каждого Engine; +- выявлять подтверждения между Engine; +- выявлять противоречия между Engine; +- оценивать полноту аналитических данных; +- формировать единый `CoordinatorResult`. + +Coordinator не изменяет результаты Engine. + +Coordinator не выполняет повторный анализ рынка. + +--- + +# Что Coordinator не делает + +Coordinator не должен: + +- запускать Engine; +- создавать Engine; +- обращаться к Runtime Registry; +- обращаться к Runtime Runner; +- выполнять анализ рыночных данных; +- рассчитывать технические индикаторы; +- принимать торговые решения; +- взаимодействовать с биржей; +- взаимодействовать с Telegram; +- работать с базой данных; +- работать с AutoTrade. + +Coordinator является исключительно аналитическим слоем согласования результатов. + +--- + +# Архитектурная схема + +```text +Market Data + ↓ +Engine + ↓ +Runtime + ↓ +RuntimeResult + ↓ +Coordinator + ↓ +CoordinatorResult + ↓ +Trading Layer +``` + +--- + +# Границы Coordinator Layer + +Coordinator располагается между Runtime Layer и будущим Trading Layer. + +Runtime отвечает за выполнение Engine. + +Coordinator отвечает за согласование результатов. + +Trading Layer принимает решения на основании `CoordinatorResult`. + +Таким образом каждый слой имеет собственную область ответственности. + +--- + +# Предварительная структура Coordinator Layer + +```text +coordinator/ + +├── __init__.py +├── models.py +├── protocol.py +├── exceptions.py +├── validation.py +├── rules.py +└── service.py +``` + +Каждый компонент имеет самостоятельную область ответственности. + +--- + +# Предполагаемые компоненты + +## models.py + +Определяет модели Coordinator Layer. + +Основной моделью станет: + +```text +CoordinatorResult +``` + +В дальнейшем могут появиться дополнительные модели диагностики и согласования. + +--- + +## protocol.py + +Определяет официальный публичный контракт Coordinator Layer. + +--- + +## exceptions.py + +Содержит исключения Coordinator Layer. + +--- + +## validation.py + +Проверяет корректность входного `RuntimeResult`. + +--- + +## rules.py + +Содержит правила согласования результатов Engine. + +Именно данный компонент будет определять принципы объединения аналитических выводов. + +--- + +## service.py + +Является единственной публичной точкой входа Coordinator Layer. + +Получает `RuntimeResult`. + +Возвращает `CoordinatorResult`. + +--- + +# Допустимые зависимости + +Coordinator может использовать: + +```text +Common Layer + ↓ +RuntimeResult + ↓ +Coordinator Models +``` + +Coordinator не должен зависеть от внутренних компонентов Runtime. + +--- + +# Архитектурные принципы + +При проектировании Coordinator подтверждены следующие инженерные принципы. + +## Single Responsibility + +Coordinator отвечает исключительно за согласование результатов Engine. + +--- + +## Stable Foundation + +Coordinator строится поверх полностью завершённого Runtime Layer. + +--- + +## One Source of Truth + +Единым результатом работы Coordinator становится `CoordinatorResult`. + +--- + +## Architecture First + +Сначала утверждается архитектура слоя. + +Только после этого начинается реализация его компонентов. + +--- + +# Architecture Review + +Во время архитектурной проверки подтверждено: + +- Runtime и Coordinator имеют различные области ответственности; +- отсутствует пересечение обязанностей; +- Runtime остаётся инфраструктурным слоем; +- Coordinator становится аналитическим слоем согласования; +- Trading Layer не зависит от внутренних компонентов Runtime. + +Architecture Review завершён успешно. + +--- + +# Architecture Decision Record (ADR) + +Принято следующее долгосрочное архитектурное решение. + +> Coordinator Layer вводится как самостоятельный архитектурный слой между Runtime Layer и Trading Layer. + +Coordinator становится единственным компонентом платформы, ответственным за объединение результатов аналитических Engine. + +--- + +# План реализации Build 016.x + +Разработка Coordinator Layer утверждена в следующей последовательности. + +```text +016.1 +Coordinator Models + + ↓ + +016.2 +Coordinator Protocol + + ↓ + +016.3 +Coordinator Exceptions + + ↓ + +016.4 +Coordinator Validation + + ↓ + +016.5 +Coordinator Rules + + ↓ + +016.6 +Coordinator Service +``` + +Такая последовательность повторяет архитектурный подход, ранее использованный при построении Runtime Layer. + +--- + +# Итог + +Build 016 завершает проектирование нового архитектурного слоя платформы. + +В результате утверждены: + +- назначение Coordinator Layer; +- область ответственности; +- архитектурные границы; +- место слоя в общей архитектуре; +- структура компонентов; +- последовательность дальнейшей реализации. + +После завершения Build архитектура платформы принимает следующий вид. + +```text +Common Layer + ↓ +Engine Layer + ↓ +Runtime Layer + ↓ +Coordinator Layer + ↓ +Trading Layer +``` + +Coordinator Layer официально включён в архитектуру подсистемы **Market Intelligence** и готов к реализации в серии Build **016.x**. \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-1-trading-models.md b/docs/market_intelligence/builds/build-017-1-trading-models.md new file mode 100644 index 0000000..b21457f --- /dev/null +++ b/docs/market_intelligence/builds/build-017-1-trading-models.md @@ -0,0 +1,187 @@ +# Build 017.1 — Trading Models / TradingDecision + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.1 | +| Название | Trading Models / TradingDecision | +| Статус | **Accepted** | +| Подсистема | Trading | +| Layer | Common / Trading Contract | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Добавить базовые модели Trading Layer. + +Основной результат Build: + +```text +TradingDecision +``` + +--- + +# Причина появления Build + +После утверждения архитектуры Trading Layer потребовался официальный выходной контракт слоя. + +`TradingDecision` является межслойной моделью: + +```text +CoordinatorResult + ↓ +Trading Layer + ↓ +TradingDecision + ↓ +Execution / Risk / Portfolio +``` + +Поэтому модель размещена в Common Layer. + +--- + +# Реализованные модели + +В файл: + +```text +app/src/trading/market_intelligence/common/models.py +``` + +добавлены: + +```text +TradingDiagnostics +TradingEvaluationMeta +TradingDecision +``` + +--- + +# Architecture Review + +Проверка подтвердила: + +- модели корректно размещены в Common Layer; +- `TradingDecision` является результатом Trading Layer; +- `TradingDecision` не является ордером; +- `TradingDecision` не исполняет сделку; +- `TradingDecision` не управляет позицией; +- модель не зависит от Execution / Risk / Portfolio слоёв. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение добавить модели Trading Layer в Common Layer, так как `TradingDecision` является межслойным контрактом между Trading Layer и будущими слоями Execution / Risk / Portfolio. + +**Статус:** Accepted + +--- + +# Implementation + +Добавлены модели: + +```text +TradingDiagnostics +TradingEvaluationMeta +TradingDecision +``` + +`TradingDecision` содержит: + +- `coordinator_result`; +- `diagnostics`; +- `meta`; +- `payload`; +- `status`; +- `score`; +- `confidence`; +- `reason`. + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- `TradingDecision` использует `CoordinatorResult` как входной аналитический контракт; +- модель не является ордером; +- модель не исполняет сделку; +- модель не содержит биржевой логики; +- модель не управляет позицией; +- модель не содержит Risk Plan / Entry Plan / Exit Plan. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Замечаний, блокирующих Build, нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 017.2 + +Trading Protocol +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-2-trading-protocol.md b/docs/market_intelligence/builds/build-017-2-trading-protocol.md new file mode 100644 index 0000000..87417c1 --- /dev/null +++ b/docs/market_intelligence/builds/build-017-2-trading-protocol.md @@ -0,0 +1,191 @@ +# Build 017.2 — Trading Protocol + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.2 | +| Название | Trading Protocol | +| Статус | **Accepted** | +| Подсистема | Trading | +| Layer | Trading | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать официальный публичный контракт Trading Layer. + +--- + +# Причина появления Build + +После появления модели `TradingDecision` Trading Layer должен получить стабильный интерфейс взаимодействия. + +Контракт Trading определяет единственную модель взаимодействия: + +```text +CoordinatorResult + ↓ +Trading + ↓ +TradingDecision +``` + +--- + +# Architecture + +`TradingProtocol` описывает только внешний интерфейс Trading Layer. + +Он не содержит: + +- реализации; +- Validation; +- Rules; +- Execution; +- Risk; +- Portfolio; +- биржевой логики; +- логики исполнения сделок. + +--- + +# Architecture Review + +Подтверждено: + +- вход — только `CoordinatorResult`; +- выход — только `TradingDecision`; +- контракт не содержит реализации; +- контракт не зависит от Runtime; +- контракт не зависит от Coordinator internals; +- контракт не работает с биржей; +- контракт не исполняет сделки. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение добавить официальный контракт: + +```text +TradingProtocol +``` + +Утверждённый метод: + +```python +async def decide( + coordinator_result: CoordinatorResult, +) -> TradingDecision: + ... +``` + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/trading/protocol.py +``` + +Содержимое контракта: + +```python +class TradingProtocol(Protocol): + async def decide( + self, + coordinator_result: CoordinatorResult, + ) -> TradingDecision: + ... +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- Trading принимает только `CoordinatorResult`; +- Trading возвращает только `TradingDecision`; +- протокол не содержит реализации; +- протокол не содержит торговых правил; +- протокол не содержит логики исполнения сделок; +- протокол не зависит от Runtime; +- протокол не зависит от Coordinator internals. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 017.3 + +Trading Exceptions + +trading/exceptions.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-3-trading-exceptions.md b/docs/market_intelligence/builds/build-017-3-trading-exceptions.md new file mode 100644 index 0000000..064b9d5 --- /dev/null +++ b/docs/market_intelligence/builds/build-017-3-trading-exceptions.md @@ -0,0 +1,180 @@ +# Build 017.3 — Trading Exceptions + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.3 | +| Название | Trading Exceptions | +| Статус | **Accepted** | +| Подсистема | Trading | +| Layer | Trading | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать собственную иерархию исключений Trading Layer. + +--- + +# Причина появления Build + +После утверждения архитектуры Trading Layer, моделей и публичного контракта необходимо сформировать собственное пространство ошибок. + +Каждый архитектурный слой платформы Dzentra использует собственную иерархию исключений, что обеспечивает слабую связанность между слоями и независимую обработку ошибок. + +--- + +# Architecture + +Trading Layer получает собственую иерархию исключений: + +```text +TradingError +├── InvalidTradingDecisionError +├── TradingValidationError +└── TradingExecutionError +``` + +Иерархия полностью независима от Runtime Layer и Coordinator Layer. + +--- + +# Architecture Review + +Подтверждено: + +- Trading Layer имеет собственное пространство ошибок; +- исключения не зависят от Coordinator; +- исключения не зависят от Runtime; +- файл не содержит Validation; +- файл не содержит Rules; +- файл не содержит Service; +- специализированные исключения преждевременно не добавляются. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение выделить собственную иерархию исключений Trading Layer. + +Trading не использует ошибки других слоёв платформы. + +Будущие компоненты Validation, Rules и Service будут использовать исключительно `TradingError` и его наследников. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/trading/exceptions.py +``` + +Реализованы исключения: + +```text +TradingError +InvalidTradingDecisionError +TradingValidationError +TradingExecutionError +``` + +Иерархия соответствует архитектурному стандарту платформы. + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- ошибки относятся только к Trading Layer; +- исключения не пересекаются с Coordinator / Runtime / Engine; +- файл не содержит Validation; +- файл не содержит Rules; +- файл не содержит Service; +- торговая логика отсутствует; +- специализированные ошибки Risk / Position / Entry / Exit не добавлены преждевременно. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- `TradingError` является базовым исключением Trading Layer; +- все специализированные исключения наследуются только от `TradingError`; +- файл не импортирует другие архитектурные слои; +- отсутствуют лишние зависимости; +- структура полностью соответствует архитектурному стандарту Dzentra. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 017.4 + +Trading Validation + +trading/validation.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-4-trading-validation.md b/docs/market_intelligence/builds/build-017-4-trading-validation.md new file mode 100644 index 0000000..3ea6fce --- /dev/null +++ b/docs/market_intelligence/builds/build-017-4-trading-validation.md @@ -0,0 +1,224 @@ +# Build 017.4 — Trading Validation + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.4 | +| Название | Trading Validation | +| Статус | **Accepted** | +| Подсистема | Trading | +| Layer | Trading | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать компонент предварительной проверки входного `CoordinatorResult` перед началом работы Trading Layer. + +--- + +# Причина появления Build + +Trading Layer принимает торговое решение только на основании результата Coordinator Layer. + +Перед применением торговых правил необходимо убедиться, что входной аналитический результат корректен и пригоден для дальнейшего использования. + +Для этого вводится отдельный компонент: + +```text +TradingValidation +``` + +Он отделяет проверку входных данных от бизнес-логики принятия решений. + +--- + +# Architecture + +TradingValidation является первым этапом жизненного цикла Trading Layer: + +```text +CoordinatorResult + │ + ▼ +TradingValidation + │ + ▼ +TradingRules + │ + ▼ +TradingDecision +``` + +Validation выполняет исключительно проверку входного контракта и не содержит торговых правил. + +--- + +# Architecture Review + +Подтверждено: + +- компонент принимает только `CoordinatorResult`; +- компонент не изменяет `CoordinatorResult`; +- компонент не формирует `TradingDecision`; +- компонент не содержит Trading Rules; +- компонент не взаимодействует с Runtime, Coordinator internals, биржей или внешними сервисами; +- используются только публичные свойства `CoordinatorResult`. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение выделить проверку входного результата в самостоятельный компонент. + +Trading Layer использует исключительно публичный API `CoordinatorResult`: + +- `is_usable`; +- `has_errors`. + +Trading Layer не анализирует внутренние статусы Coordinator и не зависит от его реализации. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/trading/validation.py +``` + +Реализован класс: + +```text +TradingValidation +``` + +Публичный метод: + +```python +validate( + coordinator_result: CoordinatorResult, +) -> None +``` + +Выполняемые проверки: + +1. Передан ли `CoordinatorResult`; +2. Пригоден ли результат (`is_usable`); +3. Отсутствуют ли критические ошибки (`has_errors`). + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- компонент проверяет только входной `CoordinatorResult`; +- компонент не изменяет входные данные; +- компонент не содержит Trading Rules; +- компонент не принимает торговых решений; +- компонент использует только публичные свойства `CoordinatorResult`; +- отсутствуют зависимости от Runtime, Coordinator internals и других слоёв. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- единственный публичный метод — `validate()`; +- используется специализированное исключение `TradingValidationError`; +- проверки разделены на приватные методы; +- отсутствует бизнес-логика; +- отсутствуют лишние зависимости. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Development Strategy +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Итог Build + +Build 017.4 завершил создание компонента предварительной проверки Trading Layer. + +Теперь фундамент Trading содержит: + +```text +Trading Foundation + +├── Models +├── Protocol +├── Exceptions +└── Validation +``` + +Trading Layer получил собственный механизм проверки входного аналитического контракта без нарушения архитектурной изоляции между слоями. + +--- + +# Следующий Build + +```text +Build 017.5 + +Trading Rules + +trading/rules.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-5-trading-rules.md b/docs/market_intelligence/builds/build-017-5-trading-rules.md new file mode 100644 index 0000000..6089ca0 --- /dev/null +++ b/docs/market_intelligence/builds/build-017-5-trading-rules.md @@ -0,0 +1,194 @@ +# Build 017.5 — Trading Rules + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.5 | +| Название | Trading Rules | +| Статус | **Accepted** | +| Подсистема | Trading | +| Layer | Trading | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать компонент базового формирования `TradingDecision` из `CoordinatorResult`. + +--- + +# Причина появления Build + +После реализации `TradingValidation` Trading Layer получил механизм проверки входного `CoordinatorResult`. + +Следующим шагом стал компонент, который преобразует: + +```text +CoordinatorResult +``` + +в: + +```text +TradingDecision +``` + +--- + +# Architecture + +Создан компонент: + +```text +TradingRules +``` + +В Build 017.5 реализован базовый каркас правил без BUY / SELL / HOLD / EXIT логики. + +--- + +# Architecture Review + +Подтверждено: + +- Rules не выполняет Validation; +- Rules не обращается к Runtime; +- Rules не обращается к Coordinator internals; +- Rules не работает с биржей; +- Rules не исполняет сделки; +- Rules не управляет позицией; +- Rules формирует только базовый `TradingDecision`. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение выделить правила формирования торгового решения в отдельный компонент Trading Layer. + +`TradingRules` отвечает за формирование `TradingDecision` из `CoordinatorResult`. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/trading/rules.py +``` + +Реализован класс: + +```text +TradingRules +``` + +Публичный метод: + +```python +decide(coordinator_result) +``` + +Внутренние методы: + +```text +_resolve_status() +_resolve_score() +_resolve_confidence() +_resolve_reason() +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- компонент преобразует `CoordinatorResult` в `TradingDecision`; +- компонент не выполняет Validation; +- компонент не обращается к Runtime; +- компонент не обращается к Coordinator internals; +- компонент не исполняет сделки; +- компонент не управляет позициями; +- компонент не содержит BUY / SELL / HOLD / EXIT логики; +- компонент формирует только базовый каркас `TradingDecision`. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- компонент минимален; +- публичный метод только один; +- Validation не дублируется; +- торговая логика не добавлена преждевременно; +- лишние зависимости отсутствуют. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Следующий Build + +```text +Build 017.6 + +Trading Service + +trading/service.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-6-trading-service.md b/docs/market_intelligence/builds/build-017-6-trading-service.md new file mode 100644 index 0000000..cb62b30 --- /dev/null +++ b/docs/market_intelligence/builds/build-017-6-trading-service.md @@ -0,0 +1,253 @@ +# Build 017.6 — Trading Service + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.6 | +| Название | Trading Service | +| Статус | **Accepted** | +| Подсистема | Trading | +| Layer | Trading | +| Тип | Architecture + Implementation | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Создать единую публичную точку входа Trading Layer. + +--- + +# Причина появления Build + +После реализации компонентов: + +- `TradingProtocol`; +- `TradingValidation`; +- `TradingRules`; + +Trading Layer получил все необходимые внутренние элементы, однако отсутствовал компонент, объединяющий их в единый рабочий процесс. + +Эту роль выполняет `TradingService`. + +--- + +# Architecture + +Создан компонент: + +```text +TradingService +``` + +Он реализует официальный контракт: + +```text +TradingProtocol +``` + +и обеспечивает последовательность выполнения: + +```text +CoordinatorResult + │ + ▼ +TradingValidation + │ + ▼ +TradingRules + │ + ▼ +TradingDecision +``` + +--- + +# Architecture Review + +Подтверждено: + +- Service является единственной публичной точкой входа Trading Layer; +- Service принимает только `CoordinatorResult`; +- Service возвращает только `TradingDecision`; +- Service реализует `TradingProtocol`; +- Service делегирует проверку `TradingValidation`; +- Service делегирует формирование решения `TradingRules`; +- Service не содержит собственной торговой логики; +- Service не зависит от внутренних компонентов Runtime или Coordinator. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение объединить внутренние компоненты Trading Layer через единый сервис. + +`TradingService` становится единственной точкой взаимодействия внешних слоёв с Trading Layer. + +Все внутренние компоненты Trading инкапсулируются внутри Service. + +**Статус:** Accepted + +--- + +# Implementation + +Создан файл: + +```text +app/src/trading/market_intelligence/trading/service.py +``` + +Реализован класс: + +```text +TradingService +``` + +Публичный метод: + +```python +decide( + coordinator_result: CoordinatorResult, +) -> TradingDecision +``` + +Внутренние зависимости: + +```text +TradingValidation +TradingRules +``` + +Последовательность работы: + +```text +Validation + ↓ +Rules + ↓ +TradingDecision +``` + +--- + +# Compile Check + +```bash +python -m compileall src/trading/market_intelligence +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- компонент является публичной точкой входа Trading Layer; +- принимает только `CoordinatorResult`; +- возвращает только `TradingDecision`; +- не содержит торговых правил; +- не изменяет `CoordinatorResult`; +- не обращается к Runtime / Coordinator internals; +- не работает с биржей; +- не исполняет сделки. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- `TradingService` реализует `TradingProtocol`; +- публичный метод только один; +- Validation и Rules не дублируются; +- отсутствуют лишние зависимости; +- отсутствует торговая логика. + +Замечаний нет. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Development Strategy +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Итог Build + +Build 017.6 завершает построение **Trading Foundation**. + +Trading Layer полностью сформирован и включает: + +```text +Trading + +├── models +├── protocol +├── exceptions +├── validation +├── rules +└── service +``` + +Теперь Trading Layer обладает: + +- единым публичным API; +- собственными моделями; +- собственным пространством исключений; +- механизмом предварительной проверки входных данных; +- базовыми правилами формирования торгового решения; +- сервисом-оркестратором. + +Архитектурный фундамент Trading завершён. + +--- + +# Следующий Build + +```text +Build 018 + +Execution Layer Architecture +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-7-trading-boundary-correction.md b/docs/market_intelligence/builds/build-017-7-trading-boundary-correction.md new file mode 100644 index 0000000..04444ba --- /dev/null +++ b/docs/market_intelligence/builds/build-017-7-trading-boundary-correction.md @@ -0,0 +1,230 @@ +# Build 017.7 — Trading Boundary Correction + +**Engineering Build Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017.7 | +| Название | Trading Boundary Correction | +| Статус | **Accepted** | +| Подсистема | Trading / Market Intelligence | +| Тип | Architecture Refactoring | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Исправить архитектурную границу между **Market Intelligence** и **Trading Decision**. + +--- + +# Причина появления Build + +В ходе подготовки к Build 018 было обнаружено, что Trading Foundation был размещён внутри: + +```text +src/trading/market_intelligence/trading/ +``` + +Это нарушало границу ответственности, поскольку `market_intelligence` должен содержать только компоненты анализа рынка. + +--- + +# Architecture + +Trading Foundation перенесён в самостоятельный слой: + +```text +src/trading/decision/ +``` + +Целевая структура: + +```text +src/trading/decision/ +├── __init__.py +├── models.py +├── protocol.py +├── exceptions.py +├── validation.py +├── rules.py +└── service.py +``` + +--- + +# Architecture Review + +Подтверждено: + +- `market_intelligence` содержит только компоненты анализа рынка; +- `TradingDecision` больше не находится в `market_intelligence/common/models.py`; +- Trading Decision вынесен в самостоятельный слой `src/trading/decision`; +- существующий `src/trading/execution/` не затронут; +- дублирования Execution Layer не создано. + +**Статус:** ✅ Passed + +--- + +# ADR + +Принято решение перенести Trading Foundation из `market_intelligence` в самостоятельный слой `decision`. + +`market_intelligence/common/models.py` должен содержать только модели Market Intelligence: + +```text +Engine* +Runtime* +Coordinator* +``` + +Trading Decision использует `CoordinatorResult`, но не является частью Market Intelligence. + +**Статус:** Accepted + +--- + +# Implementation + +Создан каталог: + +```text +app/src/trading/decision/ +``` + +Создан файл: + +```text +app/src/trading/decision/models.py +``` + +Перенесены файлы: + +```text +protocol.py +exceptions.py +validation.py +rules.py +service.py +``` + +Удалены модели Trading из: + +```text +app/src/trading/market_intelligence/common/models.py +``` + +Удалён старый каталог: + +```text +app/src/trading/market_intelligence/trading/ +``` + +--- + +# Compile Check + +Проверены оба контура: + +```bash +python -m compileall src/trading/market_intelligence +python -m compileall src/trading/decision +``` + +Результат: + +```text +Passed +``` + +--- + +# Domain Review + +Подтверждено: + +- `market_intelligence` снова содержит только Market Intelligence компоненты; +- Trading Decision вынесен в самостоятельный слой; +- `decision` корректно использует `CoordinatorResult` как входной контракт; +- существующая подсистема `execution` не затронута; +- дублирования Execution Layer не создано. + +**Статус:** ✅ Passed + +--- + +# Code Review + +Проверка пройдена. + +Подтверждено: + +- новые импорты используют `src.trading.decision`; +- старых импортов из `src.trading.market_intelligence.trading` не обнаружено; +- `TradingDecision` находится в `decision/models.py`; +- `market_intelligence/common/models.py` очищен от Trading-моделей; +- архитектурная граница восстановлена. + +**Статус:** ✅ Passed + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Development Strategy +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ ADR +- ✅ Build Design +- ✅ Implementation +- ✅ Compile Check +- ✅ Domain Review +- ✅ Code Review +- ✅ Documentation + +Статус Build: + +```text +Accepted +``` + +--- + +# Итог Build + +Build 017.7 восстановил правильную архитектурную границу: + +```text +Market Intelligence + ↓ +CoordinatorResult + ↓ +Decision Layer + ↓ +TradingDecision + ↓ +Execution +``` + +Теперь `market_intelligence` отвечает только за аналитику рынка, а `decision` отвечает за формирование торгового решения. + +--- + +# Следующий Build + +```text +Build 018 + +Execution Layer Architecture +``` \ No newline at end of file diff --git a/docs/market_intelligence/builds/build-017-trading-layer-architecture.md b/docs/market_intelligence/builds/build-017-trading-layer-architecture.md new file mode 100644 index 0000000..729f657 --- /dev/null +++ b/docs/market_intelligence/builds/build-017-trading-layer-architecture.md @@ -0,0 +1,325 @@ +# Build 017 — Trading Layer Architecture + +**Engineering Architecture Document** + +--- + +# Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Build | 017 | +| Название | Trading Layer Architecture | +| Статус | **Accepted** | +| Подсистема | Trading | +| Тип | Architecture | +| Версия | 1.0 | +| Язык | Русский | + +--- + +# Цель Build + +Утвердить архитектуру Trading Layer как самостоятельного слоя платформы Dzentra. + +Build не содержит реализации кода и фиксирует место Trading Layer в общей архитектуре системы. + +--- + +# Причина появления Trading Layer + +После завершения Coordinator Layer аналитическая часть платформы заканчивается формированием объекта: + +```text +CoordinatorResult +``` + +Дальнейшая задача платформы — принять торговое решение на основе уже готовой аналитики. + +Поэтому вводится отдельный слой Trading Layer. + +--- + +# Архитектурная роль + +Trading Layer располагается между аналитической подсистемой Market Intelligence и слоем исполнения сделок. + +Архитектурная цепочка выглядит следующим образом: + +```text +Market Data + │ + ▼ +Market Intelligence + │ + ▼ +CoordinatorResult + │ + ▼ +Trading Layer + │ + ▼ +TradingDecision + │ + ▼ +Execution Layer +``` + +Таким образом Trading Layer полностью отделяет аналитическую часть платформы от исполнения торговых операций. + +--- + +# Вход Trading Layer + +Единственным входом является: + +```text +CoordinatorResult +``` + +Trading Layer не обращается напрямую к: + +- Engine; +- Runtime; +- Coordinator internals; +- Market Data. + +Вся аналитическая информация поступает исключительно через публичную модель `CoordinatorResult`. + +--- + +# Выход Trading Layer + +Результатом работы Trading Layer станет: + +```text +TradingDecision +``` + +Модель `TradingDecision` будет разработана в последующих Build. + +На данном этапе фиксируется только её архитектурная роль. + +--- + +# Ответственность Trading Layer + +Trading Layer отвечает за: + +- интерпретацию аналитического состояния рынка; +- применение торговых правил; +- выбор торгового действия; +- формирование итогового `TradingDecision`. + +--- + +# Trading Layer не отвечает за + +Следующие задачи находятся вне ответственности Trading Layer: + +- анализ рыночных данных; +- запуск Engine; +- выполнение Runtime; +- внутреннюю работу Coordinator; +- исполнение ордеров; +- управление биржевым API; +- работу с Telegram; +- управление позициями; +- управление портфелем. + +--- + +# Архитектурная структура + +Trading Layer повторяет архитектурный шаблон Runtime Layer и Coordinator Layer. + +Предварительная структура: + +```text +trading/ + +├── common/ +│ +├── models.py +├── protocol.py +├── exceptions.py +├── validation.py +├── rules.py +└── service.py +``` + +--- + +# Ответственность компонентов + +## models.py + +Содержит модели Trading Layer. + +Например: + +- TradingDecision; +- диагностические модели; +- служебные структуры. + +--- + +## protocol.py + +Определяет официальный публичный контракт Trading Layer. + +Будущий интерфейс: + +```python +async def decide( + coordinator_result: CoordinatorResult, +) -> TradingDecision: + ... +``` + +--- + +## exceptions.py + +Содержит собственную иерархию исключений Trading Layer. + +Не использует исключения Coordinator Layer. + +--- + +## validation.py + +Проверяет корректность входного `CoordinatorResult`. + +Не содержит торговых правил. + +--- + +## rules.py + +Центральный компонент Trading Layer. + +Отвечает за: + +- интерпретацию аналитики; +- применение торговых правил; +- выбор итогового торгового решения; +- формирование `TradingDecision`. + +--- + +## service.py + +Единая публичная точка входа Trading Layer. + +Связывает: + +```text +Validation + │ + ▼ +Rules +``` + +и предоставляет единый API внешним слоям платформы. + +--- + +# Ограничения зависимостей + +Trading Layer не должен иметь прямых зависимостей от: + +- Engine Layer; +- Runtime internals; +- Coordinator internals; +- Exchange API; +- Telegram; +- базы данных; +- EventBus. + +Взаимодействие с аналитической подсистемой осуществляется исключительно через `CoordinatorResult`. + +--- + +# Architecture Review + +Проверка подтвердила: + +- Trading Layer является самостоятельным архитектурным слоем; +- аналитика полностью отделена от торговых решений; +- определены чёткие входные и выходные контракты; +- соблюдена изоляция между слоями платформы. + +**Статус:** ✅ Passed + +--- + +# Architecture Decision (ADR) + +Принято решение выделить принятие торговых решений в отдельный слой платформы. + +Trading Layer использует исключительно результат Coordinator и формирует независимый объект `TradingDecision`, который в дальнейшем станет входом для Execution Layer. + +**Статус:** Accepted + +--- + +# Последовательность Build 017.x + +План дальнейшего развития Trading Layer: + +```text +017 Trading Layer Architecture + +017.1 Trading Models +017.2 Trading Protocol +017.3 Trading Exceptions +017.4 Trading Validation +017.5 Trading Rules +017.6 Trading Service +``` + +--- + +# Acceptance + +Build завершён. + +Выполнены: + +- ✅ Development Strategy +- ✅ Architecture Design +- ✅ Architecture Review +- ✅ Architecture Decision (ADR) +- ✅ Build Design +- ✅ Documentation + +Build не содержит реализации программного кода, поэтому этапы Implementation и Compile Check не требуются. + +Статус Build: + +```text +Accepted +``` + +--- + +# Итог + +Build 017 открывает новый этап развития платформы Dzentra. + +Если Build 013–016 сформировали аналитическую подсистему **Market Intelligence**, то начиная с Build 017 начинается построение **Trading Layer** — подсистемы принятия торговых решений. + +Это завершает проектирование аналитической части платформы и создаёт фундамент для разработки профессиональной системы управления торговлей. + +--- + +# Следующий Build + +```text +Build 017.1 + +Trading Models + +trading/common/models.py +``` \ No newline at end of file diff --git a/docs/market_intelligence/common/README.md b/docs/market_intelligence/common/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/decisions/README.md b/docs/market_intelligence/decisions/README.md new file mode 100644 index 0000000..07e8773 --- /dev/null +++ b/docs/market_intelligence/decisions/README.md @@ -0,0 +1,130 @@ +# Architecture Decisions + +Каталог **decisions/** содержит архитектурные решения (ADR — Architecture Decision Record), принятые во время разработки подсистемы **Market Intelligence**. + +В отличие от технической документации, ADR отвечают не на вопрос: + +> **Что реализовано?** + +а на вопрос: + +> **Почему архитектура построена именно так?** + +Каждое решение принимается только после возникновения реальной инженерной необходимости. + +Создание ADR "на будущее" не допускается. + +--- + +# Иерархия архитектурных решений + +Архитектурные решения разделены на три уровня. + +```text +Философия разработки + ↓ +Правила разработки + ↓ +Архитектурные ограничения +``` + +Каждый следующий уровень основывается на предыдущем. + +--- + +# Level 1 — Философия разработки + +Данные решения определяют общий подход к развитию платформы. + +| Decision | Назначение | +|----------|------------| +| 001 | Architecture First | +| 002 | Build Lifecycle | + +--- + +## Основная идея + +Сначала проектируется архитектура. + +Затем архитектура развивается небольшими завершёнными Build. + +--- + +# Level 2 — Правила разработки + +Данные решения определяют инженерный процесс разработки. + +| Decision | Назначение | +|----------|------------| +| 003 | Domain Review | +| 004 | No Existing Code Assumptions | +| 005 | Human Readable Comments | +| 007 | Documentation Is Code | + +--- + +## Основная идея + +Каждый Build проходит обязательные проверки качества. + +Разработка ведётся только на основе существующего кода проекта. + +Комментарии объясняют архитектурный смысл. + +Документация развивается одновременно с кодом. + +--- + +# Level 3 — Архитектурные ограничения + +Данные решения определяют фундаментальные ограничения архитектуры платформы. + +| Decision | Назначение | +|----------|------------| +| 006 | Immutable Engine Contract | + +--- + +## Основная идея + +Все аналитические движки используют единый неизменяемый контракт результатов. + +--- + +# Правила создания новых Decision + +Новый ADR создаётся только при выполнении следующих условий: + +- возникла реальная инженерная проблема; +- принято архитектурное решение, влияющее на развитие платформы; +- решение невозможно корректно описать только комментариями в коде; +- решение будет полезно при дальнейшем сопровождении проекта. + +Создание ADR "на всякий случай" запрещается. + +--- + +# Жизненный цикл ADR + +Каждый Architecture Decision проходит одинаковый цикл. + +```text +Проблема + ↓ +Анализ вариантов + ↓ +Принятие решения + ↓ +Документирование + ↓ +Использование в проекте +``` + +--- + +# Главный принцип + +Architecture Decision Record являются частью архитектуры Dzentra. + +Они сохраняют инженерные знания проекта и позволяют развивать платформу последовательно даже спустя годы после принятия первоначальных решений. \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-001-architecture-first.md b/docs/market_intelligence/decisions/decision-001-architecture-first.md new file mode 100644 index 0000000..0ff8932 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-001-architecture-first.md @@ -0,0 +1,157 @@ +# Decision 001 — Architecture First + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время начала проектирования Stage-08.2 **Engine Runtime Contract**. + +--- + +# Контекст + +Проект Dzentra развивается как долгосрочная автономная торговая платформа. + +На ранних этапах развития стало очевидно, что традиционный подход: + +```text +Идея + ↓ +Написание кода + ↓ +Попытка встроить код в архитектуру +``` + +со временем приводит к накоплению технического долга, усложнению зависимостей и постепенной деградации структуры проекта. + +Для платформы, которая должна развиваться на протяжении многих лет, такой подход признан неприемлемым. + +--- + +# Проблема + +При разработке без предварительного архитектурного проектирования обычно возникают следующие последствия: + +- смешивание зон ответственности компонентов; +- появление временных решений, остающихся в проекте навсегда; +- дублирование логики; +- циклические зависимости; +- усложнение сопровождения; +- постоянный рефакторинг уже написанного кода. + +Каждое новое изменение становится дороже предыдущего. + +--- + +# Рассмотренные варианты + +## Вариант 1 + +Сначала писать код, затем при необходимости выполнять рефакторинг. + +### Преимущества + +- быстрый старт реализации; +- минимальные затраты на начальном этапе. + +### Недостатки + +- рост технического долга; +- потеря целостности архитектуры; +- постоянные изменения уже работающего кода; +- усложнение тестирования; +- снижение предсказуемости развития проекта. + +--- + +## Вариант 2 + +Сначала проектировать архитектуру, затем реализовывать код. + +### Преимущества + +- понятные зоны ответственности; +- отсутствие случайных зависимостей; +- возможность масштабирования; +- предсказуемое развитие платформы; +- уменьшение объёма последующего рефакторинга; +- единый стиль разработки. + +### Недостатки + +- увеличение времени подготовки перед реализацией; +- необходимость поддерживать архитектурную документацию в актуальном состоянии. + +--- + +# Принятое решение + +Перед началом реализации любого нового компонента обязательно выполняется архитектурное проектирование. + +Минимальный цикл разработки выглядит следующим образом: + +```text +Идея + ↓ +Architecture Design + ↓ +Architecture Review + ↓ +Implementation + ↓ +Compile Check + ↓ +Domain Review + ↓ +Documentation Update + ↓ +Build Closed +``` + +Переход к написанию кода допускается только после определения места компонента в общей архитектуре платформы. + +--- + +# Причины принятия решения + +Использование подхода **Architecture First** позволяет: + +- сохранять целостность архитектуры; +- уменьшать технический долг; +- заранее определять ответственность компонентов; +- предотвращать появление случайных зависимостей; +- упрощать сопровождение проекта; +- обеспечивать предсказуемое развитие платформы. + +--- + +# Последствия + +После принятия данного решения: + +- архитектура всегда проектируется раньше реализации; +- новый код не появляется без заранее определённой роли; +- каждый компонент имеет понятную область ответственности; +- архитектурные решения фиксируются документально; +- развитие платформы становится последовательным и контролируемым. + +--- + +# Связанные документы + +- `architecture_principles.md` +- `runtime_contract.md` +- `development_process.md` +- `build_history.md` + +--- + +# История изменений + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-002-build-lifecycle.md b/docs/market_intelligence/decisions/decision-002-build-lifecycle.md new file mode 100644 index 0000000..bd05605 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-002-build-lifecycle.md @@ -0,0 +1,209 @@ +# Decision 002 — Build Lifecycle + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время проектирования Stage-08.2 **Engine Runtime Contract**. + +--- + +# Контекст + +После принятия решения **Architecture First** возник вопрос: + +> Каким образом должна развиваться архитектура платформы? + +Были рассмотрены различные подходы к организации процесса разработки. + +Главной целью являлось обеспечение стабильного роста платформы без накопления незавершённых изменений и архитектурной деградации. + +--- + +# Проблема + +При длительной разработке больших систем часто возникают следующие проблемы: + +- одновременно изменяется большое количество компонентов; +- невозможно определить момент логического завершения работы; +- документация перестаёт соответствовать коду; +- проверки выполняются нерегулярно; +- ошибки обнаруживаются слишком поздно; +- архитектурные решения принимаются уже после написания кода. + +В результате становится сложно определить, какая часть системы действительно готова. + +--- + +# Рассмотренные варианты + +## Вариант 1 + +Разрабатывать большие функциональные блоки без промежуточных этапов. + +### Преимущества + +- меньше организационной работы; +- меньше промежуточной документации. + +### Недостатки + +- большие объёмы изменений; +- сложность проверки; +- высокий риск архитектурных ошибок; +- длительный цикл обратной связи; +- увеличение сложности сопровождения. + +--- + +## Вариант 2 + +Разбивать разработку на небольшие логически завершённые Build. + +Каждый Build представляет собой самостоятельный этап разработки. + +После завершения Build выполняются все проверки качества. + +### Преимущества + +- небольшие объёмы изменений; +- высокая предсказуемость; +- возможность проверки каждого этапа; +- документация развивается одновременно с кодом; +- упрощается поиск ошибок; +- архитектурные решения принимаются постепенно. + +### Недостатки + +- требуется сопровождать историю Build; +- увеличивается объём инженерной документации. + +--- + +# Принятое решение + +Разработка Dzentra ведётся исключительно последовательностью небольших логически завершённых Build. + +Каждый Build должен иметь: + +- понятную цель; +- ограниченную область изменений; +- завершённую реализацию; +- обязательную проверку качества; +- обновлённую документацию. + +Build считается завершённым только после прохождения полного жизненного цикла. + +--- + +# Жизненный цикл Build + +Каждый Build проходит следующие этапы: + +```text +Architecture Design + ↓ +Implementation + ↓ +Compile Check + ↓ +Architecture Review + ↓ +Domain Review + ↓ +Documentation Update + ↓ +User Confirmation + ↓ +Build Closed +``` + +До завершения всех этапов переход к следующему Build не допускается. + +--- + +# Причины принятия решения + +Использование небольших Build обеспечивает: + +- постепенное развитие архитектуры; +- раннее обнаружение ошибок; +- минимизацию технического долга; +- постоянную актуальность документации; +- простоту сопровождения; +- возможность безопасного возврата к предыдущим решениям. + +--- + +# Последствия + +После принятия настоящего решения: + +- каждая новая функциональность реализуется отдельным Build; +- Build имеет собственную документацию; +- Build фиксируется в общей истории разработки; +- архитектурные решения принимаются постепенно; +- документация обновляется одновременно с исходным кодом. + +--- + +# Правила Build + +Каждый Build должен удовлетворять следующим требованиям. + +## Логическая завершённость + +Build реализует одну законченную архитектурную задачу. + +--- + +## Независимость + +Build должен быть понятен без изучения будущих Build. + +--- + +## Проверяемость + +После завершения Build должны существовать все необходимые проверки. + +--- + +## Документирование + +Каждый Build сопровождается отдельным документом в каталоге: + +```text +docs/market_intelligence/builds/ +``` + +--- + +## История + +Каждый завершённый Build регистрируется в: + +```text +build_history.md +``` + +--- + +# Связанные документы + +- `development_process.md` +- `architecture_principles.md` +- `build_history.md` +- `runtime_contract.md` + +--- + +# История изменений + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-003-domain-review.md b/docs/market_intelligence/decisions/decision-003-domain-review.md new file mode 100644 index 0000000..b682858 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-003-domain-review.md @@ -0,0 +1,181 @@ +# Decision 003 — Domain Review + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время проектирования Stage-08.2 **Engine Runtime Contract**. + +--- + +# Контекст + +После внедрения обязательного **Architecture Review** стало очевидно, что архитектурной проверки недостаточно. + +Компонент может быть: + +- технически корректным; +- соответствовать архитектуре; +- успешно компилироваться; + +но при этом нарушать предметную область Market Intelligence. + +Например, аналитический движок может начать принимать торговые решения, рассчитывать размер позиции или управлять открытой сделкой. + +Подобные изменения не являются архитектурными ошибками, но полностью нарушают назначение аналитического уровня платформы. + +--- + +# Проблема + +Обычная архитектурная проверка отвечает на вопрос: + +> **Правильно ли построен компонент?** + +Однако она не отвечает на другой вопрос: + +> **Правильно ли компонент выполняет свою роль в предметной области?** + +Без отдельной проверки постепенно появляются следующие проблемы: + +- аналитика начинает принимать торговые решения; +- смешиваются обязанности разных Engine; +- появляются термины с разным смыслом; +- один и тот же объект начинает означать разные вещи; +- снижается объяснимость результатов анализа. + +Подобные изменения сложно обнаружить техническими средствами. + +--- + +# Рассмотренные варианты + +## Вариант 1 + +Использовать только Compile Check и Architecture Review. + +### Преимущества + +- проще процесс разработки; +- меньше этапов проверки. + +### Недостатки + +- отсутствует контроль предметной области; +- возможно постепенное смешивание аналитики и торговли; +- ошибки обнаруживаются слишком поздно. + +--- + +## Вариант 2 + +Ввести отдельный Domain Review. + +После проверки архитектуры дополнительно анализируется соответствие предметной области. + +### Преимущества + +- сохраняется чистота аналитического уровня; +- исключается смешивание ответственности; +- поддерживается единая терминология; +- повышается качество архитектурных решений; +- сохраняется объяснимость системы. + +### Недостатки + +- увеличивается время проверки Build; +- требуется дополнительная инженерная дисциплина. + +--- + +# Принятое решение + +Каждый логически завершённый Build проходит обязательный **Domain Review**. + +Domain Review выполняется после успешного прохождения: + +- Compile Check; +- Architecture Review. + +--- + +# Цель Domain Review + +Domain Review проверяет не качество кода, а корректность поведения компонента относительно предметной области. + +Главный вопрос проверки: + +> **Соответствует ли данный компонент своей архитектурной роли?** + +--- + +# Что проверяется + +Во время Domain Review анализируются: + +- корректность используемой терминологии; +- соответствие названий реальным рыночным процессам; +- отсутствие торговых решений внутри аналитических компонентов; +- отсутствие смешивания обязанностей разных Engine; +- понятность комментариев разработчику; +- соответствие Engine Runtime Contract; +- объяснимость результатов анализа; +- отсутствие скрытых смыслов и неоднозначных терминов. + +--- + +# Что не проверяется + +Domain Review не оценивает: + +- стиль оформления Python-кода; +- синтаксис; +- производительность реализации; +- качество алгоритмов; +- оптимизацию вычислений. + +Эти вопросы относятся к другим этапам разработки. + +--- + +# Причины принятия решения + +Использование Domain Review позволяет: + +- сохранить чистоту предметной области; +- избежать постепенного смешивания анализа рынка и торговли; +- поддерживать единый словарь терминов; +- обеспечить объяснимость результатов; +- сохранить независимость аналитических движков. + +--- + +# Последствия + +После принятия настоящего решения: + +- каждый Build проходит Domain Review; +- нарушение предметной области считается архитектурным дефектом; +- аналитические компоненты остаются независимыми от торговой логики; +- новые Engine обязаны соответствовать принятой терминологии. + +--- + +# Связанные документы + +- `architecture_principles.md` +- `development_process.md` +- `runtime_contract.md` +- `reviews/domain_reviews.md` + +--- + +# История изменений + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-004-no-existing-code-assumptions.md b/docs/market_intelligence/decisions/decision-004-no-existing-code-assumptions.md new file mode 100644 index 0000000..e9edc61 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-004-no-existing-code-assumptions.md @@ -0,0 +1,171 @@ +# Decision 004 — No Existing Code Assumptions + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время реализации первых Build подсистемы **Market Intelligence**. + +--- + +# Контекст + +Во время разработки новых компонентов неоднократно возникала ситуация, когда для продолжения работы требовалось знать текущее состояние проекта. + +Например: + +- существующие модели; +- типы; +- контракты; +- Runtime State; +- Engine Contract; +- общие структуры данных. + +Использование предположений о содержимом существующих файлов приводило бы к риску расхождения между проектируемым кодом и реальным состоянием проекта. + +Для долгоживущей платформы подобный подход признан неприемлемым. + +--- + +# Проблема + +Во время проектирования новых компонентов существует соблазн предположить, что существующий файл имеет ожидаемую структуру. + +Например: + +> «Наверное, эта модель уже существует.» + +или + +> «Скорее всего, поле называется именно так.» + +Подобные предположения могут привести к следующим последствиям: + +- дублирование уже существующих сущностей; +- несовместимые контракты; +- повторная реализация одинаковой логики; +- нарушение принципа единого источника истины; +- увеличение объёма последующего рефакторинга. + +--- + +# Рассмотренные варианты + +## Вариант 1 + +Использовать предположения о текущем состоянии проекта. + +### Преимущества + +- быстрее писать код; +- меньше дополнительных запросов. + +### Недостатки + +- высокий риск ошибок; +- потеря синхронизации с реальным проектом; +- появление дублирующих сущностей; +- снижение качества архитектуры. + +--- + +## Вариант 2 + +Использовать существующий код как единственный источник истины. + +Если для реализации требуется информация о существующем компоненте, соответствующий файл предварительно запрашивается у пользователя. + +После получения файла все архитектурные решения принимаются исключительно на основании его фактического содержимого. + +### Преимущества + +- отсутствие ложных предположений; +- единый источник истины; +- отсутствие дублирования; +- минимизация архитектурных ошибок; +- синхронное развитие проекта и документации. + +### Недостатки + +- требуется дополнительный шаг перед началом реализации. + +--- + +# Принятое решение + +При реализации новых компонентов запрещается делать предположения о содержимом существующих файлов. + +Если новый компонент зависит от уже существующей части проекта, соответствующий файл должен быть предварительно получен и использован как единственный источник истины. + +--- + +# Правило разработки + +Перед началом реализации необходимо определить, зависит ли новый компонент от существующего кода. + +Если зависимость существует, разработчик обязан запросить соответствующий файл до начала проектирования или написания кода. + +После получения файла запрещается создавать альтернативные реализации уже существующих сущностей. + +--- + +# Причины принятия решения + +Использование существующего кода как единственного источника истины позволяет: + +- исключить дублирование; +- избежать несовместимых контрактов; +- сохранить целостность архитектуры; +- уменьшить объём последующего рефакторинга; +- обеспечить единое понимание структуры проекта. + +--- + +# Последствия + +После принятия настоящего решения: + +- архитектурные решения принимаются только на основании реального состояния проекта; +- существующие модели повторно используются вместо повторной реализации; +- новые сущности создаются только при их фактическом отсутствии; +- вероятность архитектурных расхождений существенно снижается. + +--- + +# Практическое применение + +Перед созданием любого нового файла выполняется следующий вопрос: + +> **Требуется ли для его реализации существующий код проекта?** + +Если ответ положительный, необходимые файлы запрашиваются до начала работы. + +Данное правило распространяется на: + +- модели; +- контракты; +- состояния Runtime; +- общие типы; +- перечисления; +- вспомогательные функции; +- архитектурные ограничения. + +--- + +# Связанные документы + +- `architecture_principles.md` +- `development_process.md` +- `runtime_contract.md` + +--- + +# История изменений + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-005-human-readable-comments.md b/docs/market_intelligence/decisions/decision-005-human-readable-comments.md new file mode 100644 index 0000000..e566d22 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-005-human-readable-comments.md @@ -0,0 +1,198 @@ +# Decision 005 — Human Readable Comments + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время разработки подсистемы **Market Intelligence**. + +--- + +# Контекст + +Во время проектирования первых компонентов Market Intelligence стало очевидно, что большая часть сложности проекта связана не с алгоритмами, а с пониманием их назначения. + +Даже технически корректный код может быть труден для сопровождения, если разработчику приходится самостоятельно догадываться: + +- зачем существует компонент; +- какую задачу он решает; +- какие ограничения необходимо учитывать; +- почему архитектура построена именно таким образом. + +Стандартные комментарии, объясняющие синтаксис Python, практически не помогают решить эти задачи. + +--- + +# Проблема + +Большинство комментариев в программных проектах описывают очевидные действия языка программирования. + +Например: + +```python +# увеличиваем счётчик +counter += 1 +``` + +или + +```python +# проверяем условие +if value > limit: +``` + +Подобные комментарии быстро устаревают и не помогают понять архитектуру системы. + +Гораздо более ценными являются ответы на вопросы: + +- зачем существует данный объект; +- почему принято именно такое решение; +- какие ограничения существуют; +- что произойдёт при нарушении данного правила. + +--- + +# Рассмотренные варианты + +## Вариант 1 + +Использовать минимальное количество комментариев. + +### Преимущества + +- меньше текста; +- проще поддерживать. + +### Недостатки + +- ухудшается сопровождаемость; +- сложнее понимать архитектуру; +- новые разработчики дольше погружаются в проект. + +--- + +## Вариант 2 + +Комментировать синтаксис Python. + +### Преимущества + +- большое количество комментариев. + +### Недостатки + +- комментарии не несут архитектурной ценности; +- быстро устаревают; +- отвлекают от действительно важных пояснений. + +--- + +## Вариант 3 + +Комментарии объясняют назначение компонента и архитектурный смысл. + +### Преимущества + +- легче сопровождать проект; +- проще понимать архитектуру; +- быстрее находить причины существования компонентов; +- комментарии остаются актуальными значительно дольше. + +### Недостатки + +- требуется больше внимания при проектировании. + +--- + +# Принятое решение + +Комментарии в Dzentra должны объяснять **назначение**, **роль** и **ограничения** компонентов. + +Комментарии не должны пересказывать синтаксис языка Python. + +--- + +# Основные правила + +Комментарии должны отвечать хотя бы на один из следующих вопросов: + +- зачем существует данный компонент; +- какую задачу он решает; +- почему используется именно такое решение; +- какие ограничения необходимо учитывать; +- где проходит граница ответственности компонента. + +--- + +# Следует избегать + +Не рекомендуется писать комментарии, объясняющие очевидные конструкции языка. + +Например: + +```python +# складываем два числа +total = a + b +``` + +или + +```python +# возвращаем результат +return result +``` + +Такие комментарии не добавляют полезной информации. + +--- + +# Комментарии в Market Intelligence + +При разработке аналитических движков комментарии должны быть понятны разработчику, который не является профессиональным трейдером. + +Предпочтительно использовать простые технические формулировки. + +Если существует возможность заменить узкоспециализированный термин более понятным описанием без потери смысла, следует использовать более понятное описание. + +--- + +# Причины принятия решения + +Использование человекочитаемых комментариев позволяет: + +- уменьшить порог входа в проект; +- повысить сопровождаемость; +- сделать архитектуру более понятной; +- уменьшить зависимость от автора исходного кода; +- сохранить знания внутри проекта. + +--- + +# Последствия + +После принятия настоящего решения: + +- новые комментарии ориентируются на смысл, а не на синтаксис; +- архитектурные ограничения описываются непосредственно в коде; +- комментарии становятся частью инженерной документации проекта; +- разработчик может понять назначение большинства компонентов без обращения к внешним источникам. + +--- + +# Связанные документы + +- `architecture_principles.md` +- `development_process.md` +- `runtime_contract.md` + +--- + +# История изменений + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-006-immutable-engine-contract.md b/docs/market_intelligence/decisions/decision-006-immutable-engine-contract.md new file mode 100644 index 0000000..b700920 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-006-immutable-engine-contract.md @@ -0,0 +1,157 @@ +# Decision 006 — Immutable Engine Contract + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время реализации **Build №006 — `common/models.py`**. + +--- + +# Контекст + +Во время проектирования единого контракта аналитических движков возник вопрос: + +> **Могут ли модели результата изменяться после завершения расчёта?** + +Рассматривались два варианта. + +--- + +# Вариант 1 + +Изменяемые (`mutable`) модели. + +После создания объекта любой компонент платформы может изменить его поля. + +Например: + +```python +result.score = ... +result.reason = ... +result.direction = ... +``` + +### Преимущества + +- проще писать код; +- меньше ограничений. + +### Недостатки + +- невозможно гарантировать целостность результата; +- результат может измениться после публикации; +- журнал может содержать значения, отличающиеся от реально использованных; +- сложнее искать ошибки; +- возрастает риск скрытых побочных эффектов. + +--- + +# Вариант 2 + +Неизменяемые (`immutable`) модели. + +После создания объекта изменение его полей невозможно. + +При необходимости формируется новый объект результата. + +### Преимущества + +- результат всегда остаётся неизменным; +- журнал отражает фактическое состояние расчёта; +- безопасная передача между компонентами; +- отсутствуют скрытые изменения; +- упрощается диагностика; +- упрощается тестирование; +- повышается предсказуемость поведения платформы. + +### Недостатки + +- при изменении необходимо создавать новый экземпляр объекта. + +--- + +# Принятое решение + +Для всех моделей, описывающих результат работы аналитических движков, используется **неизменяемый контракт**. + +Все основные модели объявляются как: + +```python +@dataclass(frozen=True, slots=True) +``` + +--- + +# Область применения + +Правило распространяется на: + +- `EngineResult`; +- `EngineContext`; +- `EngineMetric`; +- `EngineDiagnostics`; +- `EngineEvaluationMeta`; +- `EngineDependencyResult`; + +а также на все будущие модели, описывающие результаты работы Engine. + +--- + +# Исключения + +Настоящее правило **не распространяется** на: + +- Runtime State; +- AutoTrade State; +- Position State; +- Execution Runtime; +- внутренние рабочие объекты расчёта. + +Эти объекты отражают изменяющееся состояние платформы и по своей природе являются изменяемыми. + +--- + +# Причины принятия решения + +Использование неизменяемых моделей обеспечивает: + +- целостность результатов анализа; +- безопасную передачу данных между Engine; +- корректное журналирование; +- предсказуемость поведения системы; +- отсутствие скрытых побочных эффектов; +- упрощение сопровождения проекта в долгосрочной перспективе. + +--- + +# Архитектурные последствия + +После принятия настоящего решения: + +- результаты Engine не изменяются после создания; +- каждый новый расчёт формирует новый объект результата; +- публикация событий всегда выполняется на основе завершённого результата; +- журнал содержит фактически опубликованные данные; +- Coordinator работает только с завершёнными результатами анализа. + +--- + +# Связанные документы + +- `runtime_contract.md` +- `architecture_principles.md` +- `development_process.md` +- `builds/build-006-common-models.md` + +--- + +# История + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-007-documentation-is-code.md b/docs/market_intelligence/decisions/decision-007-documentation-is-code.md new file mode 100644 index 0000000..c4dc7d8 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-007-documentation-is-code.md @@ -0,0 +1,179 @@ +# Decision 007 — Documentation Is Code + +## Статус + +**Accepted** + +--- + +# Дата принятия + +Принято во время разработки Stage-08.2 **Engine Runtime Contract**. + +--- + +# Контекст + +Во время разработки первых компонентов Market Intelligence стало очевидно, что исходный код и документация развиваются одновременно. + +Если документация обновляется позже кода, очень быстро возникает расхождение между: + +- архитектурой; +- реализацией; +- инженерными решениями; +- фактическим состоянием проекта. + +В результате документация перестаёт отражать реальное устройство системы. + +--- + +# Проблема + +Во многих проектах документация рассматривается как дополнительный материал. + +Обычно процесс выглядит следующим образом: + +```text +Написание кода + ↓ +Код работает + ↓ +Документацию обновим позже +``` + +На практике "позже" часто не наступает. + +Через некоторое время: + +- документация устаревает; +- архитектурные решения теряются; +- новые разработчики вынуждены изучать проект только по исходному коду. + +--- + +# Рассмотренные варианты + +## Вариант 1 + +Документация обновляется по мере возможности. + +### Преимущества + +- меньше работы во время реализации. + +### Недостатки + +- документация быстро устаревает; +- теряются архитектурные решения; +- сложно понять историю развития проекта. + +--- + +## Вариант 2 + +Документация обновляется одновременно с кодом. + +Build считается завершённым только после обновления всей связанной документации. + +### Преимущества + +- документация всегда соответствует проекту; +- архитектурные решения сохраняются; +- упрощается сопровождение; +- сохраняется история развития платформы; +- новые разработчики быстрее понимают проект. + +### Недостатки + +- требуется дополнительное время на оформление документации. + +--- + +# Принятое решение + +Документация является частью исходного кода проекта. + +Каждый Build считается завершённым только после обновления всей связанной документации. + +Документация имеет такую же обязательность, как Compile Check или Architecture Review. + +--- + +# Обязательная документация Build + +После завершения каждого Build обязательно обновляются все связанные документы. + +Например: + +- Build Document; +- Build History; +- Architecture Decisions (при необходимости); +- Runtime Contract (при необходимости); +- Development Process (при необходимости); +- README соответствующего раздела (при необходимости). + +--- + +# Правило завершения Build + +Build считается завершённым только после прохождения полного цикла: + +```text +Architecture Design + ↓ +Implementation + ↓ +Compile Check + ↓ +Architecture Review + ↓ +Domain Review + ↓ +Documentation Update + ↓ +User Confirmation + ↓ +Build Closed +``` + +Если документация не обновлена, Build остаётся незавершённым. + +--- + +# Причины принятия решения + +Использование данного подхода позволяет: + +- сохранять актуальность документации; +- фиксировать архитектурные решения; +- поддерживать единый источник знаний; +- облегчать сопровождение проекта; +- исключать расхождение между кодом и документацией. + +--- + +# Последствия + +После принятия настоящего решения: + +- документация развивается одновременно с кодом; +- архитектурные изменения всегда фиксируются; +- история развития проекта полностью сохраняется; +- Build не может считаться завершённым без обновления документации. + +--- + +# Связанные документы + +- `development_process.md` +- `build_history.md` +- `architecture_principles.md` +- `runtime_contract.md` + +--- + +# История изменений + +| Версия | Изменение | +|---------|-----------| +| 1.0 | Первое принятие архитектурного решения. | \ No newline at end of file diff --git a/docs/market_intelligence/decisions/decision-008-engine-metadata-separation.md b/docs/market_intelligence/decisions/decision-008-engine-metadata-separation.md new file mode 100644 index 0000000..2fde557 --- /dev/null +++ b/docs/market_intelligence/decisions/decision-008-engine-metadata-separation.md @@ -0,0 +1,257 @@ +# Architecture Decision Record 008 + +# Engine Metadata Separation + +Статус: **Accepted** + +Версия: **1.0** + +--- + +# Контекст + +Подсистема **Market Intelligence** строится как долгосрочная аналитическая платформа, состоящая из множества независимых специализированных Engine. + +Ожидается, что со временем количество Engine будет постепенно увеличиваться. + +Каждый Engine должен быть независимым компонентом, который можно: + +- зарегистрировать; +- проверить; +- документировать; +- подключить к Runtime; +- использовать Coordinator; +- заменить новой реализацией. + +До начала реализации Runtime было необходимо определить единый способ описания любого Engine. + +--- + +# Проблема + +Наивная реализация предполагает, что Runtime работает непосредственно с экземпляром Engine. + +Например: + +```python +engine.run(context) +``` + +При таком подходе Runtime ничего не знает о самом Engine до его создания. + +Это приводит к нескольким проблемам. + +Runtime не может заранее определить: + +- имя Engine; +- назначение Engine; +- зависимости Engine; +- поддерживаемые таймфреймы; +- минимальные требования к данным; +- совместимость версии; +- возможность регистрации Engine. + +В результате описание Engine смешивается с его логикой. + +По мере роста количества Engine подобная архитектура становится всё менее масштабируемой. + +--- + +# Решение + +Каждый Engine разделяется на две полностью независимые части. + +```text +Engine + +├── Metadata +└── Logic +``` + +Metadata описывает Engine. + +Logic реализует анализ рынка. + +Runtime работает с Metadata. + +Engine выполняет только аналитический алгоритм. + +--- + +# Engine Metadata + +Metadata представляет собой неизменяемое описание Engine. + +Metadata должна содержать всю информацию, необходимую инфраструктуре платформы. + +Например: + +- имя Engine; +- версия Engine; +- отображаемое имя; +- краткое описание; +- зависимости; +- поддерживаемые таймфреймы; +- минимальные требования к входным данным; +- дополнительные возможности Engine. + +Metadata не содержит вычисляемых значений. + +Metadata не зависит от состояния рынка. + +Metadata не изменяется во время выполнения Engine. + +--- + +# Engine Logic + +Logic содержит исключительно алгоритм анализа. + +Logic получает: + +```text +EngineContext +``` + +и возвращает + +```text +EngineResult +``` + +Logic не должна хранить архитектурную информацию о себе. + +Она не отвечает за: + +- регистрацию; +- описание возможностей; +- зависимости; +- документацию; +- совместимость. + +Эти сведения находятся исключительно в Metadata. + +--- + +# Immutable Metadata + +Metadata является полностью неизменяемой структурой. + +После создания Metadata запрещается изменять её содержимое. + +Runtime рассматривает Metadata как константу. + +Engine не имеет права изменять собственное описание во время выполнения. + +--- + +# Metadata Access + +Metadata должна быть доступна инфраструктуре платформы без создания экземпляра Engine. + +Runtime, Registry и Coordinator должны иметь возможность получить полное описание Engine до начала его выполнения. + +Предпочтительной реализацией является хранение Metadata как неизменяемого атрибута класса Engine. + +Например: + +```python +TrendEngine.metadata +``` + +или отдельной неизменяемой структуры: + +```python +TREND_ENGINE_METADATA +``` + +Создание экземпляра Engine исключительно для получения Metadata не допускается. + +Это позволяет Runtime: + +- регистрировать Engine; +- строить граф зависимостей; +- проверять совместимость; +- формировать документацию; +- анализировать архитектуру платформы; + +без запуска аналитических алгоритмов. + +--- + +# Причины принятия решения + +Разделение Metadata и Logic обеспечивает: + +- единый способ описания Engine; +- независимость инфраструктуры от реализации Engine; +- возможность автоматической регистрации Engine; +- возможность построения графа зависимостей; +- возможность автоматического формирования документации; +- возможность проверки совместимости; +- упрощение тестирования; +- упрощение сопровождения. + +--- + +# Последствия + +После принятия настоящего решения любой новый Engine обязан состоять из двух независимых частей: + +```text +Metadata + +↓ + +Logic +``` + +Создание Engine без Metadata считается нарушением архитектурного стандарта платформы. + +--- + +# Влияние на Runtime + +Runtime работает исключительно через Metadata. + +Runtime не должен получать архитектурную информацию путём анализа реализации Engine. + +Все инфраструктурные механизмы используют Metadata как единственный источник архитектурного описания Engine. + +--- + +# Влияние на Registry + +Engine Registry использует Metadata для: + +- регистрации Engine; +- проверки уникальности; +- поиска Engine; +- проверки зависимостей; +- формирования списка доступных Engine. + +Registry не анализирует реализацию Engine. + +--- + +# Влияние на Coordinator + +Coordinator использует Metadata для определения порядка выполнения Engine. + +Coordinator не должен знать внутреннее устройство конкретного Engine. + +--- + +# Совместимость + +Настоящее решение является обязательным для всех существующих и будущих Engine подсистемы **Market Intelligence**. + +Изменение данного правила допускается только посредством нового Architecture Decision Record. + +--- + +# Статус решения + +Настоящее решение принято как долгосрочный архитектурный стандарт платформы **Dzentra Market Intelligence**. + +Все последующие Runtime и Engine Build должны соответствовать данному ADR. \ No newline at end of file diff --git a/docs/market_intelligence/development/build_task_standard.md b/docs/market_intelligence/development/build_task_standard.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/development/how_to_work_with_chatgpt.md b/docs/market_intelligence/development/how_to_work_with_chatgpt.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/development_process.md b/docs/market_intelligence/development_process.md new file mode 100644 index 0000000..97b9def --- /dev/null +++ b/docs/market_intelligence/development_process.md @@ -0,0 +1,3060 @@ +# Dzentra Market Intelligence Development Process + +**Engineering Standard Release** + +--- + +## Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Документ | Dzentra Market Intelligence Development Process | +| Тип документа | Engineering Standard | +| Версия | 2.0 | +| Статус | **Release** | +| Подсистема | Market Intelligence | +| Проект | Dzentra | +| Владелец стандарта | Dzentra Architecture | +| Язык | Русский | +| Применяется к | Всем компонентам Market Intelligence | + +--- + +## Оглавление + +### Общие положения + +- Контроль документа +- Статус документа +- Управление стандартом +- Соответствие стандарту +- Нормативная терминология +- Официальная терминология +- Назначение +- Область применения +- Цель инженерного процесса +- Развитие стандарта +- Общие положения + +### Part I. Инженерная основа + +- Инженерная философия +- Главный принцип Dzentra +- Инженерные принципы + - Принцип 1. Architecture First + - Принцип 2. Stable Foundation + - Принцип 3. One Source of Truth + - Принцип 4. Documentation Is Code + - Принцип 5. Long-Term Maintainability + - Принцип 6. Отсутствие предположений о существующей реализации + - Принцип 7. Отсутствие преждевременных абстракций + - Принцип 8. Понятная архитектура + - Принцип 9. Архитектурная согласованность + - Принцип 10. Непрерывность развития +- Взаимосвязь инженерных принципов +- Непрерывность развития архитектуры + +### Part II. Процесс разработки + +- Общие положения +- Стратегия разработки +- Архитектурные уровни разработки +- Полный жизненный цикл Build +- Этап 1. Development Strategy +- Этап 2. Architecture Design +- Этап 3. Architecture Design Review +- Этап 4. Architecture Approval +- Этап 5. Implementation +- Этап 6. Engineering Reviews +- Этап 7. Documentation Update +- Этап 8. User Confirmation +- Этап 9. Git Commit +- Этап 10. Build Closed +- Definition of Done +- Непрерывность разработки + +### Part III. Инженерные требования + +- Общие инженерные требования +- Требования к архитектуре +- Требования к архитектурным слоям +- Требования к зависимостям +- Требования к компонентам +- Требования к Runtime +- Требования к именованию +- Требования к организации файлов +- Требования к публичным интерфейсам +- Требования к рефакторингу +- Требования к управлению техническим долгом +- Непрерывность архитектурного развития + +### Part IV. Инженерные проверки + +- Общие требования к инженерным проверкам +- Принципы инженерных проверок +- Архитектурные проверки +- Проверки предметной области +- Проверки реализации +- Проверки Runtime +- Проверки зависимостей +- Проверки документации +- Проверка готовности к выпуску +- Классификация инженерных замечаний +- Отчёт по инженерной проверке +- Завершение системы инженерных проверок +- Непрерывное совершенствование системы проверок + +### Part V. Управление инженерной документацией + +- Назначение инженерной документации +- Основные принципы управления документацией +- Иерархия инженерной документации +- Ответственность инженерной документации +- Жизненный цикл инженерной документации +- Обновление инженерной документации +- Ответственность за сопровождение документации +- Полнота инженерной документации +- Сохранение инженерных знаний +- Непрерывность сопровождения документации + +### Part VI. Совместная инженерная разработка + +- Назначение совместной разработки +- Основные принципы совместной разработки +- Ответственность разработчика +- Ответственность AI +- Процесс принятия инженерных решений +- Требования к инженерному взаимодействию +- Ограничения AI +- Непрерывность разработки +- Качество совместной разработки +- Непрерывность инженерного процесса +- Развитие модели совместной разработки + +### Part VII. Обеспечение качества + +- Назначение системы обеспечения качества +- Принципы обеспечения качества +- Инженерный контрольный перечень +- Непрерывное совершенствование инженерного процесса +- Аудит инженерного процесса +- Метрики инженерного процесса +- Соответствие настоящему стандарту +- Сопровождение стандарта +- Жизненный цикл стандарта +- Заключительные положения + +### Приложения + +- Приложение А. Глоссарий инженерных терминов +- Приложение Б. Справочник статусов Build +- Приложение В. Матрица Engineering Review +- Приложение Г. Рекомендуемая структура проекта +- Приложение Д. Шаблоны инженерной документации +- Приложение Е. Каталог архитектурных диаграмм +- Приложение Ж. Руководство по классификации Architecture Decision Records + +--- + +## Статус документа + +Настоящий документ имеет статус **Release**. + +Документ является официальным инженерным стандартом подсистемы **Market Intelligence** проекта **Dzentra**. + +Требования настоящего стандарта являются обязательными при проектировании архитектуры, разработке компонентов, проведении инженерных проверок, сопровождении документации и дальнейшем развитии платформы. + +Настоящий стандарт действует до момента официального выпуска новой версии. + +Изменение утверждённой версии документа без выпуска новой редакции не допускается. + +--- + +## Управление стандартом + +Настоящий стандарт устанавливает единый инженерный процесс разработки подсистемы **Market Intelligence**. + +Все архитектурные решения, изменения реализации, инженерные проверки и изменения документации должны соответствовать требованиям настоящего стандарта. + +Включение нового инженерного правила в настоящий стандарт допускается только при одновременном выполнении следующих условий: + +- правило обладает долгосрочной архитектурной ценностью; +- правило подтверждено практикой разработки; +- правило не противоречит действующим инженерным принципам; +- правило уменьшает архитектурную сложность платформы; +- правило повышает сопровождаемость, масштабируемость или согласованность архитектуры. + +Локальные договорённости отдельных Build не могут изменять требования настоящего стандарта. + +При возникновении противоречий между настоящим документом и локальными соглашениями приоритет имеет настоящий стандарт. + +--- + +## Соответствие стандарту + +Компонент, Build или инженерный процесс считаются соответствующими настоящему стандарту только при выполнении всех обязательных требований, установленных данным документом. + +Несоблюдение хотя бы одного обязательного требования означает несоответствие настоящему стандарту. + +Рекомендации, приведённые в настоящем документе, не влияют на факт соответствия стандарту, однако их соблюдение считается предпочтительной инженерной практикой. + +Оценка соответствия выполняется в рамках Engineering Review, определённых настоящим стандартом. + +--- + +## Нормативная терминология + +Во всём тексте настоящего документа используются следующие нормативные формулировки. + +| Формулировка | Значение | +|--------------|----------| +| **Должен** | Обязательное требование. Несоблюдение означает нарушение настоящего стандарта. | +| **Не должен** | Действие запрещено настоящим стандартом. | +| **Обязан** | Обязательное действие участника инженерного процесса. | +| **Не допускается** | Полный запрет соответствующего действия. | +| **Следует** | Рекомендуемая инженерная практика, допускающая обоснованные исключения. | +| **Рекомендуется** | Предпочтительный способ реализации или организации процесса. | +| **Может** | Допустимое действие, выполняемое по усмотрению разработчика при соблюдении остальных требований настоящего стандарта. | + +Если явно не указано иное, все перечисленные формулировки используются исключительно в приведённых выше значениях. + +--- + +## Назначение + +Настоящий документ устанавливает единый инженерный процесс разработки подсистемы **Market Intelligence** проекта **Dzentra**. + +Настоящий стандарт определяет обязательные требования к: + +- проектированию архитектуры; +- развитию платформы; +- организации Build; +- инженерным стандартам; +- инженерным проверкам; +- сопровождению документации; +- совместной работе разработчика и AI; +- долгосрочному развитию архитектуры. + +Настоящий документ является основным нормативным документом подсистемы **Market Intelligence**. + +Все связанные инженерные документы должны соответствовать требованиям настоящего стандарта. + +--- + +## Область применения + +Настоящий стандарт распространяется на все архитектурные компоненты подсистемы **Market Intelligence**, включая: + +- Common Layer; +- Runtime Layer; +- Runtime Contract; +- Engine Layer; +- Coordinator Layer; +- Engine Contract; +- аналитические Engine; +- общие архитектурные компоненты; +- инженерную документацию; +- Build Documentation; +- Architecture Decision Record (ADR); +- инженерные проверки; +- полный жизненный цикл Build. + +Требования настоящего стандарта обязательны для: + +- разработчика проекта; +- AI, участвующего в разработке; +- всех последующих Build; +- всех архитектурных компонентов платформы; +- всей инженерной документации Market Intelligence. + +--- + +## Официальная терминология + +В целях единообразного понимания настоящего стандарта используются следующие определения. + +| Термин | Определение | +|---------|-------------| +| **Стандарт** | Настоящий нормативный документ. | +| **Build** | Минимальная логически завершённая архитектурная единица развития платформы. | +| **Build Lifecycle** | Полная последовательность инженерных этапов, необходимых для завершения Build. | +| **Архитектура** | Совокупность компонентов, их ответственности, границ и взаимодействий внутри платформы. | +| **Архитектурный компонент** | Любая архитектурная сущность с чётко определённой областью ответственности. | +| **Layer** | Архитектурный уровень платформы. | +| **Runtime Contract** | Официальный контракт взаимодействия компонентов Runtime Layer. | +| **Engineering Review** | Формальная инженерная проверка, предусмотренная настоящим стандартом. | +| **ADR** | Architecture Decision Record — документ, фиксирующий долгосрочное архитектурное решение. | +| **Платформа** | Подсистема Market Intelligence как единая архитектурная система. | +| **Engineering Knowledge** | Совокупность долгосрочных инженерных знаний, сохраняемых в документации проекта. | + +ADR может оформляться как самостоятельный документ либо как обязательный раздел Build-документа. Для проекта Dzentra используется второй вариант. + +Все перечисленные термины используются в настоящем документе исключительно в приведённых выше значениях. + +--- + +## Цель инженерного процесса + +Целью настоящего стандарта является обеспечение возможности многолетнего развития платформы без накопления архитектурной сложности и технического долга. + +Каждый завершённый Build должен способствовать: + +- повышению архитектурной целостности; +- уменьшению сложности сопровождения; +- повышению масштабируемости; +- повторному использованию компонентов; +- сохранению инженерных знаний; +- предсказуемому развитию платформы. + +Основным критерием качества разработки является способность платформы становиться функционально богаче без увеличения архитектурной сложности. + +--- + +## Развитие стандарта + +Настоящий Engineering Standard развивается одновременно с архитектурой платформы. + +Любая новая редакция настоящего документа должна: + +- устранять неоднозначности; +- повышать инженерную согласованность; +- улучшать сопровождаемость стандарта; +- уменьшать сложность инженерного процесса; +- сохранять совместимость с ранее принятыми архитектурными решениями. + +Основной принцип развития настоящего стандарта формулируется следующим образом. + +> **Каждая новая редакция стандарта должна делать инженерный процесс проще, понятнее и согласованнее без уменьшения полноты инженерных требований.** + +## Общие положения + +Настоящий стандарт устанавливает единый инженерный процесс разработки подсистемы Market Intelligence проекта Dzentra. + +Все требования настоящего документа распространяются на полный жизненный цикл архитектурных компонентов — от проектирования до сопровождения. + +Настоящий стандарт регулирует: + +- разработку новых компонентов; +- изменение существующих компонентов; +- архитектурный рефакторинг; +- сопровождение инженерной документации; +- проведение инженерных проверок; +- принятие архитектурных решений. + +Настоящий стандарт не описывает детали реализации отдельных алгоритмов, моделей или торговой логики. + +Такие требования определяются специализированными документами платформы. + +Настоящий документ определяет исключительно инженерные правила разработки. + +## Part I. Инженерная основа + +Настоящая часть определяет фундаментальные инженерные принципы, на которых строится архитектура подсистемы **Market Intelligence**. + +Все требования, установленные последующими частями настоящего стандарта, являются развитием принципов, изложенных в данной части. + +Ни один инженерный процесс, архитектурное решение или реализация компонентов не должны противоречить настоящим принципам. + +--- + +### Инженерная философия + +Подсистема **Market Intelligence** рассматривается как долгосрочная архитектурная платформа. + +Разработка платформы выполняется не ради создания отдельных файлов или реализации отдельных функций. + +Основной целью разработки является последовательное развитие архитектуры, способной сохранять целостность, понятность и сопровождаемость на протяжении всего жизненного цикла проекта. + +Исходный код рассматривается как средство реализации архитектуры, а не как самостоятельная ценность. + +Архитектурные решения имеют более высокий приоритет, чем особенности конкретной реализации. + +При возникновении противоречия между архитектурной целостностью и существующей реализацией предпочтение должно отдаваться архитектуре. + +--- + +### Главный принцип Dzentra + +В основе инженерной методологии проекта лежит следующий принцип. + +> **Каждый новый архитектурный компонент должен уменьшать общую сложность платформы, даже если одновременно увеличивает её функциональные возможности.** + +Архитектурное решение считается успешным только при одновременном выполнении следующих условий: + +- расширяется функциональность платформы; +- уменьшается сложность сопровождения; +- сохраняется архитектурная целостность; +- повышается возможность дальнейшего развития. + +Если после внедрения нового компонента архитектура становится менее понятной, принятое решение должно быть пересмотрено. + +--- + +### Инженерные принципы + +Настоящий раздел определяет фундаментальные принципы, обязательные для всех последующих этапов разработки. + +Каждый принцип является самостоятельным нормативным требованием настоящего стандарта. + +--- + +#### Принцип 1. Architecture First + +Архитектура является главным инженерным активом проекта. + +Проектирование архитектуры должно предшествовать реализации. + +Ни один архитектурно значимый компонент не должен реализовываться без предварительного определения: + +- назначения; +- области ответственности; +- архитектурных границ; +- зависимостей; +- Runtime Contract; +- места компонента в общей архитектуре. + +Исходный код является следствием архитектурного проектирования. + +Архитектура не должна формироваться под влиянием уже существующей реализации. + +--- + +#### Принцип 2. Stable Foundation + +Развитие платформы должно выполняться последовательно снизу вверх. + +Каждый следующий архитектурный уровень может использовать только полностью завершённый предыдущий уровень. + +```text +Architecture + ↓ +Common Layer + ↓ +Runtime Contracts + ↓ +Runtime Layer + ↓ +Engine Layer + ↓ +Coordinator Layer + ↓ +Execution Integration + ↓ +Presentation Layer +``` + +Переход к следующему архитектурному уровню допускается только после полного завершения предыдущего. + +Архитектурные исключения не допускаются. + +--- + +#### Принцип 3. One Source of Truth + +Для каждой инженерной сущности должен существовать только один официальный источник истины. + +Перед созданием новой архитектурной сущности необходимо определить: + +- существует ли эквивалентный компонент; +- возможно ли повторное использование существующего решения; +- требуется ли расширение существующей реализации; +- действительно ли необходим новый компонент. + +Дублирование архитектурных компонентов не допускается. + +Дублирование инженерных знаний также не допускается. + +--- + +#### Принцип 4. Documentation Is Code + +Документация рассматривается как полноценная часть архитектуры платформы. + +Изменение архитектуры автоматически означает необходимость анализа и актуализации соответствующей документации. + +Build не может получить статус **Accepted**, пока обязательная документация не приведена в состояние, соответствующее текущей архитектуре. + +Документация сопровождается в соответствии с теми же инженерными требованиями, что и исходный код. + +--- + +#### Принцип 5. Long-Term Maintainability + +Все инженерные решения должны приниматься с учётом долгосрочного развития платформы. + +При выборе между несколькими допустимыми архитектурными решениями применяется следующий порядок приоритетов: + +1. архитектурная целостность; +2. простота сопровождения; +3. масштабируемость; +4. повторное использование компонентов; +5. минимизация технического долга; +6. простота реализации. + +Краткосрочное упрощение реализации не должно ухудшать долгосрочную архитектуру. + +#### Принцип 6. Отсутствие предположений о существующей реализации + +Во время проектирования новых компонентов запрещается делать предположения о текущем состоянии проекта. + +При необходимости использования существующей реализации сначала должно быть выполнено изучение фактического состояния соответствующих компонентов. + +Источником истины является исключительно текущее состояние проекта. + +Предположения не являются допустимой частью инженерного процесса. + +Если архитектурное решение зависит от существующего исходного кода, разработчик или AI обязаны использовать только подтверждённые сведения. + +--- + +#### Принцип 7. Отсутствие преждевременных абстракций + +Новые архитектурные сущности создаются только при наличии подтверждённой инженерной необходимости. + +Создание компонентов исключительно в расчёте на возможное будущее использование не допускается. + +Каждая новая архитектурная сущность должна иметь практическое применение в рамках текущего Build. + +Если необходимость новой сущности не подтверждена архитектурой текущего Build, её реализация должна быть отложена. + +Преждевременное усложнение архитектуры рассматривается как источник потенциального технического долга. + +--- + +#### Принцип 8. Понятная архитектура + +Архитектура платформы должна быть понятной без необходимости детального анализа исходного кода. + +Каждый архитектурный компонент должен иметь: + +- очевидное назначение; +- чётко определённую область ответственности; +- объяснимые зависимости; +- документированный публичный контракт. + +Архитектурное решение должно быть понятно разработчику, впервые знакомящемуся с платформой. + +Если понимание назначения компонента требует изучения значительного количества связанных файлов, архитектура должна быть пересмотрена. + +--- + +#### Принцип 9. Архитектурная согласованность + +Все архитектурные решения должны соответствовать единой инженерной модели платформы. + +Добавление нового компонента не должно изменять фундаментальные принципы архитектуры. + +При расширении платформы предпочтение отдаётся развитию существующей архитектуры посредством добавления новых компонентов, а не изменению уже сложившейся структуры. + +Архитектурная согласованность рассматривается как обязательное условие долгосрочного развития платформы. + +--- + +#### Принцип 10. Непрерывность развития + +Разработка платформы рассматривается как непрерывная последовательность завершённых Build. + +Каждый Build должен оставлять платформу в полностью работоспособном, архитектурно согласованном и документированном состоянии. + +Переход к следующему Build допускается только после завершения всех обязательных этапов текущего Build. + +Незавершённые архитектурные изменения не должны переноситься на последующие этапы разработки. + +--- + +### Взаимосвязь инженерных принципов + +Инженерные принципы, определённые настоящей частью, образуют единую систему. + +Ни один принцип не должен рассматриваться изолированно. + +При возникновении инженерных вопросов настоящий стандарт должен применяться комплексно с учётом всех принципов одновременно. + +При наличии нескольких допустимых вариантов реализации предпочтение должно отдаваться решению, которое в наибольшей степени соответствует совокупности инженерных принципов, определённых настоящим стандартом. + +Настоящие принципы являются фундаментом всех последующих требований настоящего документа. + +### Непрерывность развития архитектуры + +Архитектура платформы рассматривается как непрерывно развивающаяся система. + +Каждое инженерное решение должно приниматься с учётом уже существующей архитектуры и предполагаемого долгосрочного развития платформы. + +Добавление новых компонентов не должно нарушать фундаментальные архитектурные принципы, определённые настоящим стандартом. + +Развитие платформы должно происходить посредством последовательного расширения архитектуры, а не посредством её периодического перепроектирования. + +Архитектурная преемственность рассматривается как обязательное условие долгосрочной сопровождаемости платформы. + +Каждое новое архитектурное решение должно быть совместимо с ранее принятыми инженерными принципами либо сопровождаться официальным изменением настоящего стандарта. + +## Part II. Процесс разработки + +Настоящая часть определяет обязательный инженерный процесс разработки компонентов подсистемы **Market Intelligence**. + +Если Part I определяет фундаментальные принципы архитектуры, то настоящая часть устанавливает последовательность инженерных действий, обязательных для каждого Build. + +Все Build проходят одинаковый жизненный цикл независимо от сложности реализации, количества изменяемых файлов или характера архитектурной задачи. + +Ни один этап настоящего процесса не может быть пропущен без явного архитектурного обоснования. + +--- + +### Общие положения + +Единицей развития платформы является **Build**. + +Build представляет собой минимальную логически завершённую архитектурную единицу развития платформы. + +История развития проекта строится вокруг последовательности Build. + +Git фиксирует изменения файлов. + +Build фиксирует развитие архитектуры. + +Каждый Build должен иметь одну архитектурную цель. + +Границы Build определяются архитектурой, а не количеством изменяемых файлов. + +--- + +### Стратегия разработки + +Разработка платформы выполняется как последовательное развитие архитектуры. + +Каждый новый Build должен: + +- решать одну архитектурную задачу; +- иметь чётко определённые границы; +- улучшать архитектуру платформы; +- сохранять целостность существующей системы; +- оставлять платформу готовой к дальнейшему развитию. + +Объединение нескольких независимых архитектурных задач в одном Build не допускается. + +Если в процессе реализации становится очевидно, что Build включает несколько самостоятельных архитектурных изменений, Build должен быть разделён. + +--- + +### Архитектурные уровни разработки + +Разработка платформы выполняется последовательно снизу вверх. + +Каждый следующий уровень может использовать только полностью завершённый предыдущий уровень. + +```text +Architecture + ↓ +Common Layer + ↓ +Runtime Contracts + ↓ +Runtime Layer + ↓ +Engine Layer + ↓ +Coordinator Layer + ↓ +Execution Integration + ↓ +Presentation Layer +``` + +Нарушение указанной последовательности не допускается. + +--- + +### Полный жизненный цикл Build + +Каждый Build проходит одинаковую последовательность инженерных этапов. + +```text +Development Strategy + ↓ +Architecture Design + ↓ +Architecture Design Review + ↓ +Architecture Approval + ↓ +Implementation + ↓ +Engineering Reviews + ↓ +Documentation Update + ↓ +User Confirmation + ↓ +Git Commit + ↓ +Build Closed +``` + +Переход к следующему этапу допускается только после успешного завершения предыдущего. + +Build считается завершённым только после прохождения полного жизненного цикла. + +--- + +### Этап 1. Development Strategy + +Первым этапом каждого Build является определение архитектурной задачи. + +До начала проектирования должны быть определены: + +- цель Build; +- ожидаемый архитектурный результат; +- границы Build; +- критерии завершения; +- предполагаемое влияние на архитектуру платформы. + +На данном этапе не рассматриваются детали реализации. + +Основной задачей этапа является определение архитектурной цели Build. + +--- + +### Этап 2. Architecture Design + +После определения архитектурной задачи выполняется проектирование решения. + +До начала реализации должны быть определены: + +- архитектурная ответственность нового компонента; +- место компонента в общей архитектуре; +- взаимодействие с существующими компонентами; +- Runtime Contract; +- необходимые модели; +- события; +- зависимости; +- ограничения; +- возможные точки расширения. + +Проектирование начинается с архитектуры, а не с отдельных файлов. + +После определения ответственности формируется структура реализации. + +Например: + +```text +Validation Layer + ↓ +Runtime Contract + ↓ +Models + ↓ +Events + ↓ +Runtime + ↓ +Implementation +``` + +Таким образом исходный код всегда является следствием архитектурного проектирования. + +--- + +### Этап 3. Architecture Design Review + +После завершения проектирования выполняется обязательная архитектурная проверка. + +Architecture Design Review проводится до начала реализации. + +Цель проверки заключается в подтверждении корректности предлагаемого архитектурного решения. + +Architecture Design Review обязателен для: + +- новых Engine; +- Coordinator; +- Runtime; +- Runtime Contract; +- Engine Contract; +- архитектурных моделей; +- событий; +- общих компонентов; +- компонентов, определяющих взаимодействие между архитектурными слоями. + +Во время проверки оцениваются: + +- корректность архитектурной ответственности; +- масштабируемость; +- возможность повторного использования; +- архитектурная согласованность; +- долгосрочная сопровождаемость; +- совместимость с существующей архитектурой. + +При необходимости архитектурное решение корректируется до начала реализации. + +### Этап 4. Architecture Approval + +Реализация Build может начинаться только после завершения этапа **Architecture Design Review**. + +Архитектурное решение считается утверждённым, если одновременно выполнены следующие условия: + +- определена область ответственности компонента; +- определены архитектурные границы; +- определены зависимости; +- определён Runtime Contract (если применимо); +- отсутствуют архитектурные противоречия; +- подтверждена возможность дальнейшего масштабирования; +- подтверждено соответствие инженерным принципам настоящего стандарта. + +После утверждения архитектуры допускается переход к реализации. + +Изменение утверждённой архитектуры в процессе реализации допускается только после повторного архитектурного анализа и повторного Architecture Design Review. + +--- + +### Этап 5. Implementation + +Этап Implementation представляет собой реализацию ранее утверждённого архитектурного решения. + +Во время реализации запрещается: + +- изменять архитектурную ответственность компонентов; +- нарушать границы архитектурных слоёв; +- добавлять необоснованные зависимости; +- создавать временные инженерные решения; +- вводить преждевременные абстракции; +- изменять Runtime Contract без архитектурного анализа. + +Каждый компонент должен реализовывать исключительно собственную область ответственности. + +Любая дополнительная ответственность рассматривается как потенциальное нарушение архитектурной целостности платформы. + +Если в процессе реализации возникает необходимость изменения архитектуры, реализация должна быть приостановлена до проведения соответствующего архитектурного анализа. + +--- + +### Этап 6. Engineering Reviews + +После завершения реализации выполняется обязательный комплекс инженерных проверок. + +Engineering Reviews подтверждают соответствие результата требованиям настоящего стандарта. + +Минимальный обязательный перечень включает: + +- Architecture Review; +- Domain Review; +- Code Review; +- Documentation Review. + +При необходимости дополнительно выполняются: + +- Runtime Review; +- Dependency Review; +- Release Review. + +Каждый Review оценивает только собственную область ответственности. + +Результаты одного Review не заменяют проведение других обязательных проверок. + +При наличии критических замечаний реализация должна быть доработана до перехода к следующему этапу. + +--- + +### Этап 7. Documentation Update + +После успешного завершения инженерных проверок выполняется актуализация документации. + +Во время данного этапа определяется: + +- какие документы требуют изменения; +- необходимо ли создание новых документов; +- какие инженерные знания должны быть сохранены; +- требуется ли обновление Architecture Decision Record; +- требуется ли изменение Runtime Contract; +- требуется ли обновление настоящего стандарта. + +Документация обновляется одновременно с архитектурой. + +Отложенное обновление документации не допускается. + +--- + +### Этап 8. User Confirmation + +После завершения инженерных проверок и обновления документации Build представляется разработчику для утверждения. + +Разработчик подтверждает: + +- достижение архитектурной цели; +- завершённость реализации; +- соответствие инженерным требованиям; +- готовность Build к закрытию. + +До получения подтверждения Build считается открытым. + +--- + +### Этап 9. Git Commit + +После утверждения Build рекомендуется выполнить отдельный Git Commit. + +Каждый Commit должен соответствовать одному логически завершённому Build. + +Объединение нескольких независимых Build в одном Commit не рекомендуется. + +Разделение одного Build на несколько Commit допускается только при наличии инженерного обоснования. + +Git Commit рассматривается как фиксация завершённого этапа развития архитектуры. + +--- + +### Этап 10. Build Closed + +После выполнения всех требований настоящего стандарта Build получает статус **Accepted**. + +Закрытый Build должен удовлетворять следующим условиям: + +- архитектура утверждена; +- реализация завершена; +- обязательные Engineering Review успешно выполнены; +- документация актуализирована; +- инженерные знания сохранены; +- разработчик подтвердил завершение Build. + +Только после получения статуса **Accepted** допускается переход к следующему Build. + +Незавершённые архитектурные изменения не должны переноситься на последующие Build. + +--- + +### Definition of Done + +Build считается завершённым только при одновременном выполнении всех требований настоящего стандарта. + +Definition of Done является обязательным критерием завершения любого Build. + +Build не может получить статус **Accepted**, пока хотя бы одно обязательное требование остаётся невыполненным. + +Definition of Done включает следующие обязательные критерии: + +1. Architecture Completed. +2. Implementation Completed. +3. Engineering Reviews Passed. +4. Documentation Updated. +5. User Confirmation Received. +6. Git Commit Completed. +7. Build Closed. + +Подробные требования к каждому критерию определяются настоящим стандартом. + +--- + +#### Architecture Completed + +Архитектура считается завершённой, если: + +- определено назначение компонента; +- определена область ответственности; +- определены архитектурные границы; +- определены зависимости; +- определён Runtime Contract (если применимо); +- определены ограничения; +- определено место компонента в архитектуре платформы. + +--- + +#### Implementation Completed + +Реализация считается завершённой, если: + +- полностью соответствует утверждённой архитектуре; +- отсутствуют временные решения; +- отсутствуют незавершённые участки; +- отсутствуют известные нарушения настоящего стандарта; +- отсутствуют архитектурные компромиссы, перенесённые на будущие Build. + +--- + +#### Engineering Reviews Passed + +Все обязательные Engineering Review должны быть успешно завершены. + +Наличие критических замечаний исключает возможность получения Build статуса **Accepted**. + +--- + +#### Documentation Updated + +Вся инженерная документация должна соответствовать текущему состоянию архитектуры. + +Обновлению подлежат только документы, затронутые текущим Build. + +--- + +#### User Confirmation Received + +Разработчик подтвердил завершение Build и отсутствие незавершённых архитектурных задач. + +--- + +#### Git Commit Completed + +Изменения Build зафиксированы в системе контроля версий в соответствии с инженерной практикой проекта. + +--- + +#### Build Closed + +Все обязательные требования настоящего стандарта выполнены. + +Build получил статус **Accepted** и считается завершённым. + +### Непрерывность разработки + +Разработка платформы рассматривается как непрерывная последовательность полностью завершённых Build. + +Новый Build не должен начинаться до получения текущим Build статуса **Accepted**, за исключением случаев, когда иное явно предусмотрено утверждённым планом разработки. + +Каждый Build обязан оставлять платформу в состоянии, пригодном для дальнейшего развития. + +После завершения Build должны одновременно сохраняться: + +- архитектурная целостность платформы; +- работоспособность реализации; +- актуальность инженерной документации; +- полнота инженерных знаний; +- возможность безопасного начала следующего Build. + +Незавершённые архитектурные изменения не должны переноситься на последующие этапы разработки. + +История развития платформы рассматривается как непрерывная последовательность завершённых Build, каждый из которых представляет самостоятельный этап эволюции архитектуры. + +Именно последовательность завершённых Build образует официальную историю развития архитектуры платформы Dzentra. + +По этой причине каждый Build рассматривается как самостоятельная архитектурная единица, а не как совокупность изменений исходного кода. + +## Part III. Инженерные требования + +Настоящая часть устанавливает обязательные инженерные требования, которым должны соответствовать все архитектурные компоненты подсистемы **Market Intelligence**. + +Если Part II определяет процесс разработки, то настоящая часть определяет требования к результату разработки. + +Все архитектурные компоненты платформы должны соответствовать требованиям настоящего стандарта независимо от времени их создания, способа реализации или используемых технологий. + +Настоящие требования обязательны для: + +- разработки новых компонентов; +- модификации существующих компонентов; +- архитектурного рефакторинга; +- проектирования новых Engine; +- развития Runtime; +- разработки Coordinator; +- развития общей архитектуры платформы. + +Несоответствие настоящим требованиям рассматривается как нарушение инженерного стандарта. + +--- + +### Общие инженерные требования + +Все архитектурные компоненты платформы должны удовлетворять следующим обязательным требованиям. + +Каждый компонент обязан: + +- иметь единственную область ответственности; +- иметь определённые архитектурные границы; +- использовать только допустимые архитектурные зависимости; +- взаимодействовать через официальные архитектурные контракты; +- соответствовать инженерным принципам настоящего стандарта; +- быть пригодным для долгосрочного сопровождения. + +Архитектурные требования распространяются как на новые компоненты, так и на результаты рефакторинга существующих компонентов. + +При возникновении противоречий между особенностями реализации и требованиями настоящего стандарта приоритет имеют требования настоящего стандарта. + +--- + +### Требования к архитектуре + +Архитектура является главным инженерным активом платформы. + +Каждый новый архитектурный компонент должен повышать качество общей архитектуры. + +Во время проектирования любого компонента должны быть определены: + +- область ответственности; +- архитектурные границы; +- зависимости; +- точки расширения; +- ограничения; +- ожидаемое развитие компонента. + +Каждый компонент должен иметь одну основную архитектурную ответственность. + +Объединение нескольких независимых обязанностей в одном компоненте не допускается. + +Архитектура должна оставаться понятной без необходимости анализа реализации. + +--- + +#### Архитектурная ответственность + +Каждый архитектурный компонент отвечает только за одну область ответственности. + +Если компонент начинает выполнять несколько независимых функций, он подлежит архитектурному разделению. + +Архитектурная ответственность должна быть очевидной из: + +- назначения компонента; +- структуры каталогов; +- публичного интерфейса; +- инженерной документации. + +Архитектурная ответственность не должна зависеть от особенностей реализации. + +--- + +#### Архитектурная простота + +При наличии нескольких допустимых архитектурных решений предпочтение должно отдаваться наиболее простому. + +Под архитектурной простотой понимаются: + +- минимальное количество зависимостей; +- минимальное количество уровней взаимодействия; +- отсутствие скрытого поведения; +- отсутствие неявных контрактов; +- высокая читаемость архитектуры; +- объяснимость архитектурных решений. + +Простота является обязательным инженерным требованием. + +Искусственное усложнение архитектуры не допускается. + +--- + +#### Развитие архитектуры + +Архитектура должна поддерживать непрерывное развитие платформы. + +Добавление нового Engine, Runtime, Coordinator или другого архитектурного компонента не должно требовать изменения фундаментальной структуры платформы. + +Развитие архитектуры должно происходить посредством добавления новых компонентов, а не посредством изменения уже существующей архитектуры. + +Каждое архитектурное решение должно учитывать возможность дальнейшего расширения платформы без нарушения её архитектурной целостности. + +--- + +### Требования к архитектурным слоям + +Архитектура платформы строится как последовательность независимых архитектурных слоёв. + +Каждый слой имеет собственную область ответственности. + +Архитектурные границы между слоями являются обязательными. + +Взаимодействие между слоями допускается только посредством официально определённых архитектурных контрактов. + +Нарушение границ архитектурных слоёв рассматривается как нарушение настоящего стандарта. + +--- + +#### Иерархия слоёв + +Архитектура платформы строится в следующей последовательности. + +```text +Architecture + ↓ +Common Layer + ↓ +Runtime Contracts + ↓ +Runtime Layer + ↓ +Engine Layer + ↓ +Coordinator Layer + ↓ +Execution Integration + ↓ +Presentation Layer +``` + +Каждый слой может использовать только полностью завершённый предыдущий слой. + +Нарушение указанной последовательности не допускается. + +--- + +#### Независимость слоёв + +Каждый архитектурный слой должен быть максимально независимым. + +Слой не должен зависеть от внутренней реализации других слоёв. + +Взаимодействие между слоями осуществляется исключительно посредством официальных архитектурных контрактов. + +Наличие прямых зависимостей от внутренней реализации другого слоя не допускается. + +--- + +#### Изоляция слоёв + +Внутренняя реализация архитектурного слоя является закрытой. + +Другие слои не должны использовать внутренние структуры соседнего слоя. + +Любое взаимодействие должно осуществляться только через публичный контракт соответствующего слоя. + +Изоляция слоёв является обязательным условием сопровождаемости архитектуры. + +--- + +#### Стабильность слоёв + +Архитектурные слои обладают различной степенью изменяемости. + +Наиболее стабильными являются: + +- Common Layer; +- Runtime Contract; +- Runtime Models. + +Наиболее изменяемыми являются: + +- Engine; +- Coordinator; +- пользовательские сценарии. + +При проектировании архитектуры необходимо стремиться к минимизации изменений наиболее стабильных слоёв. + +### Требования к зависимостям + +Архитектурные зависимости являются частью архитектуры платформы. + +Каждая зависимость должна иметь инженерное обоснование и соответствовать архитектурным принципам настоящего стандарта. + +Добавление новой зависимости рассматривается как архитектурное изменение и должно оцениваться с точки зрения её влияния на сопровождаемость, масштабируемость и целостность платформы. + +Использование зависимости без подтверждённой инженерной необходимости не допускается. + +--- + +#### Направление зависимостей + +Все архитектурные зависимости должны быть однонаправленными. + +Направление зависимостей определяется архитектурной иерархией платформы. + +```text +Presentation Layer + ↓ +Coordinator Layer + ↓ +Engine Layer + ↓ +Runtime Layer + ↓ +Common Layer +``` + +Нижележащий слой не должен зависеть от вышележащего слоя. + +Нарушение установленного направления зависимостей рассматривается как нарушение архитектурной целостности платформы. + +--- + +#### Циклические зависимости + +Циклические зависимости запрещаются. + +При обнаружении циклической зависимости архитектура должна быть переработана до её полного устранения. + +Использование дополнительных абстракций исключительно для сокрытия циклической зависимости не допускается. + +Любая циклическая зависимость рассматривается как архитектурный дефект. + +--- + +#### Скрытые зависимости + +Все архитектурные зависимости должны быть явными. + +Компонент не должен иметь скрытых требований к внутренней реализации другого компонента. + +Поведение архитектурного компонента должно быть полностью объяснимо на основании его публичного интерфейса и официальных контрактов. + +Наличие скрытых зависимостей ухудшает сопровождаемость платформы и не соответствует требованиям настоящего стандарта. + +--- + +### Требования к компонентам + +Каждый компонент платформы рассматривается как самостоятельная архитектурная единица. + +Компонент обязан иметь: + +- единственную область ответственности; +- официальный публичный контракт; +- понятный жизненный цикл; +- определённые архитектурные границы; +- инженерную документацию, соответствующую его назначению. + +Компонент не должен выполнять функции, относящиеся к ответственности других архитектурных компонентов. + +--- + +#### Ответственность компонентов + +Каждый компонент должен реализовывать только одну инженерную задачу. + +Если компонент начинает выполнять несколько независимых функций, он подлежит архитектурному разделению. + +Дополнительная ответственность рассматривается как признак нарушения принципа единственной ответственности. + +--- + +#### Жизненный цикл компонентов + +Архитектурные компоненты должны иметь явно определённый жизненный цикл. + +При проектировании нового компонента должны быть определены: + +- владелец жизненного цикла; +- момент создания компонента; +- момент завершения жизненного цикла; +- допустимость повторного использования экземпляров; +- требования к сохранению внутреннего состояния. + +Если компонент создаётся Runtime, жизненный цикл экземпляра должен полностью определяться Runtime. + +Компонент не должен самостоятельно управлять собственным жизненным циклом. + +Если архитектурная модель предполагает создание нового экземпляра при каждом выполнении, повторное использование экземпляров не допускается. + +Правила жизненного цикла компонента должны быть отражены в соответствующем архитектурном контракте. + +--- + +#### Контракты компонентов + +Взаимодействие компонентов допускается только через официальные архитектурные контракты. + +Использование внутренних структур другого компонента запрещается. + +Изменение публичного контракта рассматривается как архитектурное изменение и требует оценки его влияния на существующую архитектуру. + +--- + +#### Повторное использование компонентов + +Перед созданием нового компонента должна быть выполнена оценка возможности повторного использования существующего решения. + +При наличии архитектурно эквивалентного компонента предпочтение должно отдаваться его расширению или повторному использованию. + +Создание новой реализации при наличии подходящего существующего компонента требует инженерного обоснования. + +--- + +### Требования к Runtime + +Runtime представляет собой самостоятельную архитектурную подсистему платформы. + +Runtime отвечает исключительно за описание состояния системы и официальные контракты взаимодействия. + +Runtime не должен содержать аналитическую, торговую или координирующую логику. + +Ответственность Runtime ограничивается представлением состояния платформы и поддержкой взаимодействия между архитектурными компонентами. + +--- + +#### Ответственность Runtime + +Runtime должен определять: + +- модели состояния; +- Runtime Contract; +- Runtime Events; +- жизненный цикл Runtime; +- правила представления состояния. + +Runtime не должен принимать инженерные, аналитические или торговые решения. + +Любая подобная логика должна располагаться за пределами Runtime Layer. + +--- + +#### Runtime Contract + +Runtime Contract является официальным архитектурным контрактом платформы. + +Любое изменение Runtime Contract требует: + +- проведения Architecture Design Review; +- анализа совместимости; +- оценки влияния на существующие Engine; +- обновления инженерной документации. + +Изменение Runtime Contract без архитектурного анализа не допускается. + +--- + +#### Стабильность Runtime + +Runtime относится к наиболее стабильным архитектурным компонентам платформы. + +Изменение Runtime допускается только при наличии подтверждённой архитектурной необходимости. + +При проектировании новых компонентов предпочтение должно отдаваться решениям, не требующим изменения существующего Runtime. + +Минимизация изменений Runtime рассматривается как один из факторов долгосрочной стабильности архитектуры. + +### Требования к именованию + +Именование архитектурных компонентов является частью архитектуры платформы. + +Название каждого компонента должно однозначно отражать его область ответственности и назначение. + +Использование неоднозначных, временных или вводящих в заблуждение названий не допускается. + +Единообразное именование облегчает понимание архитектуры, снижает вероятность ошибок и способствует долгосрочной сопровождаемости платформы. + +--- + +#### Наименование компонентов + +Название архитектурного компонента должно: + +- отражать его основную ответственность; +- быть однозначным; +- быть кратким; +- соответствовать принятой терминологии платформы. + +Название не должно описывать детали реализации. + +--- + +#### Наименование файлов + +Имя файла должно отражать назначение содержащегося в нём компонента. + +Не допускается использование: + +- временных названий; +- неопределённых сокращений; +- имён, не отражающих архитектурную ответственность. + +Структура имён файлов должна быть единообразной во всей платформе. + +--- + +#### Единообразие терминологии + +Каждый инженерный термин должен иметь только одно официальное определение. + +Использование различных названий для одной и той же архитектурной сущности не допускается. + +Общие инженерные термины фиксируются в Glossary и используются во всей документации в неизменном виде. + +--- + +### Требования к организации файлов + +Организация исходного кода должна отражать архитектуру платформы. + +Структура каталогов и файлов должна облегчать понимание системы и не должна зависеть от особенностей реализации. + +--- + +#### Структура пакетов + +Каждый каталог представляет самостоятельную архитектурную область. + +Организация каталогов должна определяться архитектурной ответственностью компонентов, а не типом размещаемых файлов. + +Структура проекта должна быть последовательной и единообразной. + +--- + +#### Размер компонентов + +Каждый файл должен содержать только логически связанную функциональность. + +Если понимание содержимого файла становится затруднительным, должна быть выполнена оценка необходимости его разделения. + +Разделение выполняется по архитектурной ответственности, а не по количеству строк исходного кода. + +--- + +#### Независимость модулей + +Модули должны быть максимально независимыми друг от друга. + +Изменение одного модуля не должно приводить к необходимости массового изменения соседних модулей. + +Минимизация связанности рассматривается как обязательное архитектурное требование. + +--- + +### Требования к публичным интерфейсам + +Публичный интерфейс является официальным архитектурным контрактом компонента. + +Проектирование публичного интерфейса должно выполняться с той же тщательностью, что и проектирование архитектуры самого компонента. + +--- + +#### Публичный интерфейс + +Публичный интерфейс должен быть: + +- минимальным; +- стабильным; +- понятным; +- документированным; +- достаточным для выполнения задач компонента. + +Добавление новых публичных элементов должно иметь архитектурное обоснование. + +--- + +#### Внутренняя реализация + +Внутренняя реализация не является частью публичного контракта. + +Изменение внутренней реализации допускается при условии сохранения поведения публичного интерфейса. + +Другие компоненты не должны зависеть от внутренних особенностей реализации. + +--- + +### Требования к рефакторингу + +Рефакторинг является обязательной частью жизненного цикла платформы. + +Цель рефакторинга заключается в улучшении архитектуры без изменения внешнего поведения системы. + +Рефакторинг должен повышать читаемость, сопровождаемость и архитектурную согласованность платформы. + +--- + +#### Общие требования + +Во время рефакторинга не допускается: + +- изменение внешнего поведения компонентов; +- нарушение архитектурных границ; +- объединение независимых областей ответственности; +- создание временных инженерных решений. + +Каждый рефакторинг должен улучшать архитектуру платформы. + +--- + +#### Инкрементальный рефакторинг + +Рефакторинг должен выполняться небольшими логически завершёнными этапами. + +Каждый этап рекомендуется оформлять как самостоятельный Build. + +По завершении каждого этапа платформа должна оставаться работоспособной, документированной и соответствующей настоящему стандарту. + +--- + +### Требования к управлению техническим долгом + +Технический долг рассматривается как исключительная ситуация. + +Накопление технического долга не является допустимой стратегией развития платформы. + +Каждое временное инженерное решение должно иметь документированное обоснование и план устранения. + +--- + +#### Допустимый технический долг + +Временное инженерное решение допускается только при одновременном выполнении следующих условий: + +- существует объективная причина его применения; +- ограничение документировано; +- определён способ устранения; +- решение не нарушает архитектурные принципы настоящего стандарта. + +Допустимый технический долг должен устраняться при первой практической возможности. + +--- + +#### Архитектурный долг + +Архитектурный долг не допускается. + +Если архитектурное решение ухудшает сопровождаемость, масштабируемость или целостность платформы, оно должно быть пересмотрено до завершения Build. + +Архитектурные компромиссы не должны переноситься на последующие Build. + +--- + +### Непрерывность архитектурного развития + +Архитектура платформы рассматривается как непрерывно развивающаяся система. + +Каждое новое архитектурное решение должно быть совместимо с фундаментальными инженерными принципами настоящего стандарта. + +Развитие платформы должно происходить посредством последовательного расширения архитектуры, а не посредством её периодического перепроектирования. + +Добавление новых компонентов не должно нарушать существующую архитектурную целостность платформы. + +Каждый завершённый Build должен делать архитектуру: + +- более понятной; +- более согласованной; +- более масштабируемой; +- более пригодной для долгосрочного сопровождения. + +Непрерывное архитектурное развитие рассматривается как обязательное условие инженерной зрелости платформы. + +## Part IV. Инженерные проверки + +Настоящая часть устанавливает единую систему инженерных проверок, применяемую ко всем Build подсистемы **Market Intelligence**. + +Инженерные проверки являются обязательной частью жизненного цикла каждого Build. + +Их целью является подтверждение того, что результаты разработки соответствуют требованиям настоящего стандарта. + +Инженерные проверки оценивают архитектуру, реализацию, инженерную документацию и соблюдение установленного процесса разработки. + +Build не может получить статус **Accepted** до успешного завершения всех обязательных проверок. + +--- + +### Общие требования к инженерным проверкам + +Каждый Build обязан пройти полный перечень инженерных проверок, предусмотренных настоящим стандартом. + +Каждая проверка представляет собой самостоятельную инженерную процедуру и имеет собственную область ответственности. + +Результаты одной проверки не заменяют проведение других обязательных проверок. + +Все инженерные проверки выполняются независимо от объёма реализации, количества изменённых файлов или сложности архитектурной задачи. + +--- + +### Принципы инженерных проверок + +Система Engineering Review строится на следующих фундаментальных принципах. + +--- + +#### Независимость + +Каждая инженерная проверка оценивает только собственную область ответственности. + +Архитектурные вопросы рассматриваются исключительно в рамках Architecture Review. + +Предметная область оценивается исключительно в рамках Domain Review. + +Качество реализации оценивается исключительно в рамках Code Review. + +Документация оценивается исключительно в рамках Documentation Review. + +Смешивание различных видов инженерной оценки не допускается. + +--- + +#### Объективность + +Все выводы инженерных проверок должны основываться исключительно на требованиях настоящего стандарта. + +Личные предпочтения участников разработки не являются основанием для замечаний. + +Каждое замечание должно иметь инженерное обоснование. + +--- + +#### Прослеживаемость + +Каждое замечание должно быть: + +- воспроизводимым; +- проверяемым; +- связанным с конкретным требованием настоящего стандарта; +- сопровождаемым инженерным обоснованием. + +Замечания, не имеющие связи с требованиями настоящего стандарта, не рассматриваются как официальные результаты Engineering Review. + +--- + +#### Направленность на улучшение + +Основной целью Engineering Review является повышение качества платформы. + +Инженерные проверки не рассматриваются как формальная процедура контроля. + +Каждая проверка должна способствовать: + +- улучшению архитектуры; +- повышению сопровождаемости; +- уменьшению технического долга; +- сохранению инженерной согласованности платформы. + +--- + +### Архитектурные проверки + +Архитектурные проверки подтверждают соответствие архитектуры требованиям настоящего стандарта. + +Архитектурные проверки проводятся как до начала реализации, так и после её завершения. + +Система архитектурных проверок включает: + +- Architecture Design Review; +- Architecture Review. + +Каждая из указанных проверок имеет самостоятельную область ответственности. + +--- + +#### Architecture Design Review + +Architecture Design Review выполняется до начала реализации. + +Цель проверки заключается в подтверждении корректности предлагаемого архитектурного решения. + +Architecture Design Review обязателен для: + +- новых Engine; +- Coordinator; +- Runtime; +- Runtime Contract; +- Engine Contract; +- архитектурных моделей; +- событий; +- компонентов, определяющих взаимодействие архитектурных слоёв. + +Во время проверки оцениваются: + +- архитектурная ответственность; +- архитектурные границы; +- масштабируемость; +- возможность повторного использования; +- долгосрочная сопровождаемость; +- соответствие инженерным принципам настоящего стандарта. + +При отрицательном результате реализация Build не допускается. + +--- + +#### Architecture Review + +Architecture Review проводится после завершения реализации. + +Цель проверки заключается в подтверждении соответствия реализованной архитектуры требованиям настоящего стандарта. + +Во время проверки оцениваются: + +- соблюдение архитектурных требований; +- соблюдение требований к слоям; +- соблюдение требований к зависимостям; +- соблюдение требований к Runtime; +- соблюдение требований к компонентам; +- соответствие утверждённой архитектуре. + +Architecture Review оценивает исключительно архитектуру платформы и не рассматривает вопросы предметной области или качества реализации. + +### Проверки предметной области + +Проверки предметной области подтверждают соответствие реализованных компонентов требованиям предметной области **Market Intelligence**. + +Данные проверки не оценивают качество архитектуры или реализации. + +Их задачей является подтверждение корректности моделирования предметной области. + +--- + +#### Domain Review + +Domain Review проводится после завершения реализации Build. + +Цель проверки заключается в подтверждении того, что реализованные компоненты корректно отражают предметную область платформы. + +Во время проверки оцениваются: + +- корректность терминологии; +- соответствие моделей предметной области; +- отсутствие смешивания аналитической и торговой логики; +- объяснимость поведения компонентов; +- согласованность используемых понятий; +- соответствие принятой терминологии платформы. + +Domain Review не рассматривает вопросы архитектуры или качества реализации. + +Архитектурные замечания рассматриваются исключительно в рамках Architecture Review. + +--- + +### Проверки реализации + +Проверки реализации подтверждают качество инженерной реализации компонентов платформы. + +Основной задачей данных проверок является обеспечение простоты сопровождения, читаемости и соответствия инженерным требованиям настоящего стандарта. + +--- + +#### Code Review + +Code Review проводится после завершения реализации Build. + +Цель проверки заключается в подтверждении качества инженерной реализации. + +Во время проверки оцениваются: + +- читаемость исходного кода; +- простота реализации; +- отсутствие избыточной сложности; +- отсутствие дублирования; +- отсутствие временных инженерных решений; +- соблюдение требований к именованию; +- соблюдение требований к организации файлов; +- возможность повторного использования существующих компонентов. + +Code Review оценивает исключительно качество реализации. + +Архитектурные вопросы рассматриваются только в рамках Architecture Review. + +--- + +#### Общие требования к реализации + +При выполнении Code Review подтверждается соблюдение следующих требований: + +- каждая сущность имеет единственную ответственность; +- отсутствует необоснованное дублирование; +- отсутствуют скрытые зависимости; +- отсутствуют преждевременные абстракции; +- реализация соответствует утверждённой архитектуре. + +При выявлении нарушений формируются инженерные замечания в соответствии с настоящим стандартом. + +--- + +### Проверки Runtime + +Проверки Runtime применяются исключительно к компонентам Runtime Layer. + +Они подтверждают корректность реализации архитектурных контрактов Runtime и совместимость компонентов между собой. + +--- + +#### Runtime Review + +Runtime Review проводится при изменении компонентов Runtime. + +Во время проверки оцениваются: + +- корректность Runtime Contract; +- стабильность Runtime Models; +- корректность Runtime Events; +- совместимость Runtime; +- жизненный цикл Runtime; +- влияние изменений на существующие Engine. + +Runtime Review не проводится для компонентов, не относящихся к Runtime Layer. + +--- + +### Проверки зависимостей + +Проверки зависимостей подтверждают соответствие архитектурных зависимостей требованиям настоящего стандарта. + +Каждая новая зависимость рассматривается как самостоятельное архитектурное изменение. + +--- + +#### Dependency Review + +Dependency Review проводится при появлении новых архитектурных зависимостей либо при изменении существующих. + +Во время проверки оцениваются: + +- направление зависимостей; +- отсутствие циклических зависимостей; +- отсутствие скрытых зависимостей; +- необходимость каждой новой зависимости; +- соответствие требованиям к архитектурным слоям. + +При обнаружении нарушений архитектура должна быть пересмотрена до завершения Build. + +### Проверки документации + +Проверки документации подтверждают соответствие инженерной документации текущему состоянию платформы. + +Документация рассматривается как часть архитектуры и подлежит обязательной проверке после завершения каждого Build. + +--- + +#### Documentation Review + +Documentation Review проводится после завершения реализации и выполнения остальных инженерных проверок. + +Цель проверки заключается в подтверждении того, что инженерная документация полностью соответствует реализованным архитектурным изменениям. + +Во время проверки оцениваются: + +- полнота документации; +- актуальность документов; +- отсутствие противоречий; +- соответствие документации текущему состоянию платформы; +- необходимость создания новых документов; +- необходимость обновления существующих документов. + +Documentation Review является обязательной частью жизненного цикла каждого Build. + +Build не может получить статус **Accepted** до успешного завершения Documentation Review. + +--- + +#### Полнота документации + +Документация считается полной, если одновременно выполняются следующие условия: + +- описаны все архитектурные изменения; +- обновлены все обязательные документы; +- отсутствуют внутренние противоречия; +- документация соответствует текущему состоянию платформы; +- долгосрочные инженерные решения зафиксированы в соответствующих документах. + +Неполная документация рассматривается как незавершённый Build. + +--- + +### Проверка готовности к выпуску + +После успешного завершения всех обязательных инженерных проверок выполняется итоговая проверка готовности Build к выпуску. + +--- + +#### Release Review + +Release Review является заключительной инженерной проверкой Build. + +Цель проверки заключается в подтверждении завершённости инженерного процесса. + +Во время Release Review подтверждается: + +- выполнение Definition of Done; +- успешное завершение обязательных инженерных проверок; +- отсутствие открытых критических замечаний; +- актуальность инженерной документации; +- готовность Build к интеграции в основную ветвь разработки. + +Release Review не заменяет другие виды инженерных проверок. + +Он подтверждает исключительно завершённость процесса разработки. + +--- + +### Классификация инженерных замечаний + +Все замечания, сформированные в ходе Engineering Review, классифицируются по степени их влияния на качество платформы. + +Единая классификация обеспечивает одинаковую интерпретацию результатов инженерных проверок независимо от вида Review. + +--- + +#### Critical + +Критическое замечание означает нарушение обязательных требований настоящего стандарта. + +При наличии хотя бы одного критического замечания Build не может получить статус **Accepted**. + +--- + +#### Major + +Существенное замечание указывает на нарушение инженерных требований, которое должно быть устранено до завершения Build. + +--- + +#### Minor + +Незначительное замечание относится к вопросам читаемости, единообразия оформления или сопровождаемости. + +Такие замечания рекомендуется устранить, однако они не препятствуют завершению Build. + +--- + +#### Recommendation + +Рекомендация не является замечанием. + +Она содержит предложение по дальнейшему улучшению архитектуры, реализации или документации и может быть учтена в одном из последующих Build. + +--- + +### Отчёт по инженерной проверке + +Результатом каждой инженерной проверки является официальный инженерный отчёт. + +Engineering Review Report должен содержать: + +- вид проверки; +- область проверки; +- перечень выявленных замечаний; +- рекомендации; +- итоговое решение. + +Отчёт становится частью инженерной истории соответствующего Build и обеспечивает прослеживаемость принятых решений. + +--- + +### Завершение системы инженерных проверок + +Все обязательные инженерные проверки должны быть успешно завершены до закрытия Build. + +Общая последовательность выполнения проверок выглядит следующим образом. + +```text +Architecture Design Review + ↓ +Architecture Review + ↓ +Domain Review + ↓ +Code Review + ↓ +Runtime Review (при необходимости) + ↓ +Dependency Review (при необходимости) + ↓ +Documentation Review + ↓ +Release Review +``` + +Каждая проверка подтверждает только собственную область ответственности. + +Только после успешного завершения всех обязательных инженерных проверок Build может получить статус **Accepted**. + +--- + +### Непрерывное совершенствование системы проверок + +Система Engineering Review рассматривается как развивающаяся часть инженерного процесса. + +При появлении новых архитектурных требований допускается расширение перечня инженерных проверок при условии соблюдения следующих принципов: + +- каждая новая проверка должна иметь самостоятельную область ответственности; +- новая проверка не должна дублировать существующие проверки; +- необходимость новой проверки должна быть подтверждена практикой разработки; +- изменения должны быть отражены в настоящем стандарте. + +Развитие системы инженерных проверок должно повышать качество платформы без увеличения сложности инженерного процесса. + +## Part V. Управление инженерной документацией + +Настоящая часть устанавливает единые требования к управлению инженерной документацией подсистемы **Market Intelligence**. + +Инженерная документация рассматривается как неотъемлемая часть архитектуры платформы и развивается одновременно с исходным кодом. + +Каждый Build обязан сопровождаться актуализацией инженерной документации в объёме, соответствующем выполненным архитектурным изменениям. + +Изменение архитектуры без соответствующего изменения документации считается незавершённой инженерной работой. + +--- + +### Назначение инженерной документации + +Основной целью инженерной документации является сохранение инженерных знаний платформы. + +Документация должна обеспечивать возможность: + +- понимания архитектуры платформы; +- восстановления причин принятых архитектурных решений; +- безопасного продолжения разработки после длительного перерыва; +- подключения нового разработчика к проекту; +- перехода между Build без потери архитектурного контекста; +- продолжения разработки в новом AI-чате. + +Документация должна объяснять архитектуру платформы, а не дублировать исходный код. + +--- + +### Основные принципы управления документацией + +Система инженерной документации строится на следующих принципах. + +--- + +#### Документация является частью архитектуры + +Инженерная документация рассматривается как архитектурный актив платформы. + +Изменение архитектуры автоматически требует анализа необходимости обновления соответствующей документации. + +--- + +#### Документация развивается одновременно с платформой + +Каждый Build изменяет не только исходный код, но и инженерные знания проекта. + +Документация сопровождает развитие архитектуры на протяжении всего жизненного цикла платформы. + +--- + +#### Прослеживаемость инженерных решений + +Каждое долгосрочное архитектурное решение должно иметь документированное объяснение. + +Документация должна позволять определить: + +- причину принятия решения; +- момент его появления; +- последствия применения; +- документ, которым данное решение регулируется. + +--- + +#### Сопровождаемость документации + +Структура инженерной документации должна оставаться простой и понятной. + +Если изменение одного архитектурного компонента требует обновления большого количества документов, структура документации подлежит пересмотру. + +Сложность сопровождения документации должна уменьшаться одновременно со сложностью архитектуры. + +--- + +### Иерархия инженерной документации + +Инженерная документация организуется как единая иерархическая система. + +Каждый документ имеет собственную область ответственности. + +Дублирование инженерной информации между документами не допускается. + +Общая структура документации имеет следующий вид. + +```text +README + ↓ +Development Process + ↓ +Architecture Principles + ↓ +Runtime Contracts + ↓ +Architecture Decision Records + ↓ +Build Documentation + ↓ +Engineering Reviews + ↓ +Diagrams + ↓ +Glossary +``` + +Каждый уровень документации отвечает исключительно за собственную область инженерных знаний. +### Ответственность инженерной документации + +Каждый документ инженерной документации имеет единственную область ответственности. + +Пересечение областей ответственности между документами не допускается. + +Если одно инженерное правило уже подробно описано в соответствующем документе, остальные документы должны ссылаться на него, а не повторять его содержание. + +--- + +#### README + +README предоставляет общее описание подсистемы. + +README содержит обзор архитектуры, назначения подсистемы и ссылки на основную инженерную документацию. + +README не используется для описания инженерных стандартов или архитектурных правил. + +--- + +#### Development Process + +Development Process определяет официальный инженерный процесс разработки. + +Настоящий документ является основным инженерным стандартом подсистемы Market Intelligence. + +Все остальные инженерные документы должны соответствовать требованиям настоящего стандарта. + +--- + +#### Architecture Principles + +Документ Architecture Principles определяет фундаментальные архитектурные принципы платформы. + +Изменение данного документа допускается только при изменении долгосрочных архитектурных принципов. + +--- + +#### Runtime Contracts + +Документы Runtime Contracts определяют официальные контракты взаимодействия компонентов Runtime Layer. + +Изменение Runtime Contract рассматривается как архитектурное изменение и требует прохождения установленного инженерного процесса. + +--- + +#### Architecture Decision Records + +Architecture Decision Record используется для фиксации долгосрочных архитектурных решений. + +ADR должен объяснять: + +- рассматриваемую проблему; +- принятое решение; +- причины принятия решения; +- последствия данного решения; +- рассмотренные альтернативы. + +Architecture Decision Record не используется для документирования отдельных Build. + +--- + +#### Build Documentation + +Каждый завершённый Build сопровождается отдельным документом. + +Документ Build должен содержать: + +- цель Build; +- архитектурную задачу; +- реализованные изменения; +- результаты инженерных проверок; +- изменения документации; +- итоговый статус Build. + +Build Documentation является частью инженерной истории развития платформы. + +--- + +#### Engineering Reviews + +Результаты инженерных проверок сохраняются как самостоятельная документация Build. + +История Engineering Review обеспечивает прослеживаемость качества архитектуры и инженерных решений. + +--- + +#### Diagrams + +Архитектурные диаграммы документируют структуру платформы. + +Диаграммы отражают архитектуру системы и не предназначены для описания деталей реализации. + +При изменении архитектуры соответствующие диаграммы должны быть актуализированы. + +--- + +#### Glossary + +Glossary является единым словарём инженерных терминов платформы. + +Каждый термин определяется только один раз. + +Использование различных определений одного и того же термина не допускается. + +--- + +### Жизненный цикл инженерной документации + +Каждый инженерный документ проходит единый жизненный цикл. + +```text +Создание + ↓ +Проверка + ↓ +Утверждение + ↓ +Сопровождение + ↓ +Замещение + ↓ +Архивирование +``` + +Документ считается действующим только после его утверждения. + +Документы, утратившие актуальность, подлежат замещению новой версией либо архивированию. + +--- + +### Обновление инженерной документации + +После завершения каждого Build выполняется анализ необходимости обновления инженерной документации. + +Во время анализа определяется: + +- какие документы требуют изменения; +- необходимо ли создание новых документов; +- какие документы утратили актуальность; +- требуется ли синхронизация инженерной документации. + +Обновляются только документы, непосредственно затронутые текущими архитектурными изменениями. + +Массовое обновление документации без инженерной необходимости не допускается. + +### Ответственность за сопровождение документации + +Ответственность за актуальность инженерной документации распределяется между разработчиком проекта и AI. + +Каждая сторона выполняет собственные обязанности в рамках единого инженерного процесса. + +--- + +#### Ответственность разработчика + +Разработчик: + +- принимает окончательные архитектурные решения; +- утверждает изменения инженерной документации; +- определяет долгосрочное направление развития платформы; +- принимает решения об утверждении новых инженерных правил; +- подтверждает завершение Build. + +Разработчик является владельцем инженерной архитектуры проекта. + +--- + +#### Ответственность AI + +AI сопровождает инженерную документацию на протяжении всего жизненного цикла платформы. + +AI обязан: + +- определять необходимость обновления документации; +- выявлять документы, требующие изменения; +- обнаруживать противоречия между документацией и реализацией; +- предлагать создание новых документов; +- контролировать инженерную согласованность документации; +- обеспечивать соответствие документации требованиям настоящего стандарта. + +Разработчик не обязан самостоятельно отслеживать полный перечень документов, требующих актуализации. + +--- + +### Полнота инженерной документации + +Инженерная документация считается полной, если одновременно выполняются следующие условия: + +- описаны все архитектурные изменения; +- отсутствуют внутренние противоречия; +- документация соответствует текущему состоянию платформы; +- определены все долгосрочные архитектурные решения; +- отсутствуют устаревшие инженерные сведения. + +Неполная документация рассматривается как признак незавершённого Build. + +--- + +### Сохранение инженерных знаний + +Одной из основных целей инженерной документации является сохранение инженерных знаний независимо от продолжительности разработки. + +Документация должна обеспечивать возможность: + +- безопасного перехода между Build; +- продолжения разработки после длительного перерыва; +- перехода между различными AI-моделями; +- начала работы в новом AI-чате; +- передачи проекта новому разработчику без потери архитектурного контекста. + +Инженерные знания рассматриваются как долгосрочный актив платформы и подлежат обязательному сохранению. + +--- + +### Непрерывность сопровождения документации + +Система инженерной документации рассматривается как непрерывно развивающаяся часть архитектуры платформы. + +Каждое изменение архитектуры сопровождается анализом необходимости обновления соответствующей документации. + +Структура инженерной документации должна развиваться таким образом, чтобы: + +- сохранять единый источник инженерных знаний; +- предотвращать дублирование информации; +- обеспечивать долгосрочную сопровождаемость; +- уменьшать сложность сопровождения документации; +- сохранять прослеживаемость архитектурных решений. + +Документация развивается одновременно с платформой и остаётся официальным источником инженерных знаний проекта. + +## Part VI. Совместная инженерная разработка + +Настоящая часть устанавливает единые правила совместной разработки между разработчиком проекта и AI. + +Совместная разработка рассматривается как неотъемлемая часть инженерного процесса платформы **Dzentra**. + +Требования настоящей части распространяются на все Build независимо от их объёма, сложности и продолжительности разработки. + +Цель совместной разработки заключается в обеспечении непрерывного развития архитектуры платформы без потери инженерного качества. + +--- + +### Назначение совместной разработки + +Совместная разработка должна обеспечивать: + +- сохранение архитектурной целостности платформы; +- соблюдение требований настоящего стандарта; +- непрерывное развитие инженерной документации; +- сохранение инженерных знаний проекта; +- долгосрочную сопровождаемость архитектуры; +- единообразие инженерных решений. + +Совместная разработка рассматривается как единый инженерный процесс, а не как последовательность независимых действий отдельных участников. + +--- + +### Основные принципы совместной разработки + +Совместная инженерная разработка строится на следующих принципах. + +--- + +#### Владение архитектурой + +Владельцем архитектуры платформы является разработчик. + +Разработчик определяет стратегическое направление развития платформы, принимает окончательные архитектурные решения и утверждает изменения инженерных стандартов. + +AI не является владельцем архитектуры. + +--- + +#### Разделение инженерной ответственности + +Качество платформы является общей ответственностью разработчика и AI. + +Разработчик отвечает за стратегические архитектурные решения. + +AI обеспечивает инженерный анализ, контроль соблюдения стандартов и сопровождение архитектурной документации. + +Разделение ответственности не освобождает ни одну из сторон от соблюдения требований настоящего стандарта. + +--- + +#### Единый источник истины + +Источником истины является исключительно фактическое состояние проекта. + +При принятии инженерных решений используются: + +- существующая реализация; +- утверждённая архитектурная документация; +- Runtime Contracts; +- Architecture Decision Records; +- настоящий стандарт. + +Предположения относительно состояния проекта не являются допустимой основой инженерных решений. + +--- + +#### Непрерывность инженерного процесса + +Совместная разработка должна обеспечивать возможность безопасного продолжения проекта независимо от: + +- продолжительности разработки; +- количества выполненных Build; +- смены AI-модели; +- перехода в новый чат; +- длительных перерывов между этапами разработки. + +Непрерывность инженерного процесса достигается посредством ведения актуальной инженерной документации и соблюдения требований настоящего стандарта. + +--- + +### Ответственность разработчика + +Разработчик определяет стратегическое развитие платформы. + +В обязанности разработчика входит: + +- формирование архитектурных целей; +- определение приоритетов развития; +- утверждение архитектурных решений; +- утверждение изменений инженерной документации; +- подтверждение завершения Build; +- принятие решений о развитии инженерных стандартов. + +Разработчик является владельцем архитектурной стратегии платформы. + +### Ответственность AI + +AI сопровождает разработку на протяжении всего жизненного цикла платформы. + +Деятельность AI направлена на поддержку архитектурной целостности, соблюдение инженерных стандартов и сохранение инженерных знаний проекта. + +AI рассматривает платформу как единую архитектурную систему, а не как совокупность отдельных файлов. + +--- + +#### Архитектурное сопровождение + +Перед началом реализации каждого нового Build AI обязан выполнить предварительный архитектурный анализ. + +Во время анализа определяется: + +- место нового компонента в архитектуре; +- область архитектурной ответственности; +- необходимые зависимости; +- влияние изменений на существующую архитектуру; +- необходимость изменения Runtime Contracts; +- необходимость актуализации инженерной документации. + +Если предлагаемые изменения затрагивают архитектурно значимые компоненты, AI обязан инициировать этап Architecture Design Review. + +--- + +#### Инженерная оценка + +После завершения каждого логического этапа разработки AI выполняет инженерную оценку результата. + +Минимальный перечень оцениваемых аспектов включает: + +- соответствие архитектурным требованиям; +- соблюдение инженерных стандартов; +- корректность архитектурных зависимостей; +- состояние Runtime; +- полноту инженерной документации; +- долгосрочную сопровождаемость платформы. + +По результатам оценки AI предоставляет: + +- обязательные замечания; +- рекомендации; +- выявленные архитектурные риски; +- предложения по улучшению; +- итоговое инженерное заключение. + +--- + +#### Сопровождение инженерной документации + +AI обязан сопровождать инженерную документацию одновременно с развитием платформы. + +После завершения каждого Build AI определяет: + +- какие документы требуют обновления; +- необходимо ли создание новых документов; +- какие документы утратили актуальность; +- какие инженерные знания должны быть зафиксированы. + +Актуализация документации рассматривается как обязательная часть инженерного процесса. + +--- + +#### Управление инженерными знаниями + +Все долгосрочные инженерные знания, возникающие в процессе разработки, подлежат документированию. + +AI обязан определить: + +- относится ли новое правило к настоящему стандарту; +- требуется ли создание нового Architecture Decision Record; +- необходимо ли изменение Runtime Contracts; +- требуется ли изменение документа Architecture Principles; +- необходимо ли дополнение Glossary. + +Инженерные знания не должны сохраняться исключительно в переписке или устных договорённостях. + +--- + +### Процесс принятия инженерных решений + +Все долгосрочные инженерные решения принимаются по единому процессу. + +```text +Инженерная проблема + ↓ +Архитектурный анализ + ↓ +Оценка альтернатив + ↓ +Принятие решения + ↓ +Документирование + ↓ +Реализация + ↓ +Инженерная проверка +``` + +Ни одно долгосрочное архитектурное решение не должно приниматься без предварительного анализа и документирования. + +--- + +### Требования к инженерному взаимодействию + +Совместная работа разработчика и AI должна строиться на инженерных аргументах. + +Каждая рекомендация должна: + +- иметь архитектурное обоснование; +- быть воспроизводимой; +- быть проверяемой; +- соответствовать требованиям настоящего стандарта. + +Основанием для принятия инженерного решения не могут являться: + +- личные предпочтения; +- привычные подходы; +- субъективные оценки; +- удобство реализации без архитектурного обоснования. + +Приоритет всегда отдаётся инженерной аргументации. + +### Ограничения AI + +AI обязан учитывать собственные ограничения при подготовке инженерных рекомендаций. + +Если качество инженерного анализа может быть снижено вследствие недостаточности информации, AI обязан явно сообщить об этом. + +К ограничениям, требующим обязательного уведомления, относятся: + +- отсутствие необходимых файлов проекта; +- отсутствие архитектурного контекста; +- недостаточность исходной информации; +- невозможность проверить фактическое состояние реализации; +- невозможность подтвердить соответствие существующего кода требованиям настоящего стандарта. + +При наличии подобных ограничений AI не должен делать необоснованные предположения. + +--- + +### Непрерывность разработки + +Разработка платформы может продолжаться в нескольких независимых инженерных сессиях. + +Переход между отдельными сессиями разработки не должен приводить к потере архитектурного контекста или инженерных знаний. + +Для обеспечения непрерывности разработки используются: + +- Development Process; +- Architecture Principles; +- Runtime Contracts; +- Architecture Decision Records; +- Build Documentation; +- Build History; +- Engineering Reviews; +- Glossary. + +Перед началом нового этапа разработки актуальная инженерная документация используется как основной источник архитектурного контекста. + +--- + +### Качество совместной разработки + +Качество совместной разработки определяется не количеством написанного исходного кода. + +Основными критериями качества являются: + +- архитектурная целостность платформы; +- соблюдение требований настоящего стандарта; +- полнота инженерной документации; +- сохранение инженерных знаний; +- долгосрочная сопровождаемость архитектуры; +- последовательность инженерных решений. + +Любое действие, ухудшающее один из указанных критериев, рассматривается как нарушение инженерного процесса. + +--- + +### Непрерывность инженерного процесса + +Совместная инженерная разработка рассматривается как непрерывный процесс развития архитектуры платформы. + +После завершения каждого Build должно сохраняться состояние, при котором: + +- архитектура остаётся целостной; +- инженерная документация соответствует реализации; +- инженерные знания сохранены; +- следующий Build может быть начат без восстановления архитектурного контекста; +- соблюдаются требования настоящего стандарта. + +Непрерывность инженерного процесса является обязательным условием долгосрочного развития платформы. + +--- + +### Развитие модели совместной разработки + +Настоящая модель совместной инженерной разработки может развиваться одновременно с развитием платформы. + +Изменение модели взаимодействия допускается только при соблюдении следующих условий: + +- изменение повышает качество инженерного процесса; +- изменение уменьшает архитектурную сложность разработки; +- изменение подтверждено практикой; +- изменение документировано в установленном порядке; +- изменение не противоречит требованиям настоящего стандарта. + +Развитие модели совместной разработки должно обеспечивать повышение качества инженерной деятельности без нарушения архитектурной последовательности платформы. + +## Part VII. Обеспечение качества + +Настоящая часть устанавливает систему обеспечения качества инженерного процесса подсистемы **Market Intelligence**. + +Система обеспечения качества применяется ко всем Build без исключения и обеспечивает соответствие результатов разработки требованиям настоящего стандарта. + +Обеспечение качества рассматривается как непрерывный инженерный процесс, сопровождающий развитие платформы на протяжении всего её жизненного цикла. + +--- + +### Назначение системы обеспечения качества + +Основной целью системы обеспечения качества является подтверждение того, что: + +- требования настоящего стандарта соблюдаются; +- архитектура развивается последовательно; +- инженерные знания сохраняются; +- каждый Build завершается полностью; +- качество платформы непрерывно повышается. + +Система обеспечения качества распространяется на архитектуру, реализацию, инженерную документацию и процесс разработки. + +--- + +### Принципы обеспечения качества + +Система обеспечения качества строится на следующих принципах. + +--- + +#### Полнота проверки + +Каждый Build проходит полный перечень обязательных инженерных процедур. + +Пропуск обязательных этапов обеспечения качества не допускается. + +--- + +#### Прослеживаемость + +Результаты инженерных проверок должны обеспечивать возможность восстановления истории развития платформы. + +Каждое принятое инженерное решение должно быть связано с соответствующей инженерной документацией. + +--- + +#### Последовательность + +Все Build проходят одинаковый процесс обеспечения качества независимо от объёма реализации. + +Единообразие процесса рассматривается как обязательное условие инженерной зрелости платформы. + +--- + +#### Непрерывное улучшение + +Система обеспечения качества развивается одновременно с развитием платформы. + +Каждое изменение процесса обеспечения качества должно повышать эффективность инженерной деятельности без увеличения сложности процесса. + +--- + +### Инженерный контрольный перечень + +Engineering Checklist является обязательным инструментом завершения каждого Build. + +Контрольный перечень подтверждает выполнение всех обязательных этапов инженерного процесса. + +Переход к следующему Build допускается только после полного завершения Checklist. + +--- + +#### Подготовка Build + +Перед началом реализации подтверждается выполнение следующих условий. + +```text +☐ Архитектурная задача определена + +☐ Build имеет единственную цель + +☐ Определена область ответственности + +☐ Определены зависимости + +☐ Выполнено Architecture Design + +☐ Выполнен Architecture Design Review (при необходимости) + +☐ Архитектура утверждена +``` + +--- + +#### Выполнение Build + +Во время реализации подтверждается выполнение следующих требований. + +```text +☐ Реализация соответствует утверждённой архитектуре + +☐ Соблюдены требования к архитектурным слоям + +☐ Соблюдены требования к зависимостям + +☐ Отсутствуют временные инженерные решения + +☐ Отсутствуют необоснованные абстракции + +☐ Соблюдены требования к именованию + +☐ Соблюдены требования к Runtime +``` + +--- + +#### Инженерные проверки + +После завершения реализации подтверждается успешное прохождение обязательных инженерных проверок. + +```text +☐ Architecture Review + +☐ Domain Review + +☐ Code Review + +☐ Runtime Review (при необходимости) + +☐ Dependency Review (при необходимости) + +☐ Documentation Review + +☐ Release Review +``` + +#### Инженерная документация + +Перед закрытием Build подтверждается актуальность инженерной документации. + +```text +☐ Development Process + +☐ Architecture Principles + +☐ Runtime Contracts + +☐ Architecture Decision Records + +☐ Build Documentation + +☐ Build History + +☐ Engineering Reviews + +☐ Diagrams + +☐ Glossary +``` + +Обновлению подлежат только документы, непосредственно затронутые текущим Build. + +--- + +#### Закрытие Build + +Перед завершением Build подтверждается выполнение следующих требований. + +```text +☐ Definition of Done выполнен + +☐ Инженерная документация актуальна + +☐ Build подтверждён разработчиком + +☐ Выполнен Git Commit + +☐ Build получил статус Accepted +``` + +--- + +### Непрерывное совершенствование инженерного процесса + +Настоящий стандарт рассматривается как развивающаяся инженерная система. + +В процессе развития платформы могут формироваться новые инженерные практики, требующие включения в настоящий стандарт. + +Каждое новое правило проходит следующий процесс. + +```text +Практический опыт + ↓ +Инженерный анализ + ↓ +Архитектурная проверка + ↓ +Обновление стандарта + ↓ +Применение +``` + +Изменение настоящего стандарта допускается только после подтверждения практической эффективности нового инженерного правила. + +--- + +#### Критерии включения новых правил + +Новое инженерное правило может быть включено в настоящий стандарт только при одновременном выполнении следующих условий: + +- правило уменьшает архитектурную сложность; +- правило повышает сопровождаемость платформы; +- правило улучшает качество инженерного процесса; +- правило подтверждено практикой разработки; +- правило не противоречит существующим инженерным принципам. + +--- + +### Аудит инженерного процесса + +Инженерный процесс подлежит регулярной архитектурной оценке. + +Цель аудита заключается в подтверждении того, что процесс разработки продолжает соответствовать потребностям платформы. + +Во время аудита оцениваются: + +- эффективность жизненного цикла Build; +- качество инженерных стандартов; +- эффективность системы Engineering Review; +- актуальность инженерной документации; +- полнота инженерных знаний; +- сопровождаемость архитектуры. + +Результаты аудита используются исключительно для совершенствования инженерного процесса. + +--- + +### Метрики инженерного процесса + +Качество инженерного процесса оценивается не количеством написанного исходного кода. + +Основными инженерными показателями являются: + +- архитектурная стабильность; +- отсутствие архитектурного долга; +- отсутствие неконтролируемого технического долга; +- качество инженерной документации; +- повторное использование компонентов; +- простота сопровождения платформы; +- успешность инженерных проверок; +- предсказуемость развития архитектуры. + +Метрики используются для оценки инженерного процесса, а не для оценки разработчиков. + +--- + +### Соответствие настоящему стандарту + +Настоящий документ является обязательным инженерным стандартом подсистемы **Market Intelligence**. + +Все архитектурные компоненты платформы обязаны соответствовать требованиям настоящего стандарта. + +Любое отклонение допускается только при наличии документированного архитектурного обоснования. + +Локальные соглашения отдельных Build не могут изменять требования настоящего стандарта. + +При возникновении противоречий приоритет всегда имеет настоящий документ. + +--- + +#### Подтверждение соответствия + +Соответствие настоящему стандарту подтверждается посредством: + +- Engineering Checklist; +- Architecture Review; +- Domain Review; +- Code Review; +- Runtime Review (при необходимости); +- Dependency Review (при необходимости); +- Documentation Review; +- Release Review. + +Подтверждение соответствия является обязательной частью каждого Build. + +--- + +### Сопровождение стандарта + +Настоящий документ рассматривается как развивающийся инженерный стандарт. + +Изменение настоящего стандарта выполняется исключительно посредством выпуска новой версии документа. + +Каждая новая версия должна: + +- сохранять инженерную терминологию; +- сохранять архитектурную последовательность; +- содержать документированное описание изменений; +- повышать качество инженерного процесса; +- обеспечивать обратную совместимость инженерных принципов. + +Настоящий стандарт развивается одновременно с развитием платформы. + +--- + +### Жизненный цикл стандарта + +Настоящий стандарт проходит следующий жизненный цикл. + +```text +Черновик + ↓ +Рецензирование + ↓ +Release Candidate + ↓ +Release + ↓ +Сопровождение + ↓ +Следующая версия +``` + +Каждый этап представляет самостоятельную стадию развития инженерного стандарта. + +--- + +### Заключительные положения + +Настоящий документ является официальным инженерным стандартом разработки подсистемы **Market Intelligence** проекта **Dzentra**. + +Все архитектурные решения, инженерные процессы и долгосрочные правила разработки должны соответствовать требованиям настоящего стандарта. + +Главной целью настоящего стандарта является обеспечение возможности многолетнего развития платформы без архитектурной деградации. + +Каждый новый Build должен делать платформу: + +- функционально богаче; +- архитектурно чище; +- проще для сопровождения; +- понятнее для новых разработчиков; +- более пригодной для дальнейшего расширения. + +Развитие платформы рассматривается как непрерывный инженерный процесс. + +Настоящий стандарт остаётся единым официальным источником инженерных правил разработки подсистемы **Market Intelligence**. + +--- + +# Приложения + +Приложения являются справочной частью настоящего стандарта. + +Они не изменяют обязательные требования документа, но могут использоваться для унификации инженерной практики. + +В последующих версиях стандарта могут быть добавлены: + +- **Приложение А.** Глоссарий инженерных терминов. +- **Приложение Б.** Справочник статусов Build. +- **Приложение В.** Матрица Engineering Review. +- **Приложение Г.** Рекомендуемая структура проекта. +- **Приложение Д.** Шаблоны инженерной документации. +- **Приложение Е.** Каталог архитектурных диаграмм. +- **Приложение Ж.** Руководство по классификации Architecture Decision Records. + +Добавление новых приложений не изменяет обязательные требования настоящего стандарта. + diff --git a/docs/market_intelligence/diagrams/README.md b/docs/market_intelligence/diagrams/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/engines/README.md b/docs/market_intelligence/engines/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/engines/architecture.md b/docs/market_intelligence/engines/architecture.md new file mode 100644 index 0000000..227d4ae --- /dev/null +++ b/docs/market_intelligence/engines/architecture.md @@ -0,0 +1,171 @@ +# Архитектура Engine Layer + +**Версия:** 1.0 + +**Статус:** Active + +--- + +# Назначение + +Настоящий документ определяет архитектуру слоя аналитических движков (Engine Layer) +подсистемы **Market Intelligence**. + +Каждый Engine отвечает только за один аспект анализа рынка и никогда не принимает +торговых решений. + +--- + +# Базовая модель Engine + +Каждый Engine состоит из двух независимых частей: + +```text +Engine +├── Metadata +└── Logic +``` + +Разделение Metadata и Logic является обязательным архитектурным правилом. + +--- + +# Metadata + +Metadata — неизменяемое архитектурное описание Engine. + +Metadata содержит: + +- имя Engine; +- версию; +- отображаемое имя; +- назначение; +- зависимости; +- поддерживаемые таймфреймы; +- требования к входным данным; +- архитектурные возможности. + +Metadata должна быть доступна без создания экземпляра Engine. + +Во время выполнения Engine Metadata никогда не изменяется. + +--- + +# Logic + +Logic представляет собой аналитическую реализацию Engine. + +Вход: + +```text +EngineContext +``` + +Выход: + +```text +EngineResult +``` + +Logic не должна изменять Metadata. + +--- + +# Обязательная структура Engine + +```text +/ +├── metadata.py +└── engine.py +``` + +--- + +# Рекомендуемая структура Engine + +```text +/ +├── __init__.py +├── metadata.py +├── models.py +├── calculators.py +├── evaluators.py +├── checks.py +├── payloads.py +└── engine.py +``` + +--- + +# Допустимые расширения + +Сложные Engine могут содержать дополнительные специализированные модули. + +Пример: + +```text +wave/ +├── metadata.py +├── models.py +├── swing_points.py +├── wave_builder.py +├── impulse_detector.py +├── pullback_detector.py +├── evaluators.py +├── checks.py +├── payloads.py +└── engine.py +``` + +Уникальная внутренняя структура допускается, если сохраняется Runtime Contract. + +--- + +# Запрещённые зависимости + +Engine не должен напрямую зависеть от: + +- другого Engine; +- AutoTrade; +- Exchange; +- Telegram; +- Execution; +- Journal; +- базы данных. + +Результаты других Engine могут передаваться только через: + +```text +EngineContext.dependency_results +``` + +--- + +# Разрешённые зависимости + +Engine может зависеть от: + +- `market_intelligence/common`; +- Runtime Contract; +- собственных внутренних модулей; +- стандартной библиотеки Python. + +--- + +# Главное правило + +Внутренняя структура Engine может отличаться в зависимости от сложности предметной области. + +Однако для любого Engine обязательны: + +- соблюдение Runtime Contract; +- наличие Metadata; +- единая точка входа (`engine.py`). + +--- + +# Заключение + +Engine Layer развивается через расширение, а не через изменение Runtime. + +Добавление нового Engine не должно требовать изменения Runtime Layer. diff --git a/docs/market_intelligence/glossary/README.md b/docs/market_intelligence/glossary/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/information/README.md b/docs/market_intelligence/information/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/information/dzengi_market_data_inventory.md b/docs/market_intelligence/information/dzengi_market_data_inventory.md new file mode 100644 index 0000000..dda204d --- /dev/null +++ b/docs/market_intelligence/information/dzengi_market_data_inventory.md @@ -0,0 +1,3499 @@ +# Dzengi Market Data Inventory + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Dzengi Market Data Inventory | +| Тип документа | Source Data Inventory | +| Версия | 1.0 | +| Статус | Draft | +| **Степень верификации** | **Documentation Verified** | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Источник | Dzengi API | +| Язык | Русский | + +--- + +## Статус документа + +Настоящий документ представляет собой полную техническую инвентаризацию возможностей официального API биржи Dzengi. + +Inventory фиксирует исключительно возможности получения данных через API и не содержит выводов о способах использования этих данных внутри архитектуры Dzentra. + +Документ формируется на основании: + +- официальной документации Dzengi REST API; +- официальной документации Dzengi WebSocket API; +- спецификации OpenAPI (Swagger); +- официальных примеров запросов; +- официальных примеров ответов; +- результатов анализа фактического поведения API. + +До выпуска версии **Release** допускается уточнение описаний операций и структур данных по мере исследования API. + +--- + +# Степень верификации + +Настоящая редакция документа построена исключительно на основании официальной документации Dzengi: + +- REST API Documentation; +- WebSocket Request API Documentation; +- WebSocket Stream API Documentation; +- OpenAPI (Swagger) Specification; +- официальных примеров запросов; +- официальных примеров ответов. + +Фактическая проверка операций посредством выполнения реальных запросов к бирже Dzengi на момент подготовки настоящей редакции документа не проводилась. + +По этой причине: + +- обязательные параметры операций считаются соответствующими официальной документации; +- структуры Payload считаются соответствующими официальной документации; +- состав полей считается соответствующим официальной документации; +- описание операций основано исключительно на опубликованной спецификации API. + +Настоящий документ имеет статус **Documentation Verified**. + +После проведения практической проверки каждой операции с использованием реальных запросов и анализа фактических ответов API документ получит статус **Runtime Verified**. + +Все расхождения между документацией и фактическим поведением API должны фиксироваться в последующих редакциях настоящего документа. + +--- + +## Назначение + +Настоящий документ предназначен для полной инвентаризации официального API Dzengi. + +Документ фиксирует: + +- доступные операции API; +- способы вызова операций; +- параметры запросов; +- структуры ответов; +- поля Payload; +- вложенные структуры данных; +- режимы получения данных; +- технические характеристики операций. + +Настоящий документ **не определяет**: + +- архитектуру Dzentra; +- внутреннюю информационную модель Dzentra; +- модель рынка; +- знания о рынке; +- способы анализа рынка; +- торговые алгоритмы; +- способы использования данных. + +Все перечисленные вопросы рассматриваются в последующих архитектурных документах проекта. + +--- + +## Цель + +Цель настоящего документа — полностью определить технические возможности получения информации через официальный API Dzengi. + +Настоящий Inventory является первичным техническим источником для разработки: + +- Dzengi API Client; +- Dzentra Market Information Catalogue; +- Dzentra Market Knowledge Catalogue; +- подсистемы Market Intelligence; +- подсистемы Execution; +- подсистемы Portfolio; +- подсистемы Account Management. + +--- + +## Основной вопрос документа + +Настоящий документ отвечает только на один вопрос. + +> **Какие операции, структуры данных и способы получения информации предоставляет официальный API Dzengi?** + +--- + +## Источник информации + +Настоящий документ построен исключительно на основании официальных материалов Dzengi. + +Используются следующие источники: + +- REST API Documentation; +- WebSocket Request API Documentation; +- WebSocket Stream API Documentation; +- OpenAPI (Swagger) Specification; +- официальные примеры запросов; +- официальные примеры ответов. + +При возникновении расхождений между различными источниками приоритет имеют: + +1. фактическое поведение API; +2. официальные примеры запросов и ответов; +3. спецификация OpenAPI (Swagger); +4. текстовая документация. + +Предположения о внутренней реализации биржи в настоящем документе не используются. + +--- + +## Поддерживаемые интерфейсы API + +Настоящий документ рассматривает следующие интерфейсы взаимодействия с API Dzengi. + +- REST API +- WebSocket Request API +- WebSocket Stream API + +Каждый интерфейс рассматривается как самостоятельный способ взаимодействия с API и исследуется независимо. + +--- + +## Поддерживаемые версии API + +На момент подготовки настоящего документа официальная документация Dzengi содержит: + +- REST API v1; +- REST API v2. + +Если операция присутствует одновременно в нескольких версиях API и не имеет различий в структуре запросов и ответов, она описывается в настоящем документе один раз. + +Различия между версиями фиксируются только при наличии технических отличий операций или структур данных. + +--- + +--- + +## Правила описания API + +Настоящий документ использует единые правила описания операций API. + +Настоящие правила являются обязательными для всех разделов документа и не повторяются внутри описаний отдельных операций. + +### Правило выбора версии API + +Если операция присутствует одновременно в REST API v1 и REST API v2 и имеет одинаковые параметры запроса и одинаковую структуру ответа, операция описывается один раз. + +Для криптовалютных инструментов предпочтительной рабочей версией считается **REST API v1**, поскольку: + +- REST API v1 рекомендуется официальной документацией Dzengi; +- REST API v1 полностью покрывает криптовалютные инструменты; +- REST API v2 предназначен преимущественно для поддержки дополнительных типов инструментов; +- при совпадении структуры операций использование REST API v2 не предоставляет дополнительных преимуществ. + +Отдельное описание REST API v2 создаётся только при наличии хотя бы одного из следующих условий: + +- отличается состав параметров; +- отличается структура Payload; +- отличается состав полей ответа; +- отличается семантика данных; +- операция существует только в REST API v2; +- документация Dzengi требует использования REST API v2. + +--- + +### Правило исследования API + +Если официальная документация, OpenAPI (Swagger) и фактическое поведение API противоречат друг другу, в карточке операции фиксируется только подтверждённая информация. + +Все неподтверждённые сведения указываются в разделе **Примечания** как требующие дополнительной проверки. + +Настоящий документ не делает предположений о поведении API. + +--- + +### Правило объединения REST API и WebSocket Request API + +Если REST API и WebSocket Request API выполняют одну и ту же операцию и имеют: + +- одинаковое назначение; +- одинаковые параметры запроса; +- одинаковую структуру ответа, + +они рассматриваются как различные способы вызова одной операции и описываются одной карточкой. + +--- + +### Правило описания WebSocket Stream + +WebSocket Stream рассматривается как самостоятельная операция API. + +Каждый Stream описывается отдельной карточкой независимо от наличия аналогичного REST endpoint или WebSocket Request метода. + +--- + +### Правило именования операций + +Во всём документе используется официальное имя операции Dzengi API. + +Используются имена вида: + +- `exchangeInfo` +- `ticker24hr` +- `account` +- `order.create` +- `order.edit` +- `order.cancel` +- `trades.subscribe` +- `depthMarketData.subscribe` + +Путь REST endpoint и адрес WebSocket рассматриваются как способы вызова операции и приводятся только в разделе **Доступные вызовы**. + +--- + +### Правило описания Payload + +Раздел **Payload** содержит описание только полей верхнего уровня объекта ответа. + +Если поле содержит объект или массив объектов, его внутренняя структура переносится в раздел **Структура Payload**. + +--- + +### Правило описания структуры Payload + +Все вложенные объекты и массивы описываются отдельно в соответствии с фактической структурой JSON. + +Используется полное имя пути. + +Например: + +- `balances[]` +- `symbols[]` +- `symbols[].filters[]` +- `symbols[].marketModes[]` +- `positions[]` + +Настоящий документ описывает структуру JSON и не определяет внутренние модели данных Dzentra. + +--- + +### Правило обозначения структур данных + +Если операция возвращает массив однотипных объектов, в разделе **Payload** для обозначения типа элементов массива используется условное имя структуры. + +Например: + +- `Currency[]` +- `Symbol[]` +- `Trade[]` +- `Order[]` +- `Position[]` + +Данные обозначения используются исключительно для удобства чтения документа и не являются моделями данных Dzentra. + +--- + +### Правило описания характеристик операции + +Каждая операция содержит раздел **Характеристики операции**. + +В данном разделе фиксируются технические свойства операции, непосредственно следующие из официальной документации или подтверждённые фактическим поведением API. + +--- + +### Правило использования в Dzentra + +Настоящий документ не определяет способы использования операций внутри Dzentra. + +Во всех карточках используется единое значение: + +> **Не определяется настоящим документом.** + +Последующая интерпретация операций выполняется в документах: + +- Dzentra Market Information Catalogue; +- Dzentra Market Knowledge Catalogue; +- Dzentra Architecture. + +--- + +### Правило порядка операций + +Документ разделён на тематические разделы. + +Внутри каждого раздела операции располагаются в алфавитном порядке по официальному имени операции. + +Например: + +- `account` +- `aggTrades` +- `currencies` +- `depth` +- `exchangeInfo` + +Алфавитная сортировка сохраняется независимо от: + +- версии API; +- способа вызова операции; +- категории данных. + +--- + +### Правило описания операций + +Все операции настоящего документа описываются по единому стандарту. + +Структура описания операции определяется разделом **Стандарт описания операций API** настоящего документа. + +--- + +## Стандарт описания операций API + +Все операции API описываются по единому стандарту. + +Использование единой структуры обеспечивает: + +- единообразие документа; +- удобство поиска информации; +- возможность автоматической обработки документа; +- возможность генерации моделей данных; +- возможность генерации клиентского кода; +- возможность дальнейшего построения Dzentra Market Information Catalogue. + +### Структура описания операции + +Каждая операция описывается в следующем порядке. + +1. Назначение +2. Результат операции +3. Режим получения данных +4. Доступные вызовы +5. Версии API +6. Параметры запроса +7. Payload +8. Структура Payload +9. Характеристики операции +10. Использование в Dzentra +11. Примечания + +--- + +### Результат операции + +Краткое описание информации, возвращаемой операцией, либо результата её выполнения. + +--- + +### Режим получения данных + +Допустимые значения. + +| Значение | Описание | +|----------|----------| +| Snapshot | Возвращает текущее состояние объекта на момент запроса. | +| Historical | Возвращает исторические данные за указанный период. | +| Stream | Передаёт данные в режиме реального времени. | +| Command | Выполняет действие и возвращает результат выполнения. | + +--- + +### Доступные вызовы + +Перечисляются все поддерживаемые способы вызова операции. + +Пример. + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/exchangeInfo` | +| WS Request | `exchangeInfo` | + +Если операция существует только в одном интерфейсе, указывается только он. + +--- + +### Версии API + +Перечисляются версии API, поддерживающие данную операцию. + +Пример. + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +--- + +### Параметры запроса + +Описываются все параметры запроса. + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| + +--- + +### Payload + +Описываются только поля верхнего уровня объекта ответа. + +| Поле | Тип | Описание | +|------|-----|----------| + +--- + +### Структура Payload + +Все вложенные объекты и массивы описываются отдельно в соответствии с фактической структурой JSON. + +Например: + +- `balances[]` +- `symbols[]` +- `symbols[].filters[]` +- `symbols[].permissions[]` +- `positions[]` + +--- + +### Характеристики операции + +Используется единая таблица. + +| Свойство | Значение | +|----------|-----------| +| Авторизация | требуется / не требуется | +| API Key | требуется / не требуется | +| Подпись запроса | требуется / не требуется | +| Timestamp | требуется / не требуется | +| recvWindow | поддерживается / не поддерживается | +| Snapshot | да / нет | +| Historical | да / нет | +| Stream | да / нет | +| REST и WS Request эквивалентны | да / нет | + +При необходимости таблица может быть дополнена дополнительными характеристиками, непосредственно следующими из официальной документации. + +--- + +### Использование в Dzentra + +Во всех карточках используется единое значение. + +> **Не определяется настоящим документом.** + +--- + +### Примечания + +Содержат исключительно сведения, относящиеся к конкретной операции. + +Общие правила, приведённые в начале документа, в данном разделе не повторяются. + +--- + +## Содержание + +Всего карточек операций: 33 + +### Market Reference Data (3) + +- `currencies` +- `exchangeInfo` +- `time` + +### Market Data (8) + +- `aggTrades` +- `depth` +- `depthMarketData.subscribe` +- `klines` +- `marketData.subscribe` +- `OHLCMarketData.subscribe` +- `ticker24hr` +- `trades.subscribe` + +### Trading Conditions (4) + +- `fundingLimits` +- `leverageSettings` +- `tradingFees` +- `tradingLimits` + +### Execution Data (11) + +- `closeTradingPosition` +- `fetchOrder` +- `myTrades` +- `openOrders` +- `order.cancel` +- `order.create` +- `order.edit` +- `tradingPositions` +- `tradingPositionsHistory` +- `updateTradingOrder` +- `updateTradingPosition` + +### Account Data (6) + +- `account` +- `depositAddress` +- `deposits` +- `ledger` +- `transactions` +- `withdrawals` + +### Infrastructure Services (1) + +- `ping` + +--- + +## Статистика документа + +| Показатель | Значение | +|------------|---------:| +| Категорий данных | 6 | +| Карточек операций | 33 | +| Исследованных операций API | 61 | +| REST операций | 28 | +| WebSocket Request операций | 28 | +| WebSocket Stream операций | 4 | +| Infrastructure Services операций | 1 | + +--- + +## Market Reference Data + +**Назначение** + +Содержит справочную информацию о рынке и торговых инструментах, необходимую для корректной интерпретации рыночных данных. + +**Использование** + +Данные раздела используются в качестве основы для обработки Market Data, проверки торговых параметров инструментов и подготовки торговых операций. + +--- + +### currencies + +#### Назначение + +Получение справочной информации о валютах, поддерживаемых биржей Dzengi. + +#### Результат операции + +Возвращает сведения о валютах, доступных в системе Dzengi. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/currencies` | +| WS Request | `currencies` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **Currency**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `Currency[]` | Список валют. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `name` | string | Наименование валюты. | +| `displaySymbol` | string | Символ, используемый для отображения валюты. | +| `precision` | integer | Количество знаков после десятичной точки. | +| `type` | string | Тип валюты. | +| `minWithdrawal` | number | Минимально допустимая сумма вывода. | +| `maxWithdrawal` | number | Максимально допустимая сумма вывода. | +| `commissionMin` | number | Минимальная комиссия за вывод. | +| `commissionPercent` | number | Процентная комиссия за вывод. | +| `commissionFixed` | number | Фиксированная комиссия за вывод. | +| `minDeposit` | number | Минимально допустимая сумма пополнения. | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает справочную информацию о валютах, доступных в системе Dzengi. +- Верхний уровень ответа представляет собой массив объектов типа **Currency**. +- В официальной документации для данной операции вложенные структуры отсутствуют. + +--- + +### exchangeInfo + +#### Назначение + +Получение справочной информации о торговых инструментах и правилах торговли, поддерживаемых биржей Dzengi. + +#### Результат операции + +Возвращает информацию о бирже, торговых инструментах, торговых фильтрах, поддерживаемых типах ордеров, ограничениях торговли и других параметрах, необходимых для корректной работы с рынком. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/exchangeInfo` | +| WS Request | `exchangeInfo` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да* | + +\* Для криптовалютных инструментов рекомендуется использование REST API v1. + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Нет | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Нет | API-ключ пользователя. | +| `signature` | string | Нет | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **ExchangeInfo**. + +| Поле | Тип | Описание | +|------|------|----------| +| `timezone` | string | Часовой пояс биржи. | +| `serverTime` | integer | Серверное время. | +| `rateLimits` | `RateLimit[]` | Ограничения API. | +| `exchangeFilters` | `ExchangeFilter[]` | Общие фильтры биржи. | +| `symbols` | `Symbol[]` | Список торговых инструментов. | + +#### Структура Payload + +##### rateLimits + +| Поле | Тип | Описание | +|------|------|----------| +| *Определяется документацией Dzengi* | + +##### exchangeFilters + +| Поле | Тип | Описание | +|------|------|----------| +| *Определяется документацией Dzengi* | + +##### symbols + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Код торгового инструмента. | +| `name` | string | Наименование инструмента. | +| `status` | string | Статус торговли. | +| `baseAsset` | string | Базовый актив. | +| `baseAssetPrecision` | integer | Точность базового актива. | +| `quoteAsset` | string | Котируемый актив. | +| `quoteAssetId` | string | Идентификатор котируемого актива. | +| `quotePrecision` | integer | Точность котируемого актива. | +| `orderTypes` | `string[]` | Поддерживаемые типы ордеров. | +| `filters` | `Filter[]` | Торговые фильтры инструмента. | +| `marketModes` | `MarketMode[]` | Поддерживаемые режимы торговли. | +| `marketType` | string | Тип рынка. | +| `country` | string | Страна. | +| `sector` | string | Сектор экономики. | +| `industry` | string | Отрасль. | +| `tradingHours` | string | Торговые часы. | +| `tickSize` | number | Размер тика. | +| `tickValue` | number | Стоимость тика. | +| `exchangeFee` | number | Комиссия биржи. | + +##### symbols.filters + +| Поле | Тип | Описание | +|------|------|----------| +| `filterType` | string | Тип фильтра. | +| `minQty` | string | Минимальное количество. | +| `maxQty` | string | Максимальное количество. | +| `stepSize` | string | Шаг изменения количества. | +| `minNotional` | string | Минимальная стоимость сделки. | + +##### symbols.marketModes + +| Поле | Тип | Описание | +|------|------|----------| +| *Структура определяется документацией Dzengi* | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает справочную информацию о бирже и торговых инструментах. +- Ответ содержит несколько вложенных структур данных. +- При авторизованном запросе состав доступных инструментов может зависеть от юрисдикции аккаунта пользователя. + +--- + +### time + +#### Назначение + +Получение серверного времени биржи Dzengi. + +#### Результат операции + +Возвращает текущее серверное время биржи. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/time` | +| WS Request | `time` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +Отсутствуют. + +#### Payload + +Возвращает объект **ServerTime**. + +| Поле | Тип | Описание | +|------|------|----------| +| `serverTime` | integer | Серверное время в миллисекундах Unix Epoch. | + +#### Структура Payload + +Отсутствует. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция не принимает параметров запроса. +- Возвращаемое значение используется для синхронизации времени между клиентом и сервером. +- Структура ответа состоит из одного поля. + +--- + +## Market Data + +**Назначение** + +Содержит данные, непосредственно описывающие текущее или историческое состояние рынка. + +**Использование** + +Данные раздела являются основным источником информации о состоянии рынка и могут использоваться последующими документами Dzentra для построения информационной модели рынка, анализа рыночной ситуации и принятия торговых решений. + +--- + +### aggTrades + +#### Назначение + +Получение списка агрегированных сделок по указанному торговому инструменту. + +#### Результат операции + +Возвращает список агрегированных сделок, удовлетворяющих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/aggTrades` | +| WS Request | `aggTrades` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | + +#### Payload + +Возвращает массив объектов типа **AggregateTrade**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `AggregateTrade[]` | Список агрегированных сделок. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `a` | integer | Идентификатор агрегированной сделки. | +| `p` | string | Цена сделки. | +| `q` | string | Количество. | +| `T` | integer | Время сделки (Unix Timestamp, мс). | +| `m` | boolean | Признак того, что покупатель является maker. | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает агрегированные сделки в соответствии с документацией Dzengi. +- Поле `q` присутствует в официальной документации. Дополнительные особенности его использования настоящим документом не рассматриваются. +- Операция не является потоковой. Для получения данных в режиме реального времени используется отдельная операция `trades.subscribe`. + +--- + +### depth + +#### Назначение + +Получение текущего состояния стакана заявок по указанному торговому инструменту. + +#### Результат операции + +Возвращает текущее состояние стакана заявок для указанного торгового инструмента. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/depth` | +| WS Request | `depth` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | +| `limit` | integer | Нет | Максимальная глубина стакана. | + +#### Payload + +Возвращает объект **OrderBook**. + +| Поле | Тип | Описание | +|------|------|----------| +| `lastUpdateId` | integer | Идентификатор состояния стакана. | +| `asks` | `OrderBookLevel[]` | Заявки на продажу. | +| `bids` | `OrderBookLevel[]` | Заявки на покупку. | + +#### Структура Payload + +##### asks + +| Поле | Тип | Описание | +|------|------|----------| +| `[0]` | string | Цена. | +| `[1]` | string | Количество. | + +##### bids + +| Поле | Тип | Описание | +|------|------|----------| +| `[0]` | string | Цена. | +| `[1]` | string | Количество. | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает снимок (snapshot) текущего состояния стакана заявок. +- Каждая запись в массивах `asks` и `bids` представлена массивом из двух элементов: цены и количества. +- Для получения изменений стакана в режиме реального времени используется отдельная операция `depthMarketData.subscribe`. + +--- + +### depthMarketData.subscribe + +#### Назначение + +Подписка на получение изменений стакана заявок по указанному торговому инструменту в режиме реального времени. + +#### Результат операции + +Возвращает поток событий, содержащих изменения текущего состояния стакана заявок. + +#### Режим получения данных + +**Stream** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| WS Stream | `depthMarketData.subscribe` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| WebSocket Stream API | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | + +#### Payload + +Возвращает объект **DepthMarketData**. + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `Data` | `DepthData` | Данные изменения стакана. | + +#### Структура Payload + +##### Data + +| Поле | Тип | Описание | +|------|------|----------| +| `ts` | integer | Время события (Unix Timestamp, мс). | +| `Bid` | `object` | Уровни заявок на покупку. | +| `Ofr` | `object` | Уровни заявок на продажу. | + +##### Data.Bid + +*Структура определяется документацией Dzengi.* + +##### Data.Ofr + +*Структура определяется документацией Dzengi.* + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Да | +| REST и WS Request эквивалентны | Нет | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция использует WebSocket Stream API. +- Возвращает изменения состояния стакана заявок в режиме реального времени. +- Структура объектов `Bid` и `Ofr` официальной документацией полностью не раскрыта. +- Для получения полного состояния стакана используется операция `depth`. + +--- + +### klines + +#### Назначение + +Получение исторических свечей (OHLC) по указанному торговому инструменту. + +#### Результат операции + +Возвращает список свечей, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/klines` | +| WS Request | `klines` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | +| `interval` | string | Да | Интервал свечи. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых свечей. | +| `priceType` | string | Нет | Тип цены. По умолчанию `bid`. | +| `type` | string | Нет | Тип возвращаемых данных. | + +#### Payload + +Возвращает массив объектов типа **Candle**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `Candle[]` | Список свечей. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `[0]` | integer | Время открытия свечи (Unix Timestamp, мс). | +| `[1]` | string | Цена открытия (*Open*). | +| `[2]` | string | Максимальная цена (*High*). | +| `[3]` | string | Минимальная цена (*Low*). | +| `[4]` | string | Цена закрытия (*Close*). | +| `[5]` | string | Объём (*Volume*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает исторические данные в виде последовательности свечей. +- Каждая свеча представлена массивом фиксированной структуры. +- В официальной документации свеча содержит шесть элементов массива. +- Для получения свечей в режиме реального времени используется отдельная операция `OHLCMarketData.subscribe`. + +--- + +### marketData.subscribe + +#### Назначение + +Подписка на получение текущих рыночных котировок по указанному торговому инструменту в режиме реального времени. + +#### Результат операции + +Возвращает поток событий, содержащих текущие рыночные котировки. + +#### Режим получения данных + +**Stream** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| WS Stream | `marketData.subscribe` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| WebSocket Stream API | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | + +#### Payload + +Возвращает объект **MarketData**. + +| Поле | Тип | Описание | +|------|------|----------| +| `symbolName` | string | Торговый инструмент. | +| `bid` | number | Лучшая цена покупки (*Best Bid*). | +| `bidQty` | number | Количество по лучшей цене покупки (*Best Bid Quantity*). | +| `ofr` | number | Лучшая цена продажи (*Best Ask*). | +| `ofrQty` | number | Количество по лучшей цене продажи (*Best Ask Quantity*). | +| `timestamp` | integer | Время формирования котировки (Unix Timestamp, мс). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Да | +| REST и WS Request эквивалентны | Нет | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция использует WebSocket Stream API. +- Возвращает поток текущих рыночных котировок в режиме реального времени. +- Каждое сообщение содержит текущее состояние лучших цен покупки и продажи для торгового инструмента. +- Поток не содержит историю изменений котировок. + +--- + +### OHLCMarketData.subscribe + +#### Назначение + +Подписка на получение свечей (OHLC) по указанному торговому инструменту в режиме реального времени. + +#### Результат операции + +Возвращает поток событий, содержащих текущие данные свечей (OHLC). + +#### Режим получения данных + +**Stream** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| WS Stream | `OHLCMarketData.subscribe` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| WebSocket Stream API | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | +| `interval` | string | Да | Интервал свечи. | + +#### Payload + +Возвращает объект **Candle**. + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `interval` | string | Интервал свечи. | +| `T` | integer | Время открытия свечи (Unix Timestamp, мс). | +| `O` | number | Цена открытия (*Open*). | +| `H` | number | Максимальная цена (*High*). | +| `L` | number | Минимальная цена (*Low*). | +| `C` | number | Цена закрытия (*Close*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Да | +| REST и WS Request эквивалентны | Нет | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция использует WebSocket Stream API. +- Возвращает поток свечей (OHLC) в режиме реального времени. +- Каждое сообщение относится к одной свече выбранного интервала. +- В официальном примере документации поле объёма (*Volume*) отсутствует. +- Если состав сообщения будет отличаться в фактических ответах API, описание может быть уточнено после практической проверки. + +--- + +### ticker24hr + +#### Назначение + +Получение статистики торгов по указанному торговому инструменту за последние 24 часа. + +#### Результат операции + +Возвращает статистические показатели торгов за последние 24 часа. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/ticker/24hr` | +| WS Request | `ticker/24hr` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Нет | Торговый инструмент. Если параметр не указан, может возвращаться статистика по нескольким инструментам. | + +#### Payload + +Возвращает объект **Ticker24hr**. + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `priceChange` | string | Изменение цены (*Price Change*). | +| `priceChangePercent` | string | Изменение цены в процентах (*Price Change Percent*). | +| `weightedAvgPrice` | string | Средневзвешенная цена (*Weighted Average Price*). | +| `prevClosePrice` | string | Предыдущая цена закрытия (*Previous Close Price*). | +| `lastPrice` | string | Последняя цена (*Last Price*). | +| `lastQty` | string | Количество последней сделки (*Last Quantity*). | +| `bidPrice` | string | Лучшая цена покупки (*Best Bid*). | +| `askPrice` | string | Лучшая цена продажи (*Best Ask*). | +| `openPrice` | string | Цена открытия (*Open Price*). | +| `highPrice` | string | Максимальная цена (*High Price*). | +| `lowPrice` | string | Минимальная цена (*Low Price*). | +| `volume` | string | Объём (*Volume*). | +| `quoteVolume` | string | Объём в котируемом активе (*Quote Volume*). | +| `openTime` | integer | Время открытия периода (Unix Timestamp, мс). | +| `closeTime` | integer | Время окончания периода (Unix Timestamp, мс). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает статистику торгов за последние 24 часа. +- Если параметр `symbol` не указан, операция может вернуть статистику сразу по нескольким торговым инструментам. +- Значения рассчитываются для скользящего 24-часового периода (*24-hour rolling window*). + +--- + +### trades.subscribe + +#### Назначение + +Подписка на получение информации о сделках по указанному торговому инструменту в режиме реального времени. + +#### Результат операции + +Возвращает поток событий, содержащих информацию о совершаемых сделках. + +#### Режим получения данных + +**Stream** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| WS Stream | `trades.subscribe` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| WebSocket Stream API | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | + +#### Payload + +Возвращает объект **Trade**. + +| Поле | Тип | Описание | +|------|------|----------| +| `price` | number | Цена сделки (*Trade Price*). | +| `size` | number | Объём сделки (*Trade Size*). | +| `id` | integer | Идентификатор сделки (*Trade ID*). | +| `ts` | integer | Время сделки (Unix Timestamp, мс). | +| `symbol` | string | Торговый инструмент. | +| `orderId` | string | Идентификатор ордера (*Order ID*). | +| `clientOrderId` | string | Клиентский идентификатор ордера (*Client Order ID*). | +| `buyer` | boolean | Признак стороны покупателя (*Buyer Flag*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Да | +| REST и WS Request эквивалентны | Нет | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция использует WebSocket Stream API. +- Возвращает поток сделок в режиме реального времени. +- Каждое сообщение описывает отдельную сделку. +- Для получения истории агрегированных сделок используются операции `aggTrades` (REST API / WebSocket Request API). + +--- + +## Trading Conditions + +**Назначение** + +Содержит информацию о торговых условиях, ограничениях и правилах, применяемых биржей Dzengi при исполнении торговых операций. + +**Использование** + +Данные раздела могут использоваться последующими документами Dzentra для проверки допустимости торговых операций, расчёта торговых параметров и соблюдения ограничений биржи. + +--- + +### fundingLimits + +#### Назначение + +Получение информации об ограничениях операций пополнения и вывода средств. + +#### Результат операции + +Возвращает список ограничений, применяемых к операциям пополнения и вывода средств. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/fundingLimits` | +| WS Request | `fundingLimits` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **FundingLimit**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `FundingLimit[]` | Список ограничений операций пополнения и вывода средств. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `paymentOption` | string | Способ пополнения или вывода средств (*Payment Option*). | +| `accountCurrency` | string | Валюта аккаунта (*Account Currency*). | +| `minWithdrawal` | string | Минимально допустимая сумма вывода (*Minimum Withdrawal*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает ограничения, действующие для операций пополнения и вывода средств. +- Ответ представляет собой массив однотипных объектов. +- Вложенные структуры в официальной документации отсутствуют. + +--- + +### leverageSettings + +#### Назначение + +Получение доступных настроек кредитного плеча для указанного торгового инструмента. + +#### Результат операции + +Возвращает доступные значения кредитного плеча для указанного торгового инструмента. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/leverageSettings` | +| WS Request | `leverageSettings` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `symbol` | string | Да | Торговый инструмент. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **LeverageSettings**. + +| Поле | Тип | Описание | +|------|------|----------| +| `values` | `integer[]` | Доступные значения кредитного плеча. | +| `value` | integer | Значение кредитного плеча по умолчанию. | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает настройки кредитного плеча для указанного торгового инструмента. +- Ответ содержит значение кредитного плеча по умолчанию и перечень доступных значений. +- Вложенные структуры в официальной документации отсутствуют. + +--- + +### tradingFees + +#### Назначение + +Получение информации о торговых комиссиях для доступных торговых инструментов. + +#### Результат операции + +Возвращает список торговых комиссий, применяемых биржей Dzengi. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/tradingFees` | +| WS Request | `tradingFees` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **TradingFee**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `TradingFee[]` | Список торговых комиссий. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `name` | string | Наименование торгового инструмента. | +| `fee` | number | Размер торговой комиссии (*Trading Fee*). | +| `overnightRates` | object | Ставки переноса позиции (*Overnight Rates*). | +| `overnightFeeTimestamp` | integer | Время применения ставок переноса (Unix Timestamp, мс). | + +##### overnightRates + +*Структура определяется документацией Dzengi.* + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает торговые комиссии для доступных торговых инструментов. +- Ответ представляет собой массив однотипных объектов. +- Структура объекта `overnightRates` в официальной документации полностью не раскрыта. + +--- + +### tradingLimits + +#### Назначение + +Получение информации о торговых ограничениях для доступных торговых инструментов. + +#### Результат операции + +Возвращает список торговых ограничений, применяемых биржей Dzengi. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/tradingLimits` | +| WS Request | `tradingLimits` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **TradingLimit**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `TradingLimit[]` | Список торговых ограничений. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `name` | string | Наименование торгового инструмента. | +| `lastPrice` | number | Последняя цена (*Last Price*). | +| `minVolume` | number | Минимальный объём сделки (*Minimum Volume*). | +| `maxVolume` | number | Максимальный объём сделки (*Maximum Volume*). | +| `minStep` | number | Минимальный шаг изменения объёма (*Minimum Step*). | +| `tickSize` | number | Размер тика (*Tick Size*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает торговые ограничения для доступных торговых инструментов. +- Ответ представляет собой массив однотипных объектов. +- Вложенные структуры в официальной документации отсутствуют. + +--- + +## Execution Data + +**Назначение** + +Содержит операции, связанные с созданием, изменением, исполнением и закрытием торговых ордеров и позиций. + +**Использование** + +Данные раздела могут использоваться последующими документами Dzentra для реализации торговых операций, управления позициями и получения информации об исполнении ордеров. + +### Command Operations + +- closeTradingPosition +- order.cancel +- order.create +- order.edit +- updateTradingOrder +- updateTradingPosition + +### Query Operations + +- fetchOrder +- myTrades +- openOrders +- tradingPositions +- tradingPositionsHistory + +--- + +### closeTradingPosition + +#### Назначение + +Закрытие открытой торговой позиции. + +#### Результат операции + +Инициирует закрытие торговой позиции и возвращает информацию о результате регистрации запроса. + +#### Режим получения данных + +**Command** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `POST /api/{version}/closeTradingPosition` | +| WS Request | `closeTradingPosition` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `positionId` | string | Да | Идентификатор торговой позиции. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **CloseTradingPositionResponse**. + +| Поле | Тип | Описание | +|------|------|----------| +| `request` | `CloseTradingPositionRequest[]` | Список зарегистрированных запросов на закрытие позиции. | + +#### Структура Payload + +##### request + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | integer | Идентификатор запроса. | +| `accountId` | string / integer | Идентификатор аккаунта. | +| `instrumentId` | string | Идентификатор торгового инструмента. | +| `rqType` | string | Тип запроса (*Request Type*). | +| `state` | string | Состояние обработки запроса (*Request State*). | +| `createdTimestamp` | integer | Время создания запроса (Unix Timestamp, мс). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к командам исполнения торговых операций (*Command*). +- Ответ содержит информацию о зарегистрированном запросе на закрытие позиции. + +--- + +### fetchOrder + +#### Назначение + +Получение информации о торговом ордере по его идентификатору. + +#### Результат операции + +Возвращает информацию о торговом ордере, соответствующем параметрам запроса. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/fetchOrder` | +| WS Request | `fetchOrder` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `symbol` | string | Да | Торговый инструмент. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `orderId` | string | Да | Идентификатор торгового ордера. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **Order**. + +| Поле | Тип | Описание | +|------|------|----------| +| `accountId` | string / integer | Идентификатор аккаунта. | +| `orderId` | string | Идентификатор ордера. | +| `quantity` | number | Количество (*Quantity*). | +| `price` | number | Цена (*Price*). | +| `timestamp` | integer | Время создания ордера (Unix Timestamp, мс). | +| `status` | string | Статус ордера (*Order Status*). | +| `type` | string | Тип ордера (*Order Type*). | +| `expireTime` | integer | Время истечения действия ордера (Unix Timestamp, мс). | +| `timeInForceType` | string | Политика исполнения ордера (*Time In Force*). | +| `side` | string | Сторона ордера (*Order Side*). | +| `guaranteedStopLoss` | boolean | Использование гарантированного Stop Loss (*Guaranteed Stop Loss*). | +| `margin` | number | Требуемая маржа (*Margin*). | +| `takeProfit` | number | Уровень Take Profit. | +| `takeProfitType` | string | Тип Take Profit. | +| `stopLoss` | number | Уровень Stop Loss. | +| `stopLossType` | string | Тип Stop Loss. | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает информацию о торговом ордере по его идентификатору. +- В официальной OpenAPI-спецификации для модели ответа присутствует `resolver error`, поэтому состав полей может быть уточнён после проверки фактического ответа API. + +--- + +### myTrades + +#### Назначение + +Получение информации о сделках пользователя по указанному торговому инструменту. + +#### Результат операции + +Возвращает список сделок пользователя, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/myTrades` | +| WS Request | `myTrades` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `symbol` | string | Да | Торговый инструмент. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **Trade**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `Trade[]` | Список сделок пользователя. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `orderId` | string | Идентификатор ордера (*Order ID*). | +| `price` | string | Цена сделки (*Trade Price*). | +| `qty` | string | Количество (*Quantity*). | +| `quoteQty` | string | Количество в котируемом активе (*Quote Quantity*). | +| `commission` | string | Комиссия (*Commission*). | +| `commissionAsset` | string | Актив комиссии (*Commission Asset*). | +| `time` | integer | Время сделки (Unix Timestamp, мс). | +| `maker` | boolean | Признак Maker (*Maker Flag*). | +| `buyer` | boolean | Признак покупателя (*Buyer Flag*). | +| `isBuyer` | boolean | Признак стороны покупателя (*Is Buyer*). | +| `isMaker` | boolean | Признак стороны Maker (*Is Maker*). | +| `id` | integer | Идентификатор сделки (*Trade ID*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает историю сделок пользователя по указанному торговому инструменту. +- Ответ представляет собой массив однотипных объектов. +- Операция возвращает только сделки авторизованного пользователя. + +--- + +### openOrders + +#### Назначение + +Получение списка открытых торговых ордеров. + +#### Результат операции + +Возвращает список открытых торговых ордеров, соответствующих параметрам запроса. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/openOrders` | +| WS Request | `openOrders` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `symbol` | string | Нет | Торговый инструмент. Если параметр не указан, возвращаются открытые ордера по всем доступным инструментам. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **Order**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `Order[]` | Список открытых торговых ордеров. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `orderId` | string | Идентификатор ордера (*Order ID*). | +| `price` | string | Цена ордера (*Order Price*). | +| `origQty` | string | Исходное количество (*Original Quantity*). | +| `executedQty` | string | Исполненное количество (*Executed Quantity*). | +| `status` | string | Статус ордера (*Order Status*). | +| `timeInForce` | string | Политика исполнения ордера (*Time In Force*). | +| `type` | string | Тип ордера (*Order Type*). | +| `side` | string | Сторона ордера (*Order Side*). | +| `time` | integer | Время создания ордера (Unix Timestamp, мс). | +| `updateTime` | integer | Время последнего обновления ордера (Unix Timestamp, мс). | +| `leverage` | boolean | Признак использования кредитного плеча (*Leverage Flag*). | +| `working` | boolean | Признак активного ордера (*Working Flag*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает только открытые торговые ордера авторизованного пользователя. +- Ответ представляет собой массив однотипных объектов. +- Если параметр `symbol` не указан, документация Dzengi рекомендует использовать операцию с осторожностью, поскольку может быть возвращён полный список открытых ордеров пользователя. + +--- + +### order.cancel + +#### Назначение + +Отмена открытого торгового ордера. + +#### Результат операции + +Инициирует отмену торгового ордера и возвращает информацию о результате обработки запроса. + +#### Режим получения данных + +**Command** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `DELETE /api/{version}/order` | +| WS Request | `order.cancel` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `symbol` | string | Да | Торговый инструмент. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `orderId` | string | Да | Идентификатор торгового ордера. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **Order**. + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `orderId` | string | Идентификатор ордера (*Order ID*). | +| `price` | string | Цена ордера (*Order Price*). | +| `origQty` | string | Исходное количество (*Original Quantity*). | +| `executedQty` | string | Исполненное количество (*Executed Quantity*). | +| `status` | string | Статус ордера (*Order Status*). | +| `timeInForce` | string | Политика исполнения ордера (*Time In Force*). | +| `type` | string | Тип ордера (*Order Type*). | +| `side` | string | Сторона ордера (*Order Side*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к командам исполнения торговых операций (*Command*). +- Выполняет отмену открытого торгового ордера. +- Ответ содержит информацию об ордере после обработки команды отмены. + +--- + +### order.create + +#### Назначение + +Создание нового торгового ордера. + +#### Результат операции + +Инициирует создание торгового ордера и возвращает информацию о результате обработки запроса. + +#### Режим получения данных + +**Command** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `POST /api/{version}/order` | +| WS Request | `order.create` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `symbol` | string | Да | Торговый инструмент. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `type` | string | Да | Тип ордера (*Order Type*). | +| `quantity` | number | Да | Количество (*Quantity*). | +| `side` | string | Да | Сторона ордера (*Order Side*). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | +| `price` | number | Нет | Цена ордера (*Order Price*). | +| `accountId` | string | Нет | Идентификатор торгового аккаунта. | +| `leverage` | integer | Нет | Кредитное плечо (*Leverage*). | +| `stopLoss` | number | Нет | Уровень Stop Loss. | +| `takeProfit` | number | Нет | Уровень Take Profit. | +| `guaranteedStopLoss` | boolean | Нет | Использование гарантированного Stop Loss (*Guaranteed Stop Loss*). | +| `trailingStopLoss` | boolean | Нет | Использование трейлинг Stop Loss (*Trailing Stop Loss*). | +| `newOrderRespType` | string | Нет | Тип возвращаемого ответа (*Response Type*). | +| `expireTimestamp` | integer | Нет | Время окончания действия ордера (Unix Timestamp, мс). | +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | + +#### Payload + +Возвращает объект **Order**. + +| Поле | Тип | Описание | +|------|------|----------| +| `symbol` | string | Торговый инструмент. | +| `orderId` | string | Идентификатор ордера (*Order ID*). | +| `transactTime` | integer | Время регистрации ордера (Unix Timestamp, мс). | +| `price` | string | Цена ордера (*Order Price*). | +| `origQty` | string | Исходное количество (*Original Quantity*). | +| `executedQty` | string | Исполненное количество (*Executed Quantity*). | +| `status` | string | Статус ордера (*Order Status*). | +| `timeInForce` | string | Политика исполнения ордера (*Time In Force*). | +| `type` | string | Тип ордера (*Order Type*). | +| `side` | string | Сторона ордера (*Order Side*). | +| `margin` | number | Требуемая маржа (*Margin*). | +| `stopLoss` | number | Уровень Stop Loss. | +| `takeProfit` | number | Уровень Take Profit. | +| `rejectMessage` | string | Сообщение об отказе (*Reject Message*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к командам исполнения торговых операций (*Command*). +- Выполняет создание нового торгового ордера. +- В зависимости от типа ордера часть параметров запроса может быть необязательной. +- Ответ содержит информацию о зарегистрированном торговом ордере либо сообщение об отказе в его создании. + +--- + +### order.edit + +#### Назначение + +Изменение параметров существующего торгового ордера. + +#### Результат операции + +Инициирует изменение торгового ордера и возвращает информацию о результате обработки запроса. + +#### Режим получения данных + +**Command** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `PUT /api/{version}/order` | +| WS Request | `order.edit` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `orderId` | string | Да | Идентификатор торгового ордера. | +| `price` | number | Нет | Новая цена ордера (*Order Price*). | +| `expireTimestamp` | integer | Нет | Новое время окончания действия ордера (Unix Timestamp, мс). | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **Order**. + +| Поле | Тип | Описание | +|------|------|----------| +| `orderId` | string | Идентификатор изменённого ордера (*Order ID*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к командам исполнения торговых операций (*Command*). +- Позволяет изменить параметры существующего торгового ордера. +- Согласно официальной документации поддерживается изменение цены ордера и времени окончания его действия. +- Ответ содержит идентификатор успешно обработанного ордера. + +--- + +### tradingPositions + +#### Назначение + +Получение списка открытых торговых позиций пользователя. + +#### Результат операции + +Возвращает список открытых торговых позиций пользователя. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/tradingPositions` | +| WS Request | `tradingPositions` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **TradingPositions**. + +| Поле | Тип | Описание | +|------|------|----------| +| `positions` | `Position[]` | Список открытых торговых позиций. | + +#### Структура Payload + +##### positions + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | string | Идентификатор позиции (*Position ID*). | +| `accountId` | string / integer | Идентификатор аккаунта. | +| `symbol` | string | Торговый инструмент. | +| `instrumentId` | string | Идентификатор торгового инструмента. | +| `type` | string | Тип позиции (*Position Type*). | +| `state` | string | Состояние позиции (*Position State*). | +| `openPrice` | number | Цена открытия (*Open Price*). | +| `openQuantity` | number | Объём открытия (*Open Quantity*). | +| `openTimestamp` | integer | Время открытия позиции (Unix Timestamp, мс). | +| `margin` | number | Используемая маржа (*Margin*). | +| `stopLoss` | number | Уровень Stop Loss. | +| `takeProfit` | number | Уровень Take Profit. | +| `swap` | number | Начисленный своп (*Swap*). | +| `fee` | number | Торговая комиссия (*Trading Fee*). | +| `rpl` | number | Реализованная прибыль или убыток (*Realized P/L*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает только открытые торговые позиции авторизованного пользователя. +- Ответ содержит массив объектов, описывающих текущие открытые позиции. +- Состав полей соответствует официальной документации Dzengi и может быть уточнён после проверки фактических ответов API. + +--- + +### tradingPositionsHistory + +#### Назначение + +Получение истории торговых позиций пользователя. + +#### Результат операции + +Возвращает список исторических торговых позиций пользователя, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/tradingPositionsHistory` | +| WS Request | `tradingPositionsHistory` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `symbol` | string | Нет | Торговый инструмент. | +| `from` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `to` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **TradingPositionsHistory**. + +| Поле | Тип | Описание | +|------|------|----------| +| `history` | `Position[]` | Список исторических торговых позиций. | + +#### Структура Payload + +##### history + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | string | Идентификатор позиции (*Position ID*). | +| `accountId` | string / integer | Идентификатор аккаунта. | +| `symbol` | string | Торговый инструмент. | +| `instrumentId` | string | Идентификатор торгового инструмента. | +| `type` | string | Тип позиции (*Position Type*). | +| `state` | string | Состояние позиции (*Position State*). | +| `openPrice` | number | Цена открытия (*Open Price*). | +| `closePrice` | number | Цена закрытия (*Close Price*). | +| `openQuantity` | number | Объём открытия (*Open Quantity*). | +| `closeQuantity` | number | Объём закрытия (*Close Quantity*). | +| `openTimestamp` | integer | Время открытия позиции (Unix Timestamp, мс). | +| `closeTimestamp` | integer | Время закрытия позиции (Unix Timestamp, мс). | +| `margin` | number | Используемая маржа (*Margin*). | +| `fee` | number | Торговая комиссия (*Trading Fee*). | +| `swap` | number | Начисленный своп (*Swap*). | +| `rpl` | number | Реализованная прибыль или убыток (*Realized P/L*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает историю торговых позиций авторизованного пользователя. +- Ответ содержит массив объектов, описывающих закрытые торговые позиции. +- При указании параметров периода возвращаются только позиции, удовлетворяющие заданным условиям. +- Состав полей соответствует официальной документации Dzengi и может быть уточнён после проверки фактических ответов API. + +--- + +### updateTradingOrder + +#### Назначение + +Изменение параметров существующего торгового ордера. + +#### Результат операции + +Инициирует изменение торгового ордера и возвращает информацию о результате обработки запроса. + +#### Режим получения данных + +**Command** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `POST /api/{version}/updateTradingOrder` | +| WS Request | `updateTradingOrder` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `orderId` | string | Да | Идентификатор торгового ордера. | +| `signature` | string | Да | Цифровая подпись запроса. | +| `newPrice` | number | Нет | Новая цена ордера (*New Price*). | +| `stopLoss` | number | Нет | Новый уровень Stop Loss. | +| `takeProfit` | number | Нет | Новый уровень Take Profit. | +| `guaranteedStopLoss` | boolean | Нет | Использование гарантированного Stop Loss (*Guaranteed Stop Loss*). | +| `trailingStopLoss` | boolean | Нет | Использование трейлинг Stop Loss (*Trailing Stop Loss*). | +| `profitDistance` | number | Нет | Расстояние до Take Profit (*Profit Distance*). | +| `stopDistance` | number | Нет | Расстояние до Stop Loss (*Stop Distance*). | +| `expireTimestamp` | integer | Нет | Новое время окончания действия ордера (Unix Timestamp, мс). | +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | + +#### Payload + +Возвращает объект **UpdateTradingOrderResponse**. + +| Поле | Тип | Описание | +|------|------|----------| +| `requestId` | string / integer | Идентификатор запроса. | +| `state` | string | Состояние обработки запроса (*Request State*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к командам исполнения торговых операций (*Command*). +- Позволяет изменить параметры существующего торгового ордера. +- Если параметры `guaranteedStopLoss` или `trailingStopLoss` не переданы, официальная документация Dzengi указывает, что они принимают значение `false`. +- Ответ содержит информацию о зарегистрированном запросе на изменение ордера. + +--- + +### updateTradingPosition + +#### Назначение + +Изменение параметров существующей торговой позиции. + +#### Результат операции + +Инициирует изменение торговой позиции и возвращает информацию о результате обработки запроса. + +#### Режим получения данных + +**Command** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `POST /api/{version}/updateTradingPosition` | +| WS Request | `updateTradingPosition` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `positionId` | string | Да | Идентификатор торговой позиции. | +| `signature` | string | Да | Цифровая подпись запроса. | +| `stopLoss` | number | Нет | Новый уровень Stop Loss. | +| `takeProfit` | number | Нет | Новый уровень Take Profit. | +| `guaranteedStopLoss` | boolean | Нет | Использование гарантированного Stop Loss (*Guaranteed Stop Loss*). | +| `trailingStopLoss` | boolean | Нет | Использование трейлинг Stop Loss (*Trailing Stop Loss*). | +| `profitDistance` | number | Нет | Расстояние до Take Profit (*Profit Distance*). | +| `stopDistance` | number | Нет | Расстояние до Stop Loss (*Stop Distance*). | +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | + +#### Payload + +Возвращает объект **UpdateTradingPositionResponse**. + +| Поле | Тип | Описание | +|------|------|----------| +| `requestId` | string / integer | Идентификатор запроса. | +| `state` | string | Состояние обработки запроса (*Request State*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к командам исполнения торговых операций (*Command*). +- Позволяет изменить параметры существующей торговой позиции. +- Если параметры `guaranteedStopLoss` или `trailingStopLoss` не переданы, официальная документация Dzengi указывает, что они принимают значение `false`. +- Ответ содержит информацию о зарегистрированном запросе на изменение торговой позиции. + +--- + +## Account Data + +**Назначение** + +Содержит операции, связанные с торговым аккаунтом пользователя, его балансами, пополнениями, выводом средств и историей финансовых операций. + +**Использование** + +Данные раздела могут использоваться последующими документами Dzentra для управления торговым аккаунтом, расчёта доступных средств и ведения финансовой истории пользователя. + +--- + +### account + +#### Назначение + +Получение текущей информации о торговом аккаунте пользователя. + +#### Результат операции + +Возвращает текущие параметры аккаунта и список доступных балансов пользователя. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/account` | +| WS Request | `account` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `showZeroBalance` | boolean | Нет | Включить отображение нулевых балансов. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **Account**. + +| Поле | Тип | Описание | +|------|------|----------| +| `makerCommission` | number | Комиссия Maker (*Maker Commission*). | +| `takerCommission` | number | Комиссия Taker (*Taker Commission*). | +| `buyerCommission` | number | Комиссия покупателя (*Buyer Commission*). | +| `sellerCommission` | number | Комиссия продавца (*Seller Commission*). | +| `canTrade` | boolean | Разрешена торговля. | +| `canWithdraw` | boolean | Разрешён вывод средств. | +| `canDeposit` | boolean | Разрешено пополнение счёта. | +| `updateTime` | integer | Время последнего обновления (Unix Timestamp, мс). | +| `userId` | integer | Идентификатор пользователя. | +| `affiliateId` | string | Идентификатор партнёрской программы. | +| `balances` | `Balance[]` | Список балансов пользователя. | + +#### Структура Payload + +##### balances + +| Поле | Тип | Описание | +|------|------|----------| +| `accountId` | string | Идентификатор аккаунта. | +| `asset` | string | Актив. | +| `free` | number | Свободный баланс (*Free Balance*). | +| `locked` | number | Заблокированный баланс (*Locked Balance*). | +| `collateralCurrency` | boolean | Признак обеспечения (*Collateral Currency*). | +| `default` | boolean | Признак основного аккаунта (*Default Account*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает текущее состояние торгового аккаунта пользователя. +- Ответ содержит параметры аккаунта и список балансов. +- Структура массива `balances` полностью описана официальной документацией Dzengi. + +--- + +### depositAddress + +#### Назначение + +Получение адреса для пополнения счёта по указанной валюте. + +#### Результат операции + +Возвращает адрес и дополнительные реквизиты, необходимые для пополнения счёта. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/depositAddress` | +| WS Request | `depositAddress` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `coin` | string | Да | Валюта, для которой требуется получить адрес пополнения. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **DepositAddress**. + +| Поле | Тип | Описание | +|------|------|----------| +| `address` | string | Адрес для пополнения счёта. | +| `addressLegacy` | string | Legacy-адрес для пополнения счёта. | +| `destinationTag` | string | Дополнительный идентификатор перевода (*Destination Tag / Memo*). | + +#### Структура Payload + +Вложенные структуры отсутствуют. + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает реквизиты для пополнения счёта выбранной валютой. +- Для некоторых валют может использоваться дополнительный идентификатор перевода (*Destination Tag / Memo*). +- Вложенные структуры в официальной документации отсутствуют. + +--- + +### deposits + +#### Назначение + +Получение истории операций пополнения счёта пользователя. + +#### Результат операции + +Возвращает список операций пополнения счёта, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/deposits` | +| WS Request | `deposits` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **Deposit**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `Deposit[]` | Список операций пополнения счёта. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | integer | Идентификатор операции. | +| `balance` | number | Баланс после выполнения операции (*Balance*). | +| `amount` | number | Сумма операции (*Amount*). | +| `currency` | string | Валюта операции (*Currency*). | +| `type` | string | Тип операции (*Operation Type*). | +| `timestamp` | integer | Время выполнения операции (Unix Timestamp, мс). | +| `commission` | number | Комиссия (*Commission*). | +| `paymentMethod` | string | Способ проведения операции (*Payment Method*). | +| `status` | string | Статус операции (*Operation Status*). | +| `blockchainTransactionHash` | string | Хэш транзакции в блокчейне (*Blockchain Transaction Hash*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает историю пополнений авторизованного пользователя. +- Ответ представляет собой массив однотипных объектов. +- Состав полей соответствует официальной документации Dzengi и может быть уточнён после проверки фактических ответов API. + +--- + +### ledger + +#### Назначение + +Получение истории операций по бухгалтерскому журналу (*Ledger*) пользователя. + +#### Результат операции + +Возвращает список записей бухгалтерского журнала, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/ledger` | +| WS Request | `ledger` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **LedgerEntry**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `LedgerEntry[]` | Список записей бухгалтерского журнала. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | integer | Идентификатор записи. | +| `balance` | number | Баланс после выполнения операции (*Balance*). | +| `amount` | number | Сумма операции (*Amount*). | +| `currency` | string | Валюта операции (*Currency*). | +| `type` | string | Тип операции (*Operation Type*). | +| `timestamp` | integer | Время выполнения операции (Unix Timestamp, мс). | +| `commission` | number | Комиссия (*Commission*). | +| `status` | string | Статус операции (*Operation Status*). | +| `paymentMethod` | string | Способ проведения операции (*Payment Method*). | +| `blockchainTransactionHash` | string | Хэш транзакции в блокчейне (*Blockchain Transaction Hash*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает бухгалтерский журнал (*Ledger*) авторизованного пользователя. +- Ответ представляет собой массив однотипных объектов. +- Назначение отдельных типов операций (`type`) определяется правилами учёта Dzengi и выходит за рамки настоящего документа. +- Состав полей соответствует официальной документации Dzengi и может быть уточнён после проверки фактических ответов API. + +--- + +### transactions + +#### Назначение + +Получение истории финансовых транзакций пользователя. + +#### Результат операции + +Возвращает список финансовых транзакций, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/transactions` | +| WS Request | `transactions` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает объект **Transactions**. + +| Поле | Тип | Описание | +|------|------|----------| +| `transactions` | `Transaction[]` | Список финансовых транзакций. | + +#### Структура Payload + +##### transactions + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | integer | Идентификатор транзакции. | +| `balance` | number | Баланс после выполнения операции (*Balance*). | +| `amount` | number | Сумма операции (*Amount*). | +| `currency` | string | Валюта операции (*Currency*). | +| `type` | string | Тип операции (*Operation Type*). | +| `timestamp` | integer | Время выполнения операции (Unix Timestamp, мс). | +| `commission` | number | Комиссия (*Commission*). | +| `paymentMethod` | string | Способ проведения операции (*Payment Method*). | +| `status` | string | Статус операции (*Operation Status*). | +| `blockchainTransactionHash` | string | Хэш транзакции в блокчейне (*Blockchain Transaction Hash*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает историю финансовых транзакций авторизованного пользователя. +- Ответ содержит массив объектов, описывающих отдельные финансовые транзакции. +- Назначение отдельных типов операций (`type`) определяется правилами учёта Dzengi и выходит за рамки настоящего документа. +- Состав полей соответствует официальной документации Dzengi и может быть уточнён после проверки фактических ответов API. + +--- + +### withdrawals + +#### Назначение + +Получение истории операций вывода средств пользователя. + +#### Результат операции + +Возвращает список операций вывода средств, соответствующих параметрам запроса. + +#### Режим получения данных + +**Historical** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| REST | `GET /api/{version}/withdrawals` | +| WS Request | `withdrawals` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| REST API v1 | Да | +| REST API v2 | Да | + +#### Параметры запроса + +| Параметр | Тип | Обязательный | Описание | +|----------|-----|--------------|----------| +| `recvWindow` | integer | Нет | Допустимое окно времени обработки запроса. | +| `timestamp` | integer | Да | Время формирования запроса (Unix Timestamp, мс). | +| `X-MBX-APIKEY` | string | Да | API-ключ пользователя. | +| `startTime` | integer | Нет | Начало периода (Unix Timestamp, мс). | +| `endTime` | integer | Нет | Конец периода (Unix Timestamp, мс). | +| `limit` | integer | Нет | Максимальное количество возвращаемых записей. | +| `signature` | string | Да | Цифровая подпись запроса. | + +#### Payload + +Возвращает массив объектов типа **Withdrawal**. + +| Поле | Тип | Описание | +|------|------|----------| +| `[]` | `Withdrawal[]` | Список операций вывода средств. | + +#### Структура Payload + +| Поле | Тип | Описание | +|------|------|----------| +| `id` | integer | Идентификатор операции. | +| `balance` | number | Баланс после выполнения операции (*Balance*). | +| `amount` | number | Сумма операции (*Amount*). | +| `currency` | string | Валюта операции (*Currency*). | +| `type` | string | Тип операции (*Operation Type*). | +| `timestamp` | integer | Время выполнения операции (Unix Timestamp, мс). | +| `commission` | number | Комиссия (*Commission*). | +| `paymentMethod` | string | Способ проведения операции (*Payment Method*). | +| `status` | string | Статус операции (*Operation Status*). | +| `blockchainTransactionHash` | string | Хэш транзакции в блокчейне (*Blockchain Transaction Hash*). | + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Требуется | +| API Key | Требуется | +| Подпись запроса | Требуется | +| Timestamp | Требуется | +| recvWindow | Поддерживается | +| Snapshot | Нет | +| Historical | Да | +| Stream | Нет | +| REST и WS Request эквивалентны | Да | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция возвращает историю операций вывода средств авторизованного пользователя. +- Ответ представляет собой массив однотипных объектов. +- Состав полей соответствует официальной документации Dzengi и может быть уточнён после проверки фактических ответов API. + +--- + +## Infrastructure Operations + +**Назначение** + +Содержит служебные операции API, необходимые для обслуживания соединения и проверки доступности интерфейсов Dzengi. + +**Использование** + +Данные раздела могут использоваться последующими документами Dzentra для проектирования инфраструктурной части API-клиента. + +--- + +### ping + +#### Назначение + +Проверка доступности WebSocket-соединения. + +#### Результат операции + +Возвращает служебный ответ, подтверждающий доступность соединения. + +#### Режим получения данных + +**Snapshot** + +#### Доступные вызовы + +| Способ | Путь | +|--------|------| +| WS Service | `ping` | + +#### Версии API + +| Версия | Поддержка | +|---------|-----------| +| WebSocket API | Да | + +#### Параметры запроса + +Отсутствуют. + +#### Payload + +*Структура определяется документацией Dzengi.* + +#### Структура Payload + +*Структура определяется документацией Dzengi.* + +#### Характеристики операции + +| Свойство | Значение | +|----------|----------| +| Авторизация | Не требуется | +| API Key | Не требуется | +| Подпись запроса | Не требуется | +| Timestamp | Не требуется | +| recvWindow | Не поддерживается | +| Snapshot | Да | +| Historical | Нет | +| Stream | Нет | +| REST и WS Request эквивалентны | Нет | + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +- Операция относится к служебным операциям WebSocket API. +- Операция не является WebSocket Stream. +- Структура ответа требует проверки по документации или фактическому ответу API. \ No newline at end of file diff --git a/docs/market_intelligence/information/dzengi_market_intelligence_information_mapping.md b/docs/market_intelligence/information/dzengi_market_intelligence_information_mapping.md new file mode 100644 index 0000000..239fbc8 --- /dev/null +++ b/docs/market_intelligence/information/dzengi_market_intelligence_information_mapping.md @@ -0,0 +1,246 @@ +# Dzengi Market Intelligence Information Mapping + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Dzengi Market Intelligence Information Mapping | +| Тип документа | Information Mapping | +| Версия | 1.0 | +| Статус | Draft | +| Степень верификации | Documentation Based | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Источник | Dzengi API | +| Язык | Русский | + +--- + +## Статус документа + +Настоящий документ определяет соответствие между техническими данными, предоставляемыми API Dzengi, и информационными сущностями подсистемы Market Intelligence. + +Документ построен на основании **Dzengi Market Data Inventory** и не содержит интерпретации состояния рынка. + +До выпуска версии Release допускается уточнение состава информационных сущностей и их соответствия данным API по результатам проверки фактических ответов биржи. + +--- + +## Степень верификации + +Настоящая редакция документа построена на основании: + +- Dzengi Market Data Inventory; +- официальной документации Dzengi; +- OpenAPI (Swagger); +- официальных примеров запросов; +- официальных примеров ответов. + +Фактическая проверка информационных сущностей посредством выполнения реальных запросов к бирже Dzengi на момент подготовки настоящей редакции документа не проводилась. + +По этой причине: + +- соответствие информационных сущностей данным API считается соответствующим официальной документации; +- состав используемых полей считается соответствующим официальной документации; +- описание информационных сущностей основано исключительно на опубликованной спецификации API. + +Настоящий документ имеет статус **Documentation Verified**. + +После проверки информационных сущностей посредством выполнения реальных запросов и анализа фактических ответов API документ получит статус **Runtime Verified**. + +Все расхождения между документацией и фактическим поведением API должны фиксироваться в последующих редакциях настоящего документа. + +--- + +## Назначение + +Настоящий документ предназначен для формирования единого слоя информационных сущностей Market Intelligence на основании возможностей API Dzengi. + +Документ определяет: + +- какие информационные сущности могут быть получены из Dzengi; +- какие информационные сущности могут быть автоматически вычислены; +- какие поля API являются источниками каждой информационной сущности; +- канонические имена информационных сущностей; +- канонические идентификаторы информационных сущностей. + +Настоящий документ не определяет: + +- знания о рынке; +- интерпретацию информации; +- алгоритмы анализа рынка; +- архитектуру Engine; +- торговые стратегии; +- правила принятия торговых решений. + +--- + +## Цель + +Цель настоящего документа — сформировать полный каталог информационных сущностей, потенциально доступных подсистеме Market Intelligence при использовании API Dzengi. + +--- + +## Основной вопрос документа + +Настоящий документ отвечает исключительно на следующий вопрос. + +> **Какие информационные сущности Market Intelligence могут быть получены или автоматически вычислены на основании данных Dzengi API?** + +--- + +## Источник информации + +Настоящий документ построен на основании следующих источников. + +- Dzengi Market Data Inventory; +- REST API Documentation; +- WebSocket Request API Documentation; +- WebSocket Stream API Documentation; +- OpenAPI (Swagger) Specification; +- официальных примеров запросов; +- официальных примеров ответов. + +Настоящий документ не использует предположения о состоянии рынка и не содержит результатов аналитической интерпретации данных. + +--- + +## Связь с архитектурой Dzentra + +Настоящий документ является вторым уровнем архитектуры знаний подсистемы Market Intelligence. + +```text +Dzengi API + │ + ▼ +Dzengi Market Data Inventory + │ + ▼ +Dzengi Market Intelligence Information Mapping + │ + ▼ +Dzentra Market Intelligence Information Model + │ + ▼ +Dzentra Market Knowledge Catalogue +``` + +--- + +## Границы документа + +Настоящий документ описывает исключительно информационные сущности, относящиеся к подсистеме Market Intelligence. + +Документ не содержит: + +- описания операций API; +- внутренних моделей данных Dzentra; +- знаний о рынке; +- правил анализа; +- правил торговли; +- архитектуры Engine. + +--- + +## Термины и определения + +*Derived* — информация, автоматически вычисляемая на основании одной или нескольких информационных сущностей. + +*Endpoint* — операция API, предоставляющая доступ к данным или сервису. + +*Information ID* — канонический идентификатор информационной сущности, используемый во всей платформе Dzentra. + +*JSON Path* — путь к полю внутри JSON-ответа API. + +*Observed* — информация, непосредственно получаемая из внешнего источника данных без дополнительных вычислений. + +--- + +## Правила Mapping + +--- + +## Стандарт спецификации информационной сущности + +Все информационные сущности настоящего документа описываются по единому шаблону. + +```text +### + +#### Определение + +... + +#### Тип информации + +Observed | Derived + +#### Источники информации + +| Endpoint | Поле ответа (JSON Path) | Контекст получения | +|----------|-------------------------|--------------------| + +#### Зависимости + +... + +#### Использование в Dzentra + +... + +#### Примечания + +... +``` + +--- + +## Содержание + +--- + +## Информационные сущности + +### best_ask_price + +#### Определение + +Лучшая доступная цена продажи (Ask) финансового инструмента в текущем состоянии рынка. + +#### Тип информации + +Observed + +#### Статус исследования + +🟡 Частично подтверждено. + +#### Подтвержденные источники + +| Транспорт | Endpoint | Поле ответа (JSON Path) | +|-----------|----------|-------------------------| +| REST | `/api/v1/depth` | `asks[0][0]` | +| WSS Request | `/api/v1/depth` | `payload.asks[0][0]` | +| WSS Stream | `marketData.subscribe` | `payload.ofr` | + +#### Исследуемые источники + +| Транспорт | Endpoint | Поле ответа (JSON Path) | Статус | +|-----------|----------|-------------------------|--------| +| REST | `/api/v1/ticker/24hr` | `askPrice` | Требует подтверждения эквивалентности | + +#### Зависимости + +Отсутствуют. + +#### Использование в Dzentra + +> **Не определяется настоящим документом.** + +#### Примечания + +Настоящая информационная сущность формируется по результатам исследования фактических ответов API Dzengi. + +Подтверждено, что REST `/api/v1/depth`, WSS Request `/api/v1/depth` и WSS Stream `marketData.subscribe` предоставляют одну и ту же информацию о лучшей цене продажи, отличаясь только способом получения данных. + +Эквивалентность поля `askPrice` endpoint `/api/v1/ticker/24hr` на момент подготовки настоящей редакции документа не подтверждена и требует дополнительного исследования. \ No newline at end of file diff --git a/docs/market_intelligence/information/dzengi_openapi.json b/docs/market_intelligence/information/dzengi_openapi.json new file mode 100644 index 0000000..15bc30d --- /dev/null +++ b/docs/market_intelligence/information/dzengi_openapi.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"Api Documentation","termsOfService":"https://dzengi.com/agreement"},"host":"https://api-adapter.dzengi.com/ ]\n[ Base demo URL: https://demo-api-adapter.dzengi.com","basePath":"/","tags":[{"name":"rest-api","description":"Rest API"},{"name":"websocket-api","description":"WebSocket API"}],"paths":{"/api/v1/account":{"get":{"tags":["rest-api"],"summary":"accountInfo","description":"Get current account information","operationId":"accountUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"showZeroBalance","in":"query","description":"showZeroBalance","required":false,"type":"boolean"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/AccountResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/aggTrades":{"get":{"tags":["rest-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"aggTradesUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}","schema":{"type":"array","items":{"$ref":"#/definitions/AggTrades"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/closeTradingPosition":{"post":{"tags":["rest-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"closeTradingPositionUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/currencies":{"get":{"tags":["rest-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"getCurrenciesUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}","schema":{"$ref":"#/definitions/CurrencyResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/depositAddress":{"get":{"tags":["rest-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"getDepositAddressUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"coin","in":"query","description":"coin","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/deposits":{"get":{"tags":["rest-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"getDepositsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/depth":{"get":{"tags":["rest-api"],"summary":"orderBook","description":"Order book","operationId":"depthUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}","schema":{"$ref":"#/definitions/DepthResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/exchangeInfo":{"get":{"tags":["rest-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"exchangeInfoUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":false,"type":"string"},{"name":"signature","in":"query","description":"signature","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/fetchOrder":{"get":{"tags":["rest-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"getOrderUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/fundingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"getFundingLimitsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/FundingLimitsDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/klines":{"get":{"tags":["rest-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"klinesUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"interval","in":"query","description":"interval","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"priceType","in":"query","description":"priceType","required":false,"type":"string","default":"bid"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"},{"name":"type","in":"query","description":"type","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/ledger":{"get":{"tags":["rest-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"getLedgerUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/leverageSettings":{"get":{"tags":["rest-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"leverageSettingsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/myTrades":{"get":{"tags":["rest-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"myTradesUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/openOrders":{"get":{"tags":["rest-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"openOrdersUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/order":{"post":{"tags":["rest-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"orderUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"newOrderRespType","in":"query","description":"newOrderRespType in the exchange trading mode for MARKET order RESULT or FULL can be mentioned. MARKET order type default to FULL. LIMIT order type can be only RESULT. For the leverage trading mode only RESULT is available.","required":false,"type":"string","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"type","in":"query","description":"Type MARKET or LIMIT should be mentioned to open an order in the exchange trading mode. Type MARKET, LIMIT or STOP should be mentioned to open an order in the leverage trading mode.","required":true,"type":"string","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"accountId","in":"query","description":"accountId","required":false,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean"},{"name":"leverage","in":"query","description":"leverage","required":false,"type":"integer","format":"int32"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"quantity","in":"query","description":"quantity","required":true,"type":"number"},{"name":"side","in":"query","description":"side","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["rest-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"putEditOrderUsingPUT_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["rest-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"cancelOrderUsingDELETE_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/CancelOrderResponse"}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/api/v1/ticker/24hr":{"get":{"tags":["rest-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"ticker_24hrUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/time":{"get":{"tags":["rest-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"timeUsingGET_1","produces":["*/*"],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}","schema":{"$ref":"#/definitions/ServerTime"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingFees":{"get":{"tags":["rest-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"getTradingFeesUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingFeesResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"getTradingLimitsUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingLimitsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingPositions":{"get":{"tags":["rest-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"tradingPositionsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionListResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingPositionsHistory":{"get":{"tags":["rest-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"tradingPositionsHistoryUsingGET_1","produces":["*/*"],"parameters":[{"name":"from","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"to","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/transactions":{"get":{"tags":["rest-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"getTransactionsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/updateTradingOrder":{"post":{"tags":["rest-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingOrderUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"newPrice","in":"query","description":"newPrice","required":false,"type":"number"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/updateTradingPosition":{"post":{"tags":["rest-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingPositionUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string","format":"uuid"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/withdrawals":{"get":{"tags":["rest-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"getWithdrawalsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/account":{"get":{"tags":["rest-api"],"summary":"accountInfo","description":"Get current account information","operationId":"accountUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"showZeroBalance","in":"query","description":"showZeroBalance","required":false,"type":"boolean"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/AccountResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/aggTrades":{"get":{"tags":["rest-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"aggTradesUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}","schema":{"type":"array","items":{"$ref":"#/definitions/AggTrades"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/closeTradingPosition":{"post":{"tags":["rest-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"closeTradingPositionUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/currencies":{"get":{"tags":["rest-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"getCurrenciesUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}","schema":{"$ref":"#/definitions/CurrencyResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/depositAddress":{"get":{"tags":["rest-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"getDepositAddressUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"coin","in":"query","description":"coin","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/deposits":{"get":{"tags":["rest-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"getDepositsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/depth":{"get":{"tags":["rest-api"],"summary":"orderBook","description":"Order book","operationId":"depthUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}","schema":{"$ref":"#/definitions/DepthResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/exchangeInfo":{"get":{"tags":["rest-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"exchangeInfoUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":false,"type":"string"},{"name":"signature","in":"query","description":"signature","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/fetchOrder":{"get":{"tags":["rest-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"getOrderUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/fundingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"getFundingLimitsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/FundingLimitsDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/klines":{"get":{"tags":["rest-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"klinesUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"interval","in":"query","description":"interval","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"priceType","in":"query","description":"priceType","required":false,"type":"string","default":"bid"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"},{"name":"type","in":"query","description":"type","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/ledger":{"get":{"tags":["rest-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"getLedgerUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/leverageSettings":{"get":{"tags":["rest-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"leverageSettingsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/myTrades":{"get":{"tags":["rest-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"myTradesUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/openOrders":{"get":{"tags":["rest-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"openOrdersUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/order":{"post":{"tags":["rest-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"orderUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"newOrderRespType","in":"query","description":"newOrderRespType in the exchange trading mode for MARKET order RESULT or FULL can be mentioned. MARKET order type default to FULL. LIMIT order type can be only RESULT. For the leverage trading mode only RESULT is available.","required":false,"type":"string","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"type","in":"query","description":"Type MARKET or LIMIT should be mentioned to open an order in the exchange trading mode. Type MARKET, LIMIT or STOP should be mentioned to open an order in the leverage trading mode.","required":true,"type":"string","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"accountId","in":"query","description":"accountId","required":false,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean"},{"name":"leverage","in":"query","description":"leverage","required":false,"type":"integer","format":"int32"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"quantity","in":"query","description":"quantity","required":true,"type":"number"},{"name":"side","in":"query","description":"side","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["rest-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"putEditOrderUsingPUT","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["rest-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"cancelOrderUsingDELETE","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/CancelOrderResponse"}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/api/v2/ticker/24hr":{"get":{"tags":["rest-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"ticker_24hrUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/time":{"get":{"tags":["rest-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"timeUsingGET","produces":["*/*"],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}","schema":{"$ref":"#/definitions/ServerTime"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingFees":{"get":{"tags":["rest-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"getTradingFeesUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingFeesResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"getTradingLimitsUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingLimitsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingPositions":{"get":{"tags":["rest-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"tradingPositionsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionListResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingPositionsHistory":{"get":{"tags":["rest-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"tradingPositionsHistoryUsingGET","produces":["*/*"],"parameters":[{"name":"from","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"to","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/transactions":{"get":{"tags":["rest-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"getTransactionsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/updateTradingOrder":{"post":{"tags":["rest-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingOrderUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"newPrice","in":"query","description":"newPrice","required":false,"type":"number"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/updateTradingPosition":{"post":{"tags":["rest-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingPositionUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string","format":"uuid"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/withdrawals":{"get":{"tags":["rest-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"getWithdrawalsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"wss:/api/v1/account":{"get":{"tags":["websocket-api"],"summary":"accountInfo","description":"Get current account information","operationId":"websocketmethods_53","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AccountRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/AccountResponse"}}}}},"wss:/api/v1/aggTrades":{"get":{"tags":["websocket-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"websocketmethods_7","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AggTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AggTradesResponse"}}}}},"wss:/api/v1/closeTradingPosition":{"get":{"tags":["websocket-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"websocketmethods_13","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CloseTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}}}}},"wss:/api/v1/currencies":{"get":{"tags":["websocket-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"websocketmethods_20","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/CurrencyResponse"}}}}},"wss:/api/v1/depositAddress":{"get":{"tags":["websocket-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"websocketmethods_3","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BlockchainAddressRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}\n","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}}}}},"wss:/api/v1/deposits":{"get":{"tags":["websocket-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"websocketmethods_4","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v1/depth":{"get":{"tags":["websocket-api"],"summary":"orderBook","description":"Order book","operationId":"websocketmethods_12","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/DepthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/DepthResponse"}}}}},"wss:/api/v1/exchangeInfo":{"get":{"tags":["websocket-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"websocketmethods_22","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/OptionalAuthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/ExchangeInfo"}}}}},"wss:/api/v1/fetchOrder":{"get":{"tags":["websocket-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"websocketmethods","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/GetOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}}}}},"wss:/api/v1/fundingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"websocketmethods_54","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/FundingLimitsDtoResponseWS"}}}}},"wss:/api/v1/klines":{"get":{"tags":["websocket-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"websocketmethods_15","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/KLinesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}\n","schema":{"$ref":"#/definitions/KLinesResponse"}}}}},"wss:/api/v1/ledger":{"get":{"tags":["websocket-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"websocketmethods_2","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v1/leverageSettings":{"get":{"tags":["websocket-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"websocketmethods_19","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/LeverageSettingsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}\n","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}}}}},"wss:/api/v1/myTrades":{"get":{"tags":["websocket-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"websocketmethods_14","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AllMyTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AllMyTradesResponse"}}}}},"wss:/api/v1/openOrders":{"get":{"tags":["websocket-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"websocketmethods_9","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedBySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/OpenOrdersReponse"}}}}},"wss:/api/v1/order/cancel":{"get":{"tags":["websocket-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"websocketmethods_52","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CancelOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/CancelOrderResponse"}}}}},"wss:/api/v1/order/create":{"get":{"tags":["websocket-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"websocketmethods_21","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CreateOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}}}}},"wss:/api/v1/order/edit":{"get":{"tags":["websocket-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"websocketmethods_51","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EditExchangeOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}}}}},"wss:/api/v1/ticker/24hr":{"get":{"tags":["websocket-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"websocketmethods_8","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}\n","schema":{"$ref":"#/definitions/Ticker24HResponse"}}}}},"wss:/api/v1/time":{"get":{"tags":["websocket-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"websocketmethods_17","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EmptyRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}\n","schema":{"$ref":"#/definitions/ServerTime"}}}}},"wss:/api/v1/tradingFees":{"get":{"tags":["websocket-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"websocketmethods_10","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingFeesResponseWS"}}}}},"wss:/api/v1/tradingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"websocketmethods_55","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingLimitsResponseWS"}}}}},"wss:/api/v1/tradingPositions":{"get":{"tags":["websocket-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"websocketmethods_6","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionListResponse"}}}}},"wss:/api/v1/tradingPositionsHistory":{"get":{"tags":["websocket-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"websocketmethods_11","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/PositionHistoryRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}}}}},"wss:/api/v1/transactions":{"get":{"tags":["websocket-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"websocketmethods_5","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v1/updateTradingOrder":{"get":{"tags":["websocket-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_18","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}}}}},"wss:/api/v1/updateTradingPosition":{"get":{"tags":["websocket-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_1","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}}}}},"wss:/api/v1/withdrawals":{"get":{"tags":["websocket-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"websocketmethods_16","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/account":{"get":{"tags":["websocket-api"],"summary":"accountInfo","description":"Get current account information","operationId":"websocketmethods_33","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AccountRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/AccountResponse"}}}}},"wss:/api/v2/aggTrades":{"get":{"tags":["websocket-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"websocketmethods_46","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AggTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AggTradesResponse"}}}}},"wss:/api/v2/closeTradingPosition":{"get":{"tags":["websocket-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"websocketmethods_30","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CloseTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}}}}},"wss:/api/v2/currencies":{"get":{"tags":["websocket-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"websocketmethods_37","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/CurrencyResponse"}}}}},"wss:/api/v2/depositAddress":{"get":{"tags":["websocket-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"websocketmethods_32","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BlockchainAddressRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}\n","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}}}}},"wss:/api/v2/deposits":{"get":{"tags":["websocket-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"websocketmethods_24","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/depth":{"get":{"tags":["websocket-api"],"summary":"orderBook","description":"Order book","operationId":"websocketmethods_49","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/DepthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/DepthResponse"}}}}},"wss:/api/v2/exchangeInfo":{"get":{"tags":["websocket-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"websocketmethods_40","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/OptionalAuthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/ExchangeInfo"}}}}},"wss:/api/v2/fetchOrder":{"get":{"tags":["websocket-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"websocketmethods_42","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/GetOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}}}}},"wss:/api/v2/fundingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"websocketmethods_43","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/FundingLimitsDtoResponseWS"}}}}},"wss:/api/v2/klines":{"get":{"tags":["websocket-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"websocketmethods_27","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/KLinesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}\n","schema":{"$ref":"#/definitions/KLinesResponse"}}}}},"wss:/api/v2/ledger":{"get":{"tags":["websocket-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"websocketmethods_47","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/leverageSettings":{"get":{"tags":["websocket-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"websocketmethods_31","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/LeverageSettingsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}\n","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}}}}},"wss:/api/v2/myTrades":{"get":{"tags":["websocket-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"websocketmethods_48","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AllMyTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AllMyTradesResponse"}}}}},"wss:/api/v2/openOrders":{"get":{"tags":["websocket-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"websocketmethods_26","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedBySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/OpenOrdersReponse"}}}}},"wss:/api/v2/order/cancel":{"get":{"tags":["websocket-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"websocketmethods_41","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CancelOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/CancelOrderResponse"}}}}},"wss:/api/v2/order/create":{"get":{"tags":["websocket-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"websocketmethods_44","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CreateOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}}}}},"wss:/api/v2/order/edit":{"get":{"tags":["websocket-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"websocketmethods_50","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EditExchangeOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}}}}},"wss:/api/v2/ticker/24hr":{"get":{"tags":["websocket-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"websocketmethods_25","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}\n","schema":{"$ref":"#/definitions/Ticker24HResponse"}}}}},"wss:/api/v2/time":{"get":{"tags":["websocket-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"websocketmethods_28","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EmptyRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}\n","schema":{"$ref":"#/definitions/ServerTime"}}}}},"wss:/api/v2/tradingFees":{"get":{"tags":["websocket-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"websocketmethods_38","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingFeesResponseWS"}}}}},"wss:/api/v2/tradingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"websocketmethods_35","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingLimitsResponseWS"}}}}},"wss:/api/v2/tradingPositions":{"get":{"tags":["websocket-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"websocketmethods_39","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionListResponse"}}}}},"wss:/api/v2/tradingPositionsHistory":{"get":{"tags":["websocket-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"websocketmethods_36","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/PositionHistoryRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}}}}},"wss:/api/v2/transactions":{"get":{"tags":["websocket-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"websocketmethods_23","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/updateTradingOrder":{"get":{"tags":["websocket-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_29","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}}}}},"wss:/api/v2/updateTradingPosition":{"get":{"tags":["websocket-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_45","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}}}}},"wss:/api/v2/withdrawals":{"get":{"tags":["websocket-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"websocketmethods_34","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:OHLCMarketData.subscribe":{"get":{"tags":["websocket-api"],"summary":"OHLCMarketData","description":"OHLC market data stream","operationId":"websocketmethods_58","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/OHLCSubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"status\":\"OK\",\n \"Destination\":\"ohlc.event\",\n \"Payload\":{\n \"interval\":\"1m\",\n \"symbol\":\"TS\",\n \"T\":1597850100000,\n \"H\":11.89,\n \"L\":11.88,\n \"O\":11.89,\n \"C\":11.89\n }\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}},"wss:depthMarketData.subscribe":{"get":{"tags":["websocket-api"],"summary":"DepthMarketData","description":"Depth market data stream","operationId":"websocketmethods_56","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"Destination\":\"marketdepth.event\",\n \"Payload\":{\n \"Data\":{\n \"ts\":1597849462575,\n \"Bid\":{\n \"2\":25,\n \"1.94\":25.9\n },\n \"Ofr\":{\n \"3.3\":1,\n \"2.627\":6.1\n }\n },\n \"symbol\":\"Natural Gas\"\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}},"wss:marketData.subscribe":{"get":{"tags":["websocket-api"],"summary":"MarketData","description":"Market data stream","operationId":"websocketmethods_57","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"Destination\":\"internal.quote\",\n \"Payload\":{\n \"symbolName\":\"TXN\",\n \"bid\":139.85,\n \"bidQty\":2500,\n \"ofr\":139.92000000000002,\n \"ofrQty\":2500,\n \"timestamp\":1597850971558\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}},"wss:ping":{"get":{"tags":["websocket-api"],"summary":"ping","description":"Ping pong","operationId":"websocketmethods_60","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/PingRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n {} \n","schema":{"$ref":"#/definitions/PingResponse"}}}}},"wss:trades.subscribe":{"get":{"tags":["websocket-api"],"summary":"Trades","description":"Trades stream","operationId":"websocketmethods_59","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"destination\":\"internal.trade\",\n \"payload\":{\n \"price\":11400.95,\n \"size\":0.058,\n \"id\":1616651347,\n \"ts\":1596625079952,\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004020316a\",\n \"clientOrderId\":\"00a02503-0079-54c4-0000-482f00003a06\",\n \"buyer\":true\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}}},"definitions":{"AccountBalance":{"type":"object","properties":{"accountId":{"type":"string"},"asset":{"type":"string"},"collateralCurrency":{"type":"boolean"},"default":{"type":"boolean"},"free":{"type":"number"},"locked":{"type":"number"}},"title":"AccountBalance"},"AccountRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"showZeroBalance":{"type":"boolean"},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"AccountRequest"},"AccountResponse":{"type":"object","properties":{"affiliateId":{"type":"string"},"balances":{"type":"array","items":{"$ref":"#/definitions/AccountBalance"}},"buyerCommission":{"type":"number"},"canDeposit":{"type":"boolean"},"canTrade":{"type":"boolean"},"canWithdraw":{"type":"boolean"},"makerCommission":{"type":"number"},"sellerCommission":{"type":"number"},"takerCommission":{"type":"number"},"updateTime":{"type":"integer","format":"int64"},"userId":{"type":"integer","format":"int64"}},"title":"AccountResponse"},"AggTrades":{"type":"object","properties":{"T":{"type":"integer","format":"int64"},"a":{"type":"integer","format":"int64"},"m":{"type":"boolean"},"p":{"type":"string"},"q":{"type":"string"}},"title":"AggTrades"},"AggTradesRequest":{"type":"object","required":["symbol"],"properties":{"endTime":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"startTime":{"type":"integer","format":"int64"},"symbol":{"type":"string"}},"title":"AggTradesRequest"},"AggTradesResponse":{"type":"object","properties":{"aggTrades":{"type":"array","items":{"$ref":"#/definitions/AggTrades"}}},"title":"AggTradesResponse"},"AllMyTradesRequest":{"type":"object","required":["apiKey","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"endTime":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"startTime":{"type":"integer","format":"int64"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"AllMyTradesRequest"},"AllMyTradesResponse":{"type":"object","properties":{"myTrades":{"type":"array","items":{"$ref":"#/definitions/MyTradesResponse"}}},"title":"AllMyTradesResponse"},"BlockchainAddressGetResponse":{"type":"object","properties":{"address":{"type":"string"},"addressLegacy":{"type":"string"},"destinationTag":{"type":"string"}},"title":"BlockchainAddressGetResponse"},"BlockchainAddressRequest":{"type":"object","required":["apiKey","coin","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"coin":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"BlockchainAddressRequest"},"BySymbolRequest":{"type":"object","properties":{"symbol":{"type":"string"}},"title":"BySymbolRequest"},"CancelOrderRequest":{"type":"object","required":["apiKey","orderId","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"orderId":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"CancelOrderRequest"},"CancelOrderResponse":{"type":"object","properties":{"executedQty":{"type":"string"},"orderId":{"type":"string"},"origQty":{"type":"string"},"price":{"type":"string"},"side":{"type":"string","enum":["BUY","SELL"]},"status":{"type":"string","enum":["CANCELED","EXPIRED","FILLED","NEW","PARTIALLY_FILLED","PENDING_CANCEL","REJECTED"]},"symbol":{"type":"string"},"timeInForce":{"type":"string","enum":["FOK","GTC","IOC"]},"type":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]}},"title":"CancelOrderResponse"},"CloseTradingPositionRequest":{"type":"object","required":["apiKey","positionId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"positionId":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"CloseTradingPositionRequest"},"CreateOrderRequest":{"type":"object","required":["apiKey","quantity","side","signature","symbol","timestamp","type"],"properties":{"accountId":{"type":"integer","format":"int64"},"apiKey":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"leverage":{"type":"integer","format":"int32"},"newOrderRespType":{"type":"string"},"price":{"type":"number"},"profitDistance":{"type":"number"},"quantity":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"side":{"type":"string"},"signature":{"type":"string"},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string"}},"title":"CreateOrderRequest"},"CurrencyDtoResponse":{"type":"object","properties":{"commissionFixed":{"type":"number"},"commissionMin":{"type":"number"},"commissionPercent":{"type":"number"},"displaySymbol":{"type":"string"},"maxWithdrawal":{"type":"number"},"minDeposit":{"type":"number"},"minWithdrawal":{"type":"number"},"name":{"type":"string"},"precision":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["CRYPTO","EXCHANGE_TOKEN","FIAT","ICO","TOKEN","TOKENISED_SECURITY","UTILITY_TOKENS"]}},"title":"CurrencyDtoResponse"},"CurrencyResponse":{"type":"object","properties":{"currencies":{"type":"array","items":{"$ref":"#/definitions/CurrencyDtoResponse"}}},"title":"CurrencyResponse"},"DepthRequest":{"type":"object","required":["symbol"],"properties":{"limit":{"type":"integer","format":"int32"},"symbol":{"type":"string"}},"title":"DepthRequest"},"DepthResponse":{"type":"object","properties":{"asks":{"type":"array","items":{"type":"array","items":{"type":"number"}}},"bids":{"type":"array","items":{"type":"array","items":{"type":"number"}}},"lastUpdateId":{"type":"integer","format":"int64"}},"title":"DepthResponse"},"EditExchangeOrderRequest":{"type":"object","required":["apiKey","orderId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"orderId":{"type":"string"},"price":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"EditExchangeOrderRequest"},"EditExchangeOrderResponse":{"type":"object","properties":{"orderId":{"type":"string","format":"uuid"}},"title":"EditExchangeOrderResponse"},"EmptyRequest":{"type":"object","title":"EmptyRequest"},"ExchangeFilter":{"type":"object","title":"ExchangeFilter"},"ExchangeInfo":{"type":"object","properties":{"exchangeFilters":{"type":"array","items":{"$ref":"#/definitions/ExchangeFilter"}},"rateLimits":{"type":"array","items":{"$ref":"#/definitions/RateLimits"}},"serverTime":{"type":"integer","format":"int64"},"symbols":{"type":"array","items":{"$ref":"#/definitions/ExchangeSymbolInfo"}},"timezone":{"type":"string"}},"title":"ExchangeInfo"},"ExchangeSymbolInfo":{"type":"object","properties":{"assetType":{"type":"string","enum":["BOND","COMMODITY","CREDIT","CRYPTOCURRENCY","CURRENCY","EQUITY","ICO","INDEX","INTEREST_RATE","OPT_TOKENS","OTHER_ASSET","REAL_ESTATE","UTILITY_TOKENS"]},"baseAsset":{"type":"string"},"baseAssetPrecision":{"type":"integer","format":"int32"},"country":{"type":"string"},"exchangeFee":{"type":"number"},"filters":{"type":"array","items":{"$ref":"#/definitions/SymbolFilter"}},"industry":{"type":"string"},"longRate":{"type":"number","format":"double"},"makerFee":{"type":"number"},"marketModes":{"type":"array","items":{"type":"string","enum":["CLOSED_FOR_CORPORATE_ACTION","CLOSE_ONLY","DELISTING","HOLIDAY","LONG_ONLY","REGULAR","UNKNOWN","VIEW_AND_REQUEST","VIEW_ONLY"]}},"marketType":{"type":"string","enum":["LEVERAGE","SPOT"]},"maxSLGap":{"type":"number"},"maxTPGap":{"type":"number"},"minSLGap":{"type":"number"},"minTPGap":{"type":"number"},"name":{"type":"string"},"orderTypes":{"type":"array","items":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]}},"quoteAsset":{"type":"string"},"quoteAssetId":{"type":"string"},"quotePrecision":{"type":"integer","format":"int32"},"sector":{"type":"string"},"shortRate":{"type":"number","format":"double"},"status":{"type":"string","enum":["AUCTION_MATCH","BREAK","END_OF_DAY","HALT","POST_TRADING","PRE_TRADING","TRADING"]},"swapChargeInterval":{"type":"integer","format":"int64"},"symbol":{"type":"string"},"takerFee":{"type":"number"},"tickSize":{"type":"number"},"tickValue":{"type":"number"},"tradingFee":{"type":"number"},"tradingHours":{"type":"string"}},"title":"ExchangeSymbolInfo"},"FundingLimitsDtoResponse":{"type":"object","properties":{"accountCurrency":{"type":"string"},"minWithdrawal":{"type":"string"},"paymentOption":{"type":"string"}},"title":"FundingLimitsDtoResponse"},"FundingLimitsDtoResponseWS":{"type":"object","properties":{"fundingLimits":{"type":"array","items":{"$ref":"#/definitions/FundingLimitsDtoResponse"}}},"title":"FundingLimitsDtoResponseWS"},"GetOrderDtoResponseReq":{"type":"object","properties":{"accountId":{"type":"integer","format":"int64"},"execPrice":{"type":"number"},"execQuantity":{"type":"number"},"expireTime":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"margin":{"type":"number","format":"double"},"orderId":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"number"},"rejectReason":{"type":"string"},"side":{"type":"string"},"status":{"type":"string"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"symbolAndReturn":{"type":"string"},"takeProfit":{"type":"number"},"timeInForceType":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string"}},"title":"GetOrderDtoResponseReq"},"GetOrderDtoResponseRes":{"type":"object","properties":{"accountId":{"type":"integer","format":"int64"},"execPrice":{"type":"number"},"execQuantity":{"type":"number"},"expireTime":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"margin":{"type":"number","format":"double"},"orderId":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"number"},"rejectReason":{"type":"string"},"side":{"type":"string"},"status":{"type":"string"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"timeInForceType":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string"}},"title":"GetOrderDtoResponseRes"},"GetOrderRequest":{"type":"object","required":["apiKey","orderId","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"orderId":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"GetOrderRequest"},"InternalQuote":{"type":"object","properties":{"bid":{"type":"number","format":"double"},"bidQty":{"type":"number","format":"double"},"ofr":{"type":"number","format":"double"},"ofrQty":{"type":"number","format":"double"},"symbolName":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"InternalQuote"},"KLinesRequest":{"type":"object","required":["interval","symbol"],"properties":{"endTime":{"type":"integer","format":"int64"},"interval":{"type":"string"},"limit":{"type":"integer","format":"int32"},"priceType":{"type":"string"},"startTime":{"type":"integer","format":"int64"},"symbol":{"type":"string"},"type":{"type":"string"}},"title":"KLinesRequest"},"KLinesResponse":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"array","items":{"type":"object"}}}},"title":"KLinesResponse"},"LeverageSettingsRequest":{"type":"object","required":["apiKey","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"LeverageSettingsRequest"},"LeverageSettingsResponse":{"type":"object","properties":{"value":{"type":"integer","format":"int32"},"values":{"type":"array","items":{"type":"integer","format":"int32"}}},"title":"LeverageSettingsResponse"},"MarketDepthData":{"type":"object","properties":{"bid":{"type":"object","additionalProperties":{"type":"number"}},"ofr":{"type":"object","additionalProperties":{"type":"number"}},"ts":{"type":"integer","format":"int64"}},"title":"MarketDepthData"},"MarketDepthEvent":{"type":"object","properties":{"data":{"$ref":"#/definitions/MarketDepthData"},"symbol":{"type":"string"}},"title":"MarketDepthEvent"},"MyTradesResponse":{"type":"object","properties":{"buyer":{"type":"boolean"},"commission":{"type":"string"},"commissionAsset":{"type":"string"},"id":{"type":"string"},"isBuyer":{"type":"boolean"},"isMaker":{"type":"boolean"},"maker":{"type":"boolean"},"orderId":{"type":"string"},"price":{"type":"string"},"qty":{"type":"string"},"quoteQty":{"type":"string"},"symbol":{"type":"string"},"time":{"type":"integer","format":"int64"}},"title":"MyTradesResponse"},"NewOrderResponseRESULT":{"type":"object","properties":{"executedQty":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"margin":{"type":"number"},"orderId":{"type":"string"},"origQty":{"type":"string"},"price":{"type":"string"},"profitDistance":{"type":"number"},"rejectMessage":{"type":"string"},"side":{"type":"string","enum":["BUY","SELL"]},"status":{"type":"string","enum":["CANCELED","EXPIRED","FILLED","NEW","PARTIALLY_FILLED","PENDING_CANCEL","REJECTED"]},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"timeInForce":{"type":"string","enum":["FOK","GTC","IOC"]},"trailingStopLoss":{"type":"boolean"},"transactTime":{"type":"integer","format":"int64"},"type":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]}},"title":"NewOrderResponseRESULT"},"OHLCBar":{"type":"object","properties":{"c":{"type":"number","format":"double"},"h":{"type":"number","format":"double"},"interval":{"type":"string"},"l":{"type":"number","format":"double"},"o":{"type":"number","format":"double"},"symbol":{"type":"string"},"t":{"type":"integer","format":"int64"},"type":{"type":"string"}},"title":"OHLCBar"},"OHLCSubscribeRequest":{"type":"object","properties":{"intervals":{"type":"array","description":"Identifies intervals for subscription. Available: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 1m.","items":{"type":"string"}},"symbols":{"type":"array","description":"Identifies symbols for subscription.","items":{"type":"string"}},"type":{"type":"string","description":"Type of candlestick. Available: classic, heikin-ashi."}},"title":"OHLCSubscribeRequest","description":"Class representing an OHLC market data subscription."},"OpenOrdersReponse":{"type":"object","properties":{"openOrders":{"type":"array","items":{"$ref":"#/definitions/QueryOrderResponse"}}},"title":"OpenOrdersReponse"},"OptionalAuthRequest":{"type":"object","properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"OptionalAuthRequest"},"OvernightRate":{"type":"object","properties":{"longRate":{"type":"number","format":"double"},"shortRate":{"type":"number","format":"double"}},"title":"OvernightRate"},"PingRequest":{"type":"object","title":"PingRequest"},"PingResponse":{"type":"object","title":"PingResponse"},"PositionDto":{"type":"object","required":["accountId","closePrice","closeQuantity","createdTimestamp","currency","id","instrumentId","margin","openPrice","openQuantity","openTimestamp","orderId","state"],"properties":{"accountId":{"type":"string"},"closePrice":{"type":"number"},"closeQuantity":{"type":"number"},"closeTimestamp":{"type":"integer","format":"int64"},"cost":{"type":"number"},"createdTimestamp":{"type":"integer","format":"int64"},"currency":{"type":"string"},"currentTrailingPrice":{"type":"number"},"currentTrailingPriceUpdatedTimestamp":{"type":"integer","format":"int64"},"dividend":{"type":"number"},"fee":{"type":"number"},"guaranteedStopLoss":{"type":"boolean"},"id":{"type":"string","format":"uuid"},"instrumentId":{"type":"integer","format":"int64"},"margin":{"type":"number"},"openPrice":{"type":"number"},"openQuantity":{"type":"number"},"openTimestamp":{"type":"integer","format":"int64"},"orderId":{"type":"string","format":"uuid"},"rpl":{"type":"number"},"rplConverted":{"type":"number"},"state":{"type":"string","enum":["ACTIVE","INACTIVE","INVALID"]},"stopLoss":{"type":"number"},"swap":{"type":"number"},"swapConverted":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"trailingQuotedPrice":{"type":"number"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string","enum":["HEDGE","NET"]},"upl":{"type":"number"},"uplConverted":{"type":"number"}},"title":"PositionDto"},"PositionExecutionReportDto":{"type":"object","required":["accountCurrency","accountId","createdTimestamp","currency","execId","execTimestamp","instrumentId","positionId","source","status"],"properties":{"accountCurrency":{"type":"string"},"accountId":{"type":"integer","format":"int64"},"createdTimestamp":{"type":"integer","format":"int64"},"currency":{"type":"string"},"execId":{"type":"string"},"execTimestamp":{"type":"integer","format":"int64"},"executionType":{"type":"string","enum":["GTC","IOC"]},"fee":{"type":"number"},"feeDetails":{"type":"object","additionalProperties":{"type":"number"}},"fxRate":{"type":"number"},"gSL":{"type":"boolean"},"instrumentId":{"type":"integer","format":"int64"},"positionId":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"number"},"rejectReason":{"type":"string","enum":["ACCOUNT_NOT_FOUND","CLOSED_MARKET","CLOSE_ONLY","ENGINE_BUSY","HEDGING_MODE_GSL","INSTRUMENT_NOT_AVAILABLE","INSTRUMENT_NOT_FOUND","INVALID_ORDER","INVALID_ORDER_QTY","INVALID_PRICE","LONG_ONLY","OFF_MARKET","ORDER_NOT_FOUND","ORIGINAL_GSL_UPDATE","POSITION_NOT_FOUND","RC_INSTRUMENT_CLIENT_MOP","RC_INSTRUMENT_GLOBAL_MOP","RC_NOT_ENOUGH_MARGIN","RC_NOT_FOUND","RC_NO_RATES","RC_SETTLEMENT","RC_UNKNOWN","REQUIRED_GSL","RISK_CHECK","THROTTLING","UNKNOWN"]},"rpl":{"type":"number"},"rplConverted":{"type":"number"},"source":{"type":"string","enum":["CLOSE_OUT","DEALER","SL","SYSTEM","TP","USER"]},"status":{"type":"string","enum":["CLOSED","DIVIDEND","MODIFIED","MODIFY_REJECT","OPENED","SWAP"]},"stopLoss":{"type":"number"},"swap":{"type":"number"},"swapConverted":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"trailingStopLoss":{"type":"boolean"}},"title":"PositionExecutionReportDto"},"PositionHistoryRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"from":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"to":{"type":"integer","format":"int64"}},"title":"PositionHistoryRequest"},"QueryOrderResponse":{"type":"object","properties":{"accountId":{"type":"string"},"executedQty":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"icebergQty":{"type":"string"},"leverage":{"type":"boolean"},"margin":{"type":"number"},"orderId":{"type":"string"},"origQty":{"type":"string"},"price":{"type":"string"},"side":{"type":"string","enum":["BUY","SELL"]},"status":{"type":"string","enum":["CANCELED","EXPIRED","FILLED","NEW","PARTIALLY_FILLED","PENDING_CANCEL","REJECTED"]},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"time":{"type":"integer","format":"int64"},"timeInForce":{"type":"string","enum":["FOK","GTC","IOC"]},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]},"updateTime":{"type":"integer","format":"int64"},"working":{"type":"boolean"}},"title":"QueryOrderResponse"},"RateLimits":{"type":"object","properties":{"interval":{"type":"string"},"intervalNum":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"rateLimitType":{"type":"string"}},"title":"RateLimits"},"RequestDto":{"type":"object","required":["accountId","createdTimestamp","id","rqType","state"],"properties":{"accountId":{"type":"string"},"createdTimestamp":{"type":"integer","format":"int64"},"id":{"type":"integer","format":"int64"},"orderId":{"type":"string"},"positionId":{"type":"string"},"rejectReason":{"type":"string","enum":["ACCOUNT_NOT_FOUND","CLOSED_MARKET","CLOSE_ONLY","ENGINE_BUSY","HEDGING_MODE_GSL","INSTRUMENT_NOT_AVAILABLE","INSTRUMENT_NOT_FOUND","INVALID_ORDER","INVALID_ORDER_QTY","INVALID_PRICE","LONG_ONLY","OFF_MARKET","ORDER_NOT_FOUND","ORIGINAL_GSL_UPDATE","POSITION_NOT_FOUND","RC_INSTRUMENT_CLIENT_MOP","RC_INSTRUMENT_GLOBAL_MOP","RC_NOT_ENOUGH_MARGIN","RC_NOT_FOUND","RC_NO_RATES","RC_SETTLEMENT","RC_UNKNOWN","REQUIRED_GSL","RISK_CHECK","THROTTLING","UNKNOWN"]},"rqType":{"type":"string","enum":["ORDER_CANCEL","ORDER_MODIFY","ORDER_NEW","POSITION_MODIFY"]},"state":{"type":"string","enum":["CANCELLED","PENDING","PROCESSED"]}},"title":"RequestDto"},"ServerTime":{"type":"object","properties":{"serverTime":{"type":"integer","format":"int64"}},"title":"ServerTime"},"SignedBySymbolRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"SignedBySymbolRequest"},"SignedRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"SignedRequest"},"SubscribeRequest":{"type":"object","properties":{"symbols":{"type":"array","description":"Identifies symbols for subscription.","items":{"type":"string"}}},"title":"SubscribeRequest","description":"Class representing an subscription."},"SubscribeResponse":{"type":"object","properties":{"errorCode":{"type":"string"},"subscriptions":{"type":"object","additionalProperties":{"type":"string"}}},"title":"SubscribeResponse"},"SymbolFilter":{"type":"object","properties":{"filterType":{"type":"string"}},"title":"SymbolFilter"},"SymbolRequest":{"type":"object","properties":{"symbol":{"type":"string"}},"title":"SymbolRequest"},"Ticker24HResponse":{"type":"object","properties":{"tickers":{"type":"array","items":{"$ref":"#/definitions/Ticker24hr"}}},"title":"Ticker24HResponse"},"Ticker24hr":{"type":"object","properties":{"askPrice":{"type":"string"},"bidPrice":{"type":"string"},"closeTime":{"type":"integer","format":"int64"},"highPrice":{"type":"string"},"lastPrice":{"type":"string"},"lastQty":{"type":"string"},"lowPrice":{"type":"string"},"openPrice":{"type":"string"},"openTime":{"type":"integer","format":"int64"},"prevClosePrice":{"type":"string"},"priceChange":{"type":"string"},"priceChangePercent":{"type":"string"},"quoteVolume":{"type":"string"},"symbol":{"type":"string"},"volume":{"type":"string"},"weightedAvgPrice":{"type":"string"}},"title":"Ticker24hr"},"TradeEventReq":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"orderId":{"type":"string"},"price":{"type":"number","format":"double"},"size":{"type":"number","format":"double"},"symbol":{"type":"string"},"ts":{"type":"integer","format":"int64"}},"title":"TradeEventReq"},"TradeEventRes":{"type":"object","properties":{"buyer":{"type":"boolean"},"id":{"type":"integer","format":"int32"},"orderId":{"type":"string"},"price":{"type":"number","format":"double"},"size":{"type":"number","format":"double"},"symbol":{"type":"string"},"ts":{"type":"integer","format":"int64"}},"title":"TradeEventRes"},"TradingFeesResponse":{"type":"object","properties":{"fee":{"type":"number","format":"double"},"name":{"type":"string"},"overnightFeeTimestamp":{"type":"integer","format":"int64"},"overnightRates":{"$ref":"#/definitions/OvernightRate"},"symbol":{"type":"string"}},"title":"TradingFeesResponse"},"TradingFeesResponseWS":{"type":"object","properties":{"fees":{"type":"array","items":{"$ref":"#/definitions/TradingFeesResponse"}}},"title":"TradingFeesResponseWS"},"TradingLimitsResponse":{"type":"object","properties":{"lastPrice":{"type":"number"},"maxVolume":{"type":"number","format":"double"},"minStep":{"type":"number","format":"double"},"minVolume":{"type":"number","format":"double"},"name":{"type":"string"},"symbol":{"type":"string"},"tickSize":{"type":"number","format":"double"}},"title":"TradingLimitsResponse"},"TradingLimitsResponseWS":{"type":"object","properties":{"limits":{"type":"array","items":{"$ref":"#/definitions/TradingLimitsResponse"}}},"title":"TradingLimitsResponseWS"},"TradingOrderUpdateResponse":{"type":"object","required":["requestId","state"],"properties":{"requestId":{"type":"integer","format":"int64"},"state":{"type":"string","enum":["CANCELLED","PENDING","PROCESSED"]}},"title":"TradingOrderUpdateResponse"},"TradingPositionCloseAllResponse":{"type":"object","properties":{"request":{"type":"array","items":{"$ref":"#/definitions/RequestDto"}}},"title":"TradingPositionCloseAllResponse"},"TradingPositionHistoryResponse":{"type":"object","properties":{"history":{"type":"array","items":{"$ref":"#/definitions/PositionExecutionReportDto"}}},"title":"TradingPositionHistoryResponse"},"TradingPositionListResponse":{"type":"object","properties":{"positions":{"type":"array","items":{"$ref":"#/definitions/PositionDto"}}},"title":"TradingPositionListResponse"},"TradingPositionUpdateResponse":{"type":"object","required":["requestId","state"],"properties":{"requestId":{"type":"integer","format":"int64"},"state":{"type":"string","enum":["CANCELLED","PENDING","PROCESSED"]}},"title":"TradingPositionUpdateResponse"},"TransactionDTOResponse":{"type":"object","properties":{"amount":{"type":"number"},"balance":{"type":"number"},"blockchainTransactionHash":{"type":"string"},"commission":{"type":"number"},"currency":{"type":"string"},"id":{"type":"integer","format":"int64"},"paymentMethod":{"type":"string"},"status":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"type":{"type":"string"}},"title":"TransactionDTOResponse"},"TransactionsRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"endTime":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"startTime":{"type":"integer","format":"int64"},"timestamp":{"type":"integer","format":"int64"}},"title":"TransactionsRequest"},"TransactionsResponse":{"type":"object","properties":{"transactions":{"type":"array","items":{"$ref":"#/definitions/TransactionDTOResponse"}}},"title":"TransactionsResponse"},"UpdateTradingOrderRequest":{"type":"object","required":["apiKey","orderId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"newPrice":{"type":"number"},"orderId":{"type":"string"},"profitDistance":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"takeProfit":{"type":"number"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"}},"title":"UpdateTradingOrderRequest"},"UpdateTradingPositionRequest":{"type":"object","required":["apiKey","positionId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"guaranteedStopLoss":{"type":"boolean"},"positionId":{"type":"string"},"profitDistance":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"takeProfit":{"type":"number"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"}},"title":"UpdateTradingPositionRequest"}}} \ No newline at end of file diff --git a/docs/market_intelligence/information/dzengi_rest_api.md b/docs/market_intelligence/information/dzengi_rest_api.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/information/dzengi_websocket_api.md b/docs/market_intelligence/information/dzengi_websocket_api.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/information/external_sources.md b/docs/market_intelligence/information/external_sources.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/information/market_information_backlog.md b/docs/market_intelligence/information/market_information_backlog.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/information/market_information_catalogue.md b/docs/market_intelligence/information/market_information_catalogue.md new file mode 100644 index 0000000..32b0f85 --- /dev/null +++ b/docs/market_intelligence/information/market_information_catalogue.md @@ -0,0 +1,295 @@ +# Dzentra Market Information Catalogue + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Dzentra Market Information Catalogue | +| Тип документа | Information Catalogue | +| Версия | 0.1 | +| Статус | Draft | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Язык | Русский | + +--- + +## Статус документа + +Настоящий документ определяет официальный реестр информационных показателей и информационных объектов, используемых для понимания рынка криптовалют. + +Документ разрабатывается на основании **Dzentra Market Information Catalogue Architecture Charter**. + +--- + +## Назначение + +Настоящий документ определяет информацию, которая может использоваться подсистемой **Market Intelligence** для формирования знаний о рынке криптовалют. + +Catalogue описывает: + +- информационные показатели; +- информационные объекты; +- их официальные наименования; +- канонические имена Dzentra; +- определения; +- возможные источники получения; +- периодичность обновления; +- связи между показателями и объектами. + +--- + +## Область применения + +Документ применяется при: + +- построении информационной модели Market Intelligence; +- разработке Market Knowledge Catalogue; +- разработке Market Intelligence Reference Model; +- проектировании Knowledge Architecture; +- проектировании Engine Specifications; +- проверке новых информационных сущностей. + +--- + +## Что не входит в область документа + +Документ не определяет: + +- знания о рынке; +- торговые сигналы; +- торговые стратегии; +- правила принятия решений; +- алгоритмы анализа; +- индикаторы; +- программную реализацию; +- источники данных как самостоятельные сущности. + +--- + +## Цель + +Цель документа — определить автоматически доступную или автоматически вычисляемую информацию, способную повысить качество понимания текущего состояния рынка криптовалют. + +--- + +## Основной вопрос документа + +> **Какая информация наиболее существенно влияет на качество понимания текущего состояния рынка криптовалют?** + +--- + +# Принцип ранжирования + +Информационные сущности располагаются по степени их вклада в качество понимания рынка. + +Критерий ранжирования: + +> **Насколько ухудшится способность системы понимать рынок, если данная информация будет полностью недоступна?** + +Чем сильнее ухудшается качество модели рынка при отсутствии сущности, тем выше её место в Catalogue. + +--- + +# Часть I. Информационные показатели + +Настоящая часть содержит реестр информационных показателей Dzentra. + +Информационный показатель — самостоятельная наблюдаемая характеристика предметной области, имеющая собственное значение. + +--- + +## 1. Цена + +### Официальное наименование + +Цена + +### Общепринятые наименования + +- Price +- Trade Price +- Market Price + +### Каноническое имя Dzentra + +`price` + +### Определение + +Цена — информационный показатель, отражающий числовое значение стоимости анализируемого торгового инструмента в определённый момент времени. + +Цена является самостоятельной информационной сущностью и не зависит от других информационных показателей. + +Каждое значение цены существует только относительно конкретного момента времени. + +### Тип значения + +Decimal + +### Единица измерения + +Единица котирования анализируемого торгового инструмента. + +### Обоснование включения в Catalogue + +Цена является одной из наиболее значимых информационных сущностей для понимания текущего состояния рынка. + +Без информации о цене невозможно определить текущее положение рынка, изменение стоимости торгового инструмента и большинство характеристик рыночного поведения. + +Практически все последующие знания о рынке в той или иной степени используют информацию о цене. + +### Возможные источники получения + +- торговая площадка; +- агрегатор рыночных данных; +- поставщик исторических данных; +- индексный провайдер. + +### Периодичность обновления + +Определяется источником информации. + +Обновление может происходить как при каждом изменении цены, так и через фиксированные интервалы времени. + +### Связанные информационные объекты + +- Сделка (`trade`) +- Свеча (`candle`) +- Стакан заявок (`order_book`) +- Тикер (`ticker`) +- Индексная цена +- Маркировочная цена + +### Примечания + +Рассматривается как обобщённое понятие стоимости торгового инструмента. + +Специализированные разновидности цены (например, цена последней сделки, лучшая цена покупки, лучшая цена продажи, индексная цена, расчётная цена и другие) рассматриваются как самостоятельные информационные показатели и описываются отдельно. + +Каждое значение цены интерпретируется только совместно с моментом времени, к которому оно относится. + +--- + +## 2. Объём + +### Официальное наименование + +Объём + +### Общепринятые наименования + +- Volume +- Trade Volume +- Trading Volume + +### Каноническое имя Dzentra + +`volume` + +### Определение + +Объём — информационный показатель, отражающий суммарное количество торгового инструмента, участвовавшего в совершённых сделках за определённый интервал времени. + +Объём является самостоятельной информационной сущностью и не зависит от других информационных показателей. + +В отличие от цены, объём не существует в отдельный момент времени и всегда относится к некоторому интервалу наблюдения. + +### Тип значения + +Decimal + +### Единица измерения + +Количество анализируемого торгового инструмента. + +### Обоснование включения в Catalogue + +Объём является одним из наиболее значимых информационных показателей для понимания текущего состояния рынка. + +Информация об объёме позволяет оценить интенсивность торговой активности за рассматриваемый интервал времени и существенно повышает качество интерпретации изменения цены. + +Без информации об объёме невозможно достоверно оценить значимость большинства ценовых изменений. + +### Возможные источники получения + +- торговая площадка; +- агрегатор рыночных данных; +- поставщик исторических данных. + +### Периодичность обновления + +Определяется используемым интервалом наблюдения и источником информации. + +Обновление может происходить после завершения интервала либо непрерывно по мере накопления данных внутри текущего интервала. + +### Связанные информационные объекты + +- Сделка (`trade`) +- Свеча (`candle`) +- Тикер (`ticker`) +- Агрегированная статистика торгов + +### Примечания + +Рассматривается как обобщённое понятие торгового объёма. + +Специализированные разновидности объёма (например, объём отдельной сделки, объём свечи, объём покупок, объём продаж, суточный объём и другие) рассматриваются как самостоятельные информационные показатели и описываются отдельно. + +Каждое значение объёма интерпретируется только совместно с интервалом времени, к которому оно относится. + +--- + +## 3. Время + +### Официальное наименование + +Время + +### Общепринятые наименования + +- Time +- Timestamp +- Event Time +- Open Time +- Close Time + +### Каноническое имя Dzentra + +`time` + +### Определение + +Время — значение момента или интервала, к которому относится информационная сущность. + +### Тип значения + +Datetime / Time Interval + +### Единица измерения + +Временная шкала. + +### Возможные источники получения + +- торговая площадка; +- поставщик данных; +- системное время источника; +- календарь публикаций. + +### Периодичность обновления + +Определяется информационным объектом или событием. + +### Связанные информационные объекты + +- Сделка +- Свеча +- Тикер +- Стакан заявок +- Funding Snapshot +- Макроэкономическая публикация + +### Примечания + +Время является обязательной координатой для сопоставления значений и построения истории рынка. \ No newline at end of file diff --git a/docs/market_intelligence/information/market_information_catalogue_architecture_charter.md b/docs/market_intelligence/information/market_information_catalogue_architecture_charter.md new file mode 100644 index 0000000..c7387b3 --- /dev/null +++ b/docs/market_intelligence/information/market_information_catalogue_architecture_charter.md @@ -0,0 +1,434 @@ +# Dzentra Market Information Catalogue Architecture Charter + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Dzentra Market Information Catalogue Architecture Charter | +| Тип документа | Architecture Standard | +| Версия | 0.1 | +| Статус | Release Candidate | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Язык | Русский | + +--- + +## Статус документа + +Настоящий документ определяет архитектурные принципы проектирования **Dzentra Market Information Catalogue**. + +До выпуска версии **1.0 Release** допускается изменение структуры и содержания документа при условии сохранения его архитектурной целостности. + +После выпуска версии **1.0 Release** изменение документа допускается только посредством выпуска новой версии стандарта. + +--- + +## Назначение + +Настоящий документ устанавливает правила проектирования, наполнения и сопровождения **Dzentra Market Information Catalogue**. + +Документ определяет: + +- границы предметной области; +- архитектурные принципы; +- правила включения сущностей; +- правила описания сущностей; +- требования к терминологии; +- требования к каноническим идентификаторам; +- порядок развития Catalogue. + +Настоящий документ является нормативной основой разработки и сопровождения **Dzentra Market Information Catalogue**. + +--- + +## Область применения + +Настоящий документ применяется при: + +- разработке **Dzentra Market Information Catalogue**; +- добавлении информационных объектов; +- добавлении информационных показателей; +- изменении существующих сущностей Catalogue; +- архитектурной проверке изменений; +- разработке документов, использующих **Dzentra Market Information Catalogue**. + +Положения настоящего документа обязательны для всех редакций **Dzentra Market Information Catalogue**. + +--- + +## Что не входит в область документа + +Настоящий документ не определяет: + +- состав информационных объектов; +- состав информационных показателей; +- модель знаний; +- организацию знаний; +- онтологию; +- программную архитектуру; +- программную реализацию; +- алгоритмы анализа; +- индикаторы; +- торговые стратегии; +- принятие торговых решений; +- исполнение торговых решений. + +Указанные вопросы определяются документами более низкого уровня. + +--- + +## Цель + +Цель документа — обеспечить построение единого, непротиворечивого, масштабируемого и независимого от реализации Dzentra Market Information Catalogue, содержащего информацию, обладающую практической ценностью для построения максимально достоверной модели текущего состояния рынка криптовалют. + +Настоящий документ не ставит целью достижение полноты предметной области. + +Catalogue развивается эволюционно по мере углубления понимания предметной области. + +--- + +## Основной вопрос документа + +Настоящий документ отвечает на следующий вопрос. + +> **По каким архитектурным принципам должен проектироваться Dzentra Market Information Catalogue, чтобы содержать исключительно информацию, обладающую практической ценностью для понимания текущего состояния рынка криптовалют?** + +--- + +# Определение Dzentra Market Information Catalogue + +**Dzentra Market Information Catalogue** — единый официальный нормативный реестр информационных объектов и информационных показателей предметной области Dzentra. + +Catalogue определяет состав информационной модели предметной области и является нормативной основой для всех последующих документов Knowledge Architecture. + +Catalogue не определяет знания о рынке. + +Catalogue определяет исключительно информацию, которая может использоваться для формирования таких знаний. + +--- + +# Предметная область + +## Объект моделирования + +Предметной областью **Dzentra Market Information Catalogue** является информация, используемая для построения модели текущего состояния рынка криптовалют. + +Catalogue не описывает рынок, знания о рынке или торговые решения. + +Catalogue описывает исключительно информацию, которая может использоваться для построения знаний о текущем состоянии рынка. + +--- + +## Границы предметной области + +В Catalogue включается информация, которая одновременно удовлетворяет следующим условиям: + +- может быть автоматически получена или автоматически вычислена системой; +- имеет объективное определение; +- не требует человеческой интерпретации; +- способна повысить качество понимания рынка криптовалют. + +--- + +## Объекты каталогизации + +Catalogue содержит только два типа сущностей: + +- информационные показатели; +- информационные объекты. + +Другие типы сущностей в рамках настоящего документа не рассматриваются. + +--- + +## Информационный показатель + +Информационный показатель — наблюдаемая характеристика предметной области, имеющая собственное значение и доступная для автоматического получения или вычисления системой. + +--- + +## Информационный объект + +Информационный объект — логически связанная совокупность информационных показателей, существующая как единая информационная сущность предметной области. + +--- + +## Что не является информационной сущностью + +В Catalogue не включаются: + +### Элементы предметной области + +- торговые инструменты; +- торговые площадки; +- типы рынков; +- валюты; +- блокчейны; +- организации; +- иные именованные сущности предметной области. + +### Архитектурные и вычислительные сущности + +- алгоритмы; +- модели анализа; +- индикаторы; +- торговые стратегии. + +Указанные сущности относятся к словарю предметной области Dzentra и не являются частью настоящего Catalogue. + +# Архитектурные принципы + +## Общие принципы + +### AP-001. Независимость предметной области + +Catalogue описывает предметную область. + +Catalogue не зависит от программной реализации, языка программирования, архитектуры системы и способов хранения данных. + +--- + +### AP-002. Независимость от источников данных + +Catalogue описывает информационные сущности независимо от способов их получения. + +Конкретные источники данных являются свойствами информационных сущностей и не определяют структуру Catalogue. + +--- + +### AP-003. Независимость от торговых площадок + +Catalogue не зависит от конкретных бирж, брокеров, поставщиков данных или внешних сервисов. + +--- + +### AP-004. Независимость от алгоритмов + +Catalogue не содержит алгоритмов анализа, вычислений или обработки информации. + +--- + +### AP-005. Независимость от торговых решений + +Catalogue не содержит правил открытия, сопровождения или закрытия позиций. + +--- + +## Принципы моделирования + +### AP-006. Каталогизация + +Catalogue содержит только информационные показатели и информационные объекты. + +Другие типы сущностей не допускаются. + +--- + +### AP-007. Объективность + +Каждая сущность должна иметь объективное определение. + +Субъективные оценки не допускаются. + +--- + +### AP-008. Автоматическая обработка + +Каждая сущность должна быть доступна для автоматического получения или автоматического вычисления системой. + +--- + +### AP-009. Информационная ценность + +Каждая сущность включается в Catalogue только в том случае, если она способна повысить качество понимания текущего состояния рынка криптовалют. + +Наличие информации без практической ценности для построения модели рынка не является основанием для включения сущности в Catalogue. + +--- + +### AP-010. Приоритет информационной ценности + +Информационные показатели и информационные объекты располагаются в Catalogue по степени их вклада в качество понимания текущего состояния рынка. + +Расположение сущностей не определяется происхождением информации, её предметной областью или способом получения. + +--- + +### AP-011. Критерий ранжирования + +Степень важности информационной сущности определяется ухудшением качества понимания рынка при отсутствии данной информации. + +Чем сильнее отсутствие сущности ухудшает качество модели рынка, тем выше её место в Catalogue. + +--- + +### AP-012. Однозначность + +Каждая сущность имеет единственное официальное определение в рамках Dzentra. + +--- + +## Принципы терминологии + +### AP-013. Единая терминология + +Для каждой сущности используется единый официальный термин Dzentra. + +--- + +### AP-014. Общепринятая терминология + +При наличии общепринятого профессионального термина он используется в качестве официального термина Dzentra. + +Если термин неоднозначен, Dzentra определяет собственный официальный термин. + +--- + +## Принципы идентификации + +### AP-015. Каноническое имя + +Каждая сущность имеет одно каноническое имя Dzentra. + +Каноническое имя является единственным официальным именем сущности во всей платформе. + +--- + +### AP-016. Единственность имени + +Каноническое имя используется во всех документах и компонентах платформы. + +Использование альтернативных имён не допускается. + +Каноническое имя информационной сущности является глобальным идентификатором предметной области Dzentra. Все архитектурные документы и программные компоненты, использующие данную сущность, обязаны ссылаться на неё исключительно посредством её канонического имени. + +--- + +## Принципы развития + +### AP-017. Первичность канонического имени + +Каноническое имя сущности определяется до её использования в любом документе или программной реализации. + +### AP-018. Первичность документации + +Любая новая сущность сначала включается в Catalogue. + +До утверждения сущности в Catalogue её использование в документации, архитектуре и программной реализации не допускается. + +### AP-019. Единственность сущности + +Каждая информационная сущность описывается в Catalogue только один раз. + +Дублирование сущностей не допускается. + +### AP-020. Единственность нормативного определения + +Каждое архитектурное правило определяется только в одном месте. + +Повторение нормативных правил в документации не допускается. + +### AP-021. Минимальная достаточность + +Описание каждой сущности должно содержать только сведения, необходимые для её однозначного определения и использования в рамках Information Catalogue. + +Избыточная информация не допускается. + +### AP-022. Эволюционное развитие + +Структура Dzentra Market Information Catalogue может изменяться в процессе развития предметной области. + +Добавление новых разделов, информационных показателей и информационных объектов допускается при соблюдении требований настоящего стандарта. + +### AP-023. Приоритет источников информации + +При наличии нескольких источников одной и той же информационной сущности Dzentra должна определять канонический источник, информация которого считается приоритетной для формирования модели рынка. + +Для информационных сущностей, непосредственно влияющих на исполнение торговых операций, каноническим источником является торговая площадка, на которой осуществляется исполнение сделок. + +--- + +# Правила описания сущностей + +## Общие требования + +Каждая сущность Catalogue описывается по единому шаблону. + +Изменение структуры шаблона допускается только посредством изменения настоящего стандарта. + +--- + +## Информационный показатель + +Каждый информационный показатель должен содержать: + +- официальное наименование; +- общепринятые наименования (при наличии); +- каноническое имя Dzentra; +- определение; +- тип значения; +- единицу измерения (при наличии); +- обоснование включения в Catalogue; +- возможные источники получения; +- периодичность обновления; +- связанные информационные объекты; +- примечания (при необходимости). + +--- + +## Информационный объект + +Каждый информационный объект должен содержать: + +- официальное наименование; +- общепринятые наименования (при наличии); +- каноническое имя Dzentra; +- определение; +- связанные информационные показатели; +- возможные источники получения; +- периодичность обновления; +- обоснование включения в Catalogue; +- примечания (при необходимости). + +--- + +## Критерий включения сущностей + +Информационная сущность может быть включена в Catalogue только при одновременном выполнении следующих условий: + +- информация может быть автоматически получена или автоматически вычислена; +- информация имеет объективное определение; +- информация обладает практической ценностью для понимания текущего состояния рынка; +- информация не является торговым решением, алгоритмом или интерпретацией. + +--- + +## Требования к определениям + +Определение сущности должно: + +- быть объективным; +- быть однозначным; +- не зависеть от реализации; +- не содержать алгоритмов; +- не содержать субъективных оценок. + +--- + +## Требования к каноническим именам + +Каноническое имя должно: + +- быть уникальным; +- быть неизменяемым; +- использоваться во всей платформе Dzentra; +- соответствовать официальному наименованию сущности. + +--- + +# Управление изменениями + +Изменение настоящего стандарта допускается только при изменении методологии проектирования Dzentra Market Information Catalogue. + +Изменение состава информационных объектов и информационных показателей не является основанием для изменения настоящего документа. \ No newline at end of file diff --git a/docs/market_intelligence/knowledge/auction_context.md b/docs/market_intelligence/knowledge/auction_context.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/continuation.md b/docs/market_intelligence/knowledge/continuation.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/entry_quality.md b/docs/market_intelligence/knowledge/entry_quality.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/instrument_profile.md b/docs/market_intelligence/knowledge/instrument_profile.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/liquidity.md b/docs/market_intelligence/knowledge/liquidity.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/market_cycle.md b/docs/market_intelligence/knowledge/market_cycle.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/market_regime.md b/docs/market_intelligence/knowledge/market_regime.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/market_structure.md b/docs/market_intelligence/knowledge/market_structure.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/momentum.md b/docs/market_intelligence/knowledge/momentum.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/position_context.md b/docs/market_intelligence/knowledge/position_context.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/reversal.md b/docs/market_intelligence/knowledge/reversal.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/trend.md b/docs/market_intelligence/knowledge/trend.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/volatility.md b/docs/market_intelligence/knowledge/volatility.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/knowledge/wave.md b/docs/market_intelligence/knowledge/wave.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reference_model/engine_map.md b/docs/market_intelligence/reference_model/engine_map.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reference_model/knowledge_hierarchy.md b/docs/market_intelligence/reference_model/knowledge_hierarchy.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reference_model/market_intelligence_reference_model.md b/docs/market_intelligence/reference_model/market_intelligence_reference_model.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reference_model/market_intelligence_reference_model_v0.1_intro.md b/docs/market_intelligence/reference_model/market_intelligence_reference_model_v0.1_intro.md new file mode 100644 index 0000000..83d0846 --- /dev/null +++ b/docs/market_intelligence/reference_model/market_intelligence_reference_model_v0.1_intro.md @@ -0,0 +1,261 @@ +# Dzentra Market Intelligence Reference Model + +## Контроль документа + + Свойство Значение + --------------- --------------------------------------------- + Документ Dzentra Market Intelligence Reference Model + Тип документа Architecture Reference Model + Версия 0.1 + Статус Draft + Проект Dzentra + Подсистема Market Intelligence + Язык Русский + +## Статус документа + +Настоящий документ определяет эталонную модель знаний, используемую подсистемой Market Intelligence проекта Dzentra. + +Документ находится в стадии **Draft**. + +До выпуска версии **1.0 Release** допускается изменение структуры и содержания документа при условии сохранения его архитектурной целостности. + +После выпуска версии **1.0 Release** изменение настоящего документа допускается только посредством выпуска новой версии стандарта. + +## Назначение документа + +Настоящий документ определяет модель знаний, необходимую для построения достоверной модели текущего состояния рынка. + +Документ описывает предметную область рынка и не зависит от конкретной реализации системы. + +Настоящий документ является фундаментом модели знаний подсистемы Market Intelligence и развивает положения Knowledge Architecture Charter. + +Все архитектурные документы более низкого уровня должны основываться на положениях настоящего стандарта и не могут ему противоречить. + +## Область применения + +Настоящий документ применяется при: + +- разработке модели знаний о рынке; +- проектировании специализированных доменов знаний; +- проектировании Engine; +- разработке подсистемы Market Intelligence; +- построении Decision Layer; +- архитектурной проверке новых компонентов системы. + +Положения настоящего документа являются обязательными для всех компонентов, участвующих в формировании модели рынка. + +## Что не входит в область документа + +Настоящий документ не определяет: + +- программную архитектуру; +- программный код; +- способы реализации; +- используемые алгоритмы; +- конкретные методы вычислений; +- индикаторы; +- торговые стратегии; +- правила открытия и закрытия позиций; +- методы управления капиталом; +- методы управления рисками; +- особенности отдельных торговых площадок. + +Все перечисленные вопросы рассматриваются документами более низкого уровня. + +## Цель документа + +Целью настоящего документа является создание формальной, внутренне непротиворечивой и масштабируемой модели знаний о рынке. + +Полученная модель должна быть достаточной для построения максимально достоверной модели текущего состояния рынка независимо от: + +- класса финансового инструмента; +- типа рынка; +- торговой площадки; +- используемого таймфрейма; +- программной реализации; +- конкретных методов анализа. + +## Главный вопрос документа + +Настоящий документ отвечает на следующий вопрос. + +> **Какими знаниями должна обладать система, чтобы сформировать максимально полное, достоверное и непротиворечивое понимание текущее поведения рынка, его текущее состояние и процесс, который привёл рынок к этому состоянию, независимо от способов получения этих знаний и их последующей интерпретации?** + +Все последующие главы настоящего документа являются последовательным раскрытием ответа на этот вопрос. + +## Фундаментальные аксиомы + +### Аксиома 1. Предмет моделирования + +Предметом моделирования является наблюдаемое поведение рынка. + +Настоящий документ не рассматривает причины возникновения рыночных процессов, если они не могут быть подтверждены наблюдаемыми свойствами рынка. + +### Аксиома 2. Основание знаний + +Любое знание, формируемое системой Dzentra, должно быть основано: + +- либо на непосредственно наблюдаемых свойствах рынка; +- либо на логически выводимых следствиях из ранее полученных знаний. + +### Аксиома 3. Объективность модели + +Модель рынка должна описывать рынок таким, каким он наблюдается. + +Модель не должна содержать предположений о намерениях, целях или мотивах участников рынка, если такие предположения не могут быть подтверждены наблюдением. + +### Аксиома 4. Независимость реализации + +Модель знаний не зависит от: + +- языка программирования; +- структуры программного обеспечения; +- используемых индикаторов; +- способа хранения данных; +- конкретных алгоритмов анализа. + +Реализация должна соответствовать модели знаний, а не наоборот. + +### Аксиома 5. Независимость от торговых решений + +Модель знаний не принимает торговых решений. + +Торговое решение является отдельным этапом обработки информации и основывается на уже сформированной модели рынка. + +### Аксиома 6. Целостность модели + +Рынок рассматривается как единая взаимосвязанная система. + +Отдельные знания не являются независимыми сигналами. + +Каждое знание рассматривается как часть единой модели текущего состояния рынка. + +# Часть I. Фундамент модели знаний + +Определяет фундаментальные понятия, терминологию, аксиомы и язык модели знаний. + +## 4. Фундаментальные понятия + +### 4.1 Рынок + +#### Назначение + +Настоящий раздел определяет фундаментальное понятие рынка, относительно которого формируются все остальные понятия настоящей модели знаний. + +Все последующие понятия настоящей модели определяются относительно понятия рынка и рассматриваются как описание различных аспектов одной и той же предметной области. + +#### Определение + +**Рынок --- это объективно существующая динамическая система, непрерывно изменяющаяся во времени и доступная для наблюдения.** + +В рамках настоящей модели рынок рассматривается исключительно как предметная область, знания о которой могут быть получены путём последовательного наблюдения, анализа и формализации наблюдаемых +характеристик. + +#### Фундаментальные аксиомы + +*Аксиома 4.1.1. Независимость рынка* + +Рынок существует независимо от наблюдателя. + +Его существование не зависит от наличия системы анализа, программной реализации или человека, выполняющего наблюдение. + +*Аксиома 4.1.2. Динамичность рынка* + +Рынок непрерывно изменяется. + +Любая модель рынка описывает рынок только в пределах рассматриваемого интервала наблюдения. + +*Аксиома 4.1.3. Ограниченность познания* + +Рынок не может быть полностью познан посредством одного наблюдения. + +Любые знания о рынке формируются только на основании совокупности наблюдений и их последующего анализа. + +*Аксиома 4.1.4. Разделение рынка и модели* + +Настоящий документ описывает рынок как предметную область. + +Модель рынка является результатом применения положений настоящего стандарта и рассматривается в последующих главах. + +#### Следствия + +Из настоящего определения следуют следующие положения. + +1. Все знания настоящей модели формируются на основе наблюдений рынка. +2. Любое знание относится к рынку непосредственно либо выводится логически из ранее полученных знаний. +3. Одно наблюдение не может дать полного описания рынка. +4. Полная модель текущего состояния рынка формируется посредством объединения различных видов знаний. + +#### Ограничения + +Настоящий раздел не определяет: + +- внутреннее устройство рынка; +- причины изменения рынка; +- состав участников рынка; +- механизмы формирования цены; +- механизмы торговли; +- экономические модели рынка. + +Указанные вопросы не являются необходимыми для построения формальной модели знаний и рассматриваются только в той мере, в которой они выражаются через наблюдаемые свойства рынка. + +### 4.2 Наблюдение + +(пока пусто) + +### 4.3 Свойство + +(пока пусто) + +### 4.4 Состояние + +(пока пусто) + +### 4.5 Контекст + +(пока пусто) + +### 4.6 Оценка + +(пока пусто) + +### 4.7 Решение + +(пока пусто) + +### 4.8 Иерархия понятий + +(пока пусто) + +# Часть II. Архитектура знаний + +Определяет структуру знаний, их взаимосвязи и принципы формирования модели рынка. + +# Часть III. Модель рынка + +Определяет модель наблюдаемого рынка и её составные элементы. + +# Часть IV. Домены знаний + +Определяет домены знаний, необходимые для формирования полной модели рынка. + +# Часть V. Архитектура Engine + +Определяет принципы распределения доменов знаний между специализированными Engine. + +# Часть VI. Модель состояния рынка + +Определяет состав полной модели текущего состояния рынка. + +# Часть VII. Граница принятия решений + +Определяет границу между моделью знаний и процессом принятия торговых решений. + +# Часть VIII. Развитие стандарта + +Определяет правила развития настоящего стандарта. + +Настоящая вводная часть определяет назначение документа, его границы, фундаментальные аксиомы и общую структуру. + +Все последующие главы посвящены исключительно построению формальной модели знаний о рынке. diff --git a/docs/market_intelligence/reference_model/ontology.md b/docs/market_intelligence/reference_model/ontology.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reference_model/section_4_1_market.md b/docs/market_intelligence/reference_model/section_4_1_market.md new file mode 100644 index 0000000..4703ee6 --- /dev/null +++ b/docs/market_intelligence/reference_model/section_4_1_market.md @@ -0,0 +1,77 @@ +### 4.1 Рынок + +#### Назначение + +Настоящий раздел определяет фундаментальное понятие рынка, относительно +которого формируются все остальные понятия настоящей модели знаний. + +Все последующие определения рассматриваются как описание различных +аспектов одного и того же рынка и не могут существовать вне этого +понятия. + +#### Определение + +**Рынок --- это объективно существующая динамическая система, непрерывно +изменяющаяся во времени и доступная для непосредственного наблюдения.** + +В рамках настоящей модели рынок рассматривается исключительно как +предметная область, знания о которой могут быть получены путём +последовательного наблюдения, анализа и формализации наблюдаемых +характеристик. + +#### Фундаментальные аксиомы + +##### Аксиома 4.1.1. Независимость рынка + +Рынок существует независимо от наблюдателя. + +Его существование не зависит от наличия системы анализа, программной +реализации или человека, выполняющего наблюдение. + +##### Аксиома 4.1.2. Динамичность рынка + +Рынок непрерывно изменяется. + +Любая модель рынка описывает рынок только в пределах рассматриваемого +интервала наблюдения. + +##### Аксиома 4.1.3. Ограниченность познания + +Рынок не может быть полностью познан посредством одного наблюдения. + +Любые знания о рынке формируются только на основании совокупности +наблюдений и их последующего анализа. + +##### Аксиома 4.1.4. Разделение рынка и модели + +Настоящий документ описывает рынок как предметную область. + +Модель рынка является результатом применения положений настоящего +стандарта и рассматривается в последующих главах. + +#### Следствия + +Из настоящего определения следуют следующие положения. + +1. Рынок является источником всех наблюдаемых знаний, используемых + системой Dzentra. +2. Любое знание относится к рынку непосредственно либо выводится + логически из ранее полученных знаний. +3. Одно наблюдение не может дать полного описания рынка. +4. Полная модель текущего состояния рынка формируется посредством + объединения различных видов знаний. + +#### Ограничения + +Настоящий раздел не определяет: + +- внутреннее устройство рынка; +- причины изменения рынка; +- состав участников рынка; +- механизмы формирования цены; +- механизмы торговли; +- экономические модели рынка. + +Указанные вопросы не являются необходимыми для построения формальной +модели знаний и рассматриваются только в той мере, в которой они +выражаются через наблюдаемые свойства рынка. diff --git a/docs/market_intelligence/research/methodology/research_methodology.md b/docs/market_intelligence/research/methodology/research_methodology.md new file mode 100644 index 0000000..2fd29f2 --- /dev/null +++ b/docs/market_intelligence/research/methodology/research_methodology.md @@ -0,0 +1,585 @@ +# Market Intelligence Research Methodology + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Market Intelligence Research Methodology | +| Тип документа | Engineering Standard | +| Версия | 1.0 | +| Статус | Release | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Язык | Русский | + +--- + +# Назначение + +Настоящий документ определяет единую методологию исследования источников рыночной информации. + +Методология применяется при исследовании любых источников данных независимо от: + +- биржи; +- транспортного протокола; +- версии API; +- способа получения данных. + +Настоящий документ определяет общий порядок проведения исследований, но не содержит перечень конкретных проверок. + +Конкретные проверки определяются специализированными документами Research Checklist. + +--- + +# Фундаментальная модель исследования + +Подсистема Market Intelligence строится на трех последовательно связанных уровнях. + +```text +ФАКТЫ + │ + ▼ +ЗНАНИЯ + │ + ▼ +РЕШЕНИЯ +``` + +Каждый следующий уровень может строиться исключительно на основании предыдущего. + +--- + +## Уровень 1. Факты + +Факты представляют собой объективную информацию, получаемую непосредственно от биржи. + +На данном уровне отсутствует какая-либо интерпретация данных. + +Примеры фактов: + +- bid; +- ask; +- volume; +- trade; +- candle; +- order book; +- funding rate; +- open interest; +- liquidation. + +Результатом данного уровня являются подтвержденные информационные сущности (Information Facts). + +--- + +## Уровень 2. Знания + +Знания представляют собой интерпретацию подтвержденных фактов. + +На данном уровне строится модель состояния рынка. + +Примеры знаний: + +- тренд; +- импульс; +- ликвидность; +- волатильность; +- дисбаланс стакана; +- направление движения; +- фаза рынка; +- вероятность продолжения движения; +- вероятность разворота. + +Знания никогда не извлекаются непосредственно из API. + +Они формируются исключительно на основании подтвержденных фактов. + +--- + +## Уровень 3. Решения + +Решения представляют собой результат работы торговой системы. + +Примеры решений: + +- открыть позицию; +- закрыть позицию; +- увеличить позицию; +- уменьшить позицию; +- изменить Stop Loss; +- изменить Take Profit; +- отказаться от входа в рынок. + +Решения принимаются исключительно на основании знаний. + +Факты не используются для принятия торговых решений напрямую. + +--- + +# Основные архитектурные принципы + +## Принцип последовательного построения знаний + +Построение подсистемы Market Intelligence всегда выполняется в следующем порядке: + +1. Исследование фактов. +2. Подтверждение фактов. +3. Построение модели знаний. +4. Принятие решений. + +Нарушение данной последовательности считается архитектурной ошибкой. + +--- + +## Принцип отсутствия предположений + +Подсистема Market Intelligence не использует предположения в качестве знаний. + +Любая информационная сущность должна иметь подтвержденный источник происхождения. + +Любое знание должно иметь подтвержденную зависимость от фактов. + +Любое решение должно иметь подтвержденную зависимость от знаний. + +--- + +## Принцип полной трассируемости + +Любое торговое решение должно быть полностью прослеживаемо до фактов, полученных непосредственно от биржи. + +Для любого решения должна существовать непрерывная цепочка происхождения информации. + +```text +Биржа + │ + ▼ +Факты + │ + ▼ +Знания + │ + ▼ +Решение +``` + +Каждый переход между уровнями должен быть объяснимым и проверяемым. + +--- + +## Принцип воспроизводимости + +Любой вывод, содержащийся в документации Market Intelligence, должен быть воспроизводим. + +Повторное выполнение исследования должно приводить к тем же результатам при одинаковых исходных условиях. + +--- + +# Цель исследования + +Любое исследование должно ответить на главный вопрос. + +> **Какие факты о состоянии рынка предоставляет исследуемый источник данных?** + +Исследование не должно ограничиваться описанием структуры API. + +Результатом исследования является выявление подтвержденных информационных сущностей, содержащихся в исследуемом источнике данных. + +--- + +# Основные принципы исследования + +## Исследуется источник информации + +Объектом исследования является не программный интерфейс API, а информация, передаваемая данным источником. + +--- + +## Исследуются фактические данные + +Все выводы должны подтверждаться одновременно: + +- официальной документацией; +- фактическими сообщениями биржи; +- экспериментальными исследованиями. + +Если подтверждение отсутствует, вывод считается неподтвержденным. + +--- + +## Исследуются информационные факты + +Исследование должно отвечать не на вопрос + +> Какие поля содержит JSON? + +а на вопрос + +> Какие факты о состоянии рынка передает данный источник? + +--- + +## Исследования являются воспроизводимыми + +Каждый вывод должен подтверждаться экспериментом, который может быть повторен независимо от автора исследования. + +--- + +## Принцип минимально необходимого исследования + +Исследование должно проводиться в объеме, достаточном для подтверждения или опровержения информации, содержащейся в официальной документации. + +Если официальная документация содержит однозначное описание исследуемого аспекта и фактическое поведение API соответствует этому описанию, дополнительные эксперименты не проводятся. + +Экспериментальные исследования выполняются только в случаях, когда: + +- документация отсутствует; +- документация неоднозначна; +- документация противоречит фактическому поведению API; +- требуется определить неописанное поведение системы. + +--- + +# Общий процесс исследования + +Любое исследование выполняется в строго определенной последовательности. + +Каждый следующий этап может начинаться только после завершения предыдущего. + +--- + +## Этап 1. Изучение официальной документации + +Цель этапа — определить ожидаемое назначение исследуемого источника данных. + +Результат этапа: + +- понимание назначения endpoint; +- понимание способа взаимодействия; +- понимание ожидаемой структуры сообщений; +- определение перечня исследуемых сущностей. + +На данном этапе не делаются выводы о фактическом поведении источника данных. + +--- + +## Этап 2. Получение фактических данных + +Цель этапа — получить реальные сообщения исследуемого источника. + +Получение данных выполняется посредством специализированных Probe. + +Результат этапа: + +- реальные сообщения биржи; +- реальные ответы endpoint; +- реальные особенности поведения; +- материал для последующего анализа. + +--- + +## Этап 3. Определение типов сообщений + +Для исследуемого источника необходимо определить: + +- все типы сообщений; +- назначение каждого типа сообщений; +- сообщения, содержащие рыночную информацию; +- служебные сообщения; +- сообщения управления; +- сообщения об ошибках. + +Результатом этапа является классификация сообщений исследуемого источника. + +--- + +## Этап 4. Исследование структуры сообщений + +Для каждого типа сообщений необходимо определить: + +- перечень полей; +- тип каждого поля; +- обязательность поля; +- допустимые значения; +- ограничения; +- взаимное расположение данных. + +Результатом этапа является описание структуры каждого типа сообщений. + +--- + +## Этап 5. Исследование семантики + +Для каждого поля необходимо определить: + +- фактическое назначение; +- происхождение информации; +- ограничения использования; +- степень достоверности; +- взаимосвязь с другими полями. + +На данном этапе запрещается использовать предположения как подтвержденные знания. + +--- + +## Этап 6. Выделение информационных фактов + +На основании исследованных сообщений необходимо определить: + +- какие информационные факты содержит источник; +- какие факты можно вычислить непосредственно из сообщения; +- какие факты отсутствуют. + +Результатом этапа является перечень информационных фактов, предоставляемых исследуемым источником. + +--- + +## Этап 7. Исследование поведения + +Для исследуемого источника необходимо определить: + +- особенности изменения данных; +- последовательность сообщений; +- взаимосвязь изменений; +- особенности временного поведения; +- ограничения источника; +- особенности протокола. + +Результатом этапа является описание поведения исследуемого источника. + +--- + +## Этап 8. Сравнение с другими источниками + +Для каждого информационного факта необходимо определить: + +- существует ли аналогичный источник; +- подтверждается ли эквивалентность; +- имеются ли различия; +- какой источник является приоритетным. + +Эквивалентность считается подтвержденной только после экспериментальной проверки. + +--- + +## Этап 9. Формирование экспериментальных выводов + +После завершения исследования все выводы должны быть разделены на следующие категории: + +- подтвержденные; +- предварительно подтвержденные; +- гипотезы; +- опровергнутые. + +Только подтвержденные выводы могут использоваться при построении Information Mapping. + +--- + +## Этап 10. Подготовка Endpoint Research + +Результаты исследования оформляются отдельным документом Endpoint Research. + +Документ должен содержать: + +- описание источника; +- результаты исследований; +- подтвержденные информационные факты; +- особенности поведения; +- результаты сравнений; +- экспериментальные выводы. + +Endpoint Research является основным документом, описывающим исследуемый источник данных. + +--- + +## Этап 11. Обновление Information Mapping + +После подтверждения информационных фактов обновляется документ: + +> Dzengi Market Intelligence Information Mapping. + +В Information Mapping включаются только подтвержденные информационные факты. + +--- + +## Этап 12. Обновление Information Model + +Если исследование выявило новые информационные факты или новые взаимосвязи между ними, обновляется документ: + +> Dzentra Market Intelligence Information Model. + +--- + +## Этап 13. Обновление Knowledge Catalogue + +Если исследование позволило сформировать новые знания о рынке, обновляется документ: + +> Dzentra Market Knowledge Catalogue. + +Knowledge Catalogue никогда не строится непосредственно на данных API. + +Он строится исключительно на основании подтвержденных информационных фактов и Information Model. + +--- + +## Этап 14. Завершение исследования + +Исследование считается завершенным только при выполнении всех следующих условий: + +- подготовлен Endpoint Research; +- обновлен Information Mapping; +- при необходимости обновлена Information Model; +- при необходимости обновлен Knowledge Catalogue; +- все выводы имеют степень подтверждения. + +После завершения исследования результаты могут использоваться другими подсистемами Dzentra. + +--- + +# Степени подтверждения + +Любой вывод, содержащийся в документации Market Intelligence, должен иметь явно указанную степень подтверждения. + +Использование неподтвержденных выводов в качестве знаний запрещается. + +| Статус | Описание | +|---------|----------| +| Не исследовано | Исследование соответствующего вопроса не проводилось. | +| Гипотеза | Имеется предположение, основанное на документации или наблюдениях, но отсутствует экспериментальное подтверждение. | +| Предварительно подтверждено | Подтверждено ограниченным количеством экспериментов. Требуется дополнительная проверка. | +| Подтверждено | Подтверждено официальной документацией и экспериментальными исследованиями либо многократно подтверждено экспериментально. | +| Опровергнуто | Экспериментально доказано отсутствие соответствия первоначальному предположению. | + +--- + +# Критерии качества исследования + +Исследование считается качественно выполненным, если выполняются все следующие условия. + +## Полнота + +Исследованы все типы сообщений исследуемого источника. + +Исследованы все поля сообщений. + +Исследованы все информационные факты. + +--- + +## Подтверждаемость + +Каждый вывод имеет экспериментальное подтверждение либо явно обозначен как гипотеза. + +--- + +## Воспроизводимость + +Любой эксперимент может быть повторен другим разработчиком и привести к тем же результатам. + +--- + +## Трассируемость + +Каждый информационный факт имеет подтвержденный источник происхождения. + +Каждое знание имеет подтвержденную зависимость от информационных фактов. + +Каждое торговое решение должно быть объяснимо через цепочку: + +```text +Биржа + │ + ▼ +Информационные факты + │ + ▼ +Знания + │ + ▼ +Решение +``` + +--- + +## Документированность + +Все результаты исследования отражены в соответствующих документах: + +- Endpoint Research; +- Information Mapping; +- Information Model; +- Knowledge Catalogue. + +--- + +# Жизненный цикл исследований + +Исследование источников рыночной информации является непрерывным процессом. + +Появление новых возможностей API, изменение поведения биржи или обнаружение новых фактов требует повторного проведения соответствующих исследований. + +Исследование никогда не считается завершенным окончательно. + +Каждый исследованный источник может быть повторно исследован при появлении новых обстоятельств. + +--- + +## Основания для повторного исследования + +Повторное исследование выполняется при возникновении одного или нескольких следующих событий: + +- выпуск новой версии API; +- изменение структуры сообщений; +- появление новых полей; +- изменение семантики существующих полей; +- обнаружение противоречий между источниками; +- получение новых экспериментальных данных; +- обнаружение ошибок предыдущих исследований; +- изменение архитектурных требований Dzentra. + +--- + +# Использование результатов исследований + +Результаты исследований используются исключительно как источник подтвержденных информационных фактов. + +Настоящий документ не определяет: + +- архитектуру Engine; +- алгоритмы анализа рынка; +- методы прогнозирования; +- торговые стратегии; +- правила управления капиталом; +- правила открытия или закрытия позиций. + +Указанные вопросы рассматриваются отдельными архитектурными документами Dzentra. + +--- + +# Связанные документы + +Настоящий документ используется совместно со следующими документами: + +- Market Intelligence Stream Research Checklist; +- Market Intelligence REST Research Checklist; +- Market Intelligence WebSocket Request Research Checklist; +- Endpoint Research; +- Dzengi Market Intelligence Information Mapping; +- Dzentra Market Intelligence Information Model; +- Dzentra Market Knowledge Catalogue. + +--- + +# Заключение + +Настоящая методология определяет единый инженерный подход к исследованию источников рыночной информации. + +Все знания, используемые подсистемой Market Intelligence, должны быть построены исключительно на основании подтвержденных информационных фактов. + +Таким образом обеспечиваются: + +- воспроизводимость исследований; +- объяснимость полученных знаний; +- трассируемость торговых решений; +- независимость знаний от конкретной реализации API; +- возможность повторного исследования при изменении источников данных. + +Следование настоящей методологии является обязательным требованием при разработке и сопровождении подсистемы Market Intelligence проекта Dzentra. \ No newline at end of file diff --git a/docs/market_intelligence/research/results/dzengi/README.md b/docs/market_intelligence/research/results/dzengi/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/protocols/stream/protocol_stream_marketData.subscribe.md b/docs/market_intelligence/research/results/dzengi/protocols/stream/protocol_stream_marketData.subscribe.md new file mode 100644 index 0000000..d1bc02a --- /dev/null +++ b/docs/market_intelligence/research/results/dzengi/protocols/stream/protocol_stream_marketData.subscribe.md @@ -0,0 +1,341 @@ +# Stream Research Protocol — marketData.subscribe + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Stream Research Protocol — marketData.subscribe | +| Тип документа | Research Protocol | +| Версия | 2.0 | +| Статус | Draft | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Биржа | Dzengi | +| Endpoint | `marketData.subscribe` | +| Транспорт | WebSocket Stream | +| Основан на | Market Intelligence Stream Research Standard | +| Язык | Русский | + +--- + +# 1. Исследование подключения + +## 1.1 Подключение + +### 1.1.1 Корректность подключения + +- [x] корректность подключения — подтверждена. + +#### Журнал исследования + +Получено успешное подключение к WebSocket API Dzengi. + +Получено подтверждение подписки. + +После подтверждения подписки начинают поступать сообщения `internal.quote`. + +--- + +### 1.1.2 Требования к соединению + +- [x] URL подключения — соответствует документации. +- [x] используемый транспорт (WS/WSS) — соответствует документации. + +#### Журнал исследования + +**URL подключения** + +Production: `wss://api-adapter.dzengi.com/connect` +Demo: `wss://demo-api-adapter.dzengi.com/connect` + +**Используемый транспорт** + +`WSS (WebSocket Secure)` + +--- + +### 1.1.3 Требования к авторизации + +- [x] требуется ли авторизация — соответствует документации. +- [x] механизм авторизации — соответствует документации. +- [x] обязательные заголовки авторизации — соответствует документации. +- [x] возможность работы без авторизации — соответствует документации. + +#### Журнал исследования + +Публичный поток `marketData.subscribe` доступен без передачи данных авторизации. + +--- + +### 1.2 Подписка + +- [x] формат подписки — частично соответствует документации; +- [x] подтверждение подписки — соответствует документации; +- [x] сообщения об ошибках — определено экспериментально; +- [х] повторная подписка — определено экспериментально; +- [x] подписка на несколько инструментов — определено экспериментально. + +#### Журнал исследования + +**Формат подписки** + +Документация описывает только содержимое `payload`: + +``` +{ + "symbols": [ + "string" + ] +} +``` + +Фактический формат сообщения, передаваемого через WebSocket /connect: + +``` +{ + "correlationId": "...", + "destination": "marketData.subscribe", + "payload": { + "symbols": [ + "BTC/USD_LEVERAGE" + ] + } +} +``` + +**Подтверждение подписки** + +``` +{ + "destination": "marketData.subscribe", + "status": "OK", + "payload": { + "subscriptions": { + "BTC/USD_LEVERAGE": "PROCESSED" + } + } +} +``` + +**Сообщения об ошибках** + +Экспериментально зафиксированы варианты ошибок: + +- `INVALID/SYMBOL` возвращает `status = OK`, но в `payload.subscriptions` указывается `ERROR: INVALID/SYMBOL not found`. +- пустой `symbols` возвращает `status = ERROR`, `code = -1128`. +- отсутствующий `symbols` возвращает `status = ERROR`, `code = -1128`. +- отсутствующий `payload` возвращает `status = ERROR`, `code = -1128`. +- неверный `destination` возвращает `status = ERROR`, `errorCode = BAD_REQUEST`. + +**Повторная подписка** + +- первая подписка получает статус `PROCESSED`; +- повторные подписки получают статус `ALREADY_SUBSCRIBED`; +- сообщения об ошибке не формируются; +- повторная подписка работает без предварительной отмены; +- в проведенных экспериментах признаков дублирования сообщений `internal.quote` не обнаружено. + +**Подписка на несколько инструментов** + +Экспериментально установлено: + +- команда `marketData.subscribe` принимает несколько торговых инструментов; +- подтверждение подписки содержит отдельный статус для каждого инструмента; +- поток `internal.quote` передает сообщения для всех подписанных инструментов через одно WebSocket-соединение; +- принадлежность сообщения к инструменту определяется полем `payload.symbolName`. + +--- + +# 2. Исследование структуры потока + +## 2.1 Типы сообщений + +- [ ] назначение; +- [ ] содержит ли рыночную информацию; +- [ ] содержит ли служебную информацию; +- [ ] содержит ли ошибки; +- [ ] содержит ли подтверждение подписки; +- [ ] содержит ли подтверждение отписки. + +#### Журнал исследования + +--- + +## 2.2 Структура сообщений + +- [ ] обязательные поля; +- [ ] необязательные поля; +- [ ] тип каждого поля; +- [ ] допустимые значения; +- [ ] диапазоны значений; +- [ ] ограничения. + +#### Журнал исследования + +--- + +# 3. Классификация сообщений + +- [ ] рыночное сообщение; +- [ ] служебное сообщение; +- [ ] сообщение управления; +- [ ] сообщение об ошибке; +- [ ] сообщение подтверждения. + +#### Журнал исследования + +--- + +# 4. Исследование семантики полей + +- [ ] фактическое назначение; +- [ ] источник происхождения; +- [ ] обязательность; +- [ ] изменяемость; +- [ ] диапазон допустимых значений; +- [ ] взаимосвязь с другими полями; +- [ ] ограничения использования; +- [ ] степень подтверждения семантики. + +#### Журнал исследования + +--- + +# 5. Исследование информационных фактов + +- [ ] какие информационные факты содержит сообщение; +- [ ] какие информационные факты могут быть вычислены непосредственно из сообщения; +- [ ] какие информационные факты отсутствуют; +- [ ] какие информационные факты являются первичными; +- [ ] какие информационные факты являются производными. + +#### Журнал исследования + +--- + +## 5.1 Первичные информационные факты + +- [ ] источник происхождения; +- [ ] поле сообщения; +- [ ] степень подтверждения; +- [ ] ограничения использования. + +#### Журнал исследования + +--- + +## 5.2 Производные информационные факты + +- [ ] формула вычисления; +- [ ] необходимые исходные данные; +- [ ] ограничения вычисления; +- [ ] степень достоверности. + +#### Журнал исследования + +--- + +# 6. Исследование поведения потока + +## 6.1 Частота сообщений + +- [ ] минимальная частота; +- [ ] максимальная частота; +- [ ] средняя частота; +- [ ] пиковая частота; +- [ ] условия изменения частоты. + +#### Журнал исследования + +--- + +## 6.2 Последовательность сообщений + +- [ ] порядок поступления сообщений; +- [ ] последовательность timestamp; +- [ ] наличие пропусков; +- [ ] наличие повторяющихся сообщений; +- [ ] возможность нарушения порядка. + +#### Журнал исследования + +--- + +## 6.3 Полнота сообщений + +- [ ] полный снимок состояния; +- [ ] инкрементальные изменения; +- [ ] смешанный режим передачи; +- [ ] обязательность всех полей; +- [ ] возможность частичных обновлений. + +#### Журнал исследования + +--- + +# 7. Исследование поведения данных + +- [ ] условия появления; +- [ ] условия изменения; +- [ ] условия исчезновения; +- [ ] частота изменения; +- [ ] взаимосвязь с другими фактами. + +--- + +## Дополнительно определить + +- [ ] какие факты изменяются одновременно; +- [ ] какие факты никогда не изменяются одновременно; +- [ ] какие факты являются независимыми; +- [ ] какие факты являются производными от других. + +#### Журнал исследования + +--- + +# 8. Проверка эквивалентности + +- [ ] существует ли аналогичный источник; +- [ ] полностью ли совпадает значение; +- [ ] совпадает ли семантика; +- [ ] совпадает ли точность; +- [ ] совпадает ли момент обновления; +- [ ] имеются ли расхождения. + +#### Журнал исследования + +--- + +## 8.1 Альтернативные источники + +- [ ] REST endpoint; +- [ ] WebSocket Request; +- [ ] другие Stream; +- [ ] внутренние вычисления. + +#### Журнал исследования + +--- + +## 8.2 Приоритет источников + +- [ ] основной источник; +- [ ] резервный источник; +- [ ] допустимые альтернативы; +- [ ] причины выбора приоритетного источника. + +#### Журнал исследования + +--- + +# 9. Исследование производительности + +- [ ] средний размер сообщения; +- [ ] максимальный размер сообщения; +- [ ] средняя скорость передачи; +- [ ] максимальная скорость передачи; +- [ ] объём данных в минуту; +- [ ] объём данных в час. + +#### Журнал исследования \ No newline at end of file diff --git a/docs/market_intelligence/research/results/dzengi/protocols/websocket_transport/protocol_websocket_transport.md b/docs/market_intelligence/research/results/dzengi/protocols/websocket_transport/protocol_websocket_transport.md new file mode 100644 index 0000000..f569a44 --- /dev/null +++ b/docs/market_intelligence/research/results/dzengi/protocols/websocket_transport/protocol_websocket_transport.md @@ -0,0 +1,46 @@ +### 1.1.4 Дополнительные требования клиента + +- [x] обязательный формат сообщений — частично соответствует документации; +- [ ] поддержание соединения; + - [x] heartbeat / keepalive / ping-pong — определено экспериментально; + - [ ] автоматическое закрытие соединения; + - [ ] idle timeout; +- [ ] требования к кодировке; +- [ ] другие обязательные требования. + +#### Журнал исследования + +**Обязательный формат сообщений** + +Документация указывает формат payload (json): + +``` +{ + "symbols": [ + "string" + ] +} +``` + +Фактический формат сообщения через WebSocket /connect: + +``` +{ + "correlationId": "...", + "destination": "marketData.subscribe", + "payload": { + "symbols": ["BTC/USD_LEVERAGE"] + } +} +``` + +**Поддержание соединения** + +Документация требований не содержит. + +За время наблюдения получены сообщения только следующих типов: + +- `marketData.subscribe` +- `internal.quote` + +Специальные сообщения heartbeat / keepalive / ping / pong не обнаружены. \ No newline at end of file diff --git a/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_aggTrades.md b/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_aggTrades.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_depth.md b/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_depth.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_exchangeInfo.md b/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_exchangeInfo.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_klines.md b/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_klines.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_ticker_24hr.md b/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_ticker_24hr.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_time.md b/docs/market_intelligence/research/results/dzengi/reports/rest/rest_api_v1_time.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/stream/stream_OHLCMarketData.subscribe.md b/docs/market_intelligence/research/results/dzengi/reports/stream/stream_OHLCMarketData.subscribe.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/stream/stream_depthMarketData.subscribe.md b/docs/market_intelligence/research/results/dzengi/reports/stream/stream_depthMarketData.subscribe.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/stream/stream_marketData.subscribe.md b/docs/market_intelligence/research/results/dzengi/reports/stream/stream_marketData.subscribe.md new file mode 100644 index 0000000..72a4736 --- /dev/null +++ b/docs/market_intelligence/research/results/dzengi/reports/stream/stream_marketData.subscribe.md @@ -0,0 +1,269 @@ +# Dzengi API Endpoint Research — marketData.subscribe + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Dzengi API Endpoint Research — marketData.subscribe | +| Тип документа | Endpoint Research | +| Версия | 1.0 | +| Статус | Draft | +| Степень верификации | Runtime Research | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Источник | Dzengi API | +| Endpoint | `marketData.subscribe` | +| Транспорт | WebSocket Stream | +| Язык | Русский | + +--- + +## Статус документа + +Настоящий документ содержит результаты исследования endpoint `marketData.subscribe`, выполненного посредством анализа официальной документации Dzengi и фактических сообщений, полученных от биржи. + +Документ предназначен для определения информационных сущностей, передаваемых данным endpoint, без интерпретации состояния рынка. + +--- + +## Цель исследования + +Цель настоящего исследования — определить: + +- назначение endpoint; +- структуру протокола обмена сообщениями; +- типы сообщений; +- семантику сообщений; +- информационные сущности, содержащиеся в сообщениях; +- особенности поведения потока данных; +- взаимосвязь с другими endpoint API Dzengi. + +--- + +## Основной вопрос исследования + +> **Какую информацию о состоянии рынка предоставляет поток `marketData.subscribe`?** + +--- + +## Источники исследования + +Исследование основано на: + +- официальной документации Dzengi; +- OpenAPI (Swagger); +- официальных примерах сообщений; +- фактических сообщениях, полученных посредством Dzengi API Probe. + +--- + +# 1. Назначение endpoint + +## Описание документации + +Согласно документации Dzengi, endpoint `marketData.subscribe` предназначен для получения потока рыночных котировок выбранных торговых инструментов. + +--- + +## Назначение по результатам исследования + +Исследование подтверждает, что `marketData.subscribe` является командой подписки на поток рыночных котировок. + +После успешного выполнения подписки биржа начинает передавать поток сообщений с актуальными котировками выбранных инструментов. + +Сам endpoint не является типом рыночного сообщения. + +--- + +# 2. Способ получения данных + +| Свойство | Значение | +|----------|----------| +| Транспорт | WebSocket Stream | +| Тип взаимодействия | Подписка | +| Endpoint | `marketData.subscribe` | +| Формат сообщений | JSON | + +--- + +# 3. Параметры подписки + +| Параметр | Тип | Обязательный | Назначение | +|----------|-----|--------------|------------| +| symbols | array | Да | Список торговых инструментов | + +--- + +# 4. Протокол взаимодействия + +Исследование показало, что взаимодействие состоит из двух независимых этапов. + +```text +Клиент + │ + │ marketData.subscribe + ▼ +Биржа + │ + │ Subscription confirmation + ▼ +Биржа + │ + │ internal.quote + │ internal.quote + │ internal.quote + ▼ +Клиент +``` + +Команда `marketData.subscribe` используется исключительно для оформления подписки. + +Информация о состоянии рынка передается сообщениями другого типа — `internal.quote`. + +--- + +# 5. Типы сообщений + +## 5.1 Subscription confirmation + +### Назначение + +Подтверждение успешного оформления подписки. + +### Пример сообщения + +```json +{ + "correlationId": "probe-market-data-subscribe", + "destination": "marketData.subscribe", + "payload": { + "subscriptions": { + "BTC/USD_LEVERAGE": "PROCESSED" + } + }, + "status": "OK" +} +``` + +### Семантика + +Сообщение подтверждает регистрацию подписки. + +Рыночной информации не содержит. + +--- + +## 5.2 Quote update (`internal.quote`) + +### Назначение + +Передача актуальных рыночных котировок. + +### Пример сообщения + +```json +{ + "destination": "internal.quote", + "payload": { + "bid": 62055.80, + "bidQty": 5.0, + "ofr": 62055.90, + "ofrQty": 5.0, + "symbolName": "BTC/USD_LEVERAGE", + "timestamp": 1783537924352 + }, + "status": "OK" +} +``` + +--- + +# 6. Анализ структуры сообщения + +| Поле | Тип | Изменяется | Назначение | Статус | +|------|-----|------------|------------|--------| +| bid | Number | Да | Лучшая цена покупки | Подтверждено | +| bidQty | Number | Да | Объем на лучшей цене покупки | Предварительно подтверждено | +| ofr | Number | Да | Лучшая цена продажи | Подтверждено | +| ofrQty | Number | Да | Объем на лучшей цене продажи | Предварительно подтверждено | +| symbolName | String | Нет | Торговый инструмент | Подтверждено | +| timestamp | Long | Да | Время формирования сообщения биржей | Подтверждено | + +--- + +# 7. Информационные сущности, извлекаемые из сообщений + +| Information ID | Источник (JSON Path) | Статус | +|----------------|----------------------|--------| +| best_bid_price | `payload.bid` | Подтверждено | +| best_bid_quantity | `payload.bidQty` | Предварительно подтверждено | +| best_ask_price | `payload.ofr` | Подтверждено | +| best_ask_quantity | `payload.ofrQty` | Предварительно подтверждено | +| instrument_symbol | `payload.symbolName` | Подтверждено | +| quote_timestamp | `payload.timestamp` | Подтверждено | + +--- + +# 8. Поведение потока + +## Подтвержденные наблюдения + +- Подписка выполняется один раз. +- После подтверждения подписки биржа начинает передавать поток сообщений `internal.quote`. +- Каждое сообщение содержит полный набор исследованных полей. +- Сообщения поступают только при изменении котировки. +- Каждое сообщение относится к одному торговому инструменту. + +--- + +## Требует дополнительного исследования + +- Всегда ли `bidQty` соответствует объему первого уровня стакана. +- Всегда ли `ofrQty` соответствует объему первого уровня стакана. +- Возможны ли сообщения без изменения цены. +- Возможны ли сообщения с одинаковым `timestamp`. + +--- + +# 9. Связь с другими endpoint + +| Потенциальная информационная сущность | Endpoint | Поле | Статус | +|--------------------------------------|----------|------|--------| +| best_bid_price | REST `/api/v1/depth` | `bids[0][0]` | Подтверждено | +| best_bid_price | WSS `/api/v1/depth` | `payload.bids[0][0]` | Подтверждено | +| best_bid_price | `marketData.subscribe` | `payload.bid` | Подтверждено | +| best_ask_price | REST `/api/v1/depth` | `asks[0][0]` | Подтверждено | +| best_ask_price | WSS `/api/v1/depth` | `payload.asks[0][0]` | Подтверждено | +| best_ask_price | `marketData.subscribe` | `payload.ofr` | Подтверждено | +| best_bid_price | REST `/api/v1/ticker/24hr` | `bidPrice` | Требует исследования | +| best_ask_price | REST `/api/v1/ticker/24hr` | `askPrice` | Требует исследования | + +--- + +# 10. Итоги исследования + +По результатам исследования подтверждено, что endpoint `marketData.subscribe` не является источником отдельных запросов к бирже, а представляет собой механизм подписки на поток рыночных котировок. + +Фактическая рыночная информация передается сообщениями типа `internal.quote`. + +Исследование также подтвердило эквивалентность информационных сущностей `best_bid_price` и `best_ask_price`, получаемых посредством `marketData.subscribe`, REST `/api/v1/depth` и WSS `/api/v1/depth`. + +Эквивалентность соответствующих полей endpoint `/api/v1/ticker/24hr` на момент подготовки настоящей редакции документа не подтверждена. + +--- + +# 11. Связь с архитектурой Dzentra + +Результаты настоящего исследования используются при формировании: + +- Dzengi Market Intelligence Information Mapping; +- Dzentra Market Intelligence Information Model; +- Dzentra Market Knowledge Catalogue. + +Настоящий документ не определяет: + +- знания о рынке; +- интерпретацию состояния рынка; +- алгоритмы анализа; +- торговые стратегии; +- архитектуру Engine. \ No newline at end of file diff --git a/docs/market_intelligence/research/results/dzengi/reports/stream/stream_trades.subscribe.md b/docs/market_intelligence/research/results/dzengi/reports/stream/stream_trades.subscribe.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_aggTrades.md b/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_aggTrades.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_depth.md b/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_depth.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_exchangeInfo.md b/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_exchangeInfo.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_klines.md b/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_klines.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_ticker_24hr.md b/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_ticker_24hr.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_time.md b/docs/market_intelligence/research/results/dzengi/reports/websocket/ws_api_v1_time.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/research/standards/research_standard_rest.md b/docs/market_intelligence/research/standards/research_standard_rest.md new file mode 100644 index 0000000..1e487e4 --- /dev/null +++ b/docs/market_intelligence/research/standards/research_standard_rest.md @@ -0,0 +1,261 @@ +# Market Intelligence REST Research Standard + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Market Intelligence REST Research Standard | +| Тип документа | Engineering Standard | +| Версия | 1.0 | +| Статус | Release | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Язык | Русский | + +--- + +# Назначение + +Настоящий документ определяет единый стандарт исследования REST-источников рыночной информации. + +Настоящий стандарт применяется совместно с документом: + +> **Market Intelligence Research Methodology** + +Методология определяет общий процесс проведения исследований. + +Настоящий документ определяет перечень исследований, выполняемых для REST endpoint. + +--- + +# Область применения + +Настоящий стандарт применяется при исследовании REST endpoint, возвращающих рыночную информацию. + +Примеры: + +- `/api/v1/time`; +- `/api/v1/exchangeInfo`; +- `/api/v1/depth`; +- `/api/v1/ticker/24hr`; +- `/api/v1/aggTrades`; +- `/api/v1/klines`. + +--- + +# Цель исследования + +Для каждого REST endpoint необходимо определить: + +- какие информационные факты предоставляет endpoint; +- какие параметры влияют на ответ; +- является ли ответ снимком состояния; +- насколько актуальны получаемые данные; +- как данные связаны с другими источниками; +- каким образом endpoint может использоваться подсистемой Market Intelligence. + +--- + +# 1. Исследование endpoint + +## 1.1 Назначение endpoint + +Определить: + +- [ ] официальное назначение endpoint; +- [ ] фактическое назначение по результатам исследования; +- [ ] тип возвращаемой информации; +- [ ] относится ли endpoint к Market Intelligence. + +--- + +## 1.2 Параметры запроса + +Для каждого параметра определить: + +- [ ] имя параметра; +- [ ] тип; +- [ ] обязательность; +- [ ] допустимые значения; +- [ ] значение по умолчанию; +- [ ] влияние на ответ; +- [ ] ошибки при некорректном значении. + +--- + +## 1.3 Формат ответа + +Определить: + +- [ ] тип ответа; +- [ ] структуру ответа; +- [ ] обязательные поля; +- [ ] необязательные поля; +- [ ] возможные значения; +- [ ] диапазоны значений; +- [ ] поведение при пустом результате. + +--- + +# 2. Исследование семантики полей + +Для каждого поля определить: + +- [ ] фактическое назначение; +- [ ] источник происхождения; +- [ ] тип данных; +- [ ] обязательность; +- [ ] диапазон допустимых значений; +- [ ] связь с другими полями; +- [ ] ограничения использования; +- [ ] степень подтверждения семантики. + +--- + +# 3. Исследование информационных фактов + +Для каждого ответа определить: + +- [ ] какие информационные факты содержит ответ; +- [ ] какие информационные факты могут быть вычислены непосредственно из ответа; +- [ ] какие информационные факты отсутствуют; +- [ ] какие факты являются первичными; +- [ ] какие факты являются производными. + +--- + +## 3.1 Первичные информационные факты + +Для каждого первичного информационного факта определить: + +- [ ] endpoint; +- [ ] параметр запроса; +- [ ] поле ответа; +- [ ] степень подтверждения; +- [ ] ограничения использования. + +--- + +## 3.2 Производные информационные факты + +Для каждого производного информационного факта определить: + +- [ ] формулу вычисления; +- [ ] необходимые исходные данные; +- [ ] ограничения вычисления; +- [ ] степень достоверности. + +--- + +# 4. Исследование актуальности данных + +Определить: + +- [ ] является ли ответ текущим снимком состояния; +- [ ] содержит ли ответ исторические данные; +- [ ] содержит ли ответ агрегированную статистику; +- [ ] как часто обновляется ответ; +- [ ] есть ли timestamp данных; +- [ ] есть ли задержка обновления; +- [ ] можно ли использовать ответ как источник текущего состояния рынка. + +--- + +# 5. Проверка эквивалентности + +Для каждого информационного факта определить: + +- [ ] существует ли аналогичный источник; +- [ ] полностью ли совпадает значение; +- [ ] совпадает ли семантика; +- [ ] совпадает ли точность; +- [ ] совпадает ли момент обновления; +- [ ] имеются ли расхождения. + +--- + +## 5.1 Альтернативные источники + +Для каждого информационного факта определить: + +- [ ] WebSocket Request; +- [ ] WebSocket Stream; +- [ ] другие REST endpoint; +- [ ] внутренние вычисления. + +--- + +## 5.2 Приоритет источников + +Для каждого информационного факта определить: + +- [ ] основной источник; +- [ ] резервный источник; +- [ ] допустимые альтернативы; +- [ ] причины выбора приоритетного источника. + +--- + +# 6. Исследование поведения endpoint + +Проверить: + +- [ ] повторяемость ответа при одинаковом запросе; +- [ ] изменение ответа во времени; +- [ ] поведение при частых запросах; +- [ ] ограничения частоты запросов; +- [ ] ошибки при превышении лимитов; +- [ ] поведение при недоступности данных; +- [ ] поведение при несуществующем инструменте. + +--- + +# 7. Исследование производительности + +Определить: + +- [ ] среднее время ответа; +- [ ] максимальное время ответа; +- [ ] минимальное время ответа; +- [ ] средний размер ответа; +- [ ] максимальный размер ответа; +- [ ] влияние параметров запроса на размер ответа; +- [ ] влияние параметров запроса на время ответа. + +--- + +# 8. Практическая ценность + +Определить: + +- [ ] какие информационные факты предоставляет endpoint; +- [ ] какие факты отсутствуют; +- [ ] может ли endpoint быть основным источником; +- [ ] может ли endpoint быть резервным источником; +- [ ] какие подсистемы Dzentra потенциально могут использовать endpoint; +- [ ] ограничения использования в runtime. + +--- + +# 9. Ограничения исследования + +Определить: + +- [ ] что подтверждено; +- [ ] что предварительно подтверждено; +- [ ] что осталось гипотезой; +- [ ] что опровергнуто; +- [ ] что невозможно определить данным исследованием. + +--- + +# 10. Итоги исследования + +По результатам исследования определить: + +- [ ] степень пригодности endpoint; +- [ ] степень полноты исследования; +- [ ] сильные стороны; +- [ ] ограничения; +- [ ] рекомендации по использованию; +- [ ] необходимость дополнительных исследований. \ No newline at end of file diff --git a/docs/market_intelligence/research/standards/research_standard_stream.md b/docs/market_intelligence/research/standards/research_standard_stream.md new file mode 100644 index 0000000..dc42ddd --- /dev/null +++ b/docs/market_intelligence/research/standards/research_standard_stream.md @@ -0,0 +1,334 @@ +# Market Intelligence Stream Research Standard + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Market Intelligence Stream Research Standard | +| Тип документа | Engineering Standard | +| Версия | 2.0 | +| Статус | Release | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Язык | Русский | + +--- + +# Назначение + +Настоящий документ определяет единый стандарт исследования потоковых (Streaming) источников рыночной информации. + +Настоящий стандарт применяется совместно с документом: + +> **Market Intelligence Research Methodology** + +Методология определяет общий процесс проведения исследований. + +Настоящий документ определяет перечень исследований, выполняемых для любого потокового источника рыночной информации. + +--- + +# Область применения + +Настоящий стандарт применяется при исследовании любых потоковых источников рыночной информации независимо от: + +- биржи; +- версии API; +- реализации клиента. + +Примеры: + +- WebSocket Stream; +- Push API; +- Market Data Feed; +- Streaming Gateway. + +Настоящий стандарт **не исследует транспортный уровень** (WebSocket, TCP, TLS и т.д.). + +Исследование транспортного уровня выполняется отдельным стандартом: + +> **Market Intelligence WebSocket Transport Research Standard** + +--- + +# Цель исследования + +Для каждого потокового источника необходимо определить: + +- какие информационные факты предоставляет поток; +- каким образом изменяются данные; +- насколько достоверны получаемые данные; +- как данные связаны с другими источниками; +- каким образом поток может использоваться подсистемой Market Intelligence. + +--- + +# 1. Исследование подключения + +## 1.1 Подключение + +### 1.1.1 Корректность подключения + +- [ ] корректность подключения. + +#### Журнал исследования + +--- + +### 1.1.2 Требования к соединению + +- [ ] URL подключения; +- [ ] используемый транспорт (WS/WSS). + +#### Журнал исследования + +--- + +### 1.1.3 Требования к авторизации + +- [ ] требуется ли авторизация; +- [ ] механизм авторизации; +- [ ] обязательные заголовки авторизации; +- [ ] возможность работы без авторизации. + +#### Журнал исследования + +--- + +## 1.2 Подписка + +- [ ] формат подписки; +- [ ] подтверждение подписки; +- [ ] сообщения об ошибках; +- [ ] повторную подписку; +- [ ] подписку на несколько инструментов; +- [ ] отмену подписки; +- [ ] ограничения подписки (если документированы). + +#### Журнал исследования + +--- + +# 2. Исследование структуры потока + +## 2.1 Типы сообщений + +Для каждого типа сообщений определить: + +- [ ] назначение; +- [ ] содержит ли рыночную информацию; +- [ ] содержит ли служебную информацию; +- [ ] содержит ли ошибки; +- [ ] содержит ли подтверждение подписки; +- [ ] содержит ли подтверждение отписки. + +#### Журнал исследования + +--- + +## 2.2 Структура сообщений + +Для каждого типа сообщений определить: + +- [ ] обязательные поля; +- [ ] необязательные поля; +- [ ] тип каждого поля; +- [ ] допустимые значения; +- [ ] диапазоны значений; +- [ ] ограничения. + +#### Журнал исследования + +--- + +# 3. Классификация сообщений + +Для каждого типа сообщений определить: + +- [ ] рыночное сообщение; +- [ ] служебное сообщение; +- [ ] сообщение управления; +- [ ] сообщение об ошибке; +- [ ] сообщение подтверждения. + +#### Журнал исследования + +--- + +# 4. Исследование семантики полей + +Для каждого поля определить: + +- [ ] фактическое назначение; +- [ ] источник происхождения; +- [ ] обязательность; +- [ ] изменяемость; +- [ ] диапазон допустимых значений; +- [ ] взаимосвязь с другими полями; +- [ ] ограничения использования; +- [ ] степень подтверждения семантики. + +#### Журнал исследования + +--- + +# 5. Исследование информационных фактов + +Для каждого типа рыночных сообщений определить: + +- [ ] какие информационные факты содержит сообщение; +- [ ] какие информационные факты могут быть вычислены непосредственно из сообщения; +- [ ] какие информационные факты отсутствуют; +- [ ] какие информационные факты являются первичными; +- [ ] какие информационные факты являются производными. + +#### Журнал исследования + +--- + +## 5.1 Первичные информационные факты + +Для каждого первичного информационного факта определить: + +- [ ] источник происхождения; +- [ ] поле сообщения; +- [ ] степень подтверждения; +- [ ] ограничения использования. + +#### Журнал исследования + +--- + +## 5.2 Производные информационные факты + +Для каждого производного информационного факта определить: + +- [ ] формулу вычисления; +- [ ] необходимые исходные данные; +- [ ] ограничения вычисления; +- [ ] степень достоверности. + +#### Журнал исследования + +--- + +# 6. Исследование поведения потока + +## 6.1 Частота сообщений + +Определить: + +- [ ] минимальную частоту; +- [ ] максимальную частоту; +- [ ] среднюю частоту; +- [ ] пиковую частоту; +- [ ] условия изменения частоты. + +#### Журнал исследования + +--- + +## 6.2 Последовательность сообщений + +Определить: + +- [ ] порядок поступления сообщений; +- [ ] последовательность timestamp; +- [ ] наличие пропусков; +- [ ] наличие повторяющихся сообщений; +- [ ] возможность нарушения порядка. + +#### Журнал исследования + +--- + +## 6.3 Полнота сообщений + +Определить: + +- [ ] полный снимок состояния; +- [ ] инкрементальные изменения; +- [ ] смешанный режим передачи; +- [ ] обязательность всех полей; +- [ ] возможность частичных обновлений. + +#### Журнал исследования + +--- + +# 7. Исследование поведения данных + +Для каждого информационного факта определить: + +- [ ] условия появления; +- [ ] условия изменения; +- [ ] условия исчезновения; +- [ ] частоту изменения; +- [ ] взаимосвязь с другими фактами. + +--- + +## Дополнительно определить + +- [ ] какие факты изменяются одновременно; +- [ ] какие факты никогда не изменяются одновременно; +- [ ] какие факты являются независимыми; +- [ ] какие факты являются производными от других. + +#### Журнал исследования + +--- + +# 8. Проверка эквивалентности + +Для каждого информационного факта определить: + +- [ ] существует ли аналогичный источник; +- [ ] полностью ли совпадает значение; +- [ ] совпадает ли семантика; +- [ ] совпадает ли точность; +- [ ] совпадает ли момент обновления; +- [ ] имеются ли расхождения. + +#### Журнал исследования + +--- + +## 8.1 Альтернативные источники + +Для каждого информационного факта определить: + +- [ ] REST endpoint; +- [ ] WebSocket Request; +- [ ] другие Stream; +- [ ] внутренние вычисления. + +#### Журнал исследования + +--- + +## 8.2 Приоритет источников + +Для каждого информационного факта определить: + +- [ ] основной источник; +- [ ] резервный источник; +- [ ] допустимые альтернативы; +- [ ] причины выбора приоритетного источника. + +#### Журнал исследования + +--- + +# 9. Исследование производительности + +Определить: + +- [ ] средний размер сообщения; +- [ ] максимальный размер сообщения; +- [ ] среднюю скорость передачи; +- [ ] максимальную скорость передачи; +- [ ] объём данных в минуту; +- [ ] объём данных в час. + +#### Журнал исследования \ No newline at end of file diff --git a/docs/market_intelligence/research/standards/research_standard_websocket_request.md b/docs/market_intelligence/research/standards/research_standard_websocket_request.md new file mode 100644 index 0000000..3fed3b6 --- /dev/null +++ b/docs/market_intelligence/research/standards/research_standard_websocket_request.md @@ -0,0 +1,274 @@ +# Market Intelligence WebSocket Request Research Standard + +## Контроль документа + +| Свойство | Значение | +|----------|----------| +| Документ | Market Intelligence WebSocket Request Research Standard | +| Тип документа | Engineering Standard | +| Версия | 1.0 | +| Статус | Release | +| Проект | Dzentra | +| Подсистема | Market Intelligence | +| Язык | Русский | + +--- + +# Назначение + +Настоящий документ определяет единый стандарт исследования источников рыночной информации, использующих модель **WebSocket Request / Response**. + +Настоящий стандарт применяется совместно с документом: + +> **Market Intelligence Research Methodology** + +Методология определяет общий процесс проведения исследований. + +Настоящий документ определяет перечень исследований, выполняемых для WebSocket Request endpoint. + +--- + +# Область применения + +Настоящий стандарт применяется при исследовании WebSocket endpoint, использующих модель "запрос → ответ". + +Примеры: + +- `wss:/api/v1/depth` +- `wss:/api/v1/ticker/24hr` +- `wss:/api/v1/aggTrades` +- `wss:/api/v1/klines` +- `wss:/api/v1/time` +- `wss:/api/v1/exchangeInfo` + +--- + +# Цель исследования + +Для каждого WebSocket Request endpoint необходимо определить: + +- какие информационные факты предоставляет endpoint; +- каким образом формируется запрос; +- каким образом формируется ответ; +- отличается ли ответ от REST; +- как данные связаны с другими источниками; +- каким образом endpoint может использоваться подсистемой Market Intelligence. + +--- + +# 1. Исследование подключения + +## 1.1 WebSocket-соединение + +Проверить: + +- [ ] корректность подключения; +- [ ] требования к соединению; +- [ ] требования к авторизации; +- [ ] timeout подключения; +- [ ] ограничения соединения; +- [ ] возможность повторного подключения. + +--- + +## 1.2 Формат запроса + +Для каждого запроса определить: + +- [ ] destination; +- [ ] correlationId; +- [ ] payload; +- [ ] обязательные поля; +- [ ] необязательные поля; +- [ ] допустимые значения; +- [ ] ограничения. + +--- + +## 1.3 Формат ответа + +Для каждого ответа определить: + +- [ ] структуру ответа; +- [ ] обязательные поля; +- [ ] необязательные поля; +- [ ] типы данных; +- [ ] диапазоны значений; +- [ ] сообщения об ошибках. + +--- + +# 2. Исследование семантики полей + +Для каждого поля определить: + +- [ ] фактическое назначение; +- [ ] источник происхождения; +- [ ] обязательность; +- [ ] диапазон значений; +- [ ] взаимосвязь с другими полями; +- [ ] ограничения использования; +- [ ] степень подтверждения семантики. + +--- + +# 3. Исследование информационных фактов + +Для каждого ответа определить: + +- [ ] какие информационные факты содержит ответ; +- [ ] какие информационные факты могут быть вычислены; +- [ ] какие информационные факты отсутствуют; +- [ ] какие факты являются первичными; +- [ ] какие факты являются производными. + +--- + +## 3.1 Первичные информационные факты + +Для каждого факта определить: + +- [ ] destination; +- [ ] поле ответа; +- [ ] степень подтверждения; +- [ ] ограничения использования. + +--- + +## 3.2 Производные информационные факты + +Для каждого производного факта определить: + +- [ ] формулу вычисления; +- [ ] необходимые исходные данные; +- [ ] ограничения вычисления; +- [ ] степень достоверности. + +--- + +# 4. Исследование поведения запросов + +Определить: + +- [ ] время ответа; +- [ ] повторяемость ответов; +- [ ] влияние параметров запроса; +- [ ] возможность параллельных запросов; +- [ ] ограничения частоты запросов; +- [ ] поведение при ошибках. + +--- + +# 5. Исследование поведения ответов + +Определить: + +- [ ] является ли ответ снимком состояния; +- [ ] содержит ли ответ агрегированные данные; +- [ ] содержит ли ответ исторические данные; +- [ ] содержит ли timestamp; +- [ ] отличается ли ответ от REST. + +--- + +# 6. Проверка эквивалентности + +Для каждого информационного факта определить: + +- [ ] существует ли аналогичный REST endpoint; +- [ ] существует ли аналогичный Stream; +- [ ] полностью ли совпадает значение; +- [ ] совпадает ли семантика; +- [ ] совпадает ли точность; +- [ ] совпадает ли момент формирования данных; +- [ ] имеются ли расхождения. + +--- + +## 6.1 Альтернативные источники + +Для каждого информационного факта определить: + +- [ ] REST endpoint; +- [ ] WebSocket Stream; +- [ ] другие WebSocket Request; +- [ ] внутренние вычисления. + +--- + +## 6.2 Приоритет источников + +Для каждого информационного факта определить: + +- [ ] основной источник; +- [ ] резервный источник; +- [ ] допустимые альтернативы; +- [ ] причины выбора приоритетного источника. + +--- + +# 7. Исследование протокола + +Проверить: + +- [ ] correlationId; +- [ ] destination; +- [ ] обработку ошибок; +- [ ] timeout; +- [ ] повторный запрос; +- [ ] повторное подключение; +- [ ] потерю соединения; +- [ ] восстановление соединения; +- [ ] обработку нескольких одновременных запросов; +- [ ] гарантии получения ответа. + +--- + +# 8. Исследование производительности + +Определить: + +- [ ] среднее время ответа; +- [ ] максимальное время ответа; +- [ ] минимальное время ответа; +- [ ] средний размер ответа; +- [ ] максимальный размер ответа; +- [ ] влияние параметров запроса на производительность. + +--- + +# 9. Практическая ценность + +Определить: + +- [ ] какие информационные факты предоставляет endpoint; +- [ ] какие факты отсутствуют; +- [ ] может ли endpoint быть основным источником; +- [ ] может ли endpoint быть резервным источником; +- [ ] какие подсистемы Dzentra потенциально могут использовать endpoint; +- [ ] ограничения использования в runtime. + +--- + +# 10. Ограничения исследования + +Определить: + +- [ ] что подтверждено; +- [ ] что предварительно подтверждено; +- [ ] что осталось гипотезой; +- [ ] что опровергнуто; +- [ ] что невозможно определить данным исследованием. + +--- + +# 11. Итоги исследования + +По результатам исследования определить: + +- [ ] степень пригодности endpoint; +- [ ] степень полноты исследования; +- [ ] сильные стороны; +- [ ] ограничения; +- [ ] рекомендации по использованию; +- [ ] необходимость дополнительных исследований. \ No newline at end of file diff --git a/docs/market_intelligence/research/standards/research_standard_websocket_transport.md b/docs/market_intelligence/research/standards/research_standard_websocket_transport.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reviews/architecture_reviews.md b/docs/market_intelligence/reviews/architecture_reviews.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reviews/compile_checks.md b/docs/market_intelligence/reviews/compile_checks.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/reviews/domain_reviews.md b/docs/market_intelligence/reviews/domain_reviews.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/roadmap/README.md b/docs/market_intelligence/roadmap/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/market_intelligence/runtime/README.md b/docs/market_intelligence/runtime/README.md new file mode 100644 index 0000000..e4594a0 --- /dev/null +++ b/docs/market_intelligence/runtime/README.md @@ -0,0 +1,140 @@ +# Runtime + +Статус: **Active** + +Версия: **1.0** + +--- + +# Назначение + +Каталог **Runtime** описывает архитектуру слоя выполнения (**Runtime Layer**) подсистемы **Market Intelligence**. + +Runtime является инфраструктурным уровнем платформы. + +Он обеспечивает единый жизненный цикл выполнения аналитических Engine, но сам не анализирует рынок. + +--- + +# Место Runtime в архитектуре + +Runtime располагается между фундаментальными компонентами Common и аналитическими Engine. + +```text +Common Layer + ↓ +Runtime Layer + ↓ +Engine Layer + ↓ +Coordinator Layer +``` + +Common предоставляет общие модели данных, типы и контракты. + +Runtime определяет единый способ выполнения аналитических компонентов. + +Engine реализуют конкретные алгоритмы анализа рынка. + +Coordinator объединяет результаты нескольких Engine. + +--- + +# Ответственность Runtime + +Runtime отвечает исключительно за инфраструктуру выполнения Engine. + +В область ответственности Runtime входят: + +- единый контракт Engine; +- единый жизненный цикл выполнения; +- безопасный запуск Engine; +- обработка исключений; +- проверка корректности результатов; +- управление зависимостями между Engine; +- регистрация доступных Engine; +- формирование безопасных fallback-результатов. + +Runtime не реализует аналитические алгоритмы. + +--- + +# Что Runtime не делает + +Runtime не имеет права: + +- анализировать рынок; +- рассчитывать индикаторы; +- определять тренды; +- определять волны; +- принимать торговые решения; +- открывать или закрывать позиции; +- взаимодействовать с биржей; +- работать с Telegram; +- изменять состояние AutoTrade. + +Все подобные задачи относятся к другим архитектурным уровням. + +--- + +# Планируемая структура Runtime + +```text +runtime/ + +├── protocol.py +├── base.py +├── runner.py +├── registry.py +├── dependencies.py +└── validation.py +``` + +Каждый компонент Runtime имеет собственную область ответственности. + +--- + +# Документация Runtime + +Основным архитектурным документом Runtime является: + +```text +runtime/architecture.md +``` + +В нём описываются: + +- архитектура Runtime; +- роли компонентов; +- жизненный цикл выполнения Engine; +- взаимодействие Runtime с Common; +- взаимодействие Runtime с Coordinator. + +--- + +# Связанные документы + +Основные документы Runtime: + +- `development_process.md` +- `architecture_principles.md` +- `runtime_contract.md` +- `runtime/architecture.md` + +История реализации Runtime хранится в каталоге: + +```text +builds/ +``` + +--- + +# Принцип развития Runtime + +Runtime развивается независимо от отдельных Engine. + +Добавление нового Engine не должно требовать изменения архитектуры Runtime. + +При необходимости Runtime расширяется путём добавления новых компонентов, а не изменения существующего контракта выполнения. + +Настоящий каталог является официальной документацией Runtime Layer подсистемы **Market Intelligence**. \ No newline at end of file diff --git a/docs/market_intelligence/runtime/architecture.md b/docs/market_intelligence/runtime/architecture.md new file mode 100644 index 0000000..29f1b2b --- /dev/null +++ b/docs/market_intelligence/runtime/architecture.md @@ -0,0 +1,588 @@ +# Runtime Architecture + +Версия документа: **1.0** + +Статус: **Architecture Specification** + +--- + +# Назначение документа + +Настоящий документ определяет архитектуру Runtime Layer подсистемы **Market Intelligence**. + +Runtime Layer является инфраструктурным уровнем платформы и обеспечивает единый механизм выполнения аналитических Engine. + +Документ описывает: + +- место Runtime в архитектуре; +- обязанности Runtime; +- жизненный цикл выполнения Engine; +- взаимодействие Runtime с Common; +- взаимодействие Runtime с Coordinator; +- устройство Runtime Layer. + +Настоящий документ является официальной спецификацией Runtime. + +--- + +# Назначение Runtime + +Runtime существует для того, чтобы каждый аналитический Engine выполнялся одинаковым способом. + +Любой Engine должен: + +- получать одинаковый входной объект; +- проходить одинаковые проверки; +- возвращать одинаковый результат; +- безопасно обрабатывать ошибки; +- одинаково взаимодействовать с Coordinator. + +Именно Runtime обеспечивает эту унификацию. + +--- + +# Место Runtime в архитектуре + +Архитектура Market Intelligence имеет многоуровневую структуру. + +```text +Common Layer + ↓ +Runtime Layer + ↓ +Engine Layer + ↓ +Coordinator Layer + ↓ +Execution Layer +``` + +Каждый уровень знает только о соседнем уровне. + +Это исключает прямые зависимости между аналитическими движками и системой исполнения сделок. + +--- + +# Основная ответственность Runtime + +Runtime отвечает исключительно за инфраструктуру выполнения Engine. + +В его обязанности входят: + +- запуск Engine; +- единый контракт Engine; +- единый жизненный цикл выполнения; +- обработка исключений; +- валидация входных данных; +- валидация результата; +- регистрация Engine; +- управление зависимостями; +- формирование безопасного результата при ошибке. + +--- + +# Что Runtime никогда не делает + +Runtime никогда не: + +- анализирует рынок; +- рассчитывает индикаторы; +- определяет тренды; +- принимает торговые решения; +- открывает позиции; +- закрывает позиции; +- рассчитывает размер сделки; +- взаимодействует с биржей; +- изменяет состояние AutoTrade. + +Runtime является исключительно инфраструктурным уровнем. + +--- + +# Структура Runtime + +Runtime состоит из следующих компонентов. + +```text +runtime/ + +├── protocol.py +├── metadata.py +├── registry.py +├── dependencies.py +├── runner.py +├── validation.py +└── base.py +``` + +Каждый компонент имеет единственную область ответственности. + +--- + +--- + +## Общая архитектура Engine + +Каждый аналитический Engine подсистемы **Market Intelligence** состоит из двух независимых частей. + +```text +Engine + +├── Metadata +└── Logic +``` + +Разделение Metadata и Logic является обязательным архитектурным стандартом платформы. + +Обе части имеют разные зоны ответственности и не должны смешиваться между собой. + +--- + +# Engine Metadata + +Metadata представляет собой неизменяемое архитектурное описание Engine. + +Metadata существует независимо от выполнения аналитического алгоритма. + +Runtime может работать с Metadata без создания экземпляра Engine. + +Metadata используется инфраструктурой платформы. + +--- + +## Metadata содержит + +Metadata может включать: + +- официальное имя Engine; +- отображаемое имя; +- версию; +- краткое описание; +- категорию Engine; +- список зависимостей; +- поддерживаемые таймфреймы; +- минимальные требования к данным; +- поддерживаемые возможности; +- дополнительные архитектурные свойства. + +Metadata не содержит вычисляемых результатов анализа. + +--- + +# Immutable Metadata + +Metadata является полностью неизменяемой структурой. + +После создания Metadata запрещается изменять её содержимое. + +Metadata должна быть реализована как immutable-объект. + +Например: + +```text +EngineMetadata + +↓ + +frozen dataclass +``` + +Во время выполнения Engine Metadata никогда не изменяется. + +--- + +# Engine Logic + +Logic представляет собой реализацию аналитического алгоритма. + +Logic отвечает исключительно за анализ рынка. + +Logic получает: + +```text +EngineContext +``` + +и возвращает: + +```text +EngineResult +``` + +Logic не должна: + +- хранить описание Engine; +- выполнять регистрацию; +- объявлять собственные зависимости во время выполнения; +- изменять Metadata; +- принимать инфраструктурные решения Runtime. + +--- + +# Runtime использует Metadata + +Runtime взаимодействует с Engine через Metadata. + +До запуска Engine Runtime может определить: + +- существует ли Engine; +- совместима ли версия; +- какие зависимости необходимы; +- поддерживается ли выбранный таймфрейм; +- можно ли запускать Engine в текущей конфигурации. + +Runtime не должен получать эти сведения путём анализа реализации Engine. + +--- + +# Registry использует Metadata + +Engine Registry работает исключительно с Metadata. + +Registry отвечает за: + +- регистрацию Engine; +- поиск Engine; +- проверку уникальности; +- построение графа зависимостей; +- предоставление списка доступных Engine. + +Registry не анализирует алгоритм работы Engine. + +--- + +# Coordinator использует Metadata + +Coordinator использует Metadata для определения порядка выполнения Engine. + +Metadata позволяет определить зависимости между Engine ещё до начала анализа рынка. + +Благодаря этому Coordinator не зависит от внутреннего устройства конкретного Engine. + +--- + +# Разделение ответственности + +Зоны ответственности распределяются следующим образом. + +```text +Metadata + +↓ + +Описание Engine + +↓ + +Runtime / Registry / Coordinator + + +Logic + +↓ + +Анализ рынка + +↓ + +EngineResult +``` + +Metadata отвечает за архитектурное описание. + +Logic отвечает за вычисления. + +Runtime отвечает за выполнение. + +Coordinator отвечает за оркестрацию. + +--- + +# Архитектурное правило + +Каждый новый Engine обязан состоять из двух независимых частей: + +```text +Metadata + +↓ + +Logic +``` + +Создание Engine без Metadata считается нарушением архитектурного стандарта платформы. + +Настоящее правило является обязательным для всех существующих и будущих Engine подсистемы **Market Intelligence**. + +--- + +# protocol.py + +Определяет единый контракт аналитического Engine. + +Любой Engine обязан реализовывать данный контракт. + +Минимальный контракт включает: + +- имя Engine; +- версию Engine; +- метод выполнения анализа; +- тип результата. + +Наличие общего протокола позволяет Runtime запускать любой Engine одинаковым способом. + +--- + +# base.py + +Содержит базовую реализацию Engine. + +Базовый класс предоставляет: + +- общие свойства Engine; +- единый жизненный цикл; +- стандартную обработку ошибок; +- вспомогательные методы; +- безопасное создание результата. + +Базовый класс не содержит аналитических алгоритмов. + +--- + +# runner.py + +Runner отвечает за выполнение одного Engine. + +Последовательность работы Runner: + +```text +получение Engine + +↓ + +валидация EngineContext + +↓ + +запуск Engine + +↓ + +получение EngineResult + +↓ + +валидация результата + +↓ + +возврат результата Coordinator +``` + +Runner является главным защитным механизмом Runtime. + +--- + +# registry.py + +Registry хранит перечень зарегистрированных Engine. + +Основные задачи Registry: + +- регистрация Engine; +- поиск Engine; +- предотвращение повторной регистрации; +- получение списка доступных Engine. + +Registry не выполняет Engine. + +--- + +# dependencies.py + +Dependency Layer описывает связи между Engine. + +Он определяет: + +- какие Engine могут выполняться независимо; +- какие Engine используют результаты других Engine; +- допустимый порядок выполнения; +- отсутствие циклических зависимостей. + +На первом этапе Runtime выполняет только описание зависимостей. + +--- + +# validation.py + +Runtime Validation отвечает за проверку выполнения Engine. + +Проверяются: + +- корректность EngineContext; +- корректность EngineResult; +- соблюдение Runtime Contract; +- отсутствие запрещённых полей; +- полнота результата. + +Runtime Validation использует Common Validation и не дублирует её. + +--- + +# Жизненный цикл выполнения Engine + +Каждый Engine проходит одинаковый цикл. + +```text +EngineContext + ↓ +Runtime Validation + ↓ +Engine Runner + ↓ +Engine Execution + ↓ +Engine Result + ↓ +Runtime Validation + ↓ +Coordinator +``` + +Ни один этап не должен пропускаться. + +--- + +# Обработка ошибок + +Любое исключение должно оставаться внутри Runtime. + +Runtime обязан вернуть корректный EngineResult даже при ошибке Engine. + +Типичная схема выглядит следующим образом. + +```text +Engine + +↓ + +Exception + +↓ + +Runner + +↓ + +Diagnostics + +↓ + +EngineResult(ERROR) + +↓ + +Coordinator +``` + +Ошибка одного Engine не должна останавливать выполнение остальных. + +--- + +# Работа с зависимостями + +Engine никогда не обращается напрямую к другому Engine. + +Вместо этого Runtime передаёт результаты зависимых Engine через EngineContext. + +Таким образом достигаются: + +- независимость компонентов; +- тестируемость; +- отсутствие циклических зависимостей; +- возможность повторного использования Engine. + +--- + +# Взаимодействие с Common + +Runtime использует Common как единственный источник базовых сущностей. + +Используются: + +- EngineContext; +- EngineResult; +- EngineScore; +- EngineConfidence; +- EngineDiagnostics; +- Validation; +- Checks; +- Payload Builder; +- Snapshot Builder; +- Event Builder. + +Runtime не создаёт собственные версии этих моделей. + +--- + +# Взаимодействие с Coordinator + +Coordinator никогда не запускает Engine напрямую. + +Вместо этого он использует Runtime. + +```text +Coordinator + +↓ + +Runner + +↓ + +Engine + +↓ + +EngineResult + +↓ + +Coordinator +``` + +Это позволяет сохранить единый жизненный цикл выполнения. + +--- + +# Масштабируемость + +Архитектура Runtime должна позволять: + +- добавлять новые Engine; +- изменять порядок выполнения; +- вводить новые проверки; +- расширять механизм зависимостей; +- добавлять параллельное выполнение. + +При этом существующие Engine не должны изменяться. + +--- + +# Основной архитектурный принцип + +Runtime развивается независимо от Engine. + +Добавление нового Engine не должно требовать изменения Runtime. + +Изменение Runtime допускается только при изменении самого механизма выполнения Engine. + +--- + +# Заключение + +Runtime Layer является центральным инфраструктурным компонентом подсистемы **Market Intelligence**. + +Он обеспечивает единый способ выполнения аналитических Engine и гарантирует соблюдение архитектурных контрактов платформы. + +Настоящий документ является официальной архитектурной спецификацией Runtime Layer. \ No newline at end of file diff --git a/docs/market_intelligence/runtime_contract.md b/docs/market_intelligence/runtime_contract.md new file mode 100644 index 0000000..3868cf9 --- /dev/null +++ b/docs/market_intelligence/runtime_contract.md @@ -0,0 +1,1992 @@ +# Dzentra Market Intelligence Runtime Contract + +**Engineering Standard Release** + +--- + +## Контроль документа + +| Свойство | Значение | +|----------|-----------| +| Документ | Dzentra Market Intelligence Runtime Contract | +| Тип документа | Engineering Standard | +| Версия | 2.0 | +| Статус | **Release** | +| Подсистема | Market Intelligence | +| Проект | Dzentra | +| Владелец стандарта | Dzentra Architecture | +| Язык | Русский | +| Применяется к | Runtime подсистемы Market Intelligence | + +--- + +## Оглавление + +### Общие положения + +- Контроль документа +- Статус документа +- Управление стандартом +- Соответствие стандарту +- Нормативная терминология +- Официальная терминология +- Назначение +- Область применения +- Архитектурная роль Runtime +- Общие положения + +### Part I. Runtime Architecture + +- Назначение Runtime +- Runtime в архитектуре платформы +- Ответственность Runtime +- Границы Runtime +- Runtime и аналитическая подсистема + +### Part II. Runtime Contract + +- Общая модель взаимодействия +- Runtime Context +- Engine Execution Model +- Engine Instance Lifecycle +- Runtime Result Flow +- Runtime Ownership + +### Part III. Engine Input Contract + +- Общий входной контракт +- Runtime Context +- Входные зависимости +- Допустимые источники данных +- Запрещённые источники данных + +### Part IV. Engine Result Contract + +- Общий результат Engine +- Engine Status +- Evaluation +- Metrics +- Diagnostics +- Payload +- Snapshot +- Metadata + +### Part V. Runtime Status Contract + +- Runtime Status +- Engine Status +- Допустимые состояния +- Правила изменения статусов +- Инварианты статусов + +### Part VI. Diagnostics and Explainability Contract + +- Diagnostics +- Reason Code +- Payload +- Snapshot +- Runtime Events +- Explainability + +### Part VII. Dependency Contract + +- Общая модель зависимостей +- Допустимые зависимости +- Запрещённые зависимости +- Runtime Contract +- Coordinator Contract + +### Part VIII. Runtime Safety + +- Общие требования +- Graceful Degradation +- Partial Result +- Error Isolation +- Fault Tolerance + +### Part IX. Runtime Evolution + +- Общие принципы развития +- Развитие Runtime Contract +- Расширение Runtime +- Изменение Runtime Model +- Архитектурная стабильность Runtime +- Критерии качественного развития Runtime +- Завершение Runtime Evolution + +### Part X. Заключительные положения + +- Соответствие стандарту +- Приоритет Runtime Contract +- Развитие стандарта +- Runtime аксиома +- Заключительные положения + +### Приложения + +- Приложение А. Каталог Runtime Contract +- Приложение Б. Каталог Runtime Status +- Приложение В. Примеры Runtime Flow +- Приложение Г. Примеры допустимых зависимостей +- Приложение Д. Примеры допустимых Runtime Contract +- Приложение Е. Примеры недопустимых Runtime Contract + +--- + +## Статус документа + +Настоящий документ имеет статус **Release**. + +Документ является официальным инженерным стандартом, определяющим единый Runtime Contract подсистемы **Market Intelligence** проекта **Dzentra**. + +Настоящий стандарт устанавливает обязательные требования к организации Runtime, контрактам взаимодействия аналитических компонентов, моделям обмена данными, правилам формирования результатов анализа и обработке состояний Runtime. + +Настоящий документ не определяет архитектурные принципы построения платформы. + +Фундаментальные архитектурные требования определяются документом **Architecture Principles**. + +Настоящий стандарт определяет исключительно официальные Runtime Contract подсистемы **Market Intelligence**. + +Изменение настоящего стандарта допускается исключительно посредством выпуска новой официальной версии документа. + +--- + +## Управление стандартом + +Настоящий документ определяет официальный Runtime Contract платформы **Market Intelligence**. + +Все компоненты Runtime обязаны соответствовать требованиям настоящего стандарта независимо от: + +- используемого языка программирования; +- способа реализации компонентов; +- этапа развития платформы; +- количества аналитических движков; +- используемых инженерных инструментов. + +Настоящий стандарт определяет исключительно контракты взаимодействия компонентов Runtime. + +Архитектурные принципы построения платформы определяются документом **Architecture Principles**. + +Инженерный процесс разработки определяется документом **Development Process**. + +Изменение Runtime Contract допускается только при одновременном выполнении следующих условий: + +- изменение имеет долгосрочную архитектурную ценность; +- изменение подтверждено практикой разработки; +- изменение не нарушает существующую архитектурную модель; +- изменение сохраняет обратную совместимость либо сопровождается официальным изменением стандарта; +- изменение повышает сопровождаемость, расширяемость или согласованность Runtime. + +Локальные изменения отдельных Build не могут изменять требования настоящего стандарта. + +При возникновении противоречий между Runtime Contract и локальной реализацией приоритет имеет настоящий документ. + +--- + +## Соответствие стандарту + +Runtime подсистемы **Market Intelligence** считается соответствующим настоящему стандарту только при соблюдении всех обязательных требований, определённых данным документом. + +Нарушение любого обязательного Runtime Contract рассматривается как архитектурный дефект независимо от корректности реализации отдельных компонентов. + +Рекомендации настоящего стандарта не являются обязательными, однако рассматриваются как предпочтительная инженерная практика. + +Соответствие Runtime настоящему стандарту подтверждается посредством процедур **Architecture Review** и **Domain Review**, определённых документом **Development Process**. + +--- + +## Нормативная терминология + +Во всём тексте настоящего документа используются следующие нормативные формулировки. + +| Формулировка | Значение | +|--------------|----------| +| **Должен** | Обязательное требование Runtime Contract. | +| **Не должен** | Действие запрещено Runtime Contract. | +| **Обязан** | Обязательное действие Runtime-компонента. | +| **Не допускается** | Полный запрет соответствующего решения. | +| **Следует** | Предпочтительная инженерная практика. | +| **Рекомендуется** | Наиболее предпочтительный способ реализации Runtime. | +| **Может** | Допустимое решение при соблюдении остальных требований настоящего стандарта. | + +Если явно не указано иное, все перечисленные формулировки используются исключительно в приведённых выше значениях. + +--- + +## Официальная терминология + +В целях единообразного понимания настоящего стандарта используются следующие определения. + +| Термин | Определение | +|---------|-------------| +| **Runtime** | Совокупность моделей состояния, контрактов взаимодействия и правил обмена данными между аналитическими компонентами платформы. | +| **Runtime Context** | Официальный входной контракт аналитического компонента. | +| **Runtime Contract** | Совокупность обязательных правил взаимодействия компонентов Runtime. | +| **Engine Metadata** | Официальное описание Engine как компонента платформы. Не является результатом анализа и доступно без создания экземпляра Engine. | +| **Engine Result** | Официальный результат выполнения аналитического движка. | +| **Runtime Status** | Состояние Runtime либо аналитического компонента в процессе выполнения. | +| **Engine Status** | Стандартизированное состояние выполнения аналитического движка. | +| **Reason Code** | Стандартизированный идентификатор причины полученного результата или состояния Runtime. | +| **Payload** | Структурированное представление результатов анализа, предназначенное для передачи между компонентами платформы. | +| **Snapshot** | Зафиксированное состояние аналитического результата в момент завершения расчёта. | +| **Diagnostics** | Служебная информация, предназначенная для анализа работы Runtime и диагностики выполнения аналитических компонентов. | + +Все перечисленные термины используются исключительно в приведённых выше значениях. + +--- + +## Назначение + +Настоящий документ определяет единый Runtime Contract подсистемы **Market Intelligence** проекта **Dzentra**. + +Стандарт устанавливает обязательные требования к: + +- организации Runtime; +- входным контрактам аналитических компонентов; +- результатам выполнения Engine; +- моделям обмена данными; +- статусам выполнения; +- диагностической информации; +- обработке ошибок Runtime; +- взаимодействию аналитических компонентов. + +Настоящий документ не определяет архитектурные принципы построения платформы и не регламентирует инженерный процесс разработки. + +Указанные вопросы регулируются документами **Architecture Principles** и **Development Process** соответственно. + +--- + +## Область применения + +Настоящий стандарт распространяется на все Runtime-компоненты подсистемы **Market Intelligence**, включая: + +- Runtime Context; +- Runtime Models; +- Runtime Status; +- Engine Result; +- Runtime Events; +- Diagnostics; +- Payload; +- Snapshot; +- Coordinator Runtime; +- все существующие и будущие аналитические Engine. + +Требования настоящего стандарта обязательны для всех компонентов Runtime независимо от времени их создания, способа реализации или архитектурного уровня. + +--- + +## Архитектурная роль Runtime + +Runtime является официальным механизмом взаимодействия аналитических компонентов подсистемы **Market Intelligence**. + +Runtime обеспечивает: + +- единый входной контракт; +- единый формат результатов; +- единые статусы выполнения; +- единый механизм диагностики; +- единые правила обмена аналитической информацией. + +Runtime не реализует анализ рынка. + +Runtime не принимает торговые решения. + +Runtime не определяет стратегию анализа. + +Единственной задачей Runtime является обеспечение согласованного взаимодействия компонентов аналитической платформы. + +--- + +## Общие положения + +Настоящий документ определяет официальный Runtime Contract платформы **Market Intelligence**. + +Все аналитические компоненты обязаны использовать исключительно контракты, определённые настоящим стандартом. + +Ни один компонент не должен самостоятельно изменять формат взаимодействия, модели обмена данными или жизненный цикл Runtime. + +Все изменения Runtime Contract должны сопровождаться обновлением настоящего стандарта. + +Runtime Contract является обязательной частью архитектуры платформы и применяется совместно с документом **Architecture Principles**. + +## Part I. Runtime Architecture + +Настоящая часть определяет официальную архитектурную модель Runtime подсистемы **Market Intelligence**. + +Runtime является самостоятельным архитектурным уровнем платформы, обеспечивающим единообразное взаимодействие аналитических компонентов независимо от их внутренней реализации. + +Runtime определяет: + +- официальный жизненный цикл аналитических компонентов; +- единые модели обмена данными; +- единые входные и выходные контракты; +- правила формирования результатов; +- правила представления состояний; +- правила взаимодействия компонентов. + +Runtime не определяет алгоритмы анализа рынка. + +Runtime не определяет стратегию торговли. + +Runtime не реализует бизнес-логику аналитических компонентов. + +Единственной задачей Runtime является обеспечение согласованного взаимодействия компонентов аналитической платформы. + +--- + +### Назначение Runtime + +Runtime представляет собой официальный слой взаимодействия аналитических компонентов. + +Основной задачей Runtime является создание единой среды выполнения, в которой любой аналитический компонент может функционировать независимо от особенностей реализации остальных компонентов. + +Runtime обеспечивает: + +- единый жизненный цикл выполнения; +- единые контракты обмена данными; +- единый формат результатов; +- единые модели состояний; +- единые правила диагностики; +- единые правила обработки ошибок. + +Благодаря Runtime все аналитические движки работают по одинаковым правилам независимо от выполняемого анализа. + +--- + +### Runtime в архитектуре платформы + +Runtime занимает промежуточное положение между фундаментальными архитектурными сущностями и аналитическими компонентами. + +Архитектурная модель имеет следующий вид. + +```text + Common Layer + │ + ▼ + Runtime Contract + │ + ▼ + Runtime Layer + │ + ▼ + Engine Layer + │ + ▼ + Coordinator Layer +``` + +Каждый вышележащий уровень использует возможности нижележащего уровня исключительно посредством официальных контрактов. + +Runtime является обязательным связующим звеном между архитектурным фундаментом платформы и аналитическими движками. + +--- + +### Ответственность Runtime + +Runtime отвечает исключительно за организацию взаимодействия аналитических компонентов. + +В область ответственности Runtime входят: + +- описание моделей состояния; +- описание жизненного цикла выполнения; +- определение входных контрактов; +- определение выходных контрактов; +- описание моделей диагностики; +- описание моделей результатов; +- определение допустимых состояний выполнения; +- описание правил обмена информацией между компонентами. + +Runtime не отвечает за вычисление аналитических показателей. + +Runtime не принимает решений о состоянии рынка. + +Runtime не агрегирует результаты анализа. + +Runtime не выполняет функции Coordinator. + +--- + +### Границы Runtime + +Границы Runtime определяют допустимую область его ответственности. + +Runtime может: + +- описывать контракты взаимодействия; +- описывать модели состояния; +- описывать модели результатов; +- определять допустимые статусы; +- определять правила обмена данными; +- определять правила диагностики. + +Runtime не должен: + +- выполнять анализ рынка; +- содержать алгоритмы аналитики; +- управлять последовательностью выполнения Engine; +- взаимодействовать с торговой подсистемой; +- обращаться к внешним источникам данных; +- содержать пользовательский интерфейс; +- реализовывать бизнес-логику платформы. + +Таким образом Runtime остаётся полностью нейтральным по отношению к предметной области анализа. + +--- + +### Runtime и аналитическая подсистема + +Runtime является общей инфраструктурой для всех аналитических движков платформы. + +Каждый Engine использует Runtime одинаковым образом независимо от собственной специализации. + +Runtime обеспечивает единообразное взаимодействие следующих компонентов: + +- Engine; +- Coordinator; +- Runtime Context; +- Runtime Status; +- Engine Result; +- Diagnostics; +- Payload; +- Snapshot. + +Добавление нового Engine не требует изменения Runtime, если новый компонент полностью соответствует официальным Runtime Contract. + +Это свойство обеспечивает долгосрочную масштабируемость аналитической платформы. + +--- + +### Завершение архитектурной модели Runtime + +Runtime Architecture, определённая настоящей частью, является официальной моделью построения Runtime подсистемы **Market Intelligence**. + +Все существующие и будущие Runtime-компоненты обязаны соответствовать данной модели. + +Любое изменение Runtime Architecture допускается исключительно посредством выпуска новой версии настоящего стандарта. + +## Part II. Runtime Contract + +Настоящая часть определяет официальный Runtime Contract подсистемы **Market Intelligence**. + +Runtime Contract представляет собой совокупность обязательных правил взаимодействия всех аналитических компонентов платформы. + +Любой компонент, входящий в состав Runtime, обязан использовать исключительно контракты, определённые настоящим стандартом. + +Использование альтернативных моделей взаимодействия не допускается. + +Runtime Contract обеспечивает: + +- единообразие взаимодействия компонентов; +- независимость реализации аналитических движков; +- совместимость компонентов; +- безопасное расширение платформы; +- долгосрочную сопровождаемость архитектуры. + +Runtime Contract является обязательной частью архитектуры платформы. + +--- + +### Общая модель взаимодействия + +Все аналитические компоненты взаимодействуют посредством единой модели выполнения. + +Каждый Engine получает стандартизированный входной контекст, выполняет анализ в пределах собственной области ответственности и возвращает стандартизированный результат. + +Общая модель взаимодействия имеет следующий вид. + +```text +Runtime Context + │ + ▼ + Engine + │ + ▼ + Engine Result + │ + ▼ + Coordinator + │ + ▼ +Market Intelligence Result +``` + +Каждый этап взаимодействия использует исключительно официальные Runtime Contract. + +Прямое взаимодействие компонентов в обход контрактов не допускается. + +--- + +### Runtime Context + +Runtime Context является официальным входным контрактом любого аналитического Engine. + +Каждый Engine получает данные исключительно посредством Runtime Context. + +Runtime Context определяет: + +- входные рыночные данные; +- результаты разрешённых зависимостей; +- параметры выполнения; +- конфигурацию анализа; +- служебную информацию Runtime. + +Runtime Context не содержит: + +- пользовательский интерфейс; +- торговые позиции; +- состояние исполнения ордеров; +- объекты внешних сервисов; +- произвольные данные. + +Состав Runtime Context определяется едиными моделями Runtime и одинаков для всех Engine. + +--- + +### Engine Execution Model + +Все аналитические Engine выполняются по единой модели жизненного цикла. + +Официальная последовательность выполнения имеет следующий вид. + +```text +Input Context + │ + ▼ +Input Validation + │ + ▼ +Analysis + │ + ▼ +Evaluation + │ + ▼ +Engine Result +``` + +Каждый этап жизненного цикла является обязательным. + +Пропуск этапов выполнения не допускается. + +Внутренняя реализация отдельных этапов определяется самим Engine и не регулируется настоящим документом. + +--- + +### Engine Instance Lifecycle + +Экземпляры аналитических Engine создаются исключительно компонентом `RuntimeRunner`. + +Ни один другой Runtime-компонент не имеет права самостоятельно создавать экземпляры Engine. + +Во время выполнения анализа RuntimeRunner: + +1. получает `EngineRegistration`; +2. создаёт новый экземпляр Engine; +3. передаёт Engine официальный `EngineContext`; +4. получает `EngineResult`; +5. завершает жизненный цикл экземпляра Engine. + +Официальная схема жизненного цикла имеет следующий вид. + +```text +EngineRegistration + │ + ▼ +RuntimeRunner + │ + ▼ +Create Engine Instance + │ + ▼ +analyze(context) + │ + ▼ +EngineResult + │ + ▼ +Destroy Instance +``` + +Каждый запуск анализа использует новый экземпляр Engine. + +Повторное использование экземпляров между запусками не допускается. + +Engine не должен хранить изменяемое состояние между выполнениями анализа. + +Все данные, необходимые для анализа, должны передаваться исключительно посредством `EngineContext`. + +RuntimeRegistry хранит только регистрацию Engine и никогда не создаёт экземпляры Engine. + +Coordinator использует только результаты выполнения Engine и не взаимодействует с экземплярами Engine напрямую. + +--- + +### Engine Result + +Результатом выполнения любого аналитического Engine является единый объект `EngineResult`. + +Никакие альтернативные модели результата не допускаются. + +`EngineResult` представляет собой официальный контракт обмена результатами анализа между компонентами платформы. + +Каждый Engine обязан возвращать результат независимо от успешности выполнения анализа. + +Даже при невозможности полного анализа Engine обязан вернуть корректно сформированный `EngineResult` с соответствующим статусом выполнения. + +Структура `EngineResult` определяется настоящим стандартом и является обязательной для всех аналитических движков. + +--- + +### Runtime Result Flow + +После завершения работы Engine результат передаётся Coordinator посредством официального Runtime Contract. + +Coordinator не обращается к внутреннему состоянию Engine. + +Coordinator работает исключительно с объектом `EngineResult`. + +Это обеспечивает: + +- независимость Engine; +- возможность замены реализации Engine; +- единообразную обработку результатов; +- безопасное развитие аналитической платформы. + +Любые дополнительные сведения, необходимые Coordinator, должны быть частью официального Runtime Contract. + +--- + +### Runtime Ownership + +Каждый Runtime-объект имеет единственного владельца. + +Распределение ответственности определяется следующим образом. + +| Runtime-компонент | Владелец | +|-------------------|----------| +| Runtime Context | Runtime Layer | +| Engine Registration | Runtime Registry | +| Engine Instance | RuntimeRunner | +| Engine Result | Engine | +| Runtime Status | Runtime Layer | +| Diagnostics | Engine | +| Payload | Engine | +| Snapshot | Engine | +| Runtime Events | Runtime Layer | + +Жизненный цикл экземпляра Engine полностью принадлежит RuntimeRunner. + +После завершения выполнения экземпляр Engine считается завершившим свой жизненный цикл и не должен использоваться повторно. + +Ни один другой компонент Runtime не должен хранить ссылки на экземпляры Engine после завершения анализа. + +Передача объекта другому компоненту не изменяет его владельца. + +Компонент, получивший Runtime-объект, не должен изменять его внутреннее состояние. + +Runtime-модели рассматриваются как неизменяемые после их формирования. + +--- + +### Контракт взаимодействия компонентов + +Все компоненты Runtime взаимодействуют исключительно посредством официальных контрактов. + +Каждый контракт обязан обладать следующими свойствами: + +- однозначностью; +- стабильностью; +- документированностью; +- независимостью от реализации; +- обратной совместимостью при допустимых изменениях. + +Взаимодействие посредством внутренних структур компонентов запрещается. + +Компоненты не должны зависеть от деталей реализации друг друга. + +Это обеспечивает возможность независимого развития каждого архитектурного компонента. + +--- + +### Завершение Runtime Contract + +Runtime Contract, определённый настоящей частью, является официальной моделью взаимодействия компонентов подсистемы **Market Intelligence**. + +Все существующие и будущие аналитические компоненты обязаны использовать исключительно данный контракт. + +Изменение Runtime Contract допускается только посредством официального изменения настоящего стандарта. + +## Part III. Engine Input Contract + +Настоящая часть определяет официальный входной контракт любого аналитического Engine подсистемы **Market Intelligence**. + +Входной контракт устанавливает единые требования к данным, которые могут использоваться аналитическими компонентами платформы. + +Все существующие и будущие Engine обязаны использовать исключительно входной контракт, определённый настоящим стандартом. + +Использование альтернативных способов получения данных не допускается. + +--- + +### Общий входной контракт + +Каждый Engine получает входные данные исключительно посредством Runtime Context. + +Runtime Context является единственным официальным источником входной информации для аналитических компонентов. + +Ни один Engine не должен самостоятельно формировать собственный входной контракт. + +Это обеспечивает: + +- единообразие выполнения; +- независимость аналитических компонентов; +- возможность безопасного тестирования; +- совместимость компонентов Runtime. + +--- + +### Runtime Context + +Runtime Context представляет собой полное описание информации, необходимой Engine для выполнения анализа. + +Runtime Context формируется Runtime Layer до начала выполнения Engine. + +После передачи Engine Runtime Context считается неизменяемым. + +Engine использует Runtime Context только для чтения. + +Изменение Runtime Context во время выполнения анализа не допускается. + +--- + +### Состав Runtime Context + +Runtime Context может содержать только информацию, необходимую для выполнения анализа. + +К допустимым категориям относятся: + +- рыночные данные; +- временные характеристики; +- параметры анализа; +- конфигурация выполнения; +- результаты разрешённых зависимостей; +- служебная информация Runtime. + +Конкретный состав Runtime Context определяется официальными Runtime Models. + +Все Engine используют одинаковую структуру входного контракта. + +--- + +### Входные зависимости + +При необходимости Engine может использовать результаты других аналитических компонентов. + +Такие зависимости допускаются только при соблюдении следующих условий: + +- зависимость официально определена архитектурой; +- данные предоставлены Runtime Context; +- взаимодействие осуществляется исключительно посредством Runtime Contract; +- Engine не обращается напрямую к реализации другого Engine. + +Engine не должен самостоятельно получать результаты других компонентов. + +Все зависимости разрешаются Runtime до начала выполнения Engine. + +--- + +### Допустимые источники данных + +Engine может использовать только данные, предоставленные Runtime. + +К допустимым источникам относятся: + +- Runtime Context; +- официальные Runtime Models; +- результаты разрешённых зависимостей; +- собственная внутренняя конфигурация. + +Любые другие источники данных считаются внешними и не входят в официальный входной контракт. + +--- + +### Запрещённые источники данных + +Engine не должен самостоятельно получать информацию из внешних источников. + +Не допускается прямое обращение: + +- к биржевым API; +- к базе данных; +- к пользовательскому интерфейсу; +- к журналу событий; +- к торговой подсистеме; +- к компонентам исполнения ордеров; +- к внутреннему состоянию других Engine; +- к Coordinator. + +Получение подобных данных должно выполняться Runtime до начала анализа. + +Это обеспечивает воспроизводимость выполнения Engine и независимость аналитических компонентов. + +--- + +### Неизменяемость входных данных + +После начала выполнения Engine входной контракт считается неизменяемым. + +Engine не должен: + +- изменять Runtime Context; +- изменять объекты входного контракта; +- модифицировать результаты зависимостей; +- сохранять изменения обратно в Runtime. + +Все изменения состояния платформы выполняются исключительно за пределами Engine. + +Это гарантирует повторяемость вычислений и исключает появление скрытых побочных эффектов. + +--- + +### Изоляция входного контракта + +Каждый Engine рассматривает Runtime Context как локальную копию состояния платформы на момент начала анализа. + +Engine не должен зависеть от изменения состояния Runtime во время собственного выполнения. + +Любые изменения Runtime становятся доступными Engine только при следующем запуске анализа посредством нового Runtime Context. + +Данное правило обеспечивает детерминированность выполнения аналитических компонентов. + +--- + +### Завершение Engine Input Contract + +Engine Input Contract, определённый настоящей частью, является единственным допустимым способом передачи данных аналитическим компонентам платформы. + +Все существующие и будущие Engine обязаны использовать исключительно данный входной контракт. + +Любое изменение структуры входного контракта допускается только посредством официального изменения настоящего стандарта. + +## Part IV. Engine Result Contract + +Настоящая часть определяет официальный контракт результата выполнения любого аналитического Engine подсистемы **Market Intelligence**. + +Результат выполнения Engine является основным объектом обмена аналитической информацией внутри платформы. + +Все существующие и будущие аналитические движки обязаны возвращать результат исключительно в формате, определённом настоящим стандартом. + +Использование альтернативных моделей результата не допускается. + +--- + +### Общие требования + +Каждый Engine обязан завершать выполнение формированием единого результата анализа. + +Результат выполнения должен быть стандартизирован независимо от: + +- назначения Engine; +- сложности анализа; +- используемых алгоритмов; +- полноты входных данных; +- успешности выполнения анализа. + +Единый контракт результата обеспечивает совместимость всех аналитических компонентов платформы. + +--- + +### Engine Result + +Официальным результатом выполнения любого аналитического Engine является объект `EngineResult`. + +`EngineResult` представляет собой единый Runtime Contract обмена аналитической информацией между компонентами платформы. + +Каждый Engine возвращает исключительно один экземпляр `EngineResult`. + +Возврат альтернативных структур данных не допускается. + +Даже при невозможности полного анализа Engine обязан вернуть корректно сформированный `EngineResult`. + +--- + +### Обязательные логические разделы результата + +Каждый `EngineResult` должен содержать следующие обязательные логические разделы. + +| Раздел | Назначение | +|----------|------------| +| Status | Состояние выполнения Engine | +| Evaluation | Итоговая аналитическая оценка | +| Confidence | Уровень уверенности результата | +| Metrics | Вычисленные показатели анализа | +| Diagnostics | Диагностическая информация | +| Payload | Структурированное представление результата | +| Snapshot | Снимок состояния результата | +| Metadata | Служебная информация о выполнении | + +Данные разделы являются обязательными смысловыми частями результата. + +Конкретная программная структура этих разделов определяется официальными Runtime Models. + +Runtime Contract определяет содержание и назначение разделов. + +Runtime Models определяют их конкретную реализацию. + +--- + +### Engine Status + +Каждый результат обязан содержать официальный статус выполнения. + +Статус определяет качество и полноту полученного результата. + +Статус не является аналитической оценкой рынка. + +Статус описывает исключительно процесс выполнения Engine. + +Допустимые значения статуса определяются настоящим стандартом. + +--- + +### Evaluation + +Evaluation представляет итоговую аналитическую оценку, сформированную Engine. + +Evaluation содержит исключительно выводы, относящиеся к предметной области данного Engine. + +Evaluation не должна содержать: + +- торговые решения; +- команды исполнения; +- рекомендации пользователю; +- результаты других Engine. + +Каждый Engine отвечает только за собственную аналитическую оценку. + +--- + +### Confidence + +Confidence отражает степень уверенности Engine в корректности собственного результата. + +Confidence относится исключительно к результату конкретного Engine. + +Engine не должен интерпретировать Confidence как вероятность торгового успеха. + +Правила вычисления Confidence определяются самим Engine. + +Runtime определяет только способ представления данного значения. + +--- + +### Metrics + +Metrics содержат количественные показатели, вычисленные Engine. + +Каждая метрика должна быть: + +- измеримой; +- воспроизводимой; +- документированной; +- относящейся исключительно к области ответственности данного Engine. + +Metrics не содержат итоговых решений. + +Они являются первичными аналитическими измерениями. + +--- + +### Diagnostics + +Diagnostics содержат техническую информацию о выполнении анализа. + +Диагностика предназначена исключительно для: + +- журналирования; +- анализа качества работы; +- поиска ошибок; +- проверки корректности выполнения. + +Diagnostics не участвуют в принятии аналитических решений. + +--- + +### Payload + +Payload представляет структурированное описание результата анализа. + +Payload предназначен для передачи информации другим компонентам платформы. + +Payload должен содержать только официальные данные анализа. + +Payload не должен содержать: + +- команды исполнения; +- объекты пользовательского интерфейса; +- торговые действия; +- внутреннее состояние Runtime. + +--- + +### Snapshot + +Snapshot представляет состояние результата в момент завершения анализа. + +Snapshot используется для: + +- журналирования; +- последующего анализа; +- сравнения результатов; +- воспроизводимости вычислений. + +После формирования Snapshot считается неизменяемым. + +--- + +### Metadata + +В Runtime Contract различаются два вида metadata: + +| Вид | Назначение | +|-----|------------| +| **Engine Metadata** | Описание Engine как компонента платформы. | +| **Evaluation Metadata** | Служебная информация о конкретном выполнении Engine. | + +**Engine Metadata** описывает сам Engine и включает: + +- имя Engine; +- версию Engine; +- описание назначения; +- поддерживаемые таймфреймы; +- обязательные зависимости; +- необязательные зависимости; +- признак включения по умолчанию. + +Engine Metadata не содержит аналитических выводов и не является результатом анализа. + +Engine Metadata должна быть доступна без создания экземпляра Engine. + +**Evaluation Metadata** относится к конкретному результату выполнения Engine и может включать: + +- идентификатор Engine; +- версию реализации; +- время выполнения; +- продолжительность вычислений; +- сведения о входных данных. + +Evaluation Metadata не содержит аналитических выводов. + +--- + +### Неизменяемость результата + +После формирования `EngineResult` его внутреннее состояние не должно изменяться. + +Все Runtime-модели результата рассматриваются как неизменяемые. + +Если требуется изменение результата, формируется новый экземпляр `EngineResult`. + +Это обеспечивает: + +- воспроизводимость анализа; +- корректное журналирование; +- безопасную передачу между компонентами; +- предсказуемость поведения Runtime. + +--- + +### Независимость результата + +`EngineResult` должен быть полностью самодостаточным. + +Компонент, получивший результат анализа, не должен обращаться к Engine за дополнительной информацией. + +Вся информация, необходимая для последующей обработки, должна содержаться внутри официального Runtime Contract. + +Это обеспечивает слабую связанность архитектурных компонентов. + +--- + +### Завершение Engine Result Contract + +Engine Result Contract, определённый настоящей частью, является официальным стандартом представления результатов анализа в подсистеме **Market Intelligence**. + +Все существующие и будущие аналитические компоненты обязаны возвращать результаты исключительно в соответствии с данным контрактом. + +Любое изменение структуры `EngineResult` допускается только посредством официального изменения настоящего стандарта. + +## Part V. Runtime Status Contract + +Настоящая часть определяет официальный контракт статусов Runtime и аналитических компонентов подсистемы **Market Intelligence**. + +Статусы используются для описания качества выполнения, полноты результата и состояния аналитического компонента. + +Статус не является аналитическим выводом о рынке. + +Статус описывает исключительно состояние выполнения Runtime или Engine. + +--- + +### Назначение статусов + +Runtime Status Contract обеспечивает единообразное описание состояния выполнения всех аналитических компонентов. + +Статусы позволяют: + +- отличать успешное выполнение от частичного; +- фиксировать недостаточность входных данных; +- фиксировать устаревание данных; +- безопасно обрабатывать ошибки; +- сохранять объяснимость результата; +- обеспечивать устойчивость Runtime. + +Каждый Engine обязан использовать только официальные статусы, определённые настоящим стандартом. + +--- + +### Runtime Status + +Runtime Status описывает состояние Runtime Layer как среды выполнения аналитических компонентов. + +Runtime Status может использоваться для описания: + +- готовности Runtime; +- доступности входного контекста; +- актуальности Runtime Context; +- состояния выполнения группы Engine; +- состояния Coordinator Runtime. + +Runtime Status не должен использоваться для описания аналитического вывода о рынке. + +--- + +### Engine Status + +Engine Status описывает состояние выполнения конкретного Engine. + +Engine Status является обязательной частью результата выполнения Engine. + +Engine Status должен отражать качество и полноту выполнения анализа. + +Engine Status не должен отражать торговую оценку, рыночное направление или рекомендацию к действию. + +--- + +### Допустимые состояния Engine + +Каждый Engine использует единый набор статусов выполнения. + +Допустимые состояния: + +| Статус | Значение | +|--------|----------| +| `OK` | Engine успешно выполнил анализ и сформировал полный результат. | +| `PARTIAL` | Engine выполнил анализ частично и сформировал неполный, но пригодный результат. | +| `STALE` | Engine выполнил анализ на устаревших или частично устаревших данных. | +| `INSUFFICIENT_DATA` | Engine не имеет достаточного объёма данных для полного анализа. | +| `ERROR` | Engine завершился с ошибкой, но безопасно сформировал результат. | +| `DISABLED` | Engine отключён и не выполняет анализ. | + +Использование собственных статусов Engine не допускается. + +--- + +### Правила применения статусов + +Статус должен соответствовать фактическому состоянию выполнения Engine. + +Engine должен возвращать: + +- `OK`, если анализ выполнен полностью; +- `PARTIAL`, если результат сформирован не полностью, но может быть использован; +- `STALE`, если входные данные устарели; +- `INSUFFICIENT_DATA`, если данных недостаточно для корректного анализа; +- `ERROR`, если выполнение завершилось ошибкой; +- `DISABLED`, если Engine отключён конфигурацией или архитектурным режимом. + +Статус не должен использоваться для сокрытия ошибок реализации. + +--- + +### Статус и аналитическая оценка + +Статус выполнения отделён от аналитической оценки. + +Например: + +- `OK` не означает положительный рыночный сигнал; +- `ERROR` не означает негативное состояние рынка; +- `PARTIAL` не означает слабое движение рынка; +- `STALE` не означает изменение рыночного режима. + +Аналитическая оценка должна располагаться в соответствующем разделе результата Engine. + +Статус описывает только качество выполнения. + +--- + +### Статус и Reason Code + +Каждый статус, отличный от `OK`, должен сопровождаться объяснимой причиной. + +Причина должна быть представлена через официальный Reason Code. + +Использование произвольного текстового описания причины вместо Reason Code не допускается. + +Reason Code позволяет обеспечить: + +- единообразную диагностику; +- машинную обработку причин; +- перевод в человекочитаемый текст отдельным слоем; +- стабильность логирования; +- независимость Runtime от пользовательского интерфейса. + +--- + +### Инварианты статусов + +Runtime Status Contract имеет следующие инварианты: + +- каждый Engine всегда возвращает статус; +- статус всегда относится к выполнению, а не к торговому решению; +- `OK` допускается только при полном успешном выполнении; +- ошибки не выбрасываются наружу без формирования результата; +- статус не заменяет Reason Code; +- Reason Code не заменяет Diagnostics; +- пользовательский текст не является частью статуса. + +Нарушение любого из указанных инвариантов рассматривается как нарушение Runtime Contract. + +--- + +### Завершение Runtime Status Contract + +Runtime Status Contract, определённый настоящей частью, является обязательным для всех аналитических компонентов платформы. + +Все существующие и будущие Engine обязаны использовать только официальные статусы выполнения. + +Любое изменение набора статусов допускается только посредством официального изменения настоящего стандарта. + +--- + +## Part VI. Diagnostics and Explainability Contract + +Настоящая часть определяет единый контракт диагностики и объяснимости результатов анализа подсистемы **Market Intelligence**. + +Диагностика и объяснимость являются обязательными свойствами любого аналитического результата. + +Ни один аналитический компонент не должен формировать результат, который невозможно объяснить или воспроизвести. + +Настоящий раздел устанавливает единые требования к представлению причин, диагностической информации и сопроводительных данных результатов анализа. + +--- + +### Общие положения + +Каждый аналитический Engine обязан сопровождать результат выполнения достаточной информацией, позволяющей: + +- понять процесс выполнения анализа; +- определить причину полученного результата; +- воспроизвести последовательность вычислений; +- оценить качество результата; +- выполнить последующую диагностику. + +Диагностическая информация является частью Runtime Contract. + +--- + +### Explainability + +Объяснимость результатов является обязательным свойством аналитической платформы. + +Каждый результат анализа должен позволять ответить на следующие вопросы: + +- почему получен именно данный результат; +- какие входные данные использовались; +- какие ограничения существовали во время выполнения; +- насколько результат является надёжным; +- какие факторы оказали влияние на итоговую оценку. + +Получение результата без возможности объяснить его происхождение не допускается. + +--- + +### Reason Code + +Причины формирования результата представляются посредством официального Reason Code. + +Reason Code является стандартизированным идентификатором причины результата. + +Reason Code используется для: + +- машинной обработки; +- журналирования; +- диагностики; +- последующего преобразования в человекочитаемый текст. + +Engine не должен самостоятельно генерировать текстовые описания причин. + +Преобразование Reason Code в пользовательский текст выполняется отдельным слоем платформы. + +--- + +### Diagnostics + +Diagnostics содержит служебную информацию о процессе выполнения анализа. + +Diagnostics предназначен исключительно для инженерных задач. + +Diagnostics может содержать: + +- сведения о выполненных этапах анализа; +- сведения о пропущенных этапах; +- информацию о качестве входных данных; +- сведения о внутренних проверках; +- информацию о времени выполнения; +- дополнительные диагностические показатели. + +Diagnostics не участвует в принятии аналитических решений. + +--- + +### Payload + +Payload представляет структурированное представление результатов анализа. + +Payload предназначен для передачи аналитической информации между архитектурными компонентами платформы. + +Payload должен содержать исключительно результаты анализа. + +Payload не должен содержать: + +- команды исполнения; +- пользовательский интерфейс; +- торговые инструкции; +- внутреннее состояние Runtime; +- объекты реализации Engine. + +Payload является частью официального Runtime Contract. + +--- + +### Snapshot + +Snapshot представляет состояние результата в момент завершения выполнения Engine. + +Snapshot используется исключительно для: + +- журналирования; +- последующего анализа; +- сравнения результатов; +- воспроизводимости вычислений. + +Snapshot не должен использоваться как источник аналитических решений. + +После формирования Snapshot считается неизменяемым. + +--- + +### Runtime Events + +Runtime может публиковать события, отражающие жизненный цикл выполнения аналитических компонентов. + +Runtime Events предназначены для: + +- журналирования; +- мониторинга; +- диагностики; +- анализа выполнения Runtime. + +Runtime Events не используются для передачи аналитических результатов. + +Аналитическая информация передаётся исключительно посредством официального Runtime Contract. + +--- + +### Разделение ответственности + +Диагностические сущности имеют строго определённую область ответственности. + +| Компонент | Назначение | +|-----------|------------| +| Reason Code | Причина результата | +| Diagnostics | Техническая диагностика | +| Payload | Передача аналитических данных | +| Snapshot | Фиксация результата | +| Runtime Events | События жизненного цикла Runtime | + +Пересечение областей ответственности между указанными сущностями не допускается. + +--- + +### Инварианты диагностики + +Diagnostics Contract имеет следующие инварианты. + +Каждый аналитический результат: + +- сопровождается диагностической информацией; +- имеет объяснимую причину формирования; +- допускает воспроизведение процесса анализа; +- отделяет диагностику от аналитической оценки; +- отделяет диагностику от пользовательского интерфейса. + +Диагностика должна оставаться независимой от реализации конкретного Engine. + +--- + +### Завершение Diagnostics Contract + +Diagnostics and Explainability Contract является обязательной частью Runtime Contract. + +Все аналитические компоненты платформы обязаны использовать единые механизмы диагностики и объяснимости результатов. + +Любое изменение Diagnostics Contract допускается исключительно посредством официального изменения настоящего стандарта. + +--- + +## Part VII. Dependency Contract + +Настоящая часть определяет официальный контракт зависимостей подсистемы **Market Intelligence**. + +Dependency Contract устанавливает единые правила взаимодействия архитектурных компонентов платформы. + +Основной целью настоящего раздела является обеспечение независимости компонентов, предотвращение появления скрытых зависимостей и сохранение долгосрочной сопровождаемости архитектуры. + +Все существующие и будущие компоненты платформы обязаны соответствовать требованиям настоящего раздела. + +--- + +### Общие положения + +Архитектура платформы строится на принципе явных и однонаправленных зависимостей. + +Каждая зависимость должна быть: + +- необходимой; +- явной; +- объяснимой; +- документированной; +- архитектурно обоснованной. + +Появление скрытых зависимостей не допускается. + +--- + +### Архитектурная модель зависимостей + +Допустимая модель зависимостей имеет следующий вид. + +```text +Coordinator Layer + │ + ▼ +Engine Layer + │ + ▼ +Runtime Layer + │ + ▼ +Common Layer +``` + +Каждый компонент может использовать только компоненты, расположенные ниже по архитектурной иерархии. + +Обратные зависимости запрещаются. + +--- + +### Допустимые зависимости + +Архитектурный компонент может зависеть только от компонентов, предусмотренных архитектурной моделью платформы. + +Допустимыми code dependencies являются: + +- Coordinator → Runtime Contract; +- Coordinator → Engine Contract; +- Engine Implementation → Engine Contract; +- Engine → Runtime; +- Engine → Common; +- Runtime → Common. + +Execution Flow и Code Dependency являются разными архитектурными понятиями. + +Execution Flow допускает следующую модель выполнения: + +```text +Coordinator + │ + ▼ +Engine +``` +--- + +### Запрещённые зависимости + +Не допускаются следующие зависимости: + +- Engine → Engine; +- Runtime → Engine; +- Common → Runtime; +- Common → Engine; +- Common → Coordinator; +- Runtime → Coordinator; +- Engine → Coordinator. + +Также запрещается: + +- использование внутренних структур другого компонента; +- импорт приватных сущностей; +- обход официальных контрактов; +- косвенное создание циклических зависимостей. + +--- + +### Зависимости между Engine + +Каждый Engine рассматривается как полностью независимый аналитический компонент. + +Engine не должен: + +- импортировать другой Engine; +- обращаться к внутреннему состоянию другого Engine; +- использовать внутренние модели другого Engine; +- управлять выполнением другого Engine. + +Если одному Engine требуется результат другого Engine, соответствующая информация должна предоставляться Runtime посредством официального Runtime Context. + +Таким образом взаимодействие осуществляется через контракт, а не посредством прямой зависимости между компонентами. + +--- + +### Runtime как граница взаимодействия + +Runtime является единственной официальной границей взаимодействия аналитических компонентов. + +Все данные, необходимые Engine для выполнения анализа, должны быть подготовлены Runtime до начала выполнения Engine. + +Во время выполнения Engine не должен самостоятельно получать дополнительные данные из других архитектурных компонентов. + +Это обеспечивает: + +- воспроизводимость вычислений; +- независимость Engine; +- возможность безопасного тестирования; +- минимизацию связанности компонентов. + +--- + +### Зависимости от внешних систем + +Компоненты подсистемы **Market Intelligence** не должны зависеть напрямую от внешних систем. + +Не допускаются прямые зависимости от: + +- биржевых API; +- торговой подсистемы; +- пользовательского интерфейса; +- базы данных; +- журнала событий; +- систем исполнения ордеров; +- сервисов уведомлений. + +Получение информации из внешних источников должно выполняться до формирования Runtime Context. + +--- + +### Dependency Inversion + +Архитектура платформы должна строиться на зависимости от официальных контрактов, а не от конкретных реализаций. + +Компоненты взаимодействуют посредством: + +- Runtime Contract; +- Runtime Models; +- Common Models; +- Engine Metadata; +- официальных интерфейсов взаимодействия. + +Замена внутренней реализации компонента не должна влиять на остальные компоненты платформы при сохранении официального контракта. + +--- + +### Инварианты Dependency Contract + +Dependency Contract имеет следующие инварианты. + +Архитектура всегда сохраняет: + +- однонаправленные зависимости; +- отсутствие циклических импортов; +- отсутствие скрытых зависимостей; +- взаимодействие исключительно через официальные контракты; +- независимость аналитических компонентов; +- возможность независимого тестирования Engine. + +Нарушение любого из перечисленных инвариантов рассматривается как нарушение Runtime Contract. + +--- + +### Завершение Dependency Contract + +Dependency Contract является обязательной частью Runtime Contract подсистемы **Market Intelligence**. + +Все существующие и будущие компоненты платформы обязаны соблюдать установленные правила зависимостей. + +Любое изменение архитектурной модели зависимостей допускается исключительно посредством официального изменения настоящего стандарта. + +## Part VIII. Runtime Safety + +Настоящая часть определяет требования к устойчивости Runtime подсистемы **Market Intelligence**. + +Runtime должен обеспечивать безопасное выполнение аналитических компонентов независимо от полноты входных данных, состояния отдельных Engine или возникновения внутренних ошибок. + +Основной целью Runtime Safety является сохранение работоспособности аналитической платформы при возникновении локальных отказов. + +--- + +### Общие требования + +Runtime должен обеспечивать устойчивую работу аналитической платформы независимо от состояния отдельных компонентов. + +Каждый Runtime-компонент обязан: + +- корректно завершать выполнение; +- публиковать официальный результат; +- использовать официальный Runtime Status; +- сохранять объяснимость результата; +- обеспечивать возможность последующей диагностики. + +Локальная ошибка не должна приводить к разрушению общего процесса анализа. + +--- + +### Graceful Degradation + +Подсистема **Market Intelligence** должна поддерживать контролируемую деградацию функциональности. + +При невозможности выполнения полного анализа допускается публикация частичного результата при условии, что: + +- результат явно помечен соответствующим статусом; +- ограничения результата отражены посредством Reason Code; +- сохранена диагностическая информация; +- остальные аналитические компоненты продолжают выполнение. + +Контролируемая деградация рассматривается как штатный режим работы Runtime. + +--- + +### Error Isolation + +Ошибка одного Engine не должна распространяться на остальные аналитические компоненты. + +При возникновении ошибки Engine обязан: + +- безопасно завершить выполнение; +- сформировать корректный Engine Result; +- установить соответствующий Engine Status; +- сохранить диагностическую информацию; +- передать результат Coordinator посредством официального Runtime Contract. + +Coordinator обязан продолжить обработку остальных доступных результатов. + +--- + +### Partial Result + +Если Engine не может сформировать полный результат, допускается публикация частичного результата. + +Частичный результат должен: + +- соответствовать официальному Runtime Contract; +- иметь корректный статус выполнения; +- содержать объяснимую причину неполноты; +- сохранять диагностическую информацию. + +Использование частичного результата определяется Coordinator и не входит в область ответственности Engine. + +--- + +### Fault Tolerance + +Runtime должен сохранять работоспособность при возникновении отказов отдельных компонентов. + +Runtime обязан обеспечивать: + +- локализацию ошибок; +- независимость выполнения Engine; +- отсутствие каскадных отказов; +- воспроизводимость поведения; +- сохранение официальных Runtime Contract. + +Нарушение работы одного компонента не должно приводить к потере работоспособности Runtime в целом. + +--- + +### Устойчивость Runtime Contract + +Все официальные Runtime Contract должны сохранять свою корректность независимо от результата выполнения анализа. + +Даже при возникновении ошибки Runtime обязан публиковать корректно сформированные объекты Runtime. + +Не допускается: + +- возврат неопределённых структур данных; +- нарушение официального контракта; +- отсутствие статуса выполнения; +- отсутствие диагностической информации. + +Runtime Contract должен оставаться стабильным при любых допустимых сценариях выполнения. + +--- + +### Инварианты Runtime Safety + +Runtime Safety имеет следующие инварианты. + +Подсистема **Market Intelligence** всегда сохраняет: + +- официальные Runtime Contract; +- объяснимость результатов; +- независимость аналитических компонентов; +- локализацию ошибок; +- корректность статусов; +- возможность диагностики; +- воспроизводимость поведения. + +Нарушение любого из перечисленных инвариантов рассматривается как нарушение настоящего стандарта. + +--- + +### Завершение Runtime Safety + +Требования Runtime Safety являются обязательными для всех компонентов подсистемы **Market Intelligence**. + +Все существующие и будущие аналитические компоненты обязаны обеспечивать безопасное выполнение в соответствии с настоящим разделом. + +Любое изменение требований Runtime Safety допускается исключительно посредством официального изменения настоящего стандарта. + +## Part IX. Runtime Evolution + +Настоящая часть определяет правила долгосрочного развития Runtime подсистемы **Market Intelligence**. + +Runtime рассматривается как фундаментальный архитектурный уровень платформы. + +Развитие Runtime допускается только при сохранении совместимости с фундаментальными архитектурными принципами и официальными Runtime Contract. + +--- + +### Общие принципы развития + +Runtime должен развиваться последовательно и предсказуемо. + +Любое изменение Runtime должно: + +- иметь архитектурное обоснование; +- соответствовать требованиям настоящего стандарта; +- сохранять согласованность Runtime Layer; +- не нарушать существующие архитектурные инварианты. + +Изменение Runtime не должно выполняться исключительно ради изменения внутренней реализации. + +--- + +### Развитие Runtime Contract + +Runtime Contract является официальным механизмом взаимодействия архитектурных компонентов. + +Любое изменение Runtime Contract должно: + +- быть архитектурно обоснованным; +- документироваться одновременно с изменением архитектуры; +- сохранять внутреннюю согласованность контракта; +- учитывать влияние на существующие компоненты платформы. + +Изменение Runtime Contract рассматривается как архитектурное изменение и требует соответствующего Architecture Review. + +--- + +### Расширение Runtime + +Расширение Runtime является предпочтительным способом развития платформы. + +Новые возможности должны добавляться посредством расширения существующей архитектуры без изменения фундаментальной модели Runtime. + +При расширении Runtime должны соблюдаться следующие требования: + +- новая сущность имеет единственную область ответственности; +- место новой сущности определено заранее; +- используются существующие архитектурные контракты либо их официальное расширение; +- отсутствует дублирование существующих сущностей; +- сохраняется архитектурная простота Runtime. + +Расширение Runtime не должно увеличивать связанность компонентов. + +--- + +### Изменение Runtime Model + +Runtime Model может изменяться только при наличии подтверждённой архитектурной необходимости. + +К таким случаям относятся: + +- устранение архитектурного дефекта; +- повышение сопровождаемости Runtime; +- упрощение архитектурной модели; +- развитие официальных Runtime Contract; +- устранение архитектурного долга. + +Изменение Runtime Model не должно нарушать фундаментальные свойства Runtime Layer. + +--- + +### Архитектурная стабильность Runtime + +Runtime относится к наиболее стабильным архитектурным слоям платформы. + +Чем ближе компонент расположен к Runtime Layer, тем более строгими должны быть требования к его изменению. + +Любое изменение Runtime должно рассматриваться как изменение архитектурного фундамента платформы. + +--- + +### Критерии качественного развития Runtime + +Развитие Runtime считается успешным только при одновременном выполнении следующих условий: + +- расширяются возможности аналитической платформы; +- сохраняется совместимость Runtime Contract; +- не увеличивается архитектурная сложность; +- сохраняется объяснимость Runtime; +- повышается сопровождаемость платформы. + +Если изменение Runtime приводит к усложнению архитектуры без существенной инженерной ценности, такое изменение должно быть пересмотрено. + +--- + +### Завершение Runtime Evolution + +Развитие Runtime должно обеспечивать долгосрочную стабильность архитектуры подсистемы **Market Intelligence**. + +Все изменения Runtime обязаны сохранять: + +- архитектурную целостность; +- официальные Runtime Contract; +- архитектурные инварианты; +- объяснимость архитектуры; +- возможность дальнейшего безопасного развития платформы. + +Runtime развивается посредством последовательных архитектурно согласованных изменений, каждое из которых делает платформу функционально богаче без увеличения её архитектурной сложности. + +## Part X. Заключительные положения + +Настоящая часть завершает настоящий инженерный стандарт и определяет порядок его применения, развития и сопровождения. + +Все положения настоящего документа являются обязательными для архитектуры подсистемы **Market Intelligence** проекта **Dzentra**. + +--- + +### Соответствие настоящему стандарту + +Все компоненты Runtime обязаны соответствовать требованиям настоящего стандарта. + +Соответствие подтверждается посредством процедур **Architecture Review**, определённых документом **Development Process**. + +Runtime считается соответствующим настоящему стандарту только при одновременном выполнении следующих условий: + +- соблюдены требования Runtime Layer; +- соблюдены требования Runtime Contract; +- соблюдены архитектурные ограничения; +- сохранены Runtime-инварианты; +- обеспечена архитектурная согласованность; +- документация соответствует текущему состоянию Runtime. + +Несоответствие любому обязательному требованию настоящего стандарта рассматривается как архитектурный дефект. + +--- + +### Развитие стандарта + +Настоящий документ рассматривается как долгосрочный инженерный стандарт. + +Развитие настоящего стандарта допускается только посредством выпуска новой версии документа. + +Каждая новая редакция должна: + +- сохранять фундаментальную архитектурную модель Runtime либо официально фиксировать её изменение; +- устранять неоднозначности; +- повышать согласованность Runtime Contract; +- улучшать сопровождаемость Runtime; +- уменьшать архитектурную сложность платформы. + +Изменение настоящего стандарта не должно приводить к ухудшению архитектурного качества платформы. + +--- + +### Приоритет настоящего стандарта + +Настоящий документ является официальным источником требований к Runtime подсистемы **Market Intelligence**. + +При возникновении противоречий между настоящим документом и другими инженерными материалами приоритет имеют требования настоящего стандарта. + +Документы более низкого уровня обязаны развивать положения настоящего стандарта и не могут изменять его фундаментальные требования. + +--- + +### Runtime-аксиома Dzentra + +Фундаментальной аксиомой Runtime платформы **Dzentra** является следующее утверждение. + +> **Runtime определяет архитектурные контракты взаимодействия компонентов, но не реализует предметную или аналитическую логику.** + +Данная аксиома является итоговым выражением всех требований настоящего стандарта. + +Любое изменение Runtime должно оцениваться прежде всего с точки зрения соответствия этой аксиоме. + +Если после изменения Runtime начинает содержать аналитическую, торговую или предметную логику, принятое решение должно быть пересмотрено независимо от достигнутой функциональности. + +--- + +### Заключительные положения + +Настоящий документ определяет официальный Runtime Contract подсистемы **Market Intelligence** проекта **Dzentra**. + +Все существующие и будущие Runtime-компоненты обязаны соответствовать требованиям настоящего стандарта. + +Настоящий документ является фундаментом для: + +- Engine Layer; +- Coordinator Layer; +- Runtime Models; +- Runtime Events; +- Runtime Context; +- Runtime Contract; +- Engine Contract; +- всех последующих аналитических компонентов платформы. + +Настоящий документ остаётся единым официальным источником требований к Runtime подсистемы **Market Intelligence**. + +--- + +## Приложения + +Приложения являются справочной частью настоящего стандарта. + +Они не изменяют обязательные требования документа, но используются для унификации архитектурной практики проекта. + +В последующих версиях стандарта могут быть подготовлены отдельные приложения. + +- **Приложение А.** Runtime Glossary. +- **Приложение Б.** Каталог Runtime Models. +- EngineMetadata +- EngineContext +- EngineResult +- EngineDependencyResult +- EngineEvaluationMeta +- EngineDiagnostics +- EngineMetric +- **Приложение В.** Каталог Runtime Events. +- **Приложение Г.** Runtime State Diagrams. +- **Приложение Д.** Примеры допустимых Runtime Contract. +- **Приложение Е.** Примеры недопустимых Runtime Contract. \ No newline at end of file diff --git a/docs/market_intelligence/specifications/README.md b/docs/market_intelligence/specifications/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/migrations/build_001.md b/docs/migrations/build_001.md new file mode 100644 index 0000000..dc6df20 --- /dev/null +++ b/docs/migrations/build_001.md @@ -0,0 +1,325 @@ +# Build 001 — Внутренняя модель Instrument Reference Data + +**Проект:** Dzentra +**Подсистема:** Market Data Acquisition +**Миграция:** Instrument Reference Data +**Статус:** ✅ Завершён +**Дата:** 2026-07-10 + +--- + +# Цель Build + +Создать независимую внутреннюю модель Instrument Reference Data. + +На данном этапе запрещается: + +- изменять ExchangeService; +- изменять существующий runtime; +- менять Telegram UI; +- менять Symbol Validation; +- подключать новую модель к production-коду. + +Build создаёт исключительно новую внутреннюю модель, которая станет целевой моделью для последующего mapper. + +--- + +# Причина выполнения Build + +В утверждённой архитектуре Dzentra модель предметной области должна существовать отдельно от: + +- REST API Dzengi; +- parser; +- mapper; +- ExchangeService; +- Telegram UI; +- runtime. + +Поэтому сначала создаётся независимая модель Instrument, а только затем будут строиться parser и mapper. + +--- + +# Проанализированные файлы + +В ходе Build были полностью проанализированы: + +```text +app/src/integrations/exchange/service.py +app/src/integrations/exchange/models.py +app/src/integrations/exchange/symbol_utils.py +app/src/integrations/exchange/rest_client.py +app/src/integrations/exchange/status.py + +app/src/telegram/ui/currency_ui.py + +docs/stages/stage-03_3-exchange_info.md +docs/decisions/0007-symbol-validation.md +``` + +Также были проанализированы: + +- сохранённый runtime-ответ `/exchangeInfo`; +- OpenAPI Dzengi; +- все текущие потребители ExchangeSymbol; +- результаты grep по всему проекту. + +--- + +# Основные выводы анализа + +Установлено: + +- ExchangeSymbol создаётся только в одном месте; +- parser и mapper сейчас объединены внутри ExchangeService; +- SymbolValidationResult используется только ExchangeService; +- normalize_symbol() и symbol_candidates() централизованы; +- отдельного parser ещё не существует; +- отдельного mapper ещё не существует; +- кэш справочника представляет собой неуправляемый singleton без TTL. + +Также подтверждено: + +- новый Build не должен менять существующий ExchangeService; +- новая модель не должна зависеть от Telegram UI; +- runtime-статусы не являются частью Instrument Reference Data. + +--- + +# Анализ реального ответа exchangeInfo + +Подтверждено наличие следующих полей: + +Обязательные: + +- symbol +- name +- status +- baseAsset +- quoteAsset +- marketModes +- marketType +- tickSize + +Через filters: + +- stepSize +- minQty +- maxQty +- minNotional + +Также обнаружены дополнительные справочные поля: + +- assetType +- orderTypes +- baseAssetPrecision +- quotePrecision +- tickValue +- country +- sector +- industry +- tradingHours + +--- + +# Принятые архитектурные решения + +## Новая модель не копирует ExchangeSymbol + +Новая модель является самостоятельной внутренней моделью предметной области. + +--- + +## Runtime не входит в Instrument + +Из модели исключены: + +- title +- message +- ui_line +- reason +- is_open +- is_available +- is_auth_ok + +Эти поля относятся к Runtime Status. + +--- + +## Использование Decimal + +Для следующих значений принято использовать Decimal: + +- tick_size +- tick_value +- step_size +- min_qty +- max_qty +- min_notional + +Причина: + +данные используются при нормализации цен и количества и не должны терять точность. + +Поскольку модель пока нигде не используется, изменение не влияет на работающего бота. + +Классификация: + +**Улучшение надёжности.** + +--- + +## Не включены в Instrument + +Сознательно исключены: + +- trading_fee +- long_rate +- short_rate +- swap_charge_interval +- min_sl_gap +- max_sl_gap +- min_tp_gap +- max_tp_gap + +Причина: + +данные относятся к другим предметным подсистемам. + +--- + +# Созданные файлы + +Создан: + +```text +app/src/market_data/acquisition/models/instrument.py +``` + +Создан: + +```text +app/tests/unit/market_data/acquisition/models/test_instrument.py +``` + +Другие production-файлы не изменялись. + +--- + +# Проверки + +Выполнены проверки. + +## Unit Test новой модели + +Статус: + +✅ Passed + +``` +4 passed +``` + +--- + +## Импорт модели + +Проверено: + +- импорт проходит; +- dataclass создаётся; +- Decimal работает; +- tuple работают. + +Статус: + +✅ Passed + +--- + +## Компиляция + +Проверено: + +``` +python -m py_compile +``` + +Статус: + +✅ Passed + +--- + +## Legacy Compatibility + +Проверено: + +- ExchangeSymbol +- SymbolValidationResult +- ExchangeService +- normalize_symbol() +- symbol_candidates() + +Статус: + +✅ Полностью совместимо. + +--- + +## Проверка отсутствия подключения новой модели + +Подтверждено: + +новая модель используется исключительно в unit-тестах. + +Production-код её не импортирует. + +Статус: + +✅ Passed + +--- + +## Полный набор тестов + +Выполнено: + +``` +python -m pytest -q +``` + +Результат: + +``` +5 passed +``` + +Статус: + +✅ Passed + +--- + +# Итог Build + +Build 001 завершён успешно. + +Подтверждено: + +- создана независимая модель Instrument; +- модель не зависит от legacy-кода; +- обратная совместимость полностью сохранена; +- работающий бот не изменён; +- новая архитектура готова к реализации Build 002. + +--- + +# Следующий Build + +Build 002 + +**Dzengi Raw Models** + +Следующий этап создаст модели сырого ответа REST API Dzengi. + +На этом этапе ExchangeService по-прежнему изменяться не будет. \ No newline at end of file diff --git a/docs/migrations/build_002.md b/docs/migrations/build_002.md new file mode 100644 index 0000000..60797da --- /dev/null +++ b/docs/migrations/build_002.md @@ -0,0 +1,421 @@ +# Build 002 — Raw Models Dzengi exchangeInfo + +**Проект:** Dzentra +**Подсистема:** Market Data Acquisition +**Миграция:** Instrument Reference Data +**Статус:** ✅ Завершён +**Дата:** 2026-07-10 + +--- + +# Цель Build + +Создать транспортные модели (Raw Models), полностью описывающие ответ REST API Dzengi `exchangeInfo`. + +На данном этапе запрещалось: + +- изменять ExchangeService; +- изменять ExchangeSymbol; +- изменять SymbolValidationResult; +- изменять runtime; +- изменять Telegram UI; +- выполнять parser JSON; +- выполнять mapper в предметную модель Instrument. + +Build создаёт исключительно транспортный контракт между REST API и будущим parser. + +--- + +# Причина выполнения Build + +После Build 001 уже существует независимая предметная модель: + +```text +Instrument +``` + +Следующим архитектурным слоем является транспортная модель адаптера. + +До начала Build 002 существующая реализация выглядела следующим образом: + +```text +REST + ↓ +dict + ↓ +ExchangeService + ↓ +ExchangeSymbol +``` + +Parser и mapper были объединены внутри `ExchangeService`. + +Это нарушало принцип разделения ответственности. + +После Build 002 появилась отдельная транспортная модель: + +```text +REST + ↓ +Raw Models +``` + +которая станет входом для parser в следующем Build. + +--- + +# Проанализированные материалы + +Перед реализацией были полностью проанализированы: + +```text +app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json +docs/market_intelligence/information/dzengi_openapi.json +``` + +Также были повторно использованы результаты анализа: + +```text +app/src/integrations/exchange/service.py +app/src/integrations/exchange/models.py +``` + +и результаты grep по использованию ExchangeSymbol. + +--- + +# Основные выводы анализа + +Подтверждено: + +реальный ответ API значительно богаче текущей модели ExchangeSymbol. + +В ответе присутствуют: + +- symbol +- name +- status +- assetType +- baseAsset +- baseAssetPrecision +- quoteAsset +- quoteAssetId +- quotePrecision +- orderTypes +- marketModes +- marketType +- country +- sector +- industry +- tradingHours +- tickSize +- tickValue +- tradingFee +- exchangeFee +- longRate +- shortRate +- swapChargeInterval +- minSLGap +- maxSLGap +- minTPGap +- maxTPGap +- filters +- rateLimits +- exchangeFilters + +Часть этих данных ранее полностью терялась. + +--- + +# Принятые архитектурные решения + +## 1. Raw Models полностью отделены от предметной модели + +Созданы транспортные модели. + +Они: + +- ничего не вычисляют; +- ничего не нормализуют; +- ничего не валидируют. + +Они только описывают транспортный контракт. + +--- + +## 2. Поддержка двух форматов ответа API + +Поддерживаются оба варианта: + +### Wrapped + +```text +status +correlationId +payload +``` + +и + +### Unwrapped + +```text +timezone +serverTime +symbols +``` + +Parser следующего Build сможет привести оба формата к единому контракту. + +Классификация: + +**Улучшение надёжности.** + +--- + +## 3. Decimal сознательно не используется + +Raw Models сохраняют транспортный тип. + +Например: + +```json +"stepSize": "0.001" +``` + +остаётся строкой. + +Преобразование в Decimal является обязанностью mapper. + +--- + +## 4. Filters представлены отдельной иерархией + +Созданы: + +```text +DzengiLotSizeFilter +DzengiMinNotionalFilter +DzengiUnknownFilter +``` + +Неизвестные фильтры не приводят к ошибке. + +Они сохраняются для дальнейшего анализа. + +Классификация: + +**Улучшение надёжности.** + +--- + +## 5. Все коллекции являются immutable + +Используются: + +```python +tuple +``` + +вместо + +```python +list +``` + +Причина: + +Raw Models являются снимком транспортного ответа. + +Их нельзя изменять после создания. + +--- + +## 6. Все Raw Models являются immutable + +Все dataclass объявлены как: + +```python +@dataclass(frozen=True, slots=True) +``` + +Это гарантирует неизменяемость транспортного контракта. + +--- + +# Созданные файлы + +Создан: + +```text +app/src/market_data/acquisition/adapters/dzengi/models.py +``` + +Создан: + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_models.py +``` + +Другие production-файлы не изменялись. + +--- + +# Проверки + +Выполнены все проверки Build. + +--- + +## Unit Test + +Статус: + +✅ Passed + +``` +6 passed +``` + +--- + +## Импорт полной транспортной модели + +Проверено: + +- импортируются все классы; +- создаётся полная иерархия объектов; +- вложенные dataclass работают корректно. + +Статус: + +✅ Passed + +--- + +## Компиляция + +Проверено: + +```text +python -m py_compile +``` + +Статус: + +✅ Passed + +--- + +## Проверка отсутствия использования в production + +Подтверждено: + +Raw Models используются только: + +```text +src/market_data/acquisition/adapters/dzengi/models.py + +tests/unit/market_data/acquisition/adapters/dzengi/test_models.py +``` + +ExchangeService их не использует. + +Parser их ещё не использует. + +Mapper их ещё не использует. + +Статус: + +✅ Passed + +--- + +## Полный набор тестов + +Выполнено: + +```text +python -m pytest -q +``` + +Результат: + +```text +11 passed +``` + +Статус: + +✅ Passed + +--- + +# Архитектурный результат + +После Build 002 структура подсистемы стала выглядеть следующим образом: + +```text +Dzengi REST API + │ + ▼ +Raw Models +``` + +Следующие слои пока отсутствуют: + +```text +Parser +Mapper +Instrument +Handler +Feed +Service +``` + +Именно это соответствует утверждённому плану миграции. + +--- + +# Обратная совместимость + +Полностью сохранена. + +Не изменены: + +- ExchangeService +- ExchangeSymbol +- SymbolValidationResult +- validate_symbol() +- normalize_symbol() +- symbol_candidates() +- Runtime +- Telegram UI + +Новые Raw Models пока не подключены к работающему боту. + +--- + +# Итог Build + +Build 002 завершён успешно. + +Получен полноценный транспортный контракт REST API Dzengi. + +Создан фундамент для следующих этапов миграции. + +Работающий бот не изменил своего поведения. + +--- + +# Следующий Build + +## Build 003 — Структурная валидация exchangeInfo + +Следующий этап реализует parser, который будет преобразовывать сырой JSON REST API в созданные транспортные модели. + +На Build 003 по-прежнему: + +- ExchangeService изменяться не будет; +- предметная модель Instrument использоваться не будет; +- parser останется полностью независимым от бизнес-логики. \ No newline at end of file diff --git a/docs/migrations/build_003.md b/docs/migrations/build_003.md new file mode 100644 index 0000000..20937d9 --- /dev/null +++ b/docs/migrations/build_003.md @@ -0,0 +1,850 @@ +# Dzentra — Instrument Reference Data Migration + +## Build 003 — структурная валидация `exchangeInfo` + +**Статус:** Завершён +**Подсистема:** Market Data Acquisition +**Область:** Instrument Reference Data +**Проект:** Dzentra +**Язык реализации:** Python 3.12 + +--- + +## 1. Цель Build 003 + +Цель Build 003 — создать отдельный слой структурной валидации сырого JSON-документа `exchangeInfo` до его преобразования в raw-модели Dzengi. + +После завершения Build 003 формируется следующий архитектурный поток: + +```text +JSON response + ↓ +Schema validation + ↓ +Parser + ↓ +Dzengi Raw Models +``` + +В рамках этого Build реализована только проверка формы и структуры входных данных. + +Schema validation не выполняет: + +- преобразование JSON в raw-модели Dzengi; +- преобразование camelCase в snake_case; +- преобразование строковых чисел в `Decimal`, `float` или `int`; +- нормализацию символов; +- проверку допустимости числовых значений; +- проверку торговой семантики статусов; +- создание внутренней модели `Instrument`; +- фильтрацию или отбрасывание инструментов. + +--- + +## 2. Почему Build 003 выполняется перед parser + +После Build 002 уже существуют типизированные raw-модели ответа Dzengi. + +Следующий этап — parser, однако parser не должен одновременно: + +- определять wrapped или unwrapped формат ответа; +- проверять тип корневого объекта; +- проверять наличие `symbols`; +- проверять тип `symbols`; +- проверять структуру элементов `symbols`; +- проверять структуру вложенных коллекций; +- создавать raw-модели. + +Поэтому перед parser был выделен отдельный слой: + +```text +validation/schema.py +``` + +Разделение ответственности теперь выглядит следующим образом: + +```text +schema.py + Проверяет форму и структуру данных. + +parser.py + Преобразует структурно корректные данные в raw-модели Dzengi. + +values.py + Проверяет допустимость предметных значений. + +mapper.py + Преобразует raw-модели Dzengi во внутреннюю модель Instrument. +``` + +Это является обязательным архитектурным изменением для утверждённой структуры Dzentra. + +--- + +## 3. Изменённые и созданные файлы + +В рамках Build 003 были изменены или созданы только следующие файлы: + +```text +app/src/market_data/acquisition/exceptions.py +app/src/market_data/acquisition/validation/schema.py +app/tests/unit/market_data/acquisition/validation/test_schema.py +``` + +Другие файлы проекта не изменялись. + +--- + +## 4. Реализованные исключения + +В файле: + +```text +app/src/market_data/acquisition/exceptions.py +``` + +добавлены: + +```python +class MarketDataAcquisitionError(Exception): + pass +``` + +и: + +```python +class InstrumentReferenceSchemaError(MarketDataAcquisitionError): + pass +``` + +Иерархия ошибок: + +```text +Exception + ↓ +MarketDataAcquisitionError + ↓ +InstrumentReferenceSchemaError +``` + +`InstrumentReferenceSchemaError` используется для явного обозначения ошибки структуры документа Instrument Reference Data. + +Это позволяет в последующих Build отличать структурную ошибку от: + +- сетевой ошибки; +- ошибки HTTP; +- ошибки JSON-декодирования; +- ошибки значений; +- ошибки parser; +- ошибки mapper. + +Исключение создано не «на будущее»: оно непосредственно используется schema validation в Build 003. + +--- + +## 5. Реализованный контракт schema validation + +В файле: + +```text +app/src/market_data/acquisition/validation/schema.py +``` + +создана модель: + +```python +@dataclass(frozen=True, slots=True) +class ValidatedExchangeInfoDocument: + payload: Mapping[str, object] + is_wrapped: bool + status: object | None + correlation_id: object | None +``` + +Она представляет структурно проверенный документ `exchangeInfo`. + +Модель содержит: + +| Поле | Назначение | +|---|---| +| `payload` | Проверенный payload с данными `exchangeInfo` | +| `is_wrapped` | Признак wrapped/unwrapped формата | +| `status` | Верхнеуровневый статус wrapped-ответа | +| `correlation_id` | Верхнеуровневый `correlationId` wrapped-ответа | + +Основная функция: + +```python +validate_exchange_info_schema(document: object) -> ValidatedExchangeInfoDocument +``` + +принимает произвольный объект и либо: + +- возвращает `ValidatedExchangeInfoDocument`; +- либо выбрасывает `InstrumentReferenceSchemaError`. + +--- + +## 6. Поддерживаемые форматы ответа + +### 6.1. Unwrapped-формат + +Поддерживается непосредственный payload: + +```json +{ + "timezone": "UTC", + "serverTime": 1783537921471, + "symbols": [] +} +``` + +Результат: + +```text +is_wrapped = False +status = None +correlation_id = None +``` + +--- + +### 6.2. Wrapped-формат + +Поддерживается ответ с вложенным `payload`: + +```json +{ + "status": "OK", + "correlationId": "2", + "payload": { + "timezone": "UTC", + "serverTime": 1783537921471, + "symbols": [] + } +} +``` + +Результат: + +```text +is_wrapped = True +status = "OK" +correlation_id = "2" +``` + +--- + +## 7. Что проверяет schema validation + +Build 003 проверяет следующие структурные свойства документа. + +### 7.1. Корень документа + +Корень должен быть JSON-объектом. + +Отклоняются: + +```json +[] +``` + +```json +null +``` + +```json +"invalid" +``` + +```json +123 +``` + +--- + +### 7.2. Wrapped payload + +Если присутствует ключ: + +```text +payload +``` + +его значение должно быть JSON-объектом. + +Например, отклоняется: + +```json +{ + "status": "OK", + "payload": [] +} +``` + +--- + +### 7.3. Наличие `symbols` + +Payload должен содержать: + +```text +symbols +``` + +Отсутствие `symbols` считается ошибкой структуры. + +--- + +### 7.4. Тип `symbols` + +`symbols` должен быть JSON-массивом. + +Отклоняется: + +```json +{ + "symbols": {} +} +``` + +--- + +### 7.5. Элементы `symbols` + +Каждый элемент `symbols` должен быть JSON-объектом. + +Отклоняется: + +```json +{ + "symbols": [ + "BTC/USD" + ] +} +``` + +--- + +### 7.6. `filters` + +Если у инструмента присутствует: + +```text +filters +``` + +то: + +- `filters` должен быть JSON-массивом; +- каждый элемент `filters` должен быть JSON-объектом. + +Отклоняется: + +```json +{ + "symbols": [ + { + "symbol": "BTC/USD", + "filters": {} + } + ] +} +``` + +Также отклоняется: + +```json +{ + "symbols": [ + { + "symbol": "BTC/USD", + "filters": [ + "LOT_SIZE" + ] + } + ] +} +``` + +--- + +### 7.7. `marketModes` + +Если присутствует: + +```text +marketModes +``` + +то: + +- значение должно быть JSON-массивом; +- каждый элемент должен быть строкой. + +--- + +### 7.8. `orderTypes` + +Если присутствует: + +```text +orderTypes +``` + +то: + +- значение должно быть JSON-массивом; +- каждый элемент должен быть строкой. + +--- + +### 7.9. `rateLimits` + +Если присутствует: + +```text +rateLimits +``` + +то: + +- значение должно быть JSON-массивом; +- каждый элемент должен быть JSON-объектом. + +--- + +### 7.10. `exchangeFilters` + +Если присутствует: + +```text +exchangeFilters +``` + +то: + +- значение должно быть JSON-массивом; +- каждый элемент должен быть JSON-объектом. + +--- + +## 8. Что намеренно не проверяется в Build 003 + +Build 003 не проверяет допустимость значений. + +Следующие примеры не относятся к schema validation: + +```json +{ + "tickSize": -1 +} +``` + +```json +{ + "baseAssetPrecision": -5 +} +``` + +```json +{ + "status": "" +} +``` + +```json +{ + "minQty": "not-a-number" +} +``` + +Такие проверки относятся к: + +```text +app/src/market_data/acquisition/validation/values.py +``` + +и должны реализовываться отдельно, без смешения структурной и предметной валидации. + +--- + +## 9. Защита проверенного payload + +Поле: + +```python +payload: Mapping[str, object] +``` + +возвращается через: + +```python +MappingProxyType +``` + +Это предотвращает случайное изменение корневого payload последующим parser. + +Таким образом, schema validation передаёт следующему слою проверенное read-only представление данных. + +--- + +## 10. Диагностические пути ошибок + +Schema validation формирует ошибки с указанием пути до проблемного значения. + +Примеры: + +```text +$ должен быть JSON-объектом, получен list. +``` + +```text +$.payload.symbols должен быть JSON-массивом, получен dict. +``` + +```text +$.payload.symbols[0] должен быть JSON-объектом, получен str. +``` + +```text +$.payload.symbols[0].filters должен быть JSON-массивом, получен dict. +``` + +На текущем этапе для `symbols` используется унифицированный диагностический путь: + +```text +$.payload.symbols +``` + +как для wrapped-, так и для unwrapped-формата. + +Это сознательное упрощение текущего контракта и не влияет на production-поведение. + +--- + +## 11. Реализованные тесты + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/validation/test_schema.py +``` + +Он проверяет: + +- корректный unwrapped-документ; +- корректный wrapped-документ; +- отклонение не-объекта в корне; +- отклонение некорректного wrapped payload; +- отсутствие `symbols`; +- некорректный тип `symbols`; +- некорректный элемент `symbols`; +- некорректный тип `filters`; +- некорректный элемент `filters`; +- некорректный тип `marketModes`; +- некорректный тип `orderTypes`; +- нестроковый элемент `marketModes`; +- нестроковый элемент `orderTypes`; +- некорректный тип `rateLimits`; +- некорректный элемент `rateLimits`; +- некорректный тип `exchangeFilters`; +- некорректный элемент `exchangeFilters`. + +Итог: + +```text +20 passed +``` + +--- + +## 12. Выполненные проверки + +### Проверка 1 — unit-тесты Build 003 + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/validation/test_schema.py \ + -q +``` + +Результат: + +```text +.................... [100%] +20 passed in 0.01s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — компиляция файлов Build 003 + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/exceptions.py \ + src/market_data/acquisition/validation/schema.py \ + tests/unit/market_data/acquisition/validation/test_schema.py +``` + +Результат: + +```text +Ошибок нет. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — ручная проверка wrapped/unwrapped контрактов + +Получен результат: + +```text +Unwrapped: + is_wrapped: False + status: None + correlation_id: None + symbols: [] + +Wrapped: + is_wrapped: True + status: OK + correlation_id: 2 + symbols: [] +``` + +Подтверждено: + +- unwrapped-формат определяется корректно; +- wrapped-формат определяется корректно; +- `status` сохраняется; +- `correlationId` сохраняется как `correlation_id`; +- `symbols` доступны через проверенный payload. + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — типизированные ошибки + +Проверены четыре некорректных документа. + +Получен результат: + +```text +1: InstrumentReferenceSchemaError: $ должен быть JSON-объектом, получен list. +2: InstrumentReferenceSchemaError: $.payload.symbols должен быть JSON-массивом, получен dict. +3: InstrumentReferenceSchemaError: $.payload.symbols[0] должен быть JSON-объектом, получен str. +4: InstrumentReferenceSchemaError: $.payload.symbols[0].filters должен быть JSON-массивом, получен dict. +``` + +Ни один ошибочный документ не был принят. + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 5 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +............................... [100%] +31 passed in 0.04s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 6 — отсутствие подключения к production-коду + +Выполнен поиск: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "validate_exchange_info_schema|ValidatedExchangeInfoDocument|InstrumentReferenceSchemaError" \ + src tests +``` + +Подтверждено, что новые сущности Build 003 используются только в: + +```text +src/market_data/acquisition/exceptions.py +src/market_data/acquisition/validation/schema.py +tests/unit/market_data/acquisition/validation/test_schema.py +``` + +Они пока не подключены к: + +```text +ExchangeService +legacy exchangeInfo +Telegram UI +runtime +автоторговле +market stream +production parser +production mapper +``` + +Статус: + +```text +PASSED +``` + +--- + +## 13. Обратная совместимость + +Build 003 не изменяет: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +ExchangeService.get_symbol_market_status() +``` + +Не изменены: + +```text +ExchangeSymbol +SymbolValidationResult +normalize_symbol() +symbol_candidates() +``` + +Не изменено поведение: + +- Telegram UI; +- автоторговли; +- runtime-проверок символа; +- legacy-кэша инструментов; +- получения цен; +- market stream; +- существующего `exchangeInfo`. + +Новый schema validation пока изолирован от production-кода. + +--- + +## 14. Точки обратной совместимости + +После Build 003 продолжают действовать прежние точки совместимости: + +```text +ExchangeService.get_exchange_symbols() + → list[ExchangeSymbol] + +ExchangeService.validate_symbol() + → SymbolValidationResult + +ExchangeService.get_symbol_runtime_status() + → ExchangeRuntimeStatus + +ExchangeService.get_symbol_market_status() + → dict[str, object] +``` + +Ни одна из этих сигнатур не изменена. + +--- + +## 15. Классификация изменений + +| Изменение | Классификация | +|---|---| +| Создание schema validation | Обязательное архитектурное изменение | +| Создание `MarketDataAcquisitionError` | Обязательное архитектурное изменение | +| Создание `InstrumentReferenceSchemaError` | Обязательное архитектурное изменение | +| Поддержка wrapped/unwrapped форматов | Улучшение надёжности | +| Явные структурные ошибки | Улучшение надёжности | +| Read-only представление проверенного payload | Улучшение надёжности | +| Изменение production-поведения | Отсутствует | +| Изменение публичных legacy-интерфейсов | Отсутствует | + +--- + +## 16. Условие завершения Build 003 + +Build 003 считается завершённым, потому что выполнены все условия: + +- создан отдельный schema validation layer; +- поддержан wrapped-формат; +- поддержан unwrapped-формат; +- проверяется обязательная структура `symbols`; +- проверяются вложенные коллекции; +- ошибки типизированы; +- создан read-only контракт для передачи данных parser; +- написаны unit-тесты; +- все тесты Build проходят; +- полный набор тестов проекта проходит; +- production-код не изменён; +- обратная совместимость сохранена. + +--- + +## 17. Итоговый статус + +```text +Build 003 — COMPLETED +``` + +Итоговый набор тестов проекта: + +```text +31 passed in 0.04s +``` + +Следующий этап: + +```text +Build 004 — Parser exchangeInfo +``` + +На Build 004 новый parser должен: + +1. принимать структурно проверенный `ValidatedExchangeInfoDocument`; +2. преобразовывать payload в raw-модели Dzengi, созданные в Build 002; +3. не выполнять повторную schema validation; +4. не создавать внутреннюю модель `Instrument`; +5. не подключаться к `ExchangeService`; +6. не менять production-поведение работающего бота. diff --git a/docs/migrations/build_004.md b/docs/migrations/build_004.md new file mode 100644 index 0000000..26f075c --- /dev/null +++ b/docs/migrations/build_004.md @@ -0,0 +1,775 @@ +# Dzentra — Instrument Reference Data Migration + +## Build 004 — Dzengi exchangeInfo Parser + +**Статус:** Завершён +**Подсистема:** Market Data Acquisition +**Компонент:** Dzengi Adapter / exchangeInfo Parser +**Проект:** Dzentra +**Язык:** Русский +**Python:** 3.12 + +--- + +## 1. Цель Build 004 + +Цель Build 004 — реализовать parser для ответа Dzengi `exchangeInfo`, который преобразует уже структурно проверенный документ из Build 003 в типизированные транспортные модели Dzengi, созданные в Build 002. + +Целевая цепочка после завершения Build 004: + +```text +Raw JSON document + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse +``` + +Build 004 не подключает новую реализацию к существующему `ExchangeService` и не изменяет поведение работающего бота. + +--- + +## 2. Почему Build 004 выполняется именно сейчас + +До начала Build 004 уже были завершены необходимые предыдущие этапы. + +### Build 001 — внутренняя модель Instrument + +Создана внутренняя типизированная модель: + +```text +Instrument +``` + +Она представляет инструмент внутри новой архитектуры Dzentra и не зависит от формата конкретной биржи. + +### Build 002 — транспортные модели Dzengi + +Созданы типизированные модели сырого ответа `exchangeInfo`: + +```text +DzengiExchangeInfoResponse +DzengiExchangeInfoPayload +DzengiExchangeInfoSymbol +DzengiRateLimit +DzengiInstrumentFilter +DzengiLotSizeFilter +DzengiMinNotionalFilter +DzengiUnknownFilter +``` + +### Build 003 — структурная валидация exchangeInfo + +Создан слой проверки структуры сырого JSON-документа: + +```text +validate_exchange_info_schema() +``` + +Его результат: + +```text +ValidatedExchangeInfoDocument +``` + +Таким образом, только после Build 001–003 стало безопасно реализовать parser, не смешивая: + +- проверку структуры JSON; +- разбор транспортного ответа; +- предметное преобразование в `Instrument`; +- сетевой REST-доступ; +- кэширование; +- legacy-совместимость. + +--- + +## 3. Реализованная архитектурная цепочка + +На текущем этапе действует следующая архитектура: + +```text +Raw Dzengi JSON + ↓ +Schema Validation + ↓ +ValidatedExchangeInfoDocument + ↓ +Dzengi Parser + ↓ +DzengiExchangeInfoResponse +``` + +Полная целевая цепочка миграции пока ещё не завершена: + +```text +Dzengi REST API + ↓ +Dzengi REST Adapter + ↓ +Schema Validation + ↓ +Parser + ↓ +Dzengi Raw Models + ↓ +Mapper + ↓ +Instrument + ↓ +Instrument Handler + ↓ +Instrument Feed + ↓ +Market Data Acquisition Service + ↓ +Compatibility Layer + ↓ +ExchangeService + ↓ +Existing Consumers +``` + +Build 004 реализует только участок: + +```text +ValidatedExchangeInfoDocument + ↓ +Parser + ↓ +Dzengi Raw Models +``` + +--- + +## 4. Файлы Build 004 + +### Реализован + +```text +app/src/market_data/acquisition/adapters/dzengi/parser.py +``` + +### Использованы существующие файлы + +```text +app/src/market_data/acquisition/adapters/dzengi/models.py +app/src/market_data/acquisition/validation/schema.py +app/src/market_data/acquisition/exceptions.py +``` + +### Добавлены тесты + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py +``` + +--- + +## 5. Публичный контракт parser + +Основной публичный вход Build 004: + +```python +def parse_exchange_info( + document: ValidatedExchangeInfoDocument, +) -> DzengiExchangeInfoResponse: + ... +``` + +Parser намеренно не принимает произвольный сырой `dict`. + +Корректная последовательность вызовов: + +```python +validated = validate_exchange_info_schema(raw_document) +response = parse_exchange_info(validated) +``` + +Это обеспечивает явное разделение ответственности между Build 003 и Build 004. + +--- + +## 6. Разделение ответственности + +### Build 003 — Schema Validation + +Отвечает за структурную корректность документа: + +- корневой объект должен быть JSON-объектом; +- `payload`, если используется wrapped-формат, должен быть объектом; +- `symbols` должен быть массивом; +- каждый элемент `symbols` должен быть объектом; +- `filters` должен быть массивом; +- другие структурные ограничения проверяются до parser. + +Build 003 не создаёт транспортные модели Dzengi. + +### Build 004 — Parser + +Отвечает за преобразование структурно проверенного документа в: + +```text +DzengiExchangeInfoResponse +``` + +и вложенные типизированные транспортные модели. + +Parser не отвечает за: + +- HTTP-запросы; +- кэширование; +- предметную модель `Instrument`; +- нормализацию символа для бизнес-логики; +- runtime-статус инструмента; +- UI; +- legacy-совместимость. + +--- + +## 7. Поддерживаемые форматы exchangeInfo + +Архитектура поддерживает два формата ответа. + +### Unwrapped + +```json +{ + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [] +} +``` + +После Build 003: + +```text +is_wrapped: False +status: None +correlation_id: None +``` + +После Build 004 документ преобразуется в: + +```text +DzengiExchangeInfoResponse + └── payload: DzengiExchangeInfoPayload +``` + +### Wrapped + +```json +{ + "status": "OK", + "correlationId": "2", + "payload": { + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [] + } +} +``` + +После Build 003: + +```text +is_wrapped: True +status: OK +correlation_id: 2 +``` + +После Build 004 метаданные оболочки сохраняются в: + +```text +DzengiExchangeInfoResponse.status +DzengiExchangeInfoResponse.correlation_id +``` + +--- + +## 8. Разбор инструмента + +Каждый элемент массива `symbols` преобразуется в: + +```text +DzengiExchangeInfoSymbol +``` + +Поддерживаются следующие поля: + +```text +symbol +name +status + +asset_type + +base_asset +base_asset_precision + +quote_asset +quote_asset_id +quote_precision + +order_types +filters + +market_modes +market_type + +country +sector +industry +trading_hours + +tick_size +tick_value + +trading_fee +exchange_fee + +long_rate +short_rate +swap_charge_interval + +min_sl_gap +max_sl_gap +min_tp_gap +max_tp_gap +``` + +На этом этапе поля сохраняют транспортную семантику Dzengi и ещё не преобразуются в предметную модель `Instrument`. + +--- + +## 9. Разбор filters + +Parser преобразует известные типы фильтров в отдельные типизированные модели. + +### LOT_SIZE + +Преобразуется в: + +```text +DzengiLotSizeFilter +``` + +Поля: + +```text +filter_type +min_qty +max_qty +step_size +``` + +Пример: + +```text +DzengiLotSizeFilter( + filter_type='LOT_SIZE', + min_qty='0.0001', + max_qty='1000', + step_size='0.0001', +) +``` + +### MIN_NOTIONAL + +Преобразуется в: + +```text +DzengiMinNotionalFilter +``` + +Поле: + +```text +min_notional +``` + +### Неизвестные фильтры + +Неизвестный `filterType` не отбрасывается автоматически. + +Он преобразуется в: + +```text +DzengiUnknownFilter +``` + +Это позволяет сохранить неизвестные скалярные поля транспортного ответа без добавления неподтверждённой предметной семантики. + +--- + +## 10. Числовые значения + +Build 004 сохраняет важное разделение между транспортным и предметным слоями. + +Например, значения фильтра: + +```json +{ + "minQty": "0.0001", + "maxQty": "1000", + "stepSize": "0.0001" +} +``` + +в транспортной модели остаются: + +```text +min_qty='0.0001' +max_qty='1000' +step_size='0.0001' +``` + +Parser не выполняет преждевременное преобразование этих значений в `float`. + +Преобразование в точный предметный числовой тип должно выполняться на следующем архитектурном этапе при построении `Instrument`. + +Это позволяет избежать потери точности и сохраняет исходную семантику ответа Dzengi. + +--- + +## 11. Обработка ошибок + +Для ошибок parser используется отдельное исключение: + +```text +InstrumentReferenceParseError +``` + +Оно объявлено в: + +```text +app/src/market_data/acquisition/exceptions.py +``` + +Иерархия: + +```text +MarketDataAcquisitionError + └── InstrumentReferenceParseError +``` + +`InstrumentReferenceParseError` используется только: + +- в `parser.py`; +- в unit-тестах parser. + +На момент завершения Build 004 это исключение не используется: + +- `ExchangeService`; +- Telegram UI; +- runtime-кодом; +- автоторговлей; +- существующими production-потребителями. + +--- + +## 12. Классификация изменений + +| Изменение | Классификация | Влияние на поведение | +|---|---|---| +| Реализация `exchangeInfo` parser | Обязательное архитектурное изменение | Нет | +| Типизированное преобразование symbols | Обязательное архитектурное изменение | Нет | +| Типизированный разбор известных filters | Обязательное архитектурное изменение | Нет | +| Сохранение неизвестных filters | Улучшение надёжности | Нет | +| Отдельное `InstrumentReferenceParseError` | Улучшение надёжности | Нет | +| Сохранение числовых строк без преобразования в `float` | Улучшение надёжности | Нет | +| Поддержка wrapped и unwrapped форматов | Улучшение надёжности | Нет | + +Изменений поведения работающего бота в Build 004 нет. + +--- + +## 13. Обратная совместимость + +Build 004 не изменяет существующие публичные интерфейсы. + +Без изменений продолжают работать: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +ExchangeService.get_symbol_market_status() +``` + +Также без изменений остаются: + +```text +ExchangeSymbol +SymbolValidationResult +normalize_symbol() +symbol_candidates() +``` + +Новая реализация parser пока не подключена к существующему `ExchangeService`. + +Следовательно: + +- Telegram UI не изменён; +- автоторговля не изменена; +- runtime-проверки символа не изменены; +- существующий кэш инструментов не изменён; +- старые импорты продолжают работать; +- production-поведение бота сохранено. + +--- + +## 14. Выполненные проверки + +### Проверка 1 — unit-тесты parser + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py \ + -q +``` + +Результат: + +```text +19 passed in 0.02s +``` + +Статус: + +```text +PASS +``` + +--- + +### Проверка 2 — синтаксическая компиляция + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/exceptions.py \ + src/market_data/acquisition/adapters/dzengi/parser.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py +``` + +Результат: + +```text +Команда завершена без ошибок. +``` + +Статус: + +```text +PASS +``` + +--- + +### Проверка 3 — ручная цепочка schema → parser + +Была проверена цепочка: + +```text +raw dict + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse +``` + +Полученный результат: + +```text +Symbol: BTC/USD_LEVERAGE +Status: TRADING +Asset type: CRYPTOCURRENCY +Base asset: BTC +Quote asset: USD +Tick size: 0.05 +Filters: (DzengiLotSizeFilter(filter_type='LOT_SIZE', min_qty='0.0001', max_qty='1000', step_size='0.0001'),) +``` + +Статус: + +```text +PASS +``` + +--- + +### Проверка 4 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +50 passed in 0.05s +``` + +Статус: + +```text +PASS +``` + +--- + +### Проверка 5 — контроль зависимостей parser + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "parse_exchange_info|_parse_exchange_info|DzengiExchangeInfoParseError" \ + src tests +``` + +Подтверждено: + +- `parse_exchange_info()` определён в новом `parser.py`; +- используется только unit-тестами нового parser; +- не используется существующим `ExchangeService`; +- не используется UI; +- не используется runtime; +- не используется автоторговлей. + +Статус: + +```text +PASS +``` + +--- + +### Проверка 6 — контроль использования parse-ошибки + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "InstrumentReferenceParseError" \ + src tests +``` + +Подтверждено: + +- исключение объявлено в `exceptions.py`; +- используется parser; +- используется unit-тестами parser; +- не подключено к production-потребителям. + +Статус: + +```text +PASS +``` + +--- + +## 15. Итог Build 004 + +Build 004 успешно завершён. + +Реализован типизированный parser: + +```text +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse +``` + +Подтверждено: + +```text +Build 001 — Instrument domain model PASS +Build 002 — Dzengi raw transport models PASS +Build 003 — exchangeInfo schema validation PASS +Build 004 — Dzengi exchangeInfo parser PASS +``` + +Общий результат тестов после завершения Build 004: + +```text +50 passed in 0.05s +``` + +Работающий бот не затронут. + +--- + +## 16. Условие завершения Build 004 + +Build 004 считается завершённым, поскольку выполнены все условия: + +- parser реализован; +- parser принимает только `ValidatedExchangeInfoDocument`; +- parser возвращает `DzengiExchangeInfoResponse`; +- основные поля инструмента сохраняются; +- известные filters типизированы; +- неизвестные filters сохраняются; +- ошибки parser имеют отдельный тип; +- unit-тесты проходят; +- полный набор тестов проходит; +- production-потребители не изменены; +- обратная совместимость сохранена. + +--- + +## 17. Следующий этап + +Следующий этап миграции: + +```text +Build 005 — Проверка значений Instrument Reference Data +``` + +Его задача — преобразовать: + +```text +DzengiExchangeInfoSymbol + ↓ +Mapper + ↓ +Instrument +``` + +При этом Build 005 должен: + +- использовать модель `Instrument`, созданную в Build 001; +- использовать транспортную модель `DzengiExchangeInfoSymbol` из Build 002; +- не выполнять HTTP-запросы; +- не заниматься кэшированием; +- не подключаться к `ExchangeService`; +- не менять production-поведение бота; +- явно определить правила преобразования числовых значений в `Decimal`; +- явно определить соответствие полей Dzengi полям внутренней модели `Instrument`; +- отдельно классифицировать любые потенциальные изменения поведения. + +До написания кода Build 005 необходимо сначала проанализировать существующие модели и контракты, относящиеся к преобразованию `DzengiExchangeInfoSymbol` в `Instrument`. \ No newline at end of file diff --git a/docs/migrations/build_005.md b/docs/migrations/build_005.md new file mode 100644 index 0000000..5242e41 --- /dev/null +++ b/docs/migrations/build_005.md @@ -0,0 +1,1067 @@ +# Build 005 — Value Validation для Instrument Reference Data + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime +**Результат полного набора тестов:** `84 passed` + +--- + +## 1. Цель Build 005 + +Цель Build 005 — реализовать отдельный слой проверки допустимости значений, полученных после: + +1. проверки структуры исходного документа `exchangeInfo`; +2. преобразования проверенного документа в типизированные raw-модели адаптера Dzengi. + +Build 005 добавляет третий этап обработки Instrument Reference Data: + +```text +raw JSON + ↓ +schema validation + ↓ +parser + ↓ +value validation +``` + +Value validation проверяет не структуру JSON и не типы полей исходного документа, а предметную допустимость уже распарсенных значений. + +Например: + +```text +tickSize = 0 +``` + +может быть корректным числовым значением с точки зрения JSON и parser layer, но недопустимым значением для шага цены торгового инструмента. + +--- + +## 2. Почему Build 005 реализован именно на этом этапе + +До Build 005 были завершены следующие этапы миграции: + +```text +Build 001 + ↓ +Каноническая source-independent модель Instrument + +Build 002 + ↓ +Raw transport models адаптера Dzengi + +Build 003 + ↓ +Schema validation исходного exchangeInfo + +Build 004 + ↓ +Parser: validated document → Dzengi raw models + +Build 005 + ↓ +Value validation Dzengi raw models +``` + +Такой порядок позволяет строго разделить ответственность слоёв. + +### Schema validation отвечает за: + +```text +Есть ли ожидаемые JSON-объекты? +Есть ли массив symbols? +Являются ли элементы symbols объектами? +Является ли filters массивом? +``` + +### Parser отвечает за: + +```text +Как преобразовать проверенный JSON-документ +в типизированные raw-модели Dzengi? +``` + +### Value validation отвечает за: + +```text +Допустимы ли конкретные значения полей +с точки зрения контракта Instrument Reference Data? +``` + +Это исключает смешивание: + +- проверки JSON-структуры; +- транспортного parsing; +- проверки предметных значений; +- source-independent mapping. + +--- + +## 3. Изменённые файлы + +В рамках Build 005 изменены: + +```text +app/src/market_data/acquisition/exceptions.py +app/src/market_data/acquisition/validation/values.py +``` + +Добавлен тестовый файл: + +```text +app/tests/unit/market_data/acquisition/validation/test_values.py +``` + +--- + +## 4. Новая типизированная ошибка + +В файл: + +```text +app/src/market_data/acquisition/exceptions.py +``` + +добавлена ошибка: + +```python +class InstrumentReferenceValueError(MarketDataAcquisitionError): + pass +``` + +Иерархия ошибок Instrument Reference Data теперь выглядит так: + +```text +MarketDataAcquisitionError +├── InstrumentReferenceSchemaError +├── InstrumentReferenceParseError +└── InstrumentReferenceValueError +``` + +Каждый этап обработки имеет собственную категорию ошибки: + +| Этап | Ошибка | +|---|---| +| Schema validation | `InstrumentReferenceSchemaError` | +| Parsing | `InstrumentReferenceParseError` | +| Value validation | `InstrumentReferenceValueError` | + +Это позволяет будущему orchestration/service layer точно определять, на каком этапе обработки произошла ошибка. + +--- + +## 5. Главная функция Build 005 + +В файл: + +```text +app/src/market_data/acquisition/validation/values.py +``` + +добавлена функция: + +```python +def validate_exchange_info_values( + response: DzengiExchangeInfoResponse, +) -> None: +``` + +Функция принимает: + +```python +DzengiExchangeInfoResponse +``` + +то есть результат работы parser layer из Build 004. + +При корректных значениях функция возвращает: + +```python +None +``` + +При обнаружении недопустимого значения выбрасывается: + +```python +InstrumentReferenceValueError +``` + +--- + +## 6. Проверяемые обязательные строки инструмента + +Для каждого `DzengiExchangeInfoSymbol` проверяются следующие обязательные строковые поля: + +```text +symbol +name +status +baseAsset +quoteAsset +marketType +``` + +Они не должны быть пустыми или состоять только из пробелов. + +Пример недопустимого значения: + +```python +symbol=" " +``` + +Результат: + +```text +InstrumentReferenceValueError: +$.payload.symbols[0].symbol не должен быть пустым. +``` + +--- + +## 7. Проверка `orderTypes` и `marketModes` + +Каждый элемент следующих последовательностей проверяется на непустое строковое значение: + +```text +orderTypes +marketModes +``` + +Например: + +```python +order_types=("LIMIT", " ") +``` + +отклоняется с ошибкой: + +```text +$.payload.symbols[0].orderTypes[1] не должен быть пустым. +``` + +Аналогично: + +```python +market_modes=("REGULAR", "") +``` + +отклоняется как недопустимое значение. + +--- + +## 8. Проверка integer-полей + +Следующие optional integer-поля не могут быть отрицательными: + +```text +baseAssetPrecision +quotePrecision +swapChargeInterval +``` + +Допустимо: + +```text +None +0 +1 +2 +... +``` + +Недопустимо: + +```text +-1 +``` + +При нарушении выбрасывается: + +```text +InstrumentReferenceValueError +``` + +--- + +## 9. Проверка `tickSize` + +Для: + +```text +tickSize +``` + +применяется правило: + +```text +tickSize > 0 +``` + +Допустимы только положительные конечные числа. + +Отклоняются: + +```text +0 +отрицательные значения +NaN ++Infinity +-Infinity +``` + +Это правило подтверждено анализом реального sample Dzengi: + +```text +tickSize: +present=51 +min=1E-8 +max=1 +zero=0 +negative=0 +invalid=[] +``` + +--- + +## 10. Проверка конечности числовых значений + +Для optional numeric-полей проверяется, что значение является конечным числом. + +Проверяются: + +```text +tickValue +tradingFee +exchangeFee +longRate +shortRate +minSLGap +maxSLGap +minTPGap +maxTPGap +``` + +Отклоняются: + +```text +NaN ++Infinity +-Infinity +``` + +При этом нулевые значения не запрещаются автоматически. + +--- + +## 11. Проверка LOT_SIZE + +Для: + +```text +DzengiLotSizeFilter +``` + +проверяются: + +```text +minQty +maxQty +stepSize +``` + +Если значение присутствует, оно должно: + +1. корректно преобразовываться в `Decimal`; +2. быть конечным; +3. быть строго больше нуля. + +Дополнительно проверяется отношение: + +```text +minQty <= maxQty +``` + +Если: + +```text +minQty > maxQty +``` + +выбрасывается: + +```text +InstrumentReferenceValueError +``` + +--- + +## 12. Проверка MIN_NOTIONAL + +Для: + +```text +DzengiMinNotionalFilter +``` + +проверяется: + +```text +minNotional >= 0 +``` + +Нулевое значение разрешено. + +Отрицательное значение отклоняется. + +--- + +## 13. Использование Decimal + +Строковые и числовые значения ограничений торгового инструмента преобразуются для проверки через: + +```python +Decimal(str(value)) +``` + +Это позволяет избежать ненужной потери точности при обработке таких значений, как: + +```text +0.0001 +0.00000001 +6.9E-7 +``` + +Именно такой подход соответствует уже принятому контракту канонической модели `Instrument`, где торговые числовые ограничения представлены через `Decimal`. + +--- + +## 14. Особенность полей country, sector и industry + +В ходе первой проверки Build 005 была обнаружена ошибка первоначальной реализации. + +Изначально к полям: + +```text +country +sector +industry +``` + +применялось правило: + +```text +если значение присутствует, строка не должна быть пустой +``` + +Однако анализ реального ответа Dzengi и уже созданных raw-моделей показал, что Dzengi штатно возвращает: + +```python +country="" +sector="" +industry="" +``` + +Поэтому пустая строка для этих полей является допустимым транспортным состоянием. + +После исправления Build 005: + +```text +country="" +sector="" +industry="" +``` + +не считаются ошибкой. + +Это соответствует реальному контракту Dzengi и не выполняет преждевременную нормализацию transport-specific данных. + +--- + +## 15. Проверка отрицательных longRate и shortRate + +Отрицательные значения: + +```text +longRate +shortRate +``` + +являются допустимыми. + +Это подтверждено анализом реального sample Dzengi: + +```text +longRate: +present=51 +min=-0.0684932 +max=0.1389493 +negative=43 + +shortRate: +present=51 +min=-0.1608693 +max=0.01 +negative=38 +``` + +Поэтому value validation проверяет только: + +```text +значение является корректным конечным числом +``` + +но не требует: + +```text +value >= 0 +``` + +--- + +## 16. Проверка допустимости нулевых optional numeric values + +Нулевые значения разрешены для полей, для которых реальный контракт Dzengi допускает `0`. + +В частности: + +```text +tickValue +tradingFee +exchangeFee +minSLGap +maxSLGap +minTPGap +maxTPGap +``` + +Это соответствует анализу реального sample: + +```text +tickValue: +zero=1 + +tradingFee: +zero=40 + +minSLGap: +zero=51 + +minTPGap: +zero=51 +``` + +Value validation не вводит ограничений, которые не подтверждены фактическими данными или контрактом источника. + +--- + +## 17. Проверка неизвестных instrument filters + +Для неизвестного фильтра инструмента: + +```python +DzengiUnknownFilter +``` + +поле: + +```text +filterType +``` + +должно быть непустым. + +Это позволяет сохранить forward compatibility с новыми типами фильтров Dzengi, одновременно предотвращая попадание полностью неопределённых instrument filters в дальнейшую обработку. + +--- + +## 18. Особенность global exchange filters + +Для: + +```text +exchangeFilters +``` + +сохранено более мягкое поведение. + +Реальный transport contract может содержать global filter без meaningful `filterType`, поэтому пустое значение не отклоняется автоматически. + +Таким образом, Build 005 различает: + +```text +instrument-level filters +``` + +и: + +```text +global exchange filters +``` + +и не навязывает им одинаковые ограничения без подтверждения реального контракта. + +--- + +## 19. Результаты анализа реального sample Dzengi + +Перед реализацией Build 005 был выполнен анализ реального `exchangeInfo` sample. + +Получены следующие результаты. + +### Обязательные строки + +Пустые значения отсутствуют для: + +```text +symbol +name +status +baseAsset +quoteAsset +marketType +``` + +### Precision + +```text +baseAssetPrecision: +count=51 +min=2 +max=8 +invalid=[] + +quotePrecision: +count=51 +min=2 +max=8 +invalid=[] +``` + +### Числовые диапазоны + +```text +tickSize: +present=51 +min=1E-8 +max=1 +zero=0 +negative=0 +invalid=[] + +tickValue: +present=39 +min=0 +max=50665.5 +zero=1 +negative=0 +invalid=[] + +minQty: +present=51 +min=0.0001 +max=1 +zero=0 +negative=0 +invalid=[] + +maxQty: +present=51 +min=100 +max=10000000 +zero=0 +negative=0 +invalid=[] + +stepSize: +present=51 +min=0.0001 +max=1 +zero=0 +negative=0 +invalid=[] + +minNotional: +present=39 +min=6.9E-7 +max=507 +zero=0 +negative=0 +invalid=[] + +tradingFee: +present=51 +min=0 +max=0.075 +zero=40 +negative=0 +invalid=[] + +exchangeFee: +present=0 +min=None +max=None +zero=0 +negative=0 +invalid=[] + +longRate: +present=51 +min=-0.0684932 +max=0.1389493 +zero=0 +negative=43 +invalid=[] + +shortRate: +present=51 +min=-0.1608693 +max=0.01 +zero=0 +negative=38 +invalid=[] + +minSLGap: +present=51 +min=0 +max=0 +zero=51 +negative=0 +invalid=[] + +maxSLGap: +present=51 +min=20.0 +max=50.0 +zero=0 +negative=0 +invalid=[] + +minTPGap: +present=51 +min=0 +max=0 +zero=51 +negative=0 +invalid=[] + +maxTPGap: +present=51 +min=20.0 +max=50.0 +zero=0 +negative=0 +invalid=[] +``` + +### Проверка отношения minQty и maxQty + +Не обнаружено ни одного случая: + +```text +minQty > maxQty +``` + +Результат: + +```text +[] +``` + +--- + +## 20. Реализованные тесты + +Добавлен файл: + +```text +app/tests/unit/market_data/acquisition/validation/test_values.py +``` + +В нём реализовано: + +```text +34 теста +``` + +Тестами покрыты: + +- корректный полный `exchangeInfo`; +- пустые обязательные строки; +- пустой `orderType`; +- пустой `marketMode`; +- отрицательные precision values; +- отрицательный `swapChargeInterval`; +- нулевой `tickSize`; +- отрицательный `tickSize`; +- `NaN`; +- `+Infinity`; +- `-Infinity`; +- нечисловой `minQty`; +- неположительный `minQty`; +- неположительный `maxQty`; +- неположительный `stepSize`; +- `minQty > maxQty`; +- отрицательный `minNotional`; +- нулевой `minNotional`; +- отрицательные `longRate`; +- отрицательные `shortRate`; +- допустимые нулевые optional numeric values; +- rate limits; +- неизвестные instrument filters; +- global exchange filters. + +--- + +## 21. Выполненные проверки + +### Проверка 1 — unit-тесты Build 005 + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/validation/test_values.py \ + -q +``` + +Результат: + +```text +34 passed in 0.02s +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/exceptions.py \ + src/market_data/acquisition/validation/values.py \ + tests/unit/market_data/acquisition/validation/test_values.py +``` + +Результат: + +```text +успешно, без ошибок +``` + +--- + +### Проверка 3 — полная ручная цепочка обработки + +Проверена последовательность: + +```text +raw JSON + ↓ +validate_exchange_info_schema() + ↓ +parse_exchange_info() + ↓ +validate_exchange_info_values() +``` + +Результат: + +```text +Validation result: None +Symbol: BTC/USD_LEVERAGE +Filters: ( + DzengiLotSizeFilter( + filter_type='LOT_SIZE', + min_qty='0.0001', + max_qty='1000', + step_size='0.0001' + ), + DzengiMinNotionalFilter( + filter_type='MIN_NOTIONAL', + min_notional='1' + ) +) +``` + +Это подтверждает совместимость Build 003, Build 004 и Build 005. + +--- + +### Проверка 4 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +84 passed in 0.06s +``` + +Регрессий не обнаружено. + +--- + +### Проверка 5 — контроль области использования + +Выполнен поиск: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "validate_exchange_info_values|InstrumentReferenceValueError" \ + src tests +``` + +Подтверждено: + +- `validate_exchange_info_values()` используется только в собственном модуле и unit-тестах; +- `InstrumentReferenceValueError` используется только в `exceptions.py`, `values.py` и unit-тестах; +- новый validator ещё не подключён к legacy `ExchangeService`; +- UI не изменён; +- runtime не изменён; +- автоторговля не изменена; +- production-поведение работающего бота не изменено. + +--- + +## 22. Архитектурная граница Build 005 + +Build 005 намеренно не реализует: + +```text +REST client новой подсистемы +mapping Dzengi raw models → Instrument +instrument feed +instrument handler +registry +service orchestration +cache +legacy compatibility adapter +переключение production consumers +удаление старого ExchangeSymbol +``` + +Эти задачи относятся к следующим этапам миграции. + +Build 005 отвечает только за: + +```text +проверку допустимости значений +в уже распарсенных raw-моделях Dzengi exchangeInfo +``` + +--- + +## 23. Влияние на работающего legacy-бота + +Build 005 не изменяет production-поведение существующего бота. + +Старый runtime продолжает использовать существующие компоненты: + +```text +src/integrations/exchange/models.py +src/integrations/exchange/service.py +src/integrations/exchange/status.py +src/integrations/exchange/symbol_utils.py +``` + +Новая реализация находится отдельно: + +```text +src/market_data/acquisition/ +``` + +На момент завершения Build 005 она ещё не подключена к production consumers. + +Это соответствует принятой стратегии миграции: + +```text +сначала построить и протестировать новый путь параллельно, +затем переключать consumers поэтапно, +не ломая работающего бота +``` + +--- + +## 24. Итог Build 005 + +Build 005 успешно завершён. + +Реализовано: + +```text +InstrumentReferenceValueError +validate_exchange_info_values() +валидация обязательных строк +валидация orderTypes +валидация marketModes +валидация precision +валидация tickSize +валидация finite numeric values +валидация LOT_SIZE +валидация MIN_NOTIONAL +валидация rate limits +поддержка отрицательных longRate/shortRate +поддержка допустимых нулевых значений +поддержка пустых country/sector/industry +проверка unknown filters +34 unit-теста +``` + +Финальный результат: + +```text +84 passed in 0.06s +``` + +Регрессий не обнаружено. + +Legacy production runtime не изменён. + +--- + +## 25. Текущее состояние pipeline после Build 005 + +На момент завершения Build 005 построена следующая часть новой подсистемы: + +```text +Dzengi exchangeInfo raw JSON + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +validated Dzengi raw models +``` + +Следующий архитектурный шаг должен продолжить pipeline от проверенных source-specific raw-моделей к канонической source-independent модели: + +```text +validated Dzengi raw models + ↓ +mapper + ↓ +Instrument +``` + +Таким образом, естественным следующим этапом является реализация mapping layer: + +```text +DzengiExchangeInfoSymbol → Instrument +``` + +при сохранении изоляции от legacy production runtime до завершения и проверки нового пути. \ No newline at end of file diff --git a/docs/migrations/build_006.md b/docs/migrations/build_006.md new file mode 100644 index 0000000..253042e --- /dev/null +++ b/docs/migrations/build_006.md @@ -0,0 +1,1071 @@ +# Build 006 — Dzengi → Instrument Mapper + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime +**Результат полного набора тестов:** `106 passed` + +--- + +## 1. Цель Build 006 + +Цель Build 006 — реализовать преобразование source-specific raw-моделей Dzengi во внутреннюю source-independent модель Dzentra: + +```text +DzengiExchangeInfoSymbol + ↓ +Instrument +``` + +Также реализовано преобразование полного ответа `exchangeInfo`: + +```text +DzengiExchangeInfoResponse + ↓ +tuple[Instrument, ...] +``` + +После завершения Build 006 pipeline Instrument Reference Data выглядит следующим образом: + +```text +Dzengi raw JSON + ↓ +Schema Validation + ↓ +Parser + ↓ +Dzengi Raw Models + ↓ +Value Validation + ↓ +Mapper + ↓ +Instrument +``` + +Build 006 не подключает новый mapper к существующему `ExchangeService` и не меняет поведение работающего бота. + +--- + +## 2. Почему Build 006 выполняется именно сейчас + +До начала Build 006 были завершены необходимые предыдущие этапы: + +```text +Build 001 — внутренняя модель Instrument +Build 002 — транспортные модели Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +``` + +К началу Build 006 входные данные mapper уже: + +- структурно проверены; +- преобразованы в типизированные raw-модели; +- проверены на допустимость значений; +- изолированы от legacy-кода. + +Поэтому mapper может отвечать только за преобразование модели источника во внутреннюю модель Dzentra. + +--- + +## 3. Архитектурная граница Build 006 + +Build 006 отвечает только за преобразование: + +```text +DzengiExchangeInfoSymbol + ↓ +Instrument +``` + +и: + +```text +DzengiExchangeInfoResponse + ↓ +tuple[Instrument, ...] +``` + +Mapper не выполняет: + +- REST-запросы; +- schema validation; +- parsing JSON; +- полную value validation; +- нормализацию пользовательского ввода символа; +- определение runtime-статуса рынка; +- управление кэшем; +- создание legacy-модели `ExchangeSymbol`; +- интеграцию с `ExchangeService`; +- интеграцию с Telegram UI; +- интеграцию с автоторговлей. + +Таким образом, mapper остаётся изолированным слоем между source-specific моделями адаптера Dzengi и source-independent моделью Dzentra. + +--- + +## 4. Изменённые файлы + +В рамках Build 006 изменены: + +```text +app/src/market_data/acquisition/exceptions.py +app/src/market_data/acquisition/adapters/dzengi/mapper.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py +``` + +Не изменялись: + +```text +app/src/market_data/acquisition/models/instrument.py +app/src/market_data/acquisition/adapters/dzengi/models.py +app/src/market_data/acquisition/adapters/dzengi/parser.py +app/src/market_data/acquisition/validation/schema.py +app/src/market_data/acquisition/validation/values.py +app/src/integrations/exchange/* +app/src/telegram/* +app/src/trading/* +``` + +Работающий legacy-код бота не изменён. + +--- + +## 5. Новая ошибка mapper + +В файл: + +```text +app/src/market_data/acquisition/exceptions.py +``` + +добавлена ошибка: + +```python +class InstrumentReferenceMappingError(MarketDataAcquisitionError): + pass +``` + +Итоговая иерархия ошибок Instrument Reference Data: + +```text +MarketDataAcquisitionError +├── InstrumentReferenceSchemaError +├── InstrumentReferenceParseError +├── InstrumentReferenceValueError +└── InstrumentReferenceMappingError +``` + +`InstrumentReferenceMappingError` обозначает ошибки, возникшие непосредственно при преобразовании source-specific raw-модели во внутреннюю модель `Instrument`. + +Примеры: + +```text +несколько LOT_SIZE filters +несколько MIN_NOTIONAL filters +невозможность преобразования значения в Decimal +неконечное числовое значение +``` + +--- + +## 6. Публичный контракт mapper + +В файле: + +```text +app/src/market_data/acquisition/adapters/dzengi/mapper.py +``` + +реализованы две публичные функции. + +### 6.1. Преобразование одного инструмента + +```python +def map_dzengi_symbol_to_instrument( + symbol: DzengiExchangeInfoSymbol, +) -> Instrument: + ... +``` + +Функция преобразует один объект: + +```text +DzengiExchangeInfoSymbol +``` + +в один объект: + +```text +Instrument +``` + +### 6.2. Преобразование полного ответа exchangeInfo + +```python +def map_dzengi_exchange_info_to_instruments( + response: DzengiExchangeInfoResponse, +) -> tuple[Instrument, ...]: + ... +``` + +Функция преобразует все элементы: + +```text +DzengiExchangeInfoResponse.payload.symbols +``` + +в immutable-последовательность: + +```text +tuple[Instrument, ...] +``` + +Порядок инструментов сохраняется. + +--- + +## 7. Правила преобразования полей + +Реализовано следующее соответствие: + +```text +DzengiExchangeInfoSymbol.symbol + → Instrument.symbol + +name + → name + +status + → status + +base_asset + → base_asset + +quote_asset + → quote_asset + +asset_type + → asset_type + +market_type + → market_type + +market_modes + → market_modes + +order_types + → order_types + +base_asset_precision + → base_asset_precision + +quote_precision + → quote_asset_precision + +tick_size + → tick_size + +tick_value + → tick_value + +country + → country + +sector + → sector + +industry + → industry + +trading_hours + → trading_hours +``` + +--- + +## 8. Преобразование числовых значений в Decimal + +Следующие значения преобразуются во внутренний тип `Decimal`: + +```text +tick_size +tick_value +step_size +min_qty +max_qty +min_notional +``` + +Используется преобразование: + +```python +Decimal(str(value)) +``` + +Это позволяет избежать дополнительной двоичной погрешности при непосредственном преобразовании `float` в `Decimal`. + +Примеры: + +```text +0.01 + → Decimal("0.01") + +"0.001" + → Decimal("0.001") + +0 + → Decimal("0") + +0.00000001 + → Decimal("1E-8") +``` + +Если исходное значение отсутствует: + +```text +None → None +``` + +--- + +## 9. Защита Decimal-преобразования + +Mapper предполагает, что перед его вызовом уже выполнена: + +```text +Value Validation +``` + +Однако публичная функция mapper может быть вызвана напрямую. + +Поэтому непосредственно в mapper оставлена локальная защита преобразования в `Decimal`. + +Если значение невозможно преобразовать: + +```text +"not-a-number" +``` + +возникает: + +```text +InstrumentReferenceMappingError +``` + +Также отклоняются неконечные значения: + +```text +NaN +Infinity +-Infinity +``` + +Это не повторение полной Value Validation. Mapper защищает только собственную непосредственную обязанность — корректное создание `Decimal`. + +--- + +## 10. Извлечение LOT_SIZE + +Из: + +```text +DzengiLotSizeFilter +``` + +извлекаются: + +```text +min_qty +max_qty +step_size +``` + +Пример исходного фильтра: + +```python +DzengiLotSizeFilter( + filter_type="LOT_SIZE", + min_qty="0.001", + max_qty="1000", + step_size="0.001", +) +``` + +Результат: + +```text +Instrument.min_qty + = Decimal("0.001") + +Instrument.max_qty + = Decimal("1000") + +Instrument.step_size + = Decimal("0.001") +``` + +Если `LOT_SIZE` отсутствует: + +```text +min_qty → None +max_qty → None +step_size → None +``` + +--- + +## 11. Извлечение MIN_NOTIONAL + +Из: + +```text +DzengiMinNotionalFilter +``` + +извлекается: + +```text +min_notional +``` + +Пример исходного фильтра: + +```python +DzengiMinNotionalFilter( + filter_type="MIN_NOTIONAL", + min_notional="2", +) +``` + +Результат: + +```text +Instrument.min_notional + = Decimal("2") +``` + +Если `MIN_NOTIONAL` отсутствует: + +```text +min_notional → None +``` + +--- + +## 12. Неизвестные filters + +Неизвестные фильтры представлены raw-моделью: + +```text +DzengiUnknownFilter +``` + +Mapper не переносит их во внутреннюю модель `Instrument` и не выбрасывает из-за них ошибку. + +Например: + +```python +DzengiUnknownFilter( + filter_type="FUTURE_FILTER", + fields=( + ("enabled", True), + ("limit", 10), + ), +) +``` + +не препятствует созданию `Instrument`. + +Это сознательное архитектурное решение: + +```text +Raw Models + сохраняют неизвестные source-specific данные + +Instrument + содержит только известные канонические поля Dzentra +``` + +Неизвестные данные не теряются на транспортном уровне, но не загрязняют source-independent модель. + +--- + +## 13. Защита от дублирующихся известных filters + +Mapper не допускает неоднозначного выбора значения. + +Если один инструмент содержит несколько фильтров: + +```text +LOT_SIZE +``` + +возникает: + +```text +InstrumentReferenceMappingError +``` + +Аналогичное правило действует для нескольких фильтров: + +```text +MIN_NOTIONAL +``` + +Mapper не выбирает молча первый или последний фильтр, поскольку это могло бы привести к использованию неверных торговых ограничений. + +--- + +## 14. Нормализация необязательных текстовых полей + +Для следующих optional-полей применяется предметная нормализация: + +```text +asset_type +country +sector +industry +trading_hours +``` + +Правила: + +```text +None + → None + +"" + → None + +" " + → None + +" DE " + → "DE" +``` + +Это соответствует фактическому поведению Dzengi API, где некоторые справочные поля могут присутствовать как пустые строки. + +Например: + +```text +country="" +sector="" +industry="" +``` + +преобразуются во внутреннюю модель как: + +```text +country=None +sector=None +industry=None +``` + +--- + +## 15. Обязательные строки не нормализуются mapper + +Следующие поля переносятся без изменения: + +```text +symbol +name +status +base_asset +quote_asset +market_type +``` + +Причины: + +- parser сохраняет транспортное значение; +- Value Validation уже проверяет обязательность и непустоту; +- mapper не должен незаметно менять идентификаторы или статусы источника. + +--- + +## 16. Сохранение порядка последовательностей + +Mapper сохраняет исходный порядок: + +```text +market_modes +order_types +``` + +Например: + +```text +("REGULAR", "CLOSE_ONLY", "EXTENDED") +``` + +остаётся: + +```text +("REGULAR", "CLOSE_ONLY", "EXTENDED") +``` + +А: + +```text +("MARKET", "LIMIT", "STOP") +``` + +остаётся: + +```text +("MARKET", "LIMIT", "STOP") +``` + +Mapper не выполняет: + +```text +sorting +deduplication +set conversion +``` + +--- + +## 17. Поля Dzengi, которые сознательно не входят в Instrument + +Следующие source-specific данные сохранены в `DzengiExchangeInfoSymbol`, но не переносятся в текущую модель `Instrument`: + +```text +quote_asset_id + +trading_fee +exchange_fee + +long_rate +short_rate +swap_charge_interval + +min_sl_gap +max_sl_gap +min_tp_gap +max_tp_gap +``` + +Они не потеряны на транспортном уровне. + +В дальнейшем эти данные могут использоваться отдельными предметными контрактами: + +```text +fees +financing +execution constraints +provider-specific identifiers +``` + +Build 006 не смешивает эти области с базовой моделью `Instrument`. + +--- + +## 18. Immutable-результат + +Модель: + +```text +Instrument +``` + +объявлена как: + +```python +@dataclass(frozen=True, slots=True) +``` + +Тестами подтверждено, что попытка изменения уже созданного объекта приводит к: + +```text +FrozenInstanceError +``` + +Это обеспечивает стабильность справочного value object после mapping. + +--- + +## 19. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py +``` + +Реализовано 22 теста. + +Проверены: + +1. полное преобразование `DzengiExchangeInfoSymbol → Instrument`; +2. преобразование полного `DzengiExchangeInfoResponse`; +3. возврат `tuple[Instrument, ...]`; +4. сохранение порядка инструментов; +5. точное преобразование чисел в `Decimal`; +6. извлечение `LOT_SIZE`; +7. извлечение `MIN_NOTIONAL`; +8. работа при отсутствии filters; +9. работа при отсутствии optional numeric values; +10. игнорирование неизвестных filters; +11. преобразование пустого `asset_type` в `None`; +12. преобразование пустого `country` в `None`; +13. преобразование пустого `sector` в `None`; +14. преобразование пустого `industry` в `None`; +15. преобразование пустого `trading_hours` в `None`; +16. очистка внешних пробелов optional text; +17. сохранение порядка `market_modes`; +18. сохранение порядка `order_types`; +19. отклонение нескольких `LOT_SIZE`; +20. отклонение нескольких `MIN_NOTIONAL`; +21. отклонение неконечных числовых значений; +22. immutable-поведение итогового `Instrument`. + +--- + +## 20. Выполненные проверки + +### Проверка 1 — unit-тесты mapper + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py \ + -q +``` + +Результат: + +```text +22 passed in 0.03s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/exceptions.py \ + src/market_data/acquisition/adapters/dzengi/mapper.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный pipeline на реальном Dzengi sample + +Проверена цепочка: + +```text +JSON + ↓ +Schema Validation + ↓ +Parser + ↓ +Value Validation + ↓ +Mapper + ↓ +Instrument +``` + +Результат: + +```text +Instruments: 51 +``` + +Первый инструмент: + +```text +Instrument( + symbol='ETH/EUR_LEVERAGE', + name='ETH/EUR', + status='TRADING', + base_asset='ETH', + quote_asset='EUR', + asset_type='CRYPTOCURRENCY', + market_type='LEVERAGE', + market_modes=('REGULAR',), + order_types=('LIMIT', 'MARKET', 'STOP'), + base_asset_precision=3, + quote_asset_precision=3, + tick_size=Decimal('0.01'), + tick_value=Decimal('18.3415'), + step_size=Decimal('0.001'), + min_qty=Decimal('0.001'), + max_qty=Decimal('1000'), + min_notional=Decimal('2'), + country=None, + sector=None, + industry=None, + trading_hours='UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -;Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -', +) +``` + +Подтверждено: + +```text +51 инструмент успешно прошёл полный pipeline. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +106 passed in 0.06s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 5 — отсутствие production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "map_dzengi_symbol_to_instrument|map_dzengi_exchange_info_to_instruments|InstrumentReferenceMappingError" \ + src tests +``` + +Подтверждено, что mapper используется только в: + +```text +src/market_data/acquisition/adapters/dzengi/mapper.py +tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py +``` + +Новая ошибка объявлена в: + +```text +src/market_data/acquisition/exceptions.py +``` + +Не обнаружено подключения к: + +```text +src/integrations/exchange/* +src/telegram/* +src/trading/* +``` + +Статус: + +```text +PASSED +``` + +--- + +## 21. Архитектура после Build 006 + +После завершения Build 006 реализована следующая часть новой подсистемы: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ │ └── validate_exchange_info_schema() + │ │ + │ └── values.py + │ └── validate_exchange_info_values() + │ + └── adapters/ + └── dzengi/ + ├── models.py + │ ├── DzengiExchangeInfoResponse + │ ├── DzengiExchangeInfoPayload + │ ├── DzengiExchangeInfoSymbol + │ ├── DzengiLotSizeFilter + │ ├── DzengiMinNotionalFilter + │ └── DzengiUnknownFilter + │ + ├── parser.py + │ └── parse_exchange_info() + │ + └── mapper.py + ├── map_dzengi_symbol_to_instrument() + └── map_dzengi_exchange_info_to_instruments() +``` + +Рабочая цепочка: + +```text +Raw JSON document + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +--- + +## 22. Влияние на legacy-систему + +Build 006 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +``` + +Поэтому старый бот продолжает работать по прежнему пути. + +Новая подсистема строится параллельно и пока не заменяет legacy-реализацию. + +Это соответствует стратегии миграции: + +```text +Сначала построить и протестировать новый путь. +Затем подключать его постепенно. +Старый рабочий путь не удалять до подтверждения новой реализации. +``` + +--- + +## 23. Классификация изменений + +| Изменение | Классификация | +|---|---| +| Создание Dzengi mapper | Обязательное архитектурное изменение | +| Преобразование raw-модели в `Instrument` | Обязательное архитектурное изменение | +| Преобразование чисел в `Decimal` | Обязательное архитектурное изменение | +| Извлечение `LOT_SIZE` | Обязательное архитектурное изменение | +| Извлечение `MIN_NOTIONAL` | Обязательное архитектурное изменение | +| Защита от дублирующихся известных filters | Улучшение надёжности | +| Пустые optional text → `None` | Предметная нормализация | +| Игнорирование unknown filters в `Instrument` | Разграничение source/domain layers | +| Новая `InstrumentReferenceMappingError` | Улучшение диагностируемости | +| Изменение production-поведения | Отсутствует | + +--- + +## 24. Итог Build 006 + +Build 006 завершён успешно. + +Реализовано: + +```text +DzengiExchangeInfoSymbol + ↓ +Instrument +``` + +и: + +```text +DzengiExchangeInfoResponse + ↓ +tuple[Instrument, ...] +``` + +Подтверждено: + +```text +22 mapper tests passed +106 total project tests passed +51 real Dzengi instruments successfully mapped +Python compilation passed +No production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 006 — COMPLETE +``` + +--- + +## 25. Следующий этап + +Следующий этап необходимо определить по утверждённому плану миграции. + +Новый pipeline уже умеет преобразовывать сохранённый JSON-документ в: + +```text +tuple[Instrument, ...] +``` + +Следующий Build должен добавить следующий минимальный слой, не подключая сразу новый путь ко всем legacy-потребителям и не изменяя работающий `ExchangeService` без совместимого переходного контракта. \ No newline at end of file diff --git a/docs/migrations/build_007.md b/docs/migrations/build_007.md new file mode 100644 index 0000000..6cf6499 --- /dev/null +++ b/docs/migrations/build_007.md @@ -0,0 +1,999 @@ +# Build 007 — Protocol и Exceptions + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное расширение новой архитектуры без подключения к production runtime +**Результат полного набора тестов:** `113 passed` + +--- + +## 1. Цель Build 007 + +Цель Build 007 — зафиксировать минимальные публичные контракты взаимодействия между следующими слоями новой подсистемы Instrument Reference Data: + +```text +Build 008 — Dzengi REST Adapter +Build 009 — Instrument Handler +Build 010 — Instrument Feed +Build 011 — Registry +Build 012 — Acquisition Service +``` + +Также добавлена специализированная ошибка транспортного уровня для будущего REST adapter. + +После завершения Build 007 архитектурная последовательность выглядит следующим образом: + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler + ↓ +InstrumentFeedProtocol + ↓ +Registry + ↓ +Acquisition Service +``` + +Build 007 не реализует получение или обработку данных и не подключает новую подсистему к существующему `ExchangeService`. + +--- + +## 2. Почему Build 007 выполняется именно сейчас + +До начала Build 007 были завершены предыдущие этапы: + +```text +Build 001 — внутренняя модель Instrument Reference Data +Build 002 — raw-модели ответа Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +Build 006 — mapper Dzengi → Instrument +``` + +К началу Build 007 уже существовал полный pipeline преобразования заранее полученного JSON-документа: + +```text +raw JSON document + ↓ +Schema Validation + ↓ +Parser + ↓ +Dzengi Raw Models + ↓ +Value Validation + ↓ +Mapper + ↓ +tuple[Instrument, ...] +``` + +Следующие Build должны добавить транспортный источник, handler, feed, registry и acquisition service. + +Перед их реализацией необходимо было определить минимальные интерфейсы взаимодействия между этими слоями и добавить специализированную ошибку транспортного уровня. + +--- + +## 3. Архитектурная граница Build 007 + +Build 007 отвечает только за: + +```text +определение контракта источника сырого документа; +определение контракта обработчика сырого документа; +определение контракта источника готовых Instrument; +добавление ошибки транспортного уровня. +``` + +Build 007 не выполняет: + +```text +HTTP-запросы; +получение exchangeInfo; +schema validation; +parsing; +value validation; +mapping; +создание конкретного Handler; +создание конкретного Feed; +создание Registry; +создание Acquisition Service; +кэширование; +изменение ExchangeService; +изменение runtime; +изменение Telegram UI; +изменение автоторговли. +``` + +--- + +## 4. Изменённые файлы + +В рамках Build 007 изменены: + +```text +app/src/market_data/acquisition/protocol.py +app/src/market_data/acquisition/exceptions.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/test_protocol.py +``` + +Не изменялись: + +```text +app/src/market_data/acquisition/adapters/dzengi/rest.py +app/src/market_data/acquisition/handlers/instrument_handler.py +app/src/market_data/acquisition/feeds/instrument_feed.py +app/src/market_data/acquisition/registry.py +app/src/market_data/acquisition/service.py + +app/src/market_data/acquisition/adapters/dzengi/models.py +app/src/market_data/acquisition/adapters/dzengi/parser.py +app/src/market_data/acquisition/adapters/dzengi/mapper.py + +app/src/market_data/acquisition/validation/schema.py +app/src/market_data/acquisition/validation/values.py + +app/src/integrations/exchange/* +app/src/telegram/* +app/src/trading/* +``` + +Работающий legacy-код бота не изменён. + +--- + +## 5. Созданные Protocol + +В файле: + +```text +app/src/market_data/acquisition/protocol.py +``` + +созданы три минимальных протокола: + +```text +InstrumentDocumentSource +InstrumentDocumentHandler +InstrumentFeedProtocol +``` + +Все протоколы основаны на структурной типизации Python: + +```python +typing.Protocol +``` + +и объявлены как: + +```python +@runtime_checkable +``` + +Это позволяет использовать их как для статической типизации, так и для ограниченной runtime-проверки через `isinstance()`. + +--- + +## 6. InstrumentDocumentSource + +Контракт: + +```python +@runtime_checkable +class InstrumentDocumentSource(Protocol): + def fetch_instrument_document(self) -> object: + ... +``` + +Назначение: + +```text +получить декодированный транспортный документ Instrument Reference Data. +``` + +Предполагаемый конкретный потребитель этого контракта появится в: + +```text +Build 008 — Dzengi REST Adapter +``` + +Будущий REST adapter должен реализовать метод: + +```python +fetch_instrument_document() +``` + +и вернуть сырой декодированный документ. + +--- + +## 7. Почему InstrumentDocumentSource возвращает object + +Возвращаемый тип: + +```python +object +``` + +выбран сознательно. + +Транспортный источник не должен выполнять: + +```text +schema validation; +parsing; +value validation; +mapping. +``` + +На транспортной границе REST adapter может получить произвольное декодированное JSON-значение: + +```text +dict +list +str +int +float +bool +None +``` + +Проверка структуры является обязанностью: + +```text +Schema Validation +``` + +Поэтому транспортный слой не должен преждевременно утверждать, что полученный документ является корректным JSON-объектом нужной структуры. + +Архитектурная граница остаётся следующей: + +```text +REST Adapter + ↓ +object + ↓ +Schema Validation + ↓ +ValidatedExchangeInfoDocument +``` + +--- + +## 8. InstrumentDocumentHandler + +Контракт: + +```python +@runtime_checkable +class InstrumentDocumentHandler(Protocol): + def handle_instrument_document( + self, + document: object, + ) -> tuple[Instrument, ...]: + ... +``` + +Назначение: + +```text +преобразовать сырой документ в проверенные внутренние модели Instrument. +``` + +Конкретная реализация появится в: + +```text +Build 009 — Instrument Handler +``` + +Handler должен объединить уже существующий pipeline: + +```text +object + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +При этом Protocol не знает: + +```text +какая биржа является источником; +какой parser используется; +какой mapper используется; +какие source-specific raw-модели существуют. +``` + +--- + +## 9. InstrumentFeedProtocol + +Контракт: + +```python +@runtime_checkable +class InstrumentFeedProtocol(Protocol): + def load_instruments(self) -> tuple[Instrument, ...]: + ... +``` + +Назначение: + +```text +получить полный immutable-набор внутренних моделей Instrument. +``` + +Конкретный Feed появится в: + +```text +Build 010 — Instrument Feed +``` + +Предполагаемая композиция: + +```text +InstrumentFeed + ├── InstrumentDocumentSource + └── InstrumentDocumentHandler +``` + +Рабочая последовательность: + +```text +InstrumentFeed.load_instruments() + ↓ +InstrumentDocumentSource.fetch_instrument_document() + ↓ +object + ↓ +InstrumentDocumentHandler.handle_instrument_document() + ↓ +tuple[Instrument, ...] +``` + +--- + +## 10. Почему протокол называется InstrumentFeedProtocol + +Будущий файл: + +```text +app/src/market_data/acquisition/feeds/instrument_feed.py +``` + +предназначен для конкретной реализации Feed. + +Чтобы избежать конфликта между интерфейсом и конкретным классом, протокол получил имя: + +```text +InstrumentFeedProtocol +``` + +Конкретная реализация в Build 010 сможет называться: + +```text +InstrumentFeed +``` + +Таким образом: + +```text +InstrumentFeedProtocol + контракт + +InstrumentFeed + конкретная реализация +``` + +--- + +## 11. Structural Typing + +Реализации не обязаны наследоваться от Protocol напрямую. + +Например: + +```python +class StubInstrumentDocumentSource: + def fetch_instrument_document(self) -> object: + return { + "symbols": [], + } +``` + +Такой объект удовлетворяет контракту: + +```text +InstrumentDocumentSource +``` + +без явного наследования: + +```python +class StubInstrumentDocumentSource(InstrumentDocumentSource): + ... +``` + +Это уменьшает связанность между конкретными реализациями и интерфейсами. + +--- + +## 12. Runtime Checkable + +Все три Protocol объявлены с: + +```python +@runtime_checkable +``` + +Благодаря этому допустима проверка: + +```python +isinstance(source, InstrumentDocumentSource) +``` + +Тестами подтверждено: + +```text +объект с требуемым методом + → соответствует Protocol + +объект без требуемого метода + → не соответствует Protocol +``` + +Важно: runtime-проверка Protocol подтверждает структурное наличие требуемых атрибутов и методов, но не выполняет полную глубокую проверку всех аннотаций типов и фактических возвращаемых значений. + +--- + +## 13. Новая транспортная ошибка + +В файл: + +```text +app/src/market_data/acquisition/exceptions.py +``` + +добавлена: + +```python +class InstrumentReferenceTransportError( + MarketDataAcquisitionError +): + pass +``` + +Она предназначена для будущего: + +```text +Build 008 — Dzengi REST Adapter +``` + +--- + +## 14. Назначение InstrumentReferenceTransportError + +Ошибка предназначена для проблем получения Instrument Reference Data от внешнего источника. + +Потенциальные случаи: + +```text +ошибка соединения; +timeout; +HTTP error; +ошибка JSON decoding; +непредвиденная ошибка REST-клиента. +``` + +Она не используется для: + +```text +неверной структуры документа; +ошибки parsing; +недопустимых значений; +ошибки mapping. +``` + +Эти случаи уже имеют специализированные типы исключений. + +--- + +## 15. Итоговая иерархия ошибок + +После Build 007 иерархия выглядит следующим образом: + +```text +MarketDataAcquisitionError +├── InstrumentReferenceTransportError +├── InstrumentReferenceSchemaError +├── InstrumentReferenceParseError +├── InstrumentReferenceValueError +└── InstrumentReferenceMappingError +``` + +Назначение ошибок: + +| Ошибка | Ответственность | +|---|---| +| `InstrumentReferenceTransportError` | Получение данных от внешнего источника | +| `InstrumentReferenceSchemaError` | Структура исходного документа | +| `InstrumentReferenceParseError` | Преобразование проверенного документа в raw-модели | +| `InstrumentReferenceValueError` | Допустимость значений | +| `InstrumentReferenceMappingError` | Преобразование raw-модели во внутреннюю модель `Instrument` | + +--- + +## 16. Какие дополнительные Protocol не создавались + +В Build 007 сознательно не создавались отдельные Protocol для: + +```text +Schema Validator +Parser +Value Validator +Mapper +Registry +Acquisition Service +``` + +Причины: + +```text +validators, parser и mapper уже реализованы как чистые функции; + +Registry и Acquisition Service пока не имеют нескольких реализаций; + +дополнительные интерфейсы сейчас не используются; + +создание таких контрактов было бы преждевременной абстракцией. +``` + +Это соответствует принципу: + +```text +не создавать абстракции «на будущее» без конкретного потребителя. +``` + +--- + +## 17. Какие дополнительные Exceptions не создавались + +В Build 007 сознательно не добавлялись: + +```text +InstrumentReferenceHandlerError +InstrumentReferenceFeedError +InstrumentReferenceRegistryError +InstrumentReferenceServiceError +``` + +Причины: + +```text +Handler может передавать точные ошибки Schema, Parser, Value и Mapping; + +Feed может передавать точные ошибки Transport и Processing; + +Registry ещё не реализован; + +Acquisition Service ещё не реализован; + +новые типы ошибок следует вводить только там, где появляется реальная новая категория отказа. +``` + +--- + +## 18. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/test_protocol.py +``` + +Реализовано 7 тестов. + +Проверены: + +1. соответствие корректного source объекта `InstrumentDocumentSource`; +2. соответствие корректного handler объекта `InstrumentDocumentHandler`; +3. соответствие корректного feed объекта `InstrumentFeedProtocol`; +4. отклонение объектов без обязательных методов; +5. structural typing без явного наследования; +6. наследование `InstrumentReferenceTransportError` от `MarketDataAcquisitionError`; +7. наличие общего базового типа у всех ошибок Instrument Reference Data. + +--- + +## 19. Выполненные проверки + +### Проверка 1 — unit-тесты Protocol и Exceptions + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/test_protocol.py \ + -q +``` + +Результат: + +```text +....... [100%] +7 passed in 0.01s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/protocol.py \ + src/market_data/acquisition/exceptions.py \ + tests/unit/market_data/acquisition/test_protocol.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +................................................................................................................. [100%] +113 passed in 0.06s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — отсутствие преждевременной production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "InstrumentDocumentSource|InstrumentDocumentHandler|InstrumentFeedProtocol|InstrumentReferenceTransportError" \ + src tests +``` + +Подтверждено: + +```text +InstrumentDocumentSource + используется только в protocol.py и unit-тестах + +InstrumentDocumentHandler + используется только в protocol.py и unit-тестах + +InstrumentFeedProtocol + используется только в protocol.py и unit-тестах + +InstrumentReferenceTransportError + объявлена в exceptions.py и используется только в unit-тестах +``` + +Не обнаружено подключения к: + +```text +src/integrations/exchange/* +src/telegram/* +src/trading/* +``` + +Статус: + +```text +PASSED +``` + +--- + +## 20. Архитектура после Build 007 + +После завершения Build 007 новая часть подсистемы имеет следующую структуру: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ ├── MarketDataAcquisitionError + │ ├── InstrumentReferenceTransportError + │ ├── InstrumentReferenceSchemaError + │ ├── InstrumentReferenceParseError + │ ├── InstrumentReferenceValueError + │ └── InstrumentReferenceMappingError + │ + ├── protocol.py + │ ├── InstrumentDocumentSource + │ ├── InstrumentDocumentHandler + │ └── InstrumentFeedProtocol + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ └── values.py + │ + └── adapters/ + └── dzengi/ + ├── models.py + ├── parser.py + ├── mapper.py + └── rest.py +``` + +На текущем этапе: + +```text +rest.py + ещё не реализован + +instrument_handler.py + ещё не реализован + +instrument_feed.py + ещё не реализован + +registry.py + ещё не реализован + +service.py + ещё не реализован +``` + +--- + +## 21. Полная архитектурная цепочка после Build 007 + +Уже реализовано: + +```text +raw JSON document + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +Зафиксированы контракты для будущей orchestration-цепочки: + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler + ↓ +InstrumentFeedProtocol + ↓ +Registry + ↓ +Acquisition Service +``` + +После реализации Build 008–012 предполагаемая полная цепочка будет выглядеть так: + +```text +Dzengi REST API + ↓ +Dzengi REST Adapter + implements InstrumentDocumentSource + ↓ +object + ↓ +Instrument Handler + implements InstrumentDocumentHandler + ↓ +tuple[Instrument, ...] + ↓ +Instrument Feed + implements InstrumentFeedProtocol + ↓ +Registry + ↓ +Acquisition Service +``` + +--- + +## 22. Влияние на legacy-систему + +Build 007 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +``` + +Не изменены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +normalize_symbol() +symbol_candidates() +``` + +Старый production-путь продолжает работать без изменений. + +Новая подсистема развивается параллельно. + +--- + +## 23. Обратная совместимость + +Подтверждено сохранение: + +```text +сигнатур существующих методов; +старых импортов; +существующего формата ошибок; +Telegram UI; +автоторговли; +runtime-поведения; +legacy ExchangeSymbol; +legacy-кэша. +``` + +Build 007 имеет полную обратную совместимость. + +--- + +## 24. Классификация изменений + +| Изменение | Классификация | +|---|---| +| `InstrumentDocumentSource` | Обязательное архитектурное изменение | +| `InstrumentDocumentHandler` | Обязательное архитектурное изменение | +| `InstrumentFeedProtocol` | Обязательное архитектурное изменение | +| `InstrumentReferenceTransportError` | Обязательное архитектурное изменение | +| Structural typing | Улучшение архитектурной независимости | +| `runtime_checkable` | Улучшение тестируемости и проверяемости контрактов | +| Дополнительные преждевременные Protocol | Не создавались | +| Дополнительные преждевременные Exceptions | Не создавались | +| Изменение production-поведения | Отсутствует | + +--- + +## 25. Итог Build 007 + +Build 007 завершён успешно. + +Реализовано: + +```text +InstrumentDocumentSource +InstrumentDocumentHandler +InstrumentFeedProtocol +InstrumentReferenceTransportError +``` + +Подтверждено: + +```text +7 protocol tests passed +113 total project tests passed +Python compilation passed +No production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 007 — COMPLETE +``` + +--- + +## 26. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 008 — Dzengi REST Adapter +``` + +Его задача — реализовать конкретный транспортный источник, соответствующий контракту: + +```text +InstrumentDocumentSource +``` + +Будущая граница Build 008: + +```text +Dzengi REST API + ↓ +Dzengi REST Adapter + ↓ +object +``` + +Build 008 не должен выполнять: + +```text +schema validation; +parsing; +value validation; +mapping; +создание Instrument; +создание Handler; +создание Feed; +создание Registry; +создание Acquisition Service; +изменение ExchangeService; +production-подключение. +``` \ No newline at end of file diff --git a/docs/migrations/build_008.md b/docs/migrations/build_008.md new file mode 100644 index 0000000..dc99a8c --- /dev/null +++ b/docs/migrations/build_008.md @@ -0,0 +1,1092 @@ +# Build 008 — Dzengi REST Adapter + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное добавление transport adapter без подключения к production runtime +**Результат полного набора тестов:** `123 passed` + +--- + +## 1. Цель Build 008 + +Цель Build 008 — реализовать конкретный транспортный источник Instrument Reference Data для Dzengi REST API, соответствующий созданному в Build 007 протоколу: + +```python +class InstrumentDocumentSource(Protocol): + def fetch_instrument_document(self) -> object: + ... +``` + +Реализован класс: + +```text +DzengiInstrumentDocumentSource +``` + +Архитектурная граница Build 008: + +```text +Dzengi REST API + ↓ +ExchangeRestClient + ↓ +DzengiInstrumentDocumentSource + ↓ +object +``` + +Build 008 отвечает только за получение декодированного транспортного документа. + +Он не выполняет его структурную проверку, parsing, проверку значений или преобразование во внутренние модели `Instrument`. + +--- + +## 2. Почему Build 008 выполняется именно сейчас + +До начала Build 008 были завершены: + +```text +Build 001 — внутренняя модель Instrument Reference Data +Build 002 — raw-модели ответа Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +Build 006 — mapper Dzengi → Instrument +Build 007 — Protocol и Exceptions +``` + +После Build 007 были зафиксированы контракты будущей orchestration-цепочки: + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler + ↓ +InstrumentFeedProtocol + ↓ +Registry + ↓ +Acquisition Service +``` + +Следующим необходимым шагом стала конкретная реализация первого контракта: + +```text +InstrumentDocumentSource +``` + +для источника Dzengi. + +--- + +## 3. Анализ существующей реализации + +Перед написанием кода были проанализированы: + +```text +app/src/integrations/exchange/rest_client.py +app/src/integrations/exchange/exceptions.py +app/src/core/config.py +app/src/integrations/exchange/service.py +app/src/market_data/acquisition/adapters/dzengi/rest.py +``` + +Также выполнен поиск существующих вызовов `exchangeInfo`: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "exchangeInfo|exchange_info|get_exchange_symbols" \ + src/integrations/exchange +``` + +Было подтверждено, что legacy-реализация использует: + +```python +client.get_json("/api/v1/exchangeInfo") +``` + +в методе: + +```text +ExchangeService.get_exchange_symbols() +``` + +Таким образом, фактически используемый работающим ботом endpoint: + +```text +/api/v1/exchangeInfo +``` + +--- + +## 4. Принятое решение по HTTP transport + +В Build 008 не создавалась новая параллельная HTTP-реализация. + +Вместо этого новый Dzengi REST Adapter временно переиспользует существующий: + +```text +src.integrations.exchange.rest_client.ExchangeRestClient +``` + +Причины: + +```text +клиент уже использует актуальный EXCHANGE_BASE_URL; +клиент уже применяет EXCHANGE_TIMEOUT_SEC; +клиент уже реализует HTTP GET; +клиент уже устанавливает необходимые HTTP headers; +клиент уже выполняет JSON decoding; +клиент уже преобразует HTTP/network ошибки в exchange exceptions; +создание второго HTTP transport дублировало бы существующую инфраструктуру; +legacy-код работающего бота не требуется изменять. +``` + +Это является временной переходной зависимостью. + +Условие её будущего удаления: + +```text +появление утверждённого общего transport-клиента +или +полный вывод legacy integrations/exchange из эксплуатации. +``` + +--- + +## 5. Изменённые файлы + +В рамках Build 008 изменён production-файл: + +```text +app/src/market_data/acquisition/adapters/dzengi/rest.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py +``` + +Другие production-файлы не изменялись. + +--- + +## 6. Реализованный Dzengi REST Adapter + +В файле: + +```text +app/src/market_data/acquisition/adapters/dzengi/rest.py +``` + +реализован класс: + +```python +class DzengiInstrumentDocumentSource: + ... +``` + +Его публичный метод: + +```python +def fetch_instrument_document(self) -> object: + ... +``` + +соответствует контракту: + +```text +InstrumentDocumentSource +``` + +--- + +## 7. Endpoint exchangeInfo + +В adapter зафиксирован endpoint: + +```python +_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo" +``` + +Он соответствует фактически используемому legacy-кодом endpoint. + +Путь вынесен в приватную константу, чтобы не дублировать строковый литерал внутри методов. + +Build 008: + +```text +не меняет версию endpoint; +не вводит альтернативный endpoint; +не изменяет legacy-вызов; +не переключает production runtime. +``` + +--- + +## 8. Почему используется get_payload() + +Legacy REST-клиент предоставляет два метода: + +```python +get_payload() -> object +``` + +и: + +```python +get_json() -> dict +``` + +Для нового adapter выбран: + +```python +get_payload() +``` + +Это принципиальное архитектурное решение. + +Контракт `InstrumentDocumentSource` возвращает: + +```python +object +``` + +Следовательно, transport layer не должен утверждать, что полученный JSON обязательно является объектом `dict`. + +Если внешний источник вернёт: + +```text +dict +list +str +int +float +bool +None +``` + +REST adapter должен вернуть декодированный документ следующему слою без структурной интерпретации. + +Проверка структуры относится к Build 003: + +```text +validate_exchange_info_schema() +``` + +Поэтому архитектурная граница остаётся: + +```text +Dzengi REST API + ↓ +ExchangeRestClient.get_payload() + ↓ +object + ↓ +Schema Validation +``` + +Использование `get_json()` преждевременно смешало бы: + +```text +transport responsibility +``` + +и: + +```text +schema validation responsibility +``` + +--- + +## 9. Отсутствие преобразования документа + +`DzengiInstrumentDocumentSource` возвращает результат: + +```python +client.get_payload(_EXCHANGE_INFO_PATH) +``` + +без изменения. + +Adapter не выполняет: + +```text +копирование документа; +изменение полей; +извлечение payload; +извлечение symbols; +нормализацию значений; +преобразование коллекций; +создание raw-моделей; +создание Instrument. +``` + +Тестами подтверждено сохранение identity: + +```python +assert result is document +``` + +как для `dict`, так и для `list`. + +--- + +## 10. Dependency Injection + +Adapter поддерживает передачу существующего REST-клиента: + +```python +class DzengiInstrumentDocumentSource: + def __init__( + self, + client: _PayloadRestClient | None = None, + ) -> None: + self._client = client +``` + +Поведение: + +```text +client передан + ↓ +используется переданный объект + +client не передан + ↓ +создаётся ExchangeRestClient() +``` + +Dependency injection необходим уже на текущем Build для: + +```text +unit-тестирования без реальных сетевых запросов; +точной проверки вызываемого endpoint; +эмуляции transport errors; +проверки неизменности возвращаемого документа. +``` + +--- + +## 11. Приватный транспортный Protocol + +Первоначально конструктор adapter был аннотирован следующим образом: + +```python +client: ExchangeRestClient | None = None +``` + +При передаче тестового `StubRestClient` Pylance корректно обнаружил несовместимость типов: + +```text +Argument of type "StubRestClient" cannot be assigned +to parameter "client" of type "ExchangeRestClient | None" +``` + +Оставлять такую диагностическую ошибку в Build было признано неправильным. + +Для исправления создан минимальный приватный структурный контракт: + +```python +class _PayloadRestClient(Protocol): + def get_payload( + self, + path: str, + params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + ) -> object: + ... +``` + +Теперь конструктор принимает: + +```python +client: _PayloadRestClient | None = None +``` + +Это позволяет типобезопасно передавать: + +```text +ExchangeRestClient; +StubRestClient; +любую другую реализацию с совместимым get_payload(). +``` + +--- + +## 12. Почему _PayloadRestClient является приватным + +Protocol назван: + +```text +_PayloadRestClient +``` + +с ведущим подчёркиванием сознательно. + +На текущем этапе он нужен только как внутренняя типовая граница конкретного Dzengi REST Adapter. + +Он: + +```text +не является публичным контрактом всей acquisition subsystem; +не экспортируется через __init__.py; +не используется другими production-модулями; +не создаёт новый общий transport abstraction layer. +``` + +Если в будущем нескольким adapter потребуется общий HTTP transport contract, его выделение в публичный модуль должно выполняться отдельным архитектурным решением на основании реальной потребности. + +--- + +## 13. Ленивое создание ExchangeRestClient + +Если клиент не передан, он создаётся внутри: + +```python +fetch_instrument_document() +``` + +а не в конструкторе `DzengiInstrumentDocumentSource`. + +Последовательность: + +```text +DzengiInstrumentDocumentSource() + ↓ +объект создаётся без немедленного создания ExchangeRestClient + +fetch_instrument_document() + ↓ +создаётся ExchangeRestClient + ↓ +выполняется REST-запрос +``` + +Это важно, потому что конструктор legacy `ExchangeRestClient`: + +```text +загружает настройки; +проверяет EXCHANGE_BASE_URL; +может выбросить исключение ещё до HTTP-запроса. +``` + +Создание клиента внутри `try` гарантирует, что ошибка его создания также преобразуется в: + +```text +InstrumentReferenceTransportError +``` + +--- + +## 14. Transport error boundary + +Build 007 добавил специализированную ошибку: + +```python +InstrumentReferenceTransportError +``` + +Build 008 впервые использует её в production-коде новой подсистемы. + +Любая ошибка, возникающая внутри transport boundary: + +```python +except Exception as exc: +``` + +преобразуется в: + +```python +raise InstrumentReferenceTransportError( + "Не удалось получить Instrument Reference Data " + f"от Dzengi: {exc}" +) from exc +``` + +Это обеспечивает единый публичный тип transport-ошибки для новой acquisition subsystem. + +--- + +## 15. Какие ошибки оборачиваются + +Тестами подтверждена обработка: + +```text +ExchangeConnectionError +ExchangeResponseError +RuntimeError +ошибки создания ExchangeRestClient +``` + +Также текущая граница перехватывает другие `Exception`, возникающие при получении документа. + +Наружу новой подсистемы не должны непосредственно выходить legacy-типы: + +```text +ExchangeConnectionError +ExchangeResponseError +ExchangeError +``` + +На границе нового adapter они преобразуются в: + +```text +InstrumentReferenceTransportError +``` + +--- + +## 16. Exception chaining + +При преобразовании ошибки сохраняется исходное исключение: + +```python +raise InstrumentReferenceTransportError(...) from exc +``` + +Благодаря этому: + +```python +wrapped_error.__cause__ is original_error +``` + +Тестами подтверждено сохранение исходной причины. + +Это важно для: + +```text +диагностики; +логирования; +traceback; +будущего анализа transport failures. +``` + +--- + +## 17. Что REST Adapter не делает + +Build 008 сознательно не выполняет: + +```text +schema validation; +parsing; +value validation; +mapping; +создание Instrument; +создание Handler; +создание Feed; +создание Registry; +создание Acquisition Service; +кэширование; +retry; +логирование; +формирование Telegram-сообщений; +проверку exchange_enabled; +production-подключение. +``` + +В adapter отсутствуют вызовы: + +```text +validate_exchange_info_schema() +parse_exchange_info() +validate_exchange_info_values() +map_dzengi_exchange_info_to_instruments() +``` + +--- + +## 18. Почему adapter не проверяет exchange_enabled + +`DzengiInstrumentDocumentSource` является transport adapter. + +Его ответственность: + +```text +получить документ от конкретного внешнего источника. +``` + +Решение о том: + +```text +разрешено ли приложению обращаться к бирже; +в каком режиме работает приложение; +нужно ли использовать mock; +нужно ли использовать cache; +когда именно запускать acquisition; +``` + +относится к orchestration или service layer. + +Поэтому Build 008 не добавляет проверку: + +```python +exchange_enabled +``` + +и не реализует mock-поведение. + +--- + +## 19. Почему adapter не использует format_exchange_error_for_user() + +В legacy-модуле существует: + +```python +format_exchange_error_for_user() +``` + +Он предназначен для пользовательского представления ошибок. + +REST adapter не является UI-слоем. + +Поэтому он не должен: + +```text +формировать пользовательский текст; +знать о Telegram UI; +классифицировать ошибку для экрана; +принимать решение о повторной попытке. +``` + +Его задача — предоставить точную предметную ошибку: + +```text +InstrumentReferenceTransportError +``` + +--- + +## 20. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py +``` + +Реализовано 10 тестов. + +Проверены следующие сценарии: + +1. `DzengiInstrumentDocumentSource` соответствует `InstrumentDocumentSource`; +2. вызывается endpoint `/api/v1/exchangeInfo`; +3. возвращаемый `dict` передаётся без изменения; +4. возвращаемый `list` передаётся без изменения; +5. injected client действительно используется; +6. `ExchangeConnectionError` преобразуется в `InstrumentReferenceTransportError`; +7. `ExchangeResponseError` преобразуется в `InstrumentReferenceTransportError`; +8. неизвестная `RuntimeError` также преобразуется в transport error; +9. ошибка создания `ExchangeRestClient` преобразуется в transport error; +10. adapter не преобразует wrapped document. + +Дополнительно проверено: + +```text +исходное исключение сохраняется в __cause__; +текст исходной ошибки сохраняется в transport error; +реальный сетевой запрос в unit-тестах не выполняется. +``` + +--- + +## 21. Выполненные проверки + +### Проверка 1 — unit-тесты Dzengi REST Adapter + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \ + -q +``` + +Результат: + +```text +.......... [100%] +10 passed in 0.02s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/adapters/dzengi/rest.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +........................................................................................................................... [100%] +123 passed in 0.08s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — отсутствие преждевременной production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "DzengiInstrumentDocumentSource|_PayloadRestClient|InstrumentReferenceTransportError" \ + src tests +``` + +Подтверждено: + +```text +DzengiInstrumentDocumentSource + используется только в новом Dzengi REST Adapter и его unit-тестах; + +_PayloadRestClient + остаётся приватным контрактом внутри rest.py; + +InstrumentReferenceTransportError + используется только внутри новой market_data/acquisition subsystem + и её unit-тестов. +``` + +Не обнаружено подключения к: + +```text +ExchangeService +Telegram UI +AutoTrade +Trading runtime +другим production-потребителям +``` + +Статус: + +```text +PASSED +``` + +--- + +## 22. Архитектура после Build 008 + +После завершения Build 008 новая часть подсистемы имеет следующую структуру: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ ├── MarketDataAcquisitionError + │ ├── InstrumentReferenceTransportError + │ ├── InstrumentReferenceSchemaError + │ ├── InstrumentReferenceParseError + │ ├── InstrumentReferenceValueError + │ └── InstrumentReferenceMappingError + │ + ├── protocol.py + │ ├── InstrumentDocumentSource + │ ├── InstrumentDocumentHandler + │ └── InstrumentFeedProtocol + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ └── values.py + │ + └── adapters/ + └── dzengi/ + ├── models.py + ├── parser.py + ├── mapper.py + └── rest.py + ├── _PayloadRestClient + └── DzengiInstrumentDocumentSource +``` + +На текущем этапе ещё не реализованы: + +```text +instrument_handler.py +instrument_feed.py +registry.py +service.py +``` + +--- + +## 23. Полная архитектурная цепочка после Build 008 + +Теперь реализован транспортный этап: + +```text +Dzengi REST API + ↓ +ExchangeRestClient.get_payload() + ↓ +DzengiInstrumentDocumentSource + ↓ +object +``` + +Уже реализован processing pipeline: + +```text +object + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +Однако эти две части пока намеренно не соединены. + +Их соединение относится к: + +```text +Build 009 — Instrument Handler +``` + +--- + +## 24. Влияние на legacy-систему + +Build 008 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +``` + +Не изменены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +normalize_symbol() +symbol_candidates() +``` + +Старый production-путь продолжает работать без изменений. + +Новый adapter существует изолированно и пока вызывается только unit-тестами. + +--- + +## 25. Обратная совместимость + +Подтверждено сохранение: + +```text +сигнатур существующих методов; +старых импортов; +существующего формата legacy-ошибок; +Telegram UI; +автоторговли; +runtime-поведения; +legacy ExchangeSymbol; +legacy-кэша. +``` + +Build 008 имеет полную обратную совместимость. + +--- + +## 26. Классификация изменений + +| Изменение | Классификация | +|---|---| +| `DzengiInstrumentDocumentSource` | Обязательное архитектурное изменение | +| Переиспользование `ExchangeRestClient` | Временная переходная зависимость | +| `_PayloadRestClient` | Необходимый приватный structural contract | +| Dependency injection клиента | Улучшение тестируемости и типизации | +| Использование `get_payload()` | Обязательное разделение transport и schema validation | +| `InstrumentReferenceTransportError` в production adapter | Обязательная граница ошибок новой подсистемы | +| Exception chaining | Улучшение диагностируемости | +| Изменение legacy REST-клиента | Отсутствует | +| Изменение production-поведения | Отсутствует | + +--- + +## 27. Условие завершения Build 008 + +Все условия выполнены: + +```text +DzengiInstrumentDocumentSource + реализован; + +InstrumentDocumentSource + соблюдён; + +endpoint /api/v1/exchangeInfo + вызывается через ExchangeRestClient.get_payload(); + +сырой декодированный документ + возвращается без преобразования; + +transport exceptions + преобразуются в InstrumentReferenceTransportError; + +исходная причина ошибки + сохраняется через exception chaining; + +unit-тесты + не выполняют реальных сетевых запросов; + +полный pytest + проходит; + +adapter + не подключён к production runtime. +``` + +--- + +## 28. Итог Build 008 + +Build 008 завершён успешно. + +Реализовано: + +```text +DzengiInstrumentDocumentSource +_PayloadRestClient +transport error boundary +dependency injection REST-клиента +получение /api/v1/exchangeInfo через get_payload() +``` + +Подтверждено: + +```text +10 REST adapter tests passed +123 total project tests passed +Python compilation passed +No premature production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 008 — COMPLETE +``` + +--- + +## 29. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 009 — Instrument Handler +``` + +Его задача — соединить уже реализованный processing pipeline: + +```text +object + ↓ +validate_exchange_info_schema() + ↓ +parse_exchange_info() + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +в конкретную реализацию контракта: + +```text +InstrumentDocumentHandler +``` + +Предполагаемая архитектурная граница Build 009: + +```text +object + ↓ +Instrument Handler + ↓ +tuple[Instrument, ...] +``` + +Build 009 не должен: + +```text +самостоятельно выполнять HTTP-запросы; +создавать Instrument Feed; +создавать Registry; +создавать Acquisition Service; +изменять ExchangeService; +подключаться к production runtime. +``` \ No newline at end of file diff --git a/docs/migrations/build_009.md b/docs/migrations/build_009.md new file mode 100644 index 0000000..b79c6b4 --- /dev/null +++ b/docs/migrations/build_009.md @@ -0,0 +1,1275 @@ +# Build 009 — Instrument Handler + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное добавление processing handler без подключения к production runtime +**Результат полного набора тестов:** `132 passed` + +--- + +## 1. Цель Build 009 + +Цель Build 009 — реализовать конкретный обработчик документа Instrument Reference Data для формата Dzengi `exchangeInfo`, соответствующий созданному в Build 007 протоколу: + +```python +class InstrumentDocumentHandler(Protocol): + def handle_instrument_document( + self, + document: object, + ) -> tuple[Instrument, ...]: + ... +``` + +Реализован класс: + +```text +DzengiInstrumentDocumentHandler +``` + +Архитектурная граница Build 009: + +```text +object + ↓ +DzengiInstrumentDocumentHandler + ↓ +tuple[Instrument, ...] +``` + +Handler объединяет уже реализованные стадии обработки: + +```text +object + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +Build 009 не выполняет получение документа по сети, не создаёт Feed, не работает с Registry и не подключается к production runtime. + +--- + +## 2. Почему Build 009 выполняется именно сейчас + +До начала Build 009 были завершены: + +```text +Build 001 — внутренняя модель Instrument Reference Data +Build 002 — raw-модели ответа Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +Build 006 — mapper Dzengi → Instrument +Build 007 — Protocol и Exceptions +Build 008 — Dzengi REST Adapter +``` + +После Build 008 существовали две отдельные части будущей acquisition-цепочки. + +Получение документа: + +```text +Dzengi REST API + ↓ +ExchangeRestClient.get_payload() + ↓ +DzengiInstrumentDocumentSource + ↓ +object +``` + +Обработка документа: + +```text +object + ↓ +validate_exchange_info_schema() + ↓ +parse_exchange_info() + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +До Build 009 processing pipeline представлял собой набор отдельных функций. + +Следующим необходимым шагом стало объединение этих функций в конкретную реализацию контракта: + +```text +InstrumentDocumentHandler +``` + +--- + +## 3. Изменённые файлы + +В рамках Build 009 реализован production-файл: + +```text +app/src/market_data/acquisition/handlers/instrument_handler.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py +``` + +Другие production-файлы не изменялись. + +--- + +## 4. Реализованный Instrument Handler + +В файле: + +```text +app/src/market_data/acquisition/handlers/instrument_handler.py +``` + +реализован класс: + +```python +class DzengiInstrumentDocumentHandler: + ... +``` + +Его публичный метод: + +```python +def handle_instrument_document( + self, + document: object, +) -> tuple[Instrument, ...]: + ... +``` + +соответствует контракту: + +```text +InstrumentDocumentHandler +``` + +--- + +## 5. Реализованный processing pipeline + +Внутри `DzengiInstrumentDocumentHandler` последовательно вызываются четыре ранее реализованные стадии: + +```python +validated_document = validate_exchange_info_schema(document) + +response = parse_exchange_info(validated_document) + +validate_exchange_info_values(response) + +return map_dzengi_exchange_info_to_instruments(response) +``` + +Полная последовательность: + +```text +сырой декодированный JSON-документ + ↓ +schema validation + ↓ +проверенный структурный документ + ↓ +parser + ↓ +Dzengi raw-модели + ↓ +value validation + ↓ +проверенные Dzengi raw-модели + ↓ +mapper + ↓ +внутренние source-independent модели Instrument +``` + +Таким образом, Handler является orchestration boundary для обработки одного документа, но не реализует самостоятельно логику отдельных стадий. + +--- + +## 6. Разделение ответственности + +Каждая стадия сохраняет собственную ответственность. + +### Schema Validation + +```text +validate_exchange_info_schema() +``` + +Отвечает за структуру JSON-документа: + +```text +корневой тип; +wrapped/unwrapped формат; +payload; +symbols; +filters; +типы JSON-полей. +``` + +### Parser + +```text +parse_exchange_info() +``` + +Отвечает за преобразование проверенного документа в: + +```text +DzengiExchangeInfoResponse +DzengiExchangeInfoPayload +DzengiExchangeInfoSymbol +DzengiInstrumentFilter +``` + +### Value Validation + +```text +validate_exchange_info_values() +``` + +Отвечает за допустимость значений: + +```text +обязательные непустые строки; +числовые ограничения; +finite numbers; +положительные значения; +отношение minQty ≤ maxQty; +семантику filters. +``` + +### Mapper + +```text +map_dzengi_exchange_info_to_instruments() +``` + +Отвечает за преобразование source-specific raw-моделей Dzengi во внутренние source-independent модели: + +```text +Instrument +``` + +### Instrument Handler + +```text +DzengiInstrumentDocumentHandler +``` + +Отвечает только за правильную последовательность вызова этих стадий. + +--- + +## 7. Почему Handler является source-specific + +Конкретный Handler использует: + +```text +Dzengi parser +Dzengi raw models +Dzengi mapper +``` + +Поэтому класс явно называется: + +```text +DzengiInstrumentDocumentHandler +``` + +а не: + +```text +InstrumentHandler +``` + +Это сохраняет важную архитектурную границу: + +```text +общий Protocol + ↓ +InstrumentDocumentHandler + +конкретная source-specific реализация + ↓ +DzengiInstrumentDocumentHandler +``` + +В будущем другой источник может предоставить собственную реализацию: + +```text +AnotherExchangeInstrumentDocumentHandler +``` + +при сохранении общего контракта: + +```text +InstrumentDocumentHandler +``` + +--- + +## 8. Отсутствие нового Handler exception + +В Build 009 намеренно не создавалась ошибка: + +```text +InstrumentReferenceHandlerError +``` + +Handler сохраняет без дополнительного wrapping специализированные ошибки отдельных стадий: + +```text +InstrumentReferenceSchemaError +InstrumentReferenceParseError +InstrumentReferenceValueError +InstrumentReferenceMappingError +``` + +Это позволяет точно определить этап отказа: + +```text +Schema Error + → документ имеет неверную структуру + +Parse Error + → проверенный документ невозможно корректно преобразовать + в Dzengi raw-модели + +Value Error + → структура корректна, но значения недопустимы + +Mapping Error + → raw-модель корректна, но её невозможно однозначно + преобразовать во внутреннюю модель Instrument +``` + +Добавление общей Handler-ошибки поверх этих исключений ухудшило бы диагностируемость. + +--- + +## 9. Отсутствие try/except в Handler + +В `DzengiInstrumentDocumentHandler` намеренно отсутствует: + +```python +try: + ... +except Exception: + ... +``` + +Handler не скрывает и не переклассифицирует ошибки нижележащих стадий. + +Например: + +```text +validate_exchange_info_schema() + ↓ +InstrumentReferenceSchemaError + ↓ +выходит из Handler без изменения +``` + +Аналогично: + +```text +parse_exchange_info() + ↓ +InstrumentReferenceParseError +``` + +```text +validate_exchange_info_values() + ↓ +InstrumentReferenceValueError +``` + +```text +map_dzengi_exchange_info_to_instruments() + ↓ +InstrumentReferenceMappingError +``` + +--- + +## 10. Отсутствие Dependency Injection функций pipeline + +В Build 009 не добавлялась передача через конструктор: + +```text +schema validator; +parser; +value validator; +mapper. +``` + +Причины: + +```text +все четыре компонента уже реализованы как чистые функции; +существует одна утверждённая последовательность обработки; +unit-тесты могут проверять реальный pipeline; +нет нескольких production-реализаций этих стадий; +нет текущей необходимости конфигурировать pipeline. +``` + +Добавление dependency injection четырёх функций на текущем этапе создало бы лишнюю абстракцию без практической потребности. + +--- + +## 11. Поддержка wrapped и unwrapped документов + +Handler поддерживает оба формата, уже утверждённые в Build 003. + +### Unwrapped + +```json +{ + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [] +} +``` + +### Wrapped + +```json +{ + "status": "OK", + "correlationId": "2", + "payload": { + "timezone": "UTC", + "serverTime": 1783537921471, + "rateLimits": [], + "exchangeFilters": [], + "symbols": [] + } +} +``` + +Handler не реализует отдельную логику определения формата. + +Эта ответственность остаётся у: + +```text +validate_exchange_info_schema() +``` + +--- + +## 12. Возвращаемый тип + +Handler возвращает: + +```python +tuple[Instrument, ...] +``` + +а не: + +```python +list[Instrument] +``` + +Это соответствует: + +```text +InstrumentDocumentHandler Protocol +``` + +и принятой модели immutable sequences в новой подсистеме. + +При отсутствии инструментов: + +```json +{ + "symbols": [] +} +``` + +возвращается: + +```python +() +``` + +--- + +## 13. Точное преобразование числовых значений + +Тестами подтверждено, что после прохождения полного Handler pipeline числовые значения внутренней модели представлены через: + +```python +Decimal +``` + +Проверены: + +```text +tick_size +tick_value +step_size +min_qty +max_qty +min_notional +``` + +Пример: + +```python +assert instrument.tick_size == Decimal("0.05") +assert instrument.tick_value == Decimal("3878.86") +assert instrument.step_size == Decimal("0.0001") +assert instrument.min_qty == Decimal("0.0001") +assert instrument.max_qty == Decimal("1000") +assert instrument.min_notional == Decimal("1") +``` + +Это подтверждает, что Handler корректно проводит документ через уже реализованный mapper без потери точности внутренней модели. + +--- + +## 14. Тестирование специализированных ошибок + +Тестами проверено прохождение четырёх специализированных типов ошибок. + +### Schema Error + +Использован невалидный корневой документ: + +```python +[] +``` + +Ожидаемая ошибка: + +```text +InstrumentReferenceSchemaError +``` + +Ошибка возникает на стадии: + +```text +validate_exchange_info_schema() +``` + +--- + +### Parse Error + +Использовано: + +```python +"baseAssetPrecision": True +``` + +Значение проходит структурную границу JSON, но не может быть принято parser как корректное целочисленное значение precision. + +Ожидаемая ошибка: + +```text +InstrumentReferenceParseError +``` + +Ошибка возникает на стадии: + +```text +parse_exchange_info() +``` + +--- + +### Value Error + +Использовано: + +```python +"tickSize": 0 +``` + +Документ успешно проходит: + +```text +schema validation +parser +``` + +но отклоняется: + +```text +value validation +``` + +Ожидаемая ошибка: + +```text +InstrumentReferenceValueError +``` + +--- + +### Mapping Error + +Для проверки mapper использованы два корректных фильтра: + +```text +LOT_SIZE +LOT_SIZE +``` + +Каждый фильтр отдельно является корректным: + +```json +{ + "filterType": "LOT_SIZE", + "minQty": "0.0001", + "maxQty": "1000", + "stepSize": "0.0001" +} +``` + +Документ успешно проходит: + +```text +schema validation +parser +value validation +``` + +но mapper обнаруживает неоднозначность: + +```text +несколько фильтров LOT_SIZE +``` + +и выбрасывает: + +```text +InstrumentReferenceMappingError +``` + +--- + +## 15. Исправление первоначального теста Mapping Error + +Первоначально тест использовал: + +```python +"minQty": None +``` + +и ожидал: + +```text +InstrumentReferenceMappingError +``` + +Однако тест завершился ошибкой: + +```text +Failed: DID NOT RAISE InstrumentReferenceMappingError +``` + +Анализ показал, что по текущему контракту: + +```text +minQty=None +``` + +является допустимым значением. + +Последовательность была корректной: + +```text +parser + допускает None + +value validation + допускает отсутствующий minQty + +mapper + преобразует None → None +``` + +Следовательно, production-код работал правильно, а неверным был тестовый сценарий. + +Тест был исправлен без изменения production-кода. + +Вместо `minQty=None` использованы два корректных `LOT_SIZE`, что гарантированно вызывает ошибку именно на mapper. + +--- + +## 16. Что Handler не делает + +Build 009 сознательно не выполняет: + +```text +HTTP-запросы; +создание DzengiInstrumentDocumentSource; +создание Instrument Feed; +создание Registry; +создание Acquisition Service; +кэширование; +retry; +логирование; +формирование Telegram-сообщений; +проверку exchange_enabled; +создание legacy ExchangeSymbol; +изменение ExchangeService; +production-подключение. +``` + +Handler не знает: + +```text +откуда был получен документ; +когда был получен документ; +нужно ли повторять запрос; +куда сохранять результат; +кто является потребителем результата. +``` + +Его единственная ответственность: + +```text +object + ↓ +полный processing pipeline + ↓ +tuple[Instrument, ...] +``` + +--- + +## 17. Отсутствие transport-зависимости + +`DzengiInstrumentDocumentHandler` не импортирует: + +```text +DzengiInstrumentDocumentSource +ExchangeRestClient +urllib +httpx +requests +``` + +Он не выполняет сетевые операции. + +Архитектурная граница остаётся: + +```text +Build 008 + ↓ +DzengiInstrumentDocumentSource + ↓ +object + +Build 009 + ↓ +DzengiInstrumentDocumentHandler + ↓ +tuple[Instrument, ...] +``` + +Соединение Source и Handler относится к следующему этапу: + +```text +Build 010 — Instrument Feed +``` + +--- + +## 18. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/handlers/test_instrument_handler.py +``` + +Реализовано 9 тестов. + +Проверены следующие сценарии: + +1. `DzengiInstrumentDocumentHandler` соответствует `InstrumentDocumentHandler`; +2. корректно обрабатывается valid unwrapped document; +3. корректно обрабатывается valid wrapped document; +4. возвращаются точные `Decimal`-значения; +5. для пустого `symbols` возвращается пустой `tuple`; +6. сохраняется `InstrumentReferenceSchemaError`; +7. сохраняется `InstrumentReferenceParseError`; +8. сохраняется `InstrumentReferenceValueError`; +9. сохраняется `InstrumentReferenceMappingError`. + +--- + +## 19. Выполненные проверки + +### Проверка 1 — unit-тесты Instrument Handler + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/handlers/test_instrument_handler.py \ + -q +``` + +Первоначальный результат: + +```text +........F [100%] + +FAILED test_handler_preserves_mapping_error +1 failed, 8 passed in 0.04s +``` + +Причина: + +```text +тест ожидал InstrumentReferenceMappingError для minQty=None, +но такое значение допустимо текущим контрактом. +``` + +Production-код не изменялся. + +После исправления тестового сценария: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/handlers/test_instrument_handler.py \ + -q +``` + +получен результат: + +```text +......... [100%] +9 passed in 0.02s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/handlers/instrument_handler.py \ + tests/unit/market_data/acquisition/handlers/test_instrument_handler.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +.................................................................................................................................... [100%] +132 passed in 0.08s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — отсутствие преждевременной production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "DzengiInstrumentDocumentHandler|InstrumentDocumentHandler" \ + src tests +``` + +Полученные production-использования: + +```text +src/market_data/acquisition/protocol.py: + InstrumentDocumentHandler + +src/market_data/acquisition/handlers/instrument_handler.py: + DzengiInstrumentDocumentHandler +``` + +Остальные использования находятся исключительно в unit-тестах: + +```text +tests/unit/market_data/acquisition/test_protocol.py +tests/unit/market_data/acquisition/handlers/test_instrument_handler.py +``` + +Не обнаружено подключения к: + +```text +ExchangeService +Telegram UI +AutoTrade +Trading runtime +другим production-потребителям +``` + +Статус: + +```text +PASSED +``` + +--- + +## 20. Архитектура после Build 009 + +После завершения Build 009 новая часть подсистемы имеет следующую структуру: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ ├── MarketDataAcquisitionError + │ ├── InstrumentReferenceTransportError + │ ├── InstrumentReferenceSchemaError + │ ├── InstrumentReferenceParseError + │ ├── InstrumentReferenceValueError + │ └── InstrumentReferenceMappingError + │ + ├── protocol.py + │ ├── InstrumentDocumentSource + │ ├── InstrumentDocumentHandler + │ └── InstrumentFeedProtocol + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ └── values.py + │ + ├── handlers/ + │ └── instrument_handler.py + │ └── DzengiInstrumentDocumentHandler + │ + └── adapters/ + └── dzengi/ + ├── models.py + ├── parser.py + ├── mapper.py + └── rest.py + ├── _PayloadRestClient + └── DzengiInstrumentDocumentSource +``` + +На текущем этапе ещё не реализованы: + +```text +feeds/instrument_feed.py +registry.py +service.py +``` + +--- + +## 21. Полная архитектурная цепочка после Build 009 + +После Build 009 реализованы две изолированные части. + +### Transport + +```text +Dzengi REST API + ↓ +ExchangeRestClient.get_payload() + ↓ +DzengiInstrumentDocumentSource + ↓ +object +``` + +### Processing + +```text +object + ↓ +DzengiInstrumentDocumentHandler + │ + ├── validate_exchange_info_schema() + │ ↓ + ├── parse_exchange_info() + │ ↓ + ├── validate_exchange_info_values() + │ ↓ + └── map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +Они пока намеренно не соединены в production-коде. + +Следующая архитектурная связь: + +```text +DzengiInstrumentDocumentSource + ↓ +DzengiInstrumentDocumentHandler +``` + +будет реализована через общий Protocol в: + +```text +Build 010 — Instrument Feed +``` + +--- + +## 22. Влияние на legacy-систему + +Build 009 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +``` + +Не изменены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +normalize_symbol() +symbol_candidates() +``` + +Старый production-путь продолжает работать без изменений. + +Новый Handler существует изолированно и пока вызывается только unit-тестами. + +--- + +## 23. Обратная совместимость + +Подтверждено сохранение: + +```text +сигнатур существующих методов; +старых импортов; +существующего формата legacy-ошибок; +Telegram UI; +автоторговли; +runtime-поведения; +legacy ExchangeSymbol; +legacy-кэша. +``` + +Build 009 имеет полную обратную совместимость. + +--- + +## 24. Классификация изменений + +| Изменение | Классификация | +|---|---| +| `DzengiInstrumentDocumentHandler` | Обязательное архитектурное изменение | +| Объединение schema → parser → values → mapper | Обязательное архитектурное изменение | +| Сохранение специализированных ошибок | Улучшение диагностируемости | +| Новый Handler exception | Не создаётся | +| Dependency injection функций pipeline | Не добавляется | +| HTTP-зависимость | Отсутствует | +| Изменение legacy-кода | Отсутствует | +| Изменение production-поведения | Отсутствует | + +--- + +## 25. Условие завершения Build 009 + +Все условия выполнены: + +```text +DzengiInstrumentDocumentHandler + реализован; + +InstrumentDocumentHandler + соблюдён; + +schema validation + выполняется первой; + +parser + получает только структурно проверенный документ; + +value validation + выполняется после parser; + +mapper + выполняется только после успешной value validation; + +tuple[Instrument, ...] + возвращается как итоговый результат; + +специализированные ошибки + сохраняются без дополнительного wrapping; + +HTTP-зависимость + отсутствует; + +unit-тесты + проходят; + +полный pytest + проходит; + +Handler + не подключён к production runtime. +``` + +--- + +## 26. Итог Build 009 + +Build 009 завершён успешно. + +Реализовано: + +```text +DzengiInstrumentDocumentHandler + +object + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +tuple[Instrument, ...] +``` + +Подтверждено: + +```text +9 Instrument Handler tests passed +132 total project tests passed +Python compilation passed +No premature production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 009 — COMPLETE +``` + +--- + +## 27. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 010 — Instrument Feed +``` + +Его задача — соединить уже реализованные общие контракты: + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler + ↓ +InstrumentFeedProtocol +``` + +Предполагаемая архитектурная граница Build 010: + +```text +InstrumentDocumentSource + ↓ +fetch_instrument_document() + ↓ +object + ↓ +InstrumentDocumentHandler + ↓ +handle_instrument_document() + ↓ +tuple[Instrument, ...] +``` + +Build 010 не должен: + +```text +создавать Registry; +создавать Acquisition Service; +изменять ExchangeService; +переключать legacy get_exchange_symbols(); +создавать compatibility mapper; +переносить кэш; +подключаться к Telegram UI; +изменять AutoTrade или trading runtime. +``` \ No newline at end of file diff --git a/docs/migrations/build_010.md b/docs/migrations/build_010.md new file mode 100644 index 0000000..6673b65 --- /dev/null +++ b/docs/migrations/build_010.md @@ -0,0 +1,1411 @@ +# Build 010 — Instrument Feed + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное добавление orchestration-компонента Feed без подключения к production runtime +**Результат полного набора тестов:** `142 passed` + +--- + +## 1. Цель Build 010 + +Цель Build 010 — реализовать общий Instrument Feed, соединяющий уже существующие контракты: + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler +``` + +и предоставляющий единый интерфейс: + +```text +InstrumentFeedProtocol +``` + +Реализован класс: + +```text +InstrumentFeed +``` + +Архитектурная граница Build 010: + +```text +InstrumentFeed.load_instruments() + ↓ +InstrumentDocumentSource.fetch_instrument_document() + ↓ +object + ↓ +InstrumentDocumentHandler.handle_instrument_document() + ↓ +tuple[Instrument, ...] +``` + +Build 010 не создаёт Registry, Acquisition Service, compatibility mapper и не подключается к существующему `ExchangeService`. + +--- + +## 2. Почему Build 010 выполняется именно сейчас + +До начала Build 010 были завершены: + +```text +Build 001 — внутренняя модель Instrument Reference Data +Build 002 — raw-модели ответа Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +Build 006 — mapper Dzengi → Instrument +Build 007 — Protocol и Exceptions +Build 008 — Dzengi REST Adapter +Build 009 — Instrument Handler +``` + +После Build 009 существовали две независимые части acquisition pipeline. + +### Получение документа + +```text +Dzengi REST API + ↓ +ExchangeRestClient.get_payload() + ↓ +DzengiInstrumentDocumentSource + ↓ +object +``` + +### Обработка документа + +```text +object + ↓ +DzengiInstrumentDocumentHandler + ↓ +validate_exchange_info_schema() + ↓ +parse_exchange_info() + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +До Build 010 эти две части не были соединены общим orchestration-компонентом. + +Build 010 реализовал эту связь через абстрактные контракты: + +```text +InstrumentDocumentSource +InstrumentDocumentHandler +``` + +без зависимости самого Feed от конкретной биржи или формата документа. + +--- + +## 3. Изменённые файлы + +В рамках Build 010 реализован production-файл: + +```text +app/src/market_data/acquisition/feeds/instrument_feed.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py +``` + +Другие production-файлы не изменялись. + +--- + +## 4. Реализованный Instrument Feed + +В файле: + +```text +app/src/market_data/acquisition/feeds/instrument_feed.py +``` + +реализован класс: + +```python +class InstrumentFeed: + ... +``` + +Его публичный метод: + +```python +def load_instruments(self) -> tuple[Instrument, ...]: + ... +``` + +соответствует контракту: + +```text +InstrumentFeedProtocol +``` + +--- + +## 5. Зависимости Instrument Feed + +`InstrumentFeed` получает две обязательные зависимости: + +```python +def __init__( + self, + *, + source: InstrumentDocumentSource, + handler: InstrumentDocumentHandler, +) -> None: + ... +``` + +Первая зависимость: + +```text +InstrumentDocumentSource +``` + +отвечает за получение исходного документа. + +Вторая зависимость: + +```text +InstrumentDocumentHandler +``` + +отвечает за преобразование исходного документа во внутренние модели: + +```text +tuple[Instrument, ...] +``` + +Обе зависимости передаются явно через конструктор. + +--- + +## 6. Реализованный orchestration pipeline + +Метод: + +```python +load_instruments() +``` + +выполняет только две операции: + +```python +document = self._source.fetch_instrument_document() + +return self._handler.handle_instrument_document(document) +``` + +Полная последовательность: + +```text +InstrumentFeed.load_instruments() + ↓ +source.fetch_instrument_document() + ↓ +object + ↓ +handler.handle_instrument_document(document) + ↓ +tuple[Instrument, ...] +``` + +Feed не реализует самостоятельно transport, parsing, validation или mapping. + +--- + +## 7. Разделение ответственности + +После Build 010 обязанности компонентов разделены следующим образом. + +### InstrumentDocumentSource + +Отвечает за: + +```text +получение документа от внешнего источника. +``` + +Конкретная реализация: + +```text +DzengiInstrumentDocumentSource +``` + +--- + +### InstrumentDocumentHandler + +Отвечает за: + +```text +преобразование исходного документа +во внутренние модели Instrument. +``` + +Конкретная реализация: + +```text +DzengiInstrumentDocumentHandler +``` + +--- + +### InstrumentFeed + +Отвечает только за: + +```text +получить документ через Source; +передать документ Handler; +вернуть результат Handler. +``` + +Feed не знает внутренней реализации Source и Handler. + +--- + +## 8. Независимость Instrument Feed от Dzengi + +В `instrument_feed.py` отсутствуют прямые зависимости от: + +```text +DzengiInstrumentDocumentSource +DzengiInstrumentDocumentHandler +ExchangeRestClient +DzengiExchangeInfoResponse +DzengiExchangeInfoSymbol +Dzengi parser +Dzengi mapper +Dzengi validation pipeline +``` + +Feed зависит только от общих контрактов: + +```text +InstrumentDocumentSource +InstrumentDocumentHandler +``` + +и внутренней модели: + +```text +Instrument +``` + +Архитектурная зависимость: + +```text +InstrumentFeed + ↓ +InstrumentDocumentSource Protocol + ↓ +конкретная реализация определяется снаружи +``` + +```text +InstrumentFeed + ↓ +InstrumentDocumentHandler Protocol + ↓ +конкретная реализация определяется снаружи +``` + +Таким образом, Feed остаётся source-independent orchestration-компонентом. + +--- + +## 9. Почему Feed не создаёт зависимости самостоятельно + +В Build 010 намеренно не используется: + +```python +self._source = DzengiInstrumentDocumentSource() +self._handler = DzengiInstrumentDocumentHandler() +``` + +Такое решение напрямую связало бы общий Feed с конкретным источником Dzengi. + +Вместо этого используется явная dependency injection: + +```python +InstrumentFeed( + source=source, + handler=handler, +) +``` + +Это обеспечивает: + +```text +независимость от конкретной биржи; +явные архитектурные зависимости; +тестируемость без сети; +возможность другой реализации Source; +возможность другой реализации Handler; +отсутствие скрытой composition logic. +``` + +Создание конкретной production-композиции не относится к ответственности Feed. + +--- + +## 10. Почему аргументы конструктора keyword-only + +Конструктор объявлен как: + +```python +def __init__( + self, + *, + source: InstrumentDocumentSource, + handler: InstrumentDocumentHandler, +) -> None: + ... +``` + +Символ: + +```text +* +``` + +делает аргументы keyword-only. + +Корректное создание: + +```python +InstrumentFeed( + source=source, + handler=handler, +) +``` + +Это явно показывает роли зависимостей и исключает позиционную неоднозначность. + +--- + +## 11. Отсутствие нового Feed exception + +В Build 010 намеренно не создавалась ошибка: + +```text +InstrumentReferenceFeedError +``` + +Feed сохраняет без дополнительного wrapping специализированные ошибки нижележащих компонентов: + +```text +InstrumentReferenceTransportError +InstrumentReferenceSchemaError +InstrumentReferenceParseError +InstrumentReferenceValueError +InstrumentReferenceMappingError +``` + +Это сохраняет точную диагностическую классификацию отказов. + +--- + +## 12. Отсутствие try/except в Instrument Feed + +В `load_instruments()` намеренно отсутствует: + +```python +try: + ... +except Exception: + ... +``` + +Если Source выбрасывает: + +```text +InstrumentReferenceTransportError +``` + +Feed передаёт тот же объект исключения вызывающему коду. + +Если Handler выбрасывает: + +```text +InstrumentReferenceSchemaError +InstrumentReferenceParseError +InstrumentReferenceValueError +InstrumentReferenceMappingError +``` + +Feed также передаёт исходное исключение без изменения. + +Цепочка: + +```text +нижний компонент + ↓ +специализированное исключение + ↓ +InstrumentFeed + ↓ +то же исключение + ↓ +внешний потребитель +``` + +--- + +## 13. Поведение при transport error + +Если: + +```text +source.fetch_instrument_document() +``` + +выбрасывает: + +```text +InstrumentReferenceTransportError +``` + +то: + +```text +ошибка передаётся без wrapping; +handler не вызывается; +повторный запрос не выполняется. +``` + +Последовательность: + +```text +InstrumentFeed.load_instruments() + ↓ +source.fetch_instrument_document() + ↓ +InstrumentReferenceTransportError + ↓ +выполнение прекращается +``` + +Handler в этом случае не получает документ. + +--- + +## 14. Поведение при processing error + +Если Source успешно возвращает документ: + +```text +object +``` + +но Handler выбрасывает специализированную processing-ошибку, например: + +```text +InstrumentReferenceValueError +``` + +то Feed: + +```text +не перехватывает ошибку; +не заменяет её другим типом; +не повторяет Source; +не повторяет Handler. +``` + +Последовательность: + +```text +source + ↓ +document + ↓ +handler + ↓ +InstrumentReferenceValueError + ↓ +внешний потребитель +``` + +--- + +## 15. Отсутствие retry + +Build 010 не реализует retry. + +Если Source завершился ошибкой: + +```text +InstrumentReferenceTransportError +``` + +Feed не выполняет: + +```text +повторный REST-запрос; +задержку; +backoff; +повторный вызов Source. +``` + +В рамках одного вызова: + +```python +feed.load_instruments() +``` + +Source вызывается ровно один раз. + +Retry policy, если она потребуется, должна находиться на другом архитектурном уровне и не должна скрыто появляться внутри базового Feed. + +--- + +## 16. Передача документа без изменения + +Документ, возвращённый Source: + +```python +document = self._source.fetch_instrument_document() +``` + +передаётся непосредственно Handler: + +```python +self._handler.handle_instrument_document(document) +``` + +Feed не выполняет: + +```text +копирование; +преобразование; +нормализацию; +фильтрацию; +валидацию; +оборачивание; +изменение структуры. +``` + +Тестом подтверждено сохранение identity: + +```python +handler.documents[0] is document +``` + +--- + +## 17. Возврат результата без изменения + +Если Handler возвращает: + +```python +instruments: tuple[Instrument, ...] +``` + +Feed возвращает непосредственно тот же объект: + +```python +return self._handler.handle_instrument_document(document) +``` + +Feed не выполняет: + +```python +tuple(instruments) +``` + +или другое копирование коллекции. + +Тестом подтверждено: + +```python +result is instruments +``` + +Это гарантирует отсутствие скрытого преобразования результата. + +--- + +## 18. Сохранение порядка инструментов + +Feed не выполняет: + +```text +сортировку; +фильтрацию; +дедупликацию; +перегруппировку. +``` + +Если Handler возвращает: + +```text +BTC/USD_LEVERAGE +ETH/USD_LEVERAGE +XRP/USD_LEVERAGE +``` + +Feed возвращает инструменты в том же порядке: + +```text +BTC/USD_LEVERAGE +ETH/USD_LEVERAGE +XRP/USD_LEVERAGE +``` + +Ответственность за порядок данных не переносится в Feed. + +--- + +## 19. Поведение при пустом результате + +Если Handler возвращает: + +```python +() +``` + +Feed возвращает: + +```python +() +``` + +без ошибки. + +Feed не определяет, является ли пустой справочник: + +```text +допустимым состоянием; +временной ошибкой; +критическим отказом; +условием для сохранения предыдущего snapshot. +``` + +Такая policy logic не относится к ответственности Feed. + +--- + +## 20. Что Instrument Feed не делает + +Build 010 сознательно не выполняет: + +```text +REST-запросы самостоятельно; +парсинг JSON; +schema validation; +value validation; +mapping; +retry; +backoff; +кэширование; +фильтрацию инструментов; +сортировку инструментов; +удаление дубликатов; +нормализацию символов; +проверку exchange_enabled; +создание Registry; +создание Acquisition Service; +создание legacy ExchangeSymbol; +создание compatibility mapper; +логирование; +формирование Telegram-сообщений; +изменение ExchangeService; +изменение AutoTrade; +изменение trading runtime. +``` + +Единственная ответственность Feed: + +```text +Source + ↓ +Handler + ↓ +tuple[Instrument, ...] +``` + +--- + +## 21. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py +``` + +Реализовано 10 тестов. + +Проверены следующие сценарии: + +1. `InstrumentFeed` соответствует `InstrumentFeedProtocol`; +2. Source вызывается один раз; +3. Handler вызывается один раз; +4. документ передаётся Handler без изменения; +5. результат Handler возвращается без изменения; +6. порядок инструментов сохраняется; +7. пустой `tuple` возвращается без ошибки; +8. `InstrumentReferenceTransportError` сохраняется без wrapping; +9. processing error сохраняется без wrapping; +10. после transport error Source не вызывается повторно. + +--- + +## 22. Проверка соответствия InstrumentFeedProtocol + +Тестом подтверждено: + +```python +assert isinstance(feed, InstrumentFeedProtocol) +``` + +Это возможно благодаря: + +```text +@runtime_checkable +``` + +в определении Protocol и структурному соответствию метода: + +```python +load_instruments() -> tuple[Instrument, ...] +``` + +Feed не наследуется явно от Protocol. + +Используется structural typing: + +```text +если объект реализует требуемый контракт, +он соответствует Protocol. +``` + +--- + +## 23. Проверка вызова Source + +Тестовый Source ведёт счётчик: + +```python +self.call_count = 0 +``` + +При каждом вызове: + +```python +fetch_instrument_document() +``` + +значение увеличивается. + +После: + +```python +feed.load_instruments() +``` + +подтверждено: + +```python +assert source.call_count == 1 +``` + +Таким образом, Feed не выполняет скрытых повторных запросов. + +--- + +## 24. Проверка вызова Handler + +Тестовый Handler сохраняет полученные документы: + +```python +self.documents: list[object] = [] +``` + +При вызове: + +```python +handle_instrument_document(document) +``` + +документ добавляется в список. + +После: + +```python +feed.load_instruments() +``` + +подтверждено: + +```python +assert len(handler.documents) == 1 +``` + +Handler вызывается ровно один раз. + +--- + +## 25. Проверка transport error + +Создан исходный объект ошибки: + +```python +original_error = InstrumentReferenceTransportError( + "Не удалось получить exchangeInfo." +) +``` + +Source выбрасывает именно этот объект. + +Тест подтверждает: + +```python +assert exc_info.value is original_error +``` + +Это доказывает отсутствие: + +```text +wrapping; +замены исключения; +создания нового объекта ошибки. +``` + +Дополнительно подтверждено: + +```python +assert source.call_count == 1 +assert handler.documents == [] +``` + +То есть: + +```text +Source вызван один раз; +Handler не вызван. +``` + +--- + +## 26. Проверка processing error + +Создан исходный объект: + +```python +original_error = InstrumentReferenceValueError( + "Некорректное значение." +) +``` + +Handler выбрасывает именно этот объект. + +Тест подтверждает: + +```python +assert exc_info.value is original_error +``` + +Дополнительно: + +```python +assert source.call_count == 1 +assert handler.documents == [document] +``` + +Это подтверждает корректную последовательность: + +```text +Source успешно вызван один раз + ↓ +документ передан Handler + ↓ +Handler выбросил processing error + ↓ +ошибка вышла из Feed без изменения +``` + +--- + +## 27. Выполненные проверки + +### Проверка 1 — unit-тесты Instrument Feed + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/feeds/test_instrument_feed.py \ + -q +``` + +Результат: + +```text +.......... [100%] +10 passed in 0.01s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/feeds/instrument_feed.py \ + tests/unit/market_data/acquisition/feeds/test_instrument_feed.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +.............................................................................................................................................. [100%] +142 passed in 0.08s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — отсутствие преждевременной production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "InstrumentFeed|InstrumentFeedProtocol" \ + src tests +``` + +Полученные production-использования: + +```text +src/market_data/acquisition/protocol.py: + InstrumentFeedProtocol + +src/market_data/acquisition/feeds/instrument_feed.py: + InstrumentFeed +``` + +Остальные использования находятся исключительно в unit-тестах: + +```text +tests/unit/market_data/acquisition/test_protocol.py +tests/unit/market_data/acquisition/feeds/test_instrument_feed.py +``` + +Не обнаружено подключения к: + +```text +ExchangeService +Registry +Acquisition Service +Telegram UI +AutoTrade +Trading runtime +другим production-потребителям +``` + +Статус: + +```text +PASSED +``` + +--- + +## 28. Архитектура после Build 010 + +После завершения Build 010 новая часть подсистемы имеет следующую структуру: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ ├── MarketDataAcquisitionError + │ ├── InstrumentReferenceTransportError + │ ├── InstrumentReferenceSchemaError + │ ├── InstrumentReferenceParseError + │ ├── InstrumentReferenceValueError + │ └── InstrumentReferenceMappingError + │ + ├── protocol.py + │ ├── InstrumentDocumentSource + │ ├── InstrumentDocumentHandler + │ └── InstrumentFeedProtocol + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ └── values.py + │ + ├── handlers/ + │ └── instrument_handler.py + │ └── DzengiInstrumentDocumentHandler + │ + ├── feeds/ + │ └── instrument_feed.py + │ └── InstrumentFeed + │ + └── adapters/ + └── dzengi/ + ├── models.py + ├── parser.py + ├── mapper.py + └── rest.py + ├── _PayloadRestClient + └── DzengiInstrumentDocumentSource +``` + +На текущем этапе ещё не реализованы: + +```text +registry.py +service.py +``` + +--- + +## 29. Полная архитектурная цепочка после Build 010 + +После Build 010 реализована единая техническая цепочка: + +```text +Dzengi REST API + ↓ +ExchangeRestClient.get_payload() + ↓ +DzengiInstrumentDocumentSource + ↓ +object + ↓ +InstrumentFeed + ↓ +DzengiInstrumentDocumentHandler + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +При этом сам `InstrumentFeed` не знает, что используются: + +```text +DzengiInstrumentDocumentSource +DzengiInstrumentDocumentHandler +``` + +Для него зависимости представлены только общими контрактами: + +```text +InstrumentDocumentSource +InstrumentDocumentHandler +``` + +--- + +## 30. Что ещё не реализовано + +После Build 010 новая acquisition pipeline уже способна технически выполнить: + +```text +REST API + ↓ +document + ↓ +validation + ↓ +parsing + ↓ +value validation + ↓ +mapping + ↓ +tuple[Instrument, ...] +``` + +Однако пока отсутствуют: + +```text +Registry; +Acquisition Service; +официальная production composition; +equivalence verification с legacy implementation; +compatibility mapper Instrument → ExchangeSymbol; +переключение get_exchange_symbols(); +перевод normalize_symbol()/symbol_candidates(); +переключение validate_symbol(); +переключение get_symbol_runtime_status(); +перенос кэша. +``` + +Поэтому новая цепочка остаётся изолированной от существующего production runtime. + +--- + +## 31. Влияние на legacy-систему + +Build 010 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +Trading runtime +``` + +Не изменены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +normalize_symbol() +symbol_candidates() +``` + +Старый production-путь продолжает работать без изменений. + +--- + +## 32. Обратная совместимость + +Подтверждено сохранение: + +```text +сигнатур существующих методов; +старых импортов; +существующего формата legacy-ошибок; +Telegram UI; +автоторговли; +runtime-поведения; +legacy ExchangeSymbol; +legacy-кэша. +``` + +Build 010 имеет полную обратную совместимость. + +--- + +## 33. Классификация изменений + +| Изменение | Классификация | +|---|---| +| `InstrumentFeed` | Обязательное архитектурное изменение | +| Соединение Source и Handler | Обязательное архитектурное изменение | +| Dependency injection Source и Handler | Обязательное разделение ответственности | +| Keyword-only зависимости | Улучшение надёжности API | +| Сохранение специализированных ошибок | Улучшение диагностируемости | +| Новый Feed exception | Не создаётся | +| Retry | Не добавляется | +| Кэширование | Не добавляется | +| Фильтрация и сортировка | Не выполняются | +| Изменение legacy-кода | Отсутствует | +| Изменение production-поведения | Отсутствует | + +--- + +## 34. Условие завершения Build 010 + +Все условия выполнены: + +```text +InstrumentFeed + реализован; + +InstrumentFeedProtocol + соблюдён; + +InstrumentDocumentSource + передаётся как явная зависимость; + +InstrumentDocumentHandler + передаётся как явная зависимость; + +Source + вызывается ровно один раз; + +Handler + вызывается ровно один раз после успешного Source; + +документ + передаётся Handler без изменения; + +результат + возвращается без копирования и преобразования; + +порядок инструментов + сохраняется; + +пустой tuple + поддерживается; + +специализированные ошибки + сохраняются без wrapping; + +retry + отсутствует; + +Dzengi-зависимости в Feed + отсутствуют; + +unit-тесты + проходят; + +полный pytest + проходит; + +Feed + не подключён к production runtime. +``` + +--- + +## 35. Итог Build 010 + +Build 010 завершён успешно. + +Реализовано: + +```text +InstrumentFeed + +InstrumentDocumentSource + ↓ +fetch_instrument_document() + ↓ +object + ↓ +InstrumentDocumentHandler + ↓ +handle_instrument_document() + ↓ +tuple[Instrument, ...] +``` + +Подтверждено: + +```text +10 Instrument Feed tests passed +142 total project tests passed +Python compilation passed +No premature production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 010 — COMPLETE +``` + +--- + +## 36. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 011 — Registry +``` +Registry хранит доступные Instrument Feed и предоставляет нужный Feed по идентификатору источника. +Хранение актуального справочника относится к будущему переносу кэша: + +```text +Build 019 — подготовка переноса кэша +Build 020 — перенос кэша в Storage +``` + +Предполагаемая архитектурная граница: + +```text +tuple[Instrument, ...] + ↓ +Instrument Registry + ↓ +доступ к актуальному справочнику инструментов +``` + +Build 011 не должен: + +```text +создавать Acquisition Service; +изменять ExchangeService; +переключать legacy get_exchange_symbols(); +создавать compatibility mapper; +переносить legacy-кэш; +подключаться к Telegram UI; +изменять AutoTrade; +изменять trading runtime. +``` \ No newline at end of file diff --git a/docs/migrations/build_011.md b/docs/migrations/build_011.md new file mode 100644 index 0000000..1415601 --- /dev/null +++ b/docs/migrations/build_011.md @@ -0,0 +1,1602 @@ +# Build 011 — Instrument Feed Registry + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное добавление Registry для регистрации и получения Instrument Feed без подключения к production runtime +**Результат полного набора тестов:** `167 passed` + +--- + +## 1. Цель Build 011 + +Цель Build 011 — реализовать Registry для регистрации и получения доступных Instrument Feed по идентификатору источника. + +Реализован класс: + +```text +InstrumentFeedRegistry +``` + +Его архитектурная граница: + +```text +source_name + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol +``` + +Registry позволяет: + +```text +зарегистрировать Feed; +получить Feed по имени источника; +запретить неявную повторную регистрацию; +явно сообщить об отсутствии запрошенного Feed. +``` + +Build 011 не хранит модели `Instrument`, не выполняет acquisition, не создаёт кэш и не подключается к существующему `ExchangeService`. + +--- + +## 2. Почему Build 011 выполняется именно сейчас + +До начала Build 011 были завершены: + +```text +Build 001 — внутренняя модель Instrument Reference Data +Build 002 — raw-модели ответа Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +Build 006 — mapper Dzengi → Instrument +Build 007 — Protocol и Exceptions +Build 008 — Dzengi REST Adapter +Build 009 — Instrument Handler +Build 010 — Instrument Feed +``` + +После Build 010 уже существовала полная техническая цепочка: + +```text +Dzengi REST API + ↓ +DzengiInstrumentDocumentSource + ↓ +InstrumentFeed + ↓ +DzengiInstrumentDocumentHandler + ↓ +tuple[Instrument, ...] +``` + +Однако будущему `Acquisition Service` ещё требовался механизм получения нужного Feed без прямой зависимости от конкретной реализации. + +Build 011 создаёт эту границу: + +```text +Acquisition Service + ↓ +source_name + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol +``` + +--- + +## 3. Архитектурное уточнение ответственности Registry + +Registry в Build 011 не является хранилищем актуального справочника инструментов. + +Он не хранит: + +```text +tuple[Instrument, ...]; +последний успешный справочник; +предыдущий snapshot; +TTL; +время обновления; +возраст данных; +последнюю ошибку; +индекс символов; +legacy ExchangeSymbol. +``` + +Его единственная предметная ответственность: + +```text +source_name + → +InstrumentFeedProtocol +``` + +Хранение и кэширование актуальных данных относятся к другим архитектурным слоям и будущим Build: + +```text +Build 019 — подготовка переноса кэша в Storage +Build 020 — перенос кэша +``` + +--- + +## 4. Изменённые файлы + +В рамках Build 011 изменены: + +```text +app/src/market_data/acquisition/exceptions.py +app/src/market_data/acquisition/registry.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/test_registry.py +``` + +Другие production-файлы не изменялись. + +--- + +## 5. Реализованный InstrumentFeedRegistry + +В файле: + +```text +app/src/market_data/acquisition/registry.py +``` + +реализован класс: + +```python +class InstrumentFeedRegistry: + ... +``` + +Registry предоставляет два публичных метода: + +```python +def register( + self, + source_name: str, + feed: InstrumentFeedProtocol, +) -> None: + ... +``` + +и: + +```python +def get( + self, + source_name: str, +) -> InstrumentFeedProtocol: + ... +``` + +Минимальный публичный контракт: + +```text +register(source_name, feed) + ↓ +регистрация Feed + +get(source_name) + ↓ +получение зарегистрированного Feed +``` + +--- + +## 6. Внутреннее хранение + +Registry использует внутренний типизированный словарь: + +```python +dict[str, InstrumentFeedProtocol] +``` + +Архитектурная схема: + +```text +"dzengi" + ↓ +InstrumentFeedProtocol +``` + +Пример: + +```text +{ + "dzengi": +} +``` + +Внутренний словарь: + +```text +не возвращается наружу; +не содержит Instrument; +не является предметным кэшем; +не содержит результатов Feed; +используется только как индекс зарегистрированных Feed. +``` + +--- + +## 7. Регистрация Feed + +Метод: + +```python +register( + source_name: str, + feed: InstrumentFeedProtocol, +) -> None +``` + +выполняет следующую последовательность: + +```text +получить source_name + ↓ +удалить внешние пробелы + ↓ +проверить непустое имя + ↓ +проверить соответствие InstrumentFeedProtocol + ↓ +проверить отсутствие существующей регистрации + ↓ +сохранить Feed +``` + +Пример: + +```python +registry.register( + "dzengi", + feed, +) +``` + +После этого: + +```python +registry.get("dzengi") +``` + +возвращает тот же объект `feed`. + +--- + +## 8. Получение Feed + +Метод: + +```python +get( + source_name: str, +) -> InstrumentFeedProtocol +``` + +выполняет: + +```text +получить source_name + ↓ +удалить внешние пробелы + ↓ +проверить непустое имя + ↓ +найти зарегистрированный Feed + ↓ +вернуть тот же объект Feed +``` + +Если Feed отсутствует, выбрасывается: + +```text +InstrumentFeedRegistryError +``` + +Registry не создаёт Feed автоматически и не пытается использовать default source. + +--- + +## 9. Сохранение identity Feed + +Registry сохраняет и возвращает исходный объект Feed без: + +```text +копирования; +оборачивания; +создания proxy; +создания нового Feed; +изменения объекта. +``` + +Если зарегистрирован: + +```python +feed = StubInstrumentFeed() + +registry.register("dzengi", feed) +``` + +то: + +```python +registry.get("dzengi") is feed +``` + +равно: + +```text +True +``` + +Это подтверждено unit-тестами. + +--- + +## 10. Проверка InstrumentFeedProtocol + +Перед регистрацией выполняется: + +```python +isinstance(feed, InstrumentFeedProtocol) +``` + +Это возможно благодаря тому, что `InstrumentFeedProtocol` объявлен как runtime-checkable Protocol. + +Архитектурная проверка: + +```text +объект + ↓ +соответствует InstrumentFeedProtocol? + ├── да → регистрация разрешена + └── нет → InstrumentFeedRegistryError +``` + +Registry не требует явного наследования от Protocol. + +Используется structural typing: + +```text +если объект реализует требуемый контракт, +он соответствует Protocol. +``` + +--- + +## 11. Registry не вызывает Feed + +При выполнении: + +```python +registry.register("dzengi", feed) +``` + +не вызывается: + +```python +feed.load_instruments() +``` + +При выполнении: + +```python +registry.get("dzengi") +``` + +также не вызывается: + +```python +feed.load_instruments() +``` + +Registry только хранит и возвращает Feed. + +Цепочка: + +```text +register() + ↓ +сохранить ссылку на Feed + +get() + ↓ +вернуть ссылку на Feed +``` + +Отсутствует: + +```text +load_instruments() +``` + +Это подтверждено отдельными unit-тестами. + +--- + +## 12. Нормализация имени источника + +Registry выполняет только: + +```python +source_name.strip() +``` + +Пример: + +```text +" dzengi " + ↓ +"dzengi" +``` + +Поэтому после: + +```python +registry.register(" dzengi ", feed) +``` + +оба вызова: + +```python +registry.get("dzengi") +registry.get(" dzengi ") +``` + +возвращают тот же Feed. + +--- + +## 13. Что Registry не делает с именем источника + +Registry намеренно не выполняет: + +```text +lower() +casefold() +replace() +alias resolution +automatic source mapping +``` + +Поэтому: + +```text +"dzengi" +``` + +и: + +```text +"DZENGI" +``` + +являются разными ключами. + +Можно одновременно зарегистрировать: + +```python +registry.register("dzengi", first_feed) +registry.register("DZENGI", second_feed) +``` + +После этого: + +```text +get("dzengi") + ↓ +first_feed + +get("DZENGI") + ↓ +second_feed +``` + +Такое поведение исключает скрытую нормализацию без утверждённого контракта. + +--- + +## 14. Запрет пустого имени источника + +Registry отклоняет: + +```text +"" +" " +" " +"\t" +"\n" +``` + +После: + +```python +source_name.strip() +``` + +такие значения становятся пустыми. + +Выбрасывается: + +```text +InstrumentFeedRegistryError +``` + +с сообщением: + +```text +Имя источника Instrument Feed не должно быть пустым. +``` + +Проверка действует как для: + +```text +register() +``` + +так и для: + +```text +get() +``` + +--- + +## 15. Запрет повторной регистрации + +Следующая последовательность запрещена: + +```python +registry.register("dzengi", first_feed) +registry.register("dzengi", second_feed) +``` + +Вторая операция выбрасывает: + +```text +InstrumentFeedRegistryError +``` + +с диагностикой: + +```text +Instrument Feed для источника 'dzengi' уже зарегистрирован. +``` + +Registry не выполняет молчаливую замену. + +--- + +## 16. Почему молчаливая замена запрещена + +Молчаливая операция: + +```text +existing Feed + ↓ +register same source name + ↓ +new Feed silently replaces old Feed +``` + +могла бы незаметно изменить production acquisition pipeline. + +Поэтому используется fail-fast поведение: + +```text +duplicate source name + ↓ +InstrumentFeedRegistryError +``` + +Если в будущем понадобится контролируемая замена Feed, она должна быть реализована отдельным явно определённым контрактом. + +В Build 011 такой контракт не нужен. + +--- + +## 17. Повторная регистрация после нормализации + +Проверка duplicate выполняется после: + +```python +source_name.strip() +``` + +Поэтому: + +```python +registry.register("dzengi", first_feed) +registry.register(" dzengi ", second_feed) +``` + +считается повторной регистрацией одного и того же источника. + +Вторая операция завершается: + +```text +InstrumentFeedRegistryError +``` + +Исходный Feed при этом сохраняется. + +--- + +## 18. Отсутствие молчаливой замены Feed + +После: + +```python +registry.register("dzengi", first_feed) +``` + +и неуспешной попытки: + +```python +registry.register("dzengi", second_feed) +``` + +результат: + +```python +registry.get("dzengi") is first_feed +``` + +остаётся: + +```text +True +``` + +Таким образом, ошибочная повторная регистрация не изменяет состояние Registry. + +--- + +## 19. Поведение для отсутствующего источника + +Если выполнить: + +```python +registry.get("dzengi") +``` + +до регистрации Feed, выбрасывается: + +```text +InstrumentFeedRegistryError +``` + +с диагностикой: + +```text +Instrument Feed для источника 'dzengi' не зарегистрирован. +``` + +Registry не: + +```text +возвращает None; +создаёт Feed автоматически; +выбирает default Feed; +выполняет fallback на другой источник. +``` + +Отсутствие Feed является явной ошибкой Registry. + +--- + +## 20. Новая ошибка InstrumentFeedRegistryError + +В файле: + +```text +app/src/market_data/acquisition/exceptions.py +``` + +добавлен класс: + +```python +class InstrumentFeedRegistryError(MarketDataAcquisitionError): + pass +``` + +Иерархия acquisition-ошибок после Build 011: + +```text +MarketDataAcquisitionError +├── InstrumentReferenceTransportError +├── InstrumentReferenceSchemaError +├── InstrumentReferenceParseError +├── InstrumentReferenceValueError +├── InstrumentReferenceMappingError +└── InstrumentFeedRegistryError +``` + +Ошибка используется для: + +```text +пустого имени источника; +объекта, не соответствующего InstrumentFeedProtocol; +повторной регистрации; +запроса отсутствующего Feed. +``` + +--- + +## 21. Почему не созданы отдельные Registry exceptions + +В Build 011 намеренно не добавлены: + +```text +DuplicateInstrumentFeedError +InstrumentFeedNotFoundError +InvalidInstrumentFeedNameError +InvalidInstrumentFeedError +``` + +На текущем этапе для них нет отдельных алгоритмов обработки. + +Все ошибки относятся к одной архитектурной категории: + +```text +Instrument Feed Registry error +``` + +Поэтому используется один класс: + +```text +InstrumentFeedRegistryError +``` + +Дополнительная детализация исключений без реальной необходимости только усложнила бы контракт. + +--- + +## 22. Dependency Injection + +Registry не создаёт Feed самостоятельно. + +Отсутствует: + +```python +self._feed = InstrumentFeed(...) +``` + +Также Registry не создаёт: + +```text +DzengiInstrumentDocumentSource +DzengiInstrumentDocumentHandler +ExchangeRestClient +``` + +Правильная будущая композиция: + +```text +DzengiInstrumentDocumentSource + + +DzengiInstrumentDocumentHandler + ↓ +InstrumentFeed + ↓ +registry.register("dzengi", feed) +``` + +Composition остаётся явной и находится за пределами Registry. + +--- + +## 23. Почему Registry хранит Feed, а не Source и Handler отдельно + +К моменту Build 011 уже существует архитектурная capability: + +```text +InstrumentFeedProtocol +``` + +Feed инкапсулирует взаимодействие: + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler +``` + +Если Registry начал бы отдельно хранить: + +```text +Source +Handler +``` + +ему пришлось бы знать, как их соединять. + +Это нарушило бы границу Build 010. + +Правильная схема: + +```text +Source + + +Handler + ↓ +InstrumentFeed + ↓ +InstrumentFeedRegistry +``` + +Registry работает только с готовым: + +```text +InstrumentFeedProtocol +``` + +--- + +## 24. Что Registry не хранит + +Внутри Registry отсутствуют: + +```text +Instrument; +tuple[Instrument, ...]; +результат load_instruments(); +последний успешный справочник; +предыдущий справочник; +snapshot; +timestamp; +TTL; +cache age; +последняя ошибка Feed; +legacy ExchangeSymbol. +``` + +Registry хранит только: + +```text +dict[str, InstrumentFeedProtocol] +``` + +--- + +## 25. Что Registry не делает + +Build 011 сознательно не выполняет: + +```text +REST-запросы; +получение exchangeInfo; +schema validation; +parsing; +value validation; +mapping; +вызов load_instruments(); +retry; +backoff; +кэширование; +хранение Instrument; +сравнение snapshot; +фильтрацию инструментов; +сортировку инструментов; +дедупликацию инструментов; +нормализацию торговых символов; +создание Source; +создание Handler; +создание Feed; +создание Acquisition Service; +создание compatibility mapper; +изменение ExchangeService; +изменение AutoTrade; +изменение Telegram UI; +изменение trading runtime. +``` + +Единственная ответственность: + +```text +source_name + ↔ +InstrumentFeedProtocol +``` + +--- + +## 26. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/test_registry.py +``` + +Фактически выполнено: + +```text +25 tests +``` + +Проверены следующие сценарии: + +1. регистрация и получение Feed; +2. сохранение identity Feed; +3. соответствие объекта `InstrumentFeedProtocol`; +4. несколько Feed под разными именами; +5. удаление внешних пробелов из имени; +6. запрет пустой строки при регистрации; +7. запрет строки из пробелов при регистрации; +8. запрет tab/newline при регистрации; +9. запрет пустой строки при `get()`; +10. запрет строки из пробелов при `get()`; +11. запрет tab/newline при `get()`; +12. запрет повторной регистрации; +13. отсутствие замены исходного Feed при duplicate; +14. duplicate после нормализации внешних пробелов; +15. сохранение case-sensitive поведения; +16. ошибка при запросе отсутствующего Feed; +17. запрет объекта без `InstrumentFeedProtocol`; +18. отсутствие вызова Feed при регистрации; +19. отсутствие вызова Feed при получении; +20. хранение Feed, а не результата `Instrument`; +21. наследование `InstrumentFeedRegistryError` от `MarketDataAcquisitionError`. + +Часть сценариев реализована через параметризацию, поэтому фактическое количество выполненных тестовых случаев составляет: + +```text +25 +``` + +--- + +## 27. Проверка регистрации и получения Feed + +Тест подтверждает: + +```python +registry = InstrumentFeedRegistry() +feed = StubInstrumentFeed() + +registry.register("dzengi", feed) + +result = registry.get("dzengi") + +assert result is feed +``` + +Это доказывает: + +```text +Feed зарегистрирован; +Feed доступен по source_name; +identity объекта сохранена. +``` + +--- + +## 28. Проверка нескольких источников + +Registry поддерживает несколько независимых записей: + +```text +"dzengi" + ↓ +dzengi_feed + +"secondary" + ↓ +secondary_feed +``` + +Тест подтверждает: + +```python +assert registry.get("dzengi") is dzengi_feed +assert registry.get("secondary") is secondary_feed +``` + +Registry не имеет встроенного ограничения на один источник. + +--- + +## 29. Проверка отсутствия вызова Feed + +Тестовый Feed содержит счётчик: + +```python +self.load_call_count = 0 +``` + +Метод: + +```python +load_instruments() +``` + +увеличивает счётчик. + +После: + +```python +registry.register("dzengi", feed) +``` + +подтверждено: + +```python +assert feed.load_call_count == 0 +``` + +После: + +```python +registry.get("dzengi") +``` + +также подтверждено: + +```python +assert feed.load_call_count == 0 +``` + +Таким образом, Registry не запускает acquisition pipeline. + +--- + +## 30. Проверка хранения Feed, а не результата Feed + +Тест создаёт: + +```python +instruments = ( + _instrument(), +) +``` + +и Feed: + +```python +feed = StubInstrumentFeed( + instruments=instruments, +) +``` + +После регистрации: + +```python +registered_feed = registry.get("dzengi") +``` + +подтверждено: + +```python +assert registered_feed is feed +assert registered_feed is not instruments +``` + +Registry хранит сам Feed, а не: + +```text +tuple[Instrument, ...] +``` + +--- + +## 31. Выполненные проверки + +### Проверка 1 — unit-тесты Registry + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/test_registry.py \ + -q +``` + +Результат: + +```text +......................... [100%] +25 passed in 0.02s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/exceptions.py \ + src/market_data/acquisition/registry.py \ + tests/unit/market_data/acquisition/test_registry.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +....................................................................................................................................................................... [100%] +167 passed in 0.09s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — отсутствие преждевременной production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "InstrumentFeedRegistry|InstrumentFeedRegistryError" \ + src tests +``` + +Полученные production-использования находятся только в: + +```text +src/market_data/acquisition/registry.py +src/market_data/acquisition/exceptions.py +``` + +Остальные использования находятся исключительно в: + +```text +tests/unit/market_data/acquisition/test_registry.py +``` + +Не обнаружено подключения к: + +```text +Acquisition Service +ExchangeService +Telegram UI +AutoTrade +Trading runtime +другим production-потребителям +``` + +Статус: + +```text +PASSED +``` + +--- + +## 32. Архитектура после Build 011 + +После завершения Build 011 новая часть подсистемы имеет следующую структуру: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ ├── MarketDataAcquisitionError + │ ├── InstrumentReferenceTransportError + │ ├── InstrumentReferenceSchemaError + │ ├── InstrumentReferenceParseError + │ ├── InstrumentReferenceValueError + │ ├── InstrumentReferenceMappingError + │ └── InstrumentFeedRegistryError + │ + ├── protocol.py + │ ├── InstrumentDocumentSource + │ ├── InstrumentDocumentHandler + │ └── InstrumentFeedProtocol + │ + ├── registry.py + │ └── InstrumentFeedRegistry + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ └── values.py + │ + ├── handlers/ + │ └── instrument_handler.py + │ └── DzengiInstrumentDocumentHandler + │ + ├── feeds/ + │ └── instrument_feed.py + │ └── InstrumentFeed + │ + └── adapters/ + └── dzengi/ + ├── models.py + ├── parser.py + ├── mapper.py + └── rest.py + ├── _PayloadRestClient + └── DzengiInstrumentDocumentSource +``` + +На текущем этапе ещё не реализован: + +```text +service.py +``` + +--- + +## 33. Полная архитектурная цепочка после Build 011 + +После Build 011 существуют две связанные архитектурные части. + +### Регистрация capability + +```text +DzengiInstrumentDocumentSource + + +DzengiInstrumentDocumentHandler + ↓ +InstrumentFeed + ↓ +InstrumentFeedRegistry.register( + "dzengi", + feed, +) +``` + +### Получение capability будущим сервисом + +```text +Acquisition Service + ↓ +source_name = "dzengi" + ↓ +InstrumentFeedRegistry.get("dzengi") + ↓ +InstrumentFeedProtocol +``` + +После получения Feed будущий `Acquisition Service` сможет выполнить: + +```text +InstrumentFeedProtocol.load_instruments() + ↓ +tuple[Instrument, ...] +``` + +Эта orchestration logic относится к следующему: + +```text +Build 012 — Acquisition Service +``` + +--- + +## 34. Полный acquisition pipeline после Build 011 + +Технически уже реализована следующая цепочка: + +```text +source_name + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol + ↓ +InstrumentFeed + ↓ +InstrumentDocumentSource + ↓ +DzengiInstrumentDocumentSource + ↓ +ExchangeRestClient.get_payload() + ↓ +Dzengi REST API +``` + +После получения документа: + +```text +object + ↓ +InstrumentDocumentHandler + ↓ +DzengiInstrumentDocumentHandler + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +Однако Registry сам эту цепочку не запускает. + +--- + +## 35. Что ещё не реализовано + +После Build 011 отсутствуют: + +```text +Acquisition Service; +официальная production composition; +equivalence verification с legacy implementation; +compatibility mapper Instrument → ExchangeSymbol; +переключение get_exchange_symbols(); +перевод normalize_symbol()/symbol_candidates(); +переключение validate_symbol(); +переключение get_symbol_runtime_status(); +перенос кэша. +``` + +Поэтому новая acquisition pipeline всё ещё остаётся изолированной от существующего production runtime. + +--- + +## 36. Влияние на legacy-систему + +Build 011 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +Trading runtime +``` + +Не изменены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +normalize_symbol() +symbol_candidates() +``` + +Старый production-путь продолжает работать без изменений. + +--- + +## 37. Обратная совместимость + +Подтверждено сохранение: + +```text +сигнатур существующих legacy-методов; +старых импортов; +существующего формата legacy-ошибок; +Telegram UI; +автоторговли; +runtime-поведения; +legacy ExchangeSymbol; +legacy-кэша. +``` + +Build 011 имеет полную обратную совместимость. + +--- + +## 38. Классификация изменений + +| Изменение | Классификация | +|---|---| +| `InstrumentFeedRegistry` | Обязательное архитектурное изменение | +| Индексация Feed по `source_name` | Обязательное архитектурное изменение | +| Проверка `InstrumentFeedProtocol` | Улучшение надёжности | +| Запрет повторной регистрации | Улучшение надёжности | +| `InstrumentFeedRegistryError` | Обязательная диагностическая граница | +| Удаление внешних пробелов из имени | Улучшение надёжности | +| Сохранение case-sensitive ключей | Отсутствие скрытого изменения поведения | +| Хранение `Instrument` | Не выполняется | +| Вызов Feed | Не выполняется | +| Кэширование | Не выполняется | +| Изменение legacy-кода | Отсутствует | +| Изменение production-поведения | Отсутствует | + +--- + +## 39. Условие завершения Build 011 + +Все условия выполнены: + +```text +InstrumentFeedRegistry + реализован; + +InstrumentFeedProtocol + может быть зарегистрирован; + +Feed + возвращается по source_name; + +identity Feed + сохраняется; + +несколько источников + поддерживаются; + +внешние пробелы имени + удаляются; + +регистр имени + сохраняется; + +пустые ключи + отклоняются; + +повторная регистрация + отклоняется; + +неуспешная повторная регистрация + не заменяет исходный Feed; + +отсутствующий Feed + вызывает InstrumentFeedRegistryError; + +невалидный Feed + отклоняется; + +Registry + не вызывает load_instruments(); + +Registry + не хранит Instrument; + +Registry + не выполняет кэширование; + +unit-тесты + проходят; + +полный pytest + проходит; + +production runtime + не затронут. +``` + +--- + +## 40. Итог Build 011 + +Build 011 завершён успешно. + +Реализовано: + +```text +InstrumentFeedRegistry + +source_name + ↓ +register() + ↓ +InstrumentFeedProtocol +``` + +и: + +```text +source_name + ↓ +get() + ↓ +тот же InstrumentFeedProtocol +``` + +Подтверждено: + +```text +25 Registry tests passed +167 total project tests passed +Python compilation passed +No premature production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 011 — COMPLETE +``` + +--- + +## 41. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 012 — Acquisition Service +``` + +Его задача — реализовать application-level orchestration над Registry и Feed: + +```text +source_name + ↓ +Acquisition Service + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol + ↓ +load_instruments() + ↓ +tuple[Instrument, ...] +``` + +Build 012 не должен: + +```text +изменять ExchangeService; +переключать legacy get_exchange_symbols(); +создавать compatibility mapper; +переносить legacy-кэш; +подключаться к Telegram UI; +изменять AutoTrade; +изменять trading runtime. +``` + +После успешного завершения Build 012 будет закончена изолированная новая acquisition pipeline, после чего можно будет перейти к: + +```text +Build 013 — проверка эквивалентности старой и новой реализации. +``` \ No newline at end of file diff --git a/docs/migrations/build_012.md b/docs/migrations/build_012.md new file mode 100644 index 0000000..9ac4d92 --- /dev/null +++ b/docs/migrations/build_012.md @@ -0,0 +1,1556 @@ +# Build 012 — Instrument Acquisition Service + +**Статус:** Завершён +**Подсистема:** `market_data/acquisition` +**Область:** Instrument Reference Data +**Тип изменения:** Изолированное добавление application-level сервиса получения справочника инструментов без подключения к production runtime +**Результат полного набора тестов:** `180 passed` + +--- + +## 1. Цель Build 012 + +Цель Build 012 — реализовать application-level сервис, который получает Instrument Feed из Registry и запускает загрузку справочника инструментов. + +Реализован класс: + +```text +InstrumentAcquisitionService +``` + +Его архитектурная граница: + +```text +source_name + ↓ +InstrumentAcquisitionService + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol + ↓ +load_instruments() + ↓ +tuple[Instrument, ...] +``` + +Build 012 завершает orchestration-слой новой изолированной acquisition pipeline. + +На этом этапе сервис: + +```text +получает Feed из Registry; +вызывает Feed; +возвращает результат Feed без изменения; +сохраняет специализированные ошибки без wrapping. +``` + +Build 012 не подключается к существующему `ExchangeService`, не меняет legacy runtime и не переключает production-потребителей на новую реализацию. + +--- + +## 2. Почему Build 012 выполняется именно сейчас + +До начала Build 012 были завершены: + +```text +Build 001 — внутренняя модель Instrument Reference Data +Build 002 — raw-модели ответа Dzengi +Build 003 — структурная валидация exchangeInfo +Build 004 — parser exchangeInfo +Build 005 — value validation +Build 006 — mapper Dzengi → Instrument +Build 007 — Protocol и Exceptions +Build 008 — Dzengi REST Adapter +Build 009 — Instrument Handler +Build 010 — Instrument Feed +Build 011 — Instrument Feed Registry +``` + +После Build 011 уже существовала цепочка: + +```text +source_name + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol +``` + +И отдельно существовал полный Feed pipeline: + +```text +InstrumentFeed + ↓ +InstrumentDocumentSource + ↓ +DzengiInstrumentDocumentSource + ↓ +Dzengi REST API + ↓ +raw document + ↓ +InstrumentDocumentHandler + ↓ +DzengiInstrumentDocumentHandler + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +tuple[Instrument, ...] +``` + +Однако отсутствовала application-level точка входа, соединяющая Registry с запуском Feed. + +Build 012 создаёт эту точку: + +```text +InstrumentAcquisitionService +``` + +--- + +## 3. Архитектурная ответственность Acquisition Service + +Единственная предметная ответственность сервиса: + +```text +получить source_name + ↓ +получить Feed через Registry + ↓ +вызвать load_instruments() + ↓ +вернуть tuple[Instrument, ...] +``` + +Минимальная логика сервиса: + +```python +feed = self._registry.get(source_name) + +return feed.load_instruments() +``` + +Сервис не содержит собственной transport-, parsing-, validation-, mapping- или storage-логики. + +--- + +## 4. Изменённые файлы + +В рамках Build 012 реализован: + +```text +app/src/market_data/acquisition/service.py +``` + +Создан тестовый файл: + +```text +app/tests/unit/market_data/acquisition/test_service.py +``` + +Другие production-файлы не изменялись. + +В частности, не изменялись: + +```text +app/src/market_data/acquisition/exceptions.py +app/src/market_data/acquisition/protocol.py +app/src/market_data/acquisition/registry.py +app/src/market_data/acquisition/feeds/instrument_feed.py + +app/src/market_data/acquisition/adapters/dzengi/rest.py +app/src/market_data/acquisition/handlers/instrument_handler.py + +app/src/integrations/exchange/* +app/src/telegram/* +app/src/trading/* +``` + +Re-export в `__init__.py` не добавлялся. + +--- + +## 5. Реализованный InstrumentAcquisitionService + +В файле: + +```text +app/src/market_data/acquisition/service.py +``` + +реализован класс: + +```python +class InstrumentAcquisitionService: + ... +``` + +Его публичный контракт: + +```python +def load_instruments( + self, + source_name: str, +) -> tuple[Instrument, ...]: + ... +``` + +Полная схема: + +```text +InstrumentAcquisitionService.load_instruments(source_name) + ↓ +InstrumentFeedRegistry.get(source_name) + ↓ +InstrumentFeedProtocol + ↓ +InstrumentFeedProtocol.load_instruments() + ↓ +tuple[Instrument, ...] +``` + +--- + +## 6. Явная dependency injection Registry + +Registry передаётся сервису через конструктор: + +```python +def __init__( + self, + *, + registry: InstrumentFeedRegistry, +) -> None: + self._registry = registry +``` + +Правильная композиция: + +```python +registry = InstrumentFeedRegistry() + +registry.register( + "dzengi", + feed, +) + +service = InstrumentAcquisitionService( + registry=registry, +) +``` + +После этого: + +```python +instruments = service.load_instruments("dzengi") +``` + +Сервис не создаёт Registry самостоятельно. + +--- + +## 7. Почему Service не создаёт Registry + +Внутри `InstrumentAcquisitionService` отсутствует: + +```python +self._registry = InstrumentFeedRegistry() +``` + +Это принципиальное архитектурное решение. + +Если бы Service самостоятельно создавал Registry: + +```text +Service + ↓ +создаёт Registry + ↓ +Registry изначально пуст + ↓ +требуется скрытая регистрация Feed + ↓ +Service начинает знать о конкретных источниках +``` + +Вместо этого используется: + +```text +готовый Registry + ↓ +передаётся Service +``` + +Это обеспечивает: + +```text +явные зависимости; +контролируемую composition; +независимость от конкретного источника; +простое тестирование; +отсутствие скрытой инициализации. +``` + +--- + +## 8. Независимость от Dzengi + +`InstrumentAcquisitionService` не импортирует и не создаёт: + +```text +DzengiInstrumentDocumentSource +DzengiInstrumentDocumentHandler +DzengiExchangeInfoResponse +ExchangeRestClient +InstrumentFeed +Dzengi parser +Dzengi mapper +``` + +Сервис зависит только от: + +```text +Instrument +InstrumentFeedRegistry +``` + +Архитектурная схема: + +```text +InstrumentAcquisitionService + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol +``` + +Конкретный источник определяется снаружи через регистрацию Feed. + +--- + +## 9. Передача source_name без изменения + +Service не выполняет над `source_name`: + +```text +strip() +lower() +casefold() +replace() +alias resolution +automatic source mapping +``` + +Он непосредственно передаёт полученное значение Registry: + +```python +feed = self._registry.get(source_name) +``` + +Например: + +```text +" dzengi " +``` + +передаётся в Registry именно как: + +```text +" dzengi " +``` + +Нормализация внешних пробелов является ответственностью: + +```text +InstrumentFeedRegistry +``` + +Это исключает дублирование правил между Service и Registry. + +--- + +## 10. Получение Feed через Registry + +Service не хранит Feed напрямую. + +Отсутствует: + +```python +self._feed = feed +``` + +Вместо этого для каждого вызова: + +```python +service.load_instruments(source_name) +``` + +выполняется: + +```python +feed = self._registry.get(source_name) +``` + +Таким образом: + +```text +source_name + ↓ +Registry + ↓ +соответствующий Feed +``` + +Service не определяет самостоятельно, какой Feed использовать. + +--- + +## 11. Однократный вызов Registry + +Для одного вызова: + +```python +service.load_instruments("dzengi") +``` + +метод: + +```python +registry.get("dzengi") +``` + +вызывается ровно один раз. + +Отсутствуют: + +```text +повторный lookup; +предварительная проверка наличия; +двойной get(); +fallback lookup. +``` + +Это подтверждено unit-тестом. + +--- + +## 12. Однократный вызов Feed + +После успешного получения Feed выполняется: + +```python +feed.load_instruments() +``` + +ровно один раз. + +Цепочка: + +```text +Service.load_instruments() + ↓ +Registry.get() + ↓ +Feed.load_instruments() + ↓ +return result +``` + +Отсутствуют: + +```text +retry; +повторный вызов после ошибки; +предварительный вызов; +дополнительная проверочная загрузка. +``` + +--- + +## 13. Возврат результата без копирования + +Service непосредственно возвращает: + +```python +return feed.load_instruments() +``` + +Он не выполняет: + +```python +tuple(feed.load_instruments()) +``` + +или: + +```python +result = feed.load_instruments() +return tuple(result) +``` + +Поэтому сохраняется identity результата: + +```python +result is instruments +``` + +равно: + +```text +True +``` + +Это подтверждено unit-тестами. + +--- + +## 14. Сохранение порядка инструментов + +Service не выполняет: + +```text +sorting; +filtering; +deduplication; +grouping; +reordering. +``` + +Если Feed возвращает: + +```text +BTC/USD_LEVERAGE +ETH/USD_LEVERAGE +XRP/USD_LEVERAGE +``` + +Service возвращает инструменты в том же порядке: + +```text +BTC/USD_LEVERAGE +ETH/USD_LEVERAGE +XRP/USD_LEVERAGE +``` + +Порядок результата Feed сохраняется. + +--- + +## 15. Поведение при пустом результате + +Если Feed возвращает: + +```python +() +``` + +Service также возвращает: + +```python +() +``` + +без ошибки. + +Service не интерпретирует пустой результат как: + +```text +transport error; +schema error; +value error; +mapping error; +отсутствие источника. +``` + +На Build 012 отсутствует утверждённое правило, согласно которому пустой `tuple` должен считаться ошибкой. + +--- + +## 16. Сохранение специализированных ошибок + +В `InstrumentAcquisitionService` отсутствует общий `try/except`, который заменял бы исходные ошибки новой общей ошибкой. + +Без изменения могут пройти: + +```text +InstrumentFeedRegistryError +InstrumentReferenceTransportError +InstrumentReferenceSchemaError +InstrumentReferenceParseError +InstrumentReferenceValueError +InstrumentReferenceMappingError +``` + +Схема: + +```text +Registry error + ↓ +Service + ↓ +та же Registry error +``` + +или: + +```text +Feed error + ↓ +Service + ↓ +та же Feed error +``` + +--- + +## 17. Сохранение identity ошибки + +Тесты подтверждают не только тип ошибки, но и сохранение исходного объекта. + +Если Feed выбрасывает: + +```python +original_error = InstrumentReferenceTransportError( + "Network error." +) +``` + +то Service передаёт именно этот объект: + +```python +exc_info.value is original_error +``` + +равно: + +```text +True +``` + +Ошибка не: + +```text +копируется; +оборачивается; +заменяется; +переводится в другой тип. +``` + +--- + +## 18. Ошибка отсутствующего Feed + +Если Registry не содержит источник: + +```text +"dzengi" +``` + +вызов: + +```python +service.load_instruments("dzengi") +``` + +приводит к: + +```text +InstrumentFeedRegistryError +``` + +Service не выполняет: + +```text +fallback; +создание Feed; +регистрацию Feed; +использование default source; +возврат пустого tuple. +``` + +Ошибка Registry проходит наружу без wrapping. + +--- + +## 19. Feed не вызывается при ошибке Registry + +Последовательность: + +```text +Service.load_instruments() + ↓ +Registry.get() + ↓ +InstrumentFeedRegistryError +``` + +останавливается на ошибке Registry. + +Метод: + +```text +feed.load_instruments() +``` + +не вызывается. + +Это подтверждено unit-тестом. + +--- + +## 20. Отсутствие retry после ошибки Feed + +Если Feed выбрасывает: + +```text +InstrumentReferenceTransportError +``` + +Service не повторяет вызов. + +Схема: + +```text +feed.load_instruments() + ↓ +InstrumentReferenceTransportError + ↓ +ошибка немедленно выходит из Service +``` + +Счётчик вызовов Feed остаётся: + +```text +1 +``` + +Таким образом, Build 012 не вводит скрытую retry policy. + +--- + +## 21. Почему retry отсутствует + +Retry является отдельной operational policy. + +Для его корректной реализации необходимо отдельно определить: + +```text +какие ошибки являются retryable; +максимальное число попыток; +интервалы между попытками; +backoff; +jitter; +timeout budget; +логирование повторных попыток; +поведение при исчерпании попыток. +``` + +Build 012 не должен неявно принимать эти архитектурные решения. + +Поэтому: + +```text +один вызов Service + ↓ +один вызов Feed +``` + +--- + +## 22. Почему не создан новый Service exception + +В Build 012 не добавлен: + +```text +InstrumentAcquisitionServiceError +``` + +Уже существуют специализированные ошибки: + +```text +InstrumentFeedRegistryError +InstrumentReferenceTransportError +InstrumentReferenceSchemaError +InstrumentReferenceParseError +InstrumentReferenceValueError +InstrumentReferenceMappingError +``` + +Создание общей ошибки: + +```text +InstrumentAcquisitionServiceError +``` + +и wrapping всех причин в неё ухудшило бы диагностируемость. + +Поэтому сохраняется точная причина отказа. + +--- + +## 23. Что Service не хранит + +Внутри `InstrumentAcquisitionService` отсутствуют: + +```text +tuple[Instrument, ...]; +последний успешный справочник; +предыдущий snapshot; +timestamp; +TTL; +cache age; +последняя ошибка; +индекс инструментов; +legacy ExchangeSymbol. +``` + +Service хранит только зависимость: + +```text +InstrumentFeedRegistry +``` + +--- + +## 24. Что Service не делает + +Build 012 сознательно не выполняет: + +```text +REST-запросы напрямую; +получение exchangeInfo напрямую; +schema validation; +parsing; +value validation; +mapping; +создание Registry; +создание Feed; +создание Source; +создание Handler; +автоматическую регистрацию Feed; +retry; +backoff; +кэширование; +хранение Instrument; +сравнение snapshot; +фильтрацию инструментов; +сортировку инструментов; +дедупликацию инструментов; +нормализацию торговых символов; +преобразование Instrument в ExchangeSymbol; +изменение ExchangeService; +изменение AutoTrade; +изменение Telegram UI; +изменение trading runtime; +формирование пользовательских ошибок; +логирование событий. +``` + +Единственная orchestration-ответственность: + +```text +Registry lookup + ↓ +Feed invocation +``` + +--- + +## 25. Production composition не входит в Build 012 + +Build 012 не создаёт автоматически полную production-композицию: + +```text +DzengiInstrumentDocumentSource + + +DzengiInstrumentDocumentHandler + ↓ +InstrumentFeed + ↓ +InstrumentFeedRegistry + ↓ +InstrumentAcquisitionService +``` + +На текущем этапе новая pipeline остаётся изолированной. + +Не создаётся: + +```text +global Registry; +global Service; +singleton Feed; +application startup wiring; +dependency container; +ExchangeService integration. +``` + +Это предотвращает преждевременное изменение production runtime. + +--- + +## 26. Реализованные тестовые сценарии + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/test_service.py +``` + +Фактически выполнено: + +```text +13 tests +``` + +Проверены следующие сценарии: + +1. загрузка инструментов из зарегистрированного Feed; +2. передача `source_name` Registry без изменения; +3. однократный вызов Registry; +4. однократный вызов Feed; +5. возврат результата без копирования; +6. сохранение порядка инструментов; +7. возврат пустого tuple без ошибки; +8. сохранение Registry error без wrapping; +9. отсутствие вызова Feed при ошибке Registry; +10. сохранение transport error без wrapping; +11. сохранение value error без wrapping; +12. сохранение mapping error без wrapping; +13. отсутствие retry после ошибки Feed. + +Часть сценариев реализована через параметризацию. + +Фактическое количество выполненных тестовых случаев: + +```text +13 +``` + +--- + +## 27. Проверка получения инструментов + +Тест подтверждает: + +```python +registry = InstrumentFeedRegistry() + +instruments = ( + _instrument(), +) + +feed = StubInstrumentFeed( + instruments=instruments, +) + +registry.register( + "dzengi", + feed, +) + +service = InstrumentAcquisitionService( + registry=registry, +) + +result = service.load_instruments("dzengi") + +assert result is instruments +``` + +Это доказывает: + +```text +Service получил Feed через Registry; +Feed был вызван; +результат Feed возвращён; +identity результата сохранена. +``` + +--- + +## 28. Проверка передачи source_name без изменения + +Используется тестовый Registry, который записывает полученные имена. + +Вызов: + +```python +service.load_instruments(" dzengi ") +``` + +приводит к передаче в Registry именно: + +```text +" dzengi " +``` + +Тест подтверждает: + +```python +assert registry.requested_source_names == [ + " dzengi ", +] +``` + +Service не дублирует нормализацию Registry. + +--- + +## 29. Проверка отсутствия retry + +Тестовый Feed содержит счётчик: + +```python +self.load_call_count = 0 +``` + +При вызове: + +```python +feed.load_instruments() +``` + +счётчик увеличивается. + +Feed настроен на выбрасывание: + +```text +InstrumentReferenceTransportError +``` + +После вызова Service подтверждено: + +```python +assert feed.load_call_count == 1 +``` + +Это доказывает отсутствие скрытой повторной попытки. + +--- + +## 30. Выполненные проверки + +### Проверка 1 — unit-тесты Acquisition Service + +Команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/test_service.py \ + -q +``` + +Результат: + +```text +............. [100%] +13 passed in 0.01s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 2 — Python compilation + +Команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/service.py \ + tests/unit/market_data/acquisition/test_service.py +``` + +Результат: + +```text +Команда завершилась без ошибок и без вывода. +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 3 — полный набор тестов проекта + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +.................................................................................................................................................................................... [100%] +180 passed in 0.09s +``` + +Статус: + +```text +PASSED +``` + +--- + +### Проверка 4 — отсутствие преждевременной production-интеграции + +Команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "InstrumentAcquisitionService" \ + src tests +``` + +Полученное production-определение находится только в: + +```text +src/market_data/acquisition/service.py +``` + +Остальные использования находятся исключительно в: + +```text +tests/unit/market_data/acquisition/test_service.py +``` + +Не обнаружено подключения к: + +```text +ExchangeService +Telegram UI +AutoTrade +Trading runtime +другим production-потребителям +``` + +Статус: + +```text +PASSED +``` + +--- + +## 31. Архитектура после Build 012 + +После завершения Build 012 новая часть подсистемы имеет следующую структуру: + +```text +market_data/ +└── acquisition/ + ├── exceptions.py + │ ├── MarketDataAcquisitionError + │ ├── InstrumentReferenceTransportError + │ ├── InstrumentReferenceSchemaError + │ ├── InstrumentReferenceParseError + │ ├── InstrumentReferenceValueError + │ ├── InstrumentReferenceMappingError + │ └── InstrumentFeedRegistryError + │ + ├── protocol.py + │ ├── InstrumentDocumentSource + │ ├── InstrumentDocumentHandler + │ └── InstrumentFeedProtocol + │ + ├── registry.py + │ └── InstrumentFeedRegistry + │ + ├── service.py + │ └── InstrumentAcquisitionService + │ + ├── models/ + │ └── instrument.py + │ └── Instrument + │ + ├── validation/ + │ ├── schema.py + │ └── values.py + │ + ├── handlers/ + │ └── instrument_handler.py + │ └── DzengiInstrumentDocumentHandler + │ + ├── feeds/ + │ └── instrument_feed.py + │ └── InstrumentFeed + │ + └── adapters/ + └── dzengi/ + ├── models.py + ├── parser.py + ├── mapper.py + └── rest.py + ├── _PayloadRestClient + └── DzengiInstrumentDocumentSource +``` + +--- + +## 32. Полная архитектурная цепочка после Build 012 + +После Build 012 реализована полная изолированная acquisition pipeline: + +```text +source_name + ↓ +InstrumentAcquisitionService + ↓ +InstrumentFeedRegistry + ↓ +InstrumentFeedProtocol + ↓ +InstrumentFeed + ↓ +InstrumentDocumentSource + ↓ +DzengiInstrumentDocumentSource + ↓ +ExchangeRestClient.get_payload() + ↓ +Dzengi REST API +``` + +После получения документа: + +```text +object + ↓ +InstrumentDocumentHandler + ↓ +DzengiInstrumentDocumentHandler + ↓ +validate_exchange_info_schema() + ↓ +ValidatedExchangeInfoDocument + ↓ +parse_exchange_info() + ↓ +DzengiExchangeInfoResponse + ↓ +validate_exchange_info_values() + ↓ +map_dzengi_exchange_info_to_instruments() + ↓ +tuple[Instrument, ...] +``` + +Таким образом, техническая цепочка новой acquisition-подсистемы завершена. + +--- + +## 33. Что означает завершение изолированной acquisition pipeline + +После Build 012 новая подсистема уже содержит все необходимые уровни: + +```text +Internal Model + ↓ +Raw Models + ↓ +Schema Validation + ↓ +Parser + ↓ +Value Validation + ↓ +Mapper + ↓ +Protocols + ↓ +REST Adapter + ↓ +Handler + ↓ +Feed + ↓ +Registry + ↓ +Acquisition Service +``` + +Но она ещё не заменяет legacy implementation. + +Существующий бот продолжает использовать старый путь. + +--- + +## 34. Что ещё не реализовано + +После Build 012 отсутствуют: + +```text +проверка эквивалентности старой и новой реализации; +compatibility mapper Instrument → ExchangeSymbol; +переключение get_exchange_symbols(); +перевод normalize_symbol()/symbol_candidates(); +переключение validate_symbol(); +переключение get_symbol_runtime_status(); +подготовка переноса кэша; +перенос кэша; +перевод production-потребителей; +удаление legacy-кода. +``` + +Эти задачи относятся к следующим Build. + +--- + +## 35. Влияние на legacy-систему + +Build 012 не подключён к существующим компонентам: + +```text +ExchangeService +ExchangeSymbol +SymbolValidationResult +Telegram UI +AutoTrade +Market Stream +Market Data Runner +Execution Quality +Trading runtime +``` + +Не изменены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +normalize_symbol() +symbol_candidates() +``` + +Старый production-путь продолжает работать без изменений. + +--- + +## 36. Обратная совместимость + +Подтверждено сохранение: + +```text +сигнатур существующих legacy-методов; +старых импортов; +существующего формата legacy-ошибок; +Telegram UI; +автоторговли; +runtime-поведения; +legacy ExchangeSymbol; +legacy-кэша. +``` + +Build 012 имеет полную обратную совместимость. + +--- + +## 37. Классификация изменений + +| Изменение | Классификация | +|---|---| +| `InstrumentAcquisitionService` | Обязательное архитектурное изменение | +| Registry lookup → Feed invocation | Обязательное архитектурное изменение | +| Явная dependency injection Registry | Обязательное разделение ответственности | +| Передача `source_name` без изменения | Исключение дублирования ответственности | +| Однократный вызов Registry | Предсказуемое orchestration-поведение | +| Однократный вызов Feed | Предсказуемое orchestration-поведение | +| Возврат результата без копирования | Сохранение контракта Feed | +| Сохранение специализированных ошибок | Улучшение диагностируемости | +| Retry | Не выполняется | +| Кэширование | Не выполняется | +| Production composition | Не выполняется | +| Изменение legacy-кода | Отсутствует | +| Изменение production-поведения | Отсутствует | + +--- + +## 38. Условие завершения Build 012 + +Все условия выполнены: + +```text +InstrumentAcquisitionService + реализован; + +Registry + передаётся как явная зависимость; + +source_name + передаётся Registry без изменения; + +Feed + получается через Registry; + +Registry + вызывается ровно один раз; + +Feed + вызывается ровно один раз; + +результат Feed + возвращается без копирования; + +порядок Instrument + сохраняется; + +пустой tuple + возвращается без ошибки; + +специализированные ошибки + сохраняются без wrapping; + +identity ошибки + сохраняется; + +Feed + не вызывается при ошибке Registry; + +retry + отсутствует; + +кэширование + отсутствует; + +прямые Dzengi-зависимости + отсутствуют; + +unit-тесты + проходят; + +полный pytest + проходит; + +production runtime + не затронут. +``` + +--- + +## 39. Итог Build 012 + +Build 012 завершён успешно. + +Реализовано: + +```text +InstrumentAcquisitionService + +source_name + ↓ +InstrumentFeedRegistry.get() + ↓ +InstrumentFeedProtocol + ↓ +load_instruments() + ↓ +tuple[Instrument, ...] +``` + +Подтверждено: + +```text +13 Acquisition Service tests passed +180 total project tests passed +Python compilation passed +No premature production integration detected +Legacy bot behavior unchanged +``` + +Итоговый статус: + +```text +BUILD 012 — COMPLETE +``` + +--- + +## 40. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 013 — Проверка эквивалентности старой и новой реализации +``` + +Его задача — до любого переключения production-кода доказать, что legacy и новая implementation получают эквивалентные данные из одного и того же реального `exchangeInfo`. + +Целевая схема: + +```text +один реальный exchangeInfo document + ├──→ legacy implementation + │ ↓ + │ list[ExchangeSymbol] + │ + └──→ new acquisition pipeline + ↓ + tuple[Instrument, ...] + + ↓ + equivalence comparison +``` + +На Build 013 необходимо определить точный набор полей для сравнения: + +```text +symbol; +name; +status; +base_asset; +quote_asset; +asset_type; +market_type; +market_modes; +order_types; +precisions; +tick_size; +tick_value; +step_size; +min_qty; +max_qty; +min_notional; +country; +sector; +industry; +trading_hours. +``` + +Build 013 не должен: + +```text +переключать ExchangeService.get_exchange_symbols(); +изменять validate_symbol(); +изменять get_symbol_runtime_status(); +создавать compatibility mapper; +переносить кэш; +изменять Telegram UI; +изменять AutoTrade; +изменять trading runtime. +``` + +Только после доказанной эквивалентности можно переходить к: + +```text +Build 014 — Compatibility mapper Instrument → ExchangeSymbol +``` \ No newline at end of file diff --git a/docs/migrations/build_013.md b/docs/migrations/build_013.md new file mode 100644 index 0000000..e57408f --- /dev/null +++ b/docs/migrations/build_013.md @@ -0,0 +1,812 @@ +# Dzentra — Instrument Reference Data Migration — Build 013 + +> Статус: Завершён + +## Название + +**Проверка эквивалентности старой и новой реализации** + +--- + +## Цель + +Доказать эквивалентность существующей legacy-реализации обработки `exchangeInfo` и новой подсистемы Instrument Reference Data до начала переключения production-потребителей. + +Проверка должна подтвердить, что один и тот же исходный документ `exchangeInfo`, обработанный двумя независимыми путями, приводит к эквивалентным результатам в части полей, существующих одновременно в legacy-модели `ExchangeSymbol` и новой модели `Instrument`. + +Build 013 не изменяет production-код и не переключает существующий runtime на новую реализацию. + +--- + +## Исходная архитектура проверки + +Один и тот же сохранённый документ используется обеими реализациями: + +```text +один exchangeInfo document + │ + ├──→ legacy implementation + │ │ + │ ├──→ _extract_exchange_symbols_raw() + │ │ + │ └──→ _parse_exchange_symbol() + │ │ + │ ↓ + │ list[ExchangeSymbol] + │ + └──→ new implementation + │ + └──→ DzengiInstrumentDocumentHandler + │ + ├──→ schema validation + ├──→ parser + ├──→ value validation + └──→ mapper + │ + ↓ + tuple[Instrument, ...] + + │ + ↓ + equivalence comparator + │ + ↓ + structured comparison report +``` + +Сетевые запросы в проверке не выполняются. + +Обе реализации получают один и тот же сохранённый документ, что исключает влияние изменений данных биржи между двумя отдельными REST-запросами. + +--- + +## Реализованные файлы + +Созданы: + +```text +app/tests/support/instrument_reference_equivalence.py + +app/tests/unit/market_data/acquisition/ +└── test_equivalence_comparator.py + +app/tests/integration/market_data/acquisition/ +└── test_instrument_reference_equivalence.py +``` + +Production-код не изменялся. + +Не создавался production-модуль: + +```text +app/src/market_data/acquisition/equivalence.py +``` + +Это принципиальное архитектурное решение: механизм проверки эквивалентности является временным миграционным инструментом и не должен создавать зависимость новой production-подсистемы от legacy-модели `ExchangeSymbol`. + +--- + +## Проверяемые реализации + +### Legacy implementation + +Проверяется существующая логика: + +```text +ExchangeService._extract_exchange_symbols_raw() +ExchangeService._parse_exchange_symbol() +``` + +Результат: + +```text +list[ExchangeSymbol] +``` + +Для запуска legacy parsing logic используется: + +```python +service = object.__new__(ExchangeService) +``` + +Это позволяет проверить существующие parsing helpers без запуска: + +```text +ExchangeService.__init__() +load_settings() +JournalService() +REST request +exchange symbols cache +``` + +На корректном документе используемые parsing helpers не требуют `settings` или `journal`. + +Legacy-логика не копируется и не переписывается внутри теста. + +--- + +### New implementation + +Проверяется существующий Handler: + +```text +DzengiInstrumentDocumentHandler +``` + +Вызов: + +```python +DzengiInstrumentDocumentHandler().handle_instrument_document(document) +``` + +Через него запускается новая pipeline: + +```text +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +tuple[Instrument, ...] +``` + +Таким образом Build 013 проверяет реальную реализацию, созданную в предыдущих Build, а не её тестовую копию. + +--- + +## Источник тестовых данных + +Для integration-проверки используется сохранённый реальный sample: + +```text +app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json +``` + +Один и тот же JSON document передаётся обеим реализациям. + +Это гарантирует корректность сравнения: + +```text +same input + ↓ +legacy implementation + ↓ +new implementation + ↓ +equivalence comparison +``` + +--- + +## Общие поля моделей + +Legacy-модель: + +```text +ExchangeSymbol +``` + +Новая модель: + +```text +Instrument +``` + +Сравниваются только поля, существующие одновременно в обеих моделях: + +```text +symbol +name +status +base_asset +quote_asset +market_modes +market_type +tick_size +step_size +min_qty +min_notional +``` + +Всего: + +```text +11 общих полей +``` + +--- + +## Поля новой модели, не участвующие в проверке эквивалентности + +Новая модель `Instrument` содержит дополнительные поля: + +```text +asset_type +order_types +base_asset_precision +quote_asset_precision +tick_value +max_qty +country +sector +industry +trading_hours +``` + +Их отсутствие в legacy-модели `ExchangeSymbol` не является нарушением эквивалентности. + +Эти поля являются расширением новой внутренней модели Instrument Reference Data. + +--- + +## Каноническое сравнение числовых значений + +Legacy implementation хранит числовые ограничения как: + +```text +float | None +``` + +Новая модель хранит их как: + +```text +Decimal | None +``` + +Для корректного сравнения обе стороны приводятся к общей канонической форме: + +```python +Decimal(str(value)) +``` + +Пример: + +```text +legacy: +0.0001 + +new: +Decimal("0.0001") + +canonical comparison: +Decimal("0.0001") == Decimal("0.0001") +``` + +Таким образом различие представления: + +```text +float +Decimal +``` + +не считается различием значения. + +Для сравнения не используется: + +```text +math.isclose() +``` + +Поскольку биржевые ограничения являются точными справочными десятичными значениями, а не измерениями с допустимой погрешностью. + +--- + +## Сравнение market_modes + +Legacy-модель использует: + +```python +list[str] +``` + +Новая модель использует: + +```python +tuple[str, ...] +``` + +Обе стороны приводятся к: + +```python +tuple(value) +``` + +Поэтому: + +```text +["REGULAR"] +``` + +эквивалентно: + +```text +("REGULAR",) +``` + +Тип контейнера не считается расхождением. + +Порядок значений сохраняется и участвует в сравнении. + +Например: + +```text +("REGULAR", "CLOSE_ONLY") +``` + +не эквивалентно: + +```text +("CLOSE_ONLY", "REGULAR") +``` + +--- + +## Сравнение строковых значений + +Следующие поля сравниваются точно: + +```text +symbol +name +status +base_asset +quote_asset +market_type +``` + +Не выполняется дополнительная нормализация: + +```text +lower() +upper() +casefold() +alias mapping +``` + +Причина: Build 013 должен обнаруживать реальные различия интерпретации между двумя реализациями, а не скрывать их дополнительной логикой comparator. + +--- + +## Проверка состава инструментов + +До сравнения полей проверяются: + +```text +дубликаты symbol в legacy implementation; +дубликаты symbol в new implementation; +symbol существует только в legacy; +symbol существует только в new. +``` + +После проверки дубликатов строятся индексы: + +```text +symbol → ExchangeSymbol +symbol → Instrument +``` + +Для построения индексов используется сохранение первого встретившегося объекта. + +Дубликаты при этом уже отдельно фиксируются как mismatch и не могут быть незаметно скрыты перезаписью значения в словаре. + +--- + +## Модель расхождения + +Каждое найденное расхождение представлено структурой: + +```text +InstrumentReferenceMismatch +``` + +Поля: + +```text +kind +symbol +field +legacy_value +new_value +message +``` + +Поддерживаемые категории: + +```text +duplicate_legacy +duplicate_new +missing_in_legacy +missing_in_new +field_mismatch +``` + +Пример расхождения поля: + +```text +kind: field_mismatch +symbol: ETH/EUR_LEVERAGE +field: min_notional +legacy: None +new: Decimal("2") +``` + +--- + +## Модель итогового отчёта + +Результат сравнения представлен структурой: + +```text +InstrumentReferenceEquivalenceReport +``` + +Поля: + +```text +legacy_count +new_count +compared_count +mismatches +``` + +Также предоставляется свойство: + +```text +is_equivalent +``` + +Логика: + +```text +mismatches == () + ↓ +is_equivalent == True +``` + +При наличии хотя бы одного mismatch: + +```text +is_equivalent == False +``` + +--- + +## Диагностический отчёт + +Метод: + +```text +InstrumentReferenceEquivalenceReport.format() +``` + +формирует человекочитаемый диагностический отчёт. + +Успешный результат имеет вид: + +```text +Instrument Reference Data equivalence report +legacy_count: 51 +new_count: 51 +compared_count: 51 +mismatches: 0 +is_equivalent: True +``` + +При обнаружении различий отчёт содержит для каждого mismatch: + +```text +kind +symbol +field +legacy value +new value +message +``` + +Это позволяет анализировать все обнаруженные расхождения, а не только первое. + +--- + +## Unit-тесты comparator + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/test_equivalence_comparator.py +``` + +Реализовано 11 тестов. + +Проверяются: + +1. полностью эквивалентные инструменты; +2. эквивалентность `float` и `Decimal`; +3. эквивалентность `list` и `tuple` для `market_modes`; +4. несовпадение строкового поля; +5. несовпадение числового поля; +6. символ отсутствует в новой реализации; +7. символ отсутствует в legacy-реализации; +8. duplicate symbol в legacy; +9. duplicate symbol в новой реализации; +10. несколько расхождений одновременно; +11. корректное формирование диагностического отчёта. + +Результат: + +```text +11 passed in 0.02s +``` + +--- + +## Integration-проверка реального exchangeInfo sample + +Создан файл: + +```text +app/tests/integration/market_data/acquisition/ +└── test_instrument_reference_equivalence.py +``` + +Проверяется полный путь: + +```text +all.json + │ + ├──→ legacy parser + │ ↓ + │ list[ExchangeSymbol] + │ + └──→ DzengiInstrumentDocumentHandler + ↓ + tuple[Instrument, ...] + │ + ↓ + compare_instrument_reference_data() + │ + ↓ + InstrumentReferenceEquivalenceReport +``` + +Основное утверждение: + +```python +assert report.is_equivalent, report.format() +``` + +Результат: + +```text +1 passed in 0.10s +``` + +Проверка подтвердила эквивалентность legacy- и новой реализации на одном и том же реальном `exchangeInfo` sample. + +--- + +## Проверка синтаксиса + +Выполнена команда: + +```bash +python -m py_compile \ + tests/support/instrument_reference_equivalence.py \ + tests/unit/market_data/acquisition/test_equivalence_comparator.py \ + tests/integration/market_data/acquisition/test_instrument_reference_equivalence.py +``` + +Результат: + +```text +успешно +``` + +Ошибок синтаксиса не обнаружено. + +--- + +## Полный регрессионный прогон + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +192 passed in 0.15s +``` + +Регрессий существующего проекта не обнаружено. + +--- + +## Проверка отсутствия production-зависимостей + +Выполнена команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "compare_instrument_reference_data|InstrumentReferenceMismatch|InstrumentReferenceEquivalenceReport" \ + src tests +``` + +Результат подтвердил, что все сущности механизма проверки эквивалентности находятся только в: + +```text +tests/support/ +tests/unit/ +tests/integration/ +``` + +В каталоге: + +```text +src/ +``` + +упоминаний нет. + +Следовательно: + +```text +production-код не зависит от comparator; +новая acquisition-подсистема не зависит от legacy-модели ради runtime; +ExchangeService не изменён; +Telegram UI не изменён; +AutoTrade не изменён; +trading runtime не изменён. +``` + +--- + +## Изменения production-кода + +В рамках Build 013 production-код не изменялся. + +Не изменены: + +```text +app/src/integrations/exchange/service.py +app/src/integrations/exchange/models.py + +app/src/market_data/acquisition/ +app/src/telegram/ +app/src/trading/ +``` + +Не создавался: + +```text +app/src/market_data/acquisition/equivalence.py +``` + +--- + +## Обратная совместимость + +Полностью сохранены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +``` + +Также не изменены: + +```text +сигнатуры существующих production-методов; +legacy imports; +формат runtime-ошибок; +Telegram UI; +автоторговля; +существующее runtime-поведение; +exchange symbols cache. +``` + +--- + +## Что Build 013 намеренно не делает + +Build 013 не выполняет: + +```text +изменение legacy parser; +изменение нового parser; +изменение mapper; +переключение get_exchange_symbols(); +создание compatibility mapper Instrument → ExchangeSymbol; +перенос кэша; +изменение validate_symbol(); +изменение get_symbol_runtime_status(); +подключение InstrumentAcquisitionService к production runtime; +изменение Telegram UI; +изменение AutoTrade. +``` + +Эти изменения относятся к последующим Build утверждённого Migration Plan. + +--- + +## Классификация изменений + +| Изменение | Классификация | +|---|---| +| Comparator legacy/new | Миграционная проверка | +| Структурированный отчёт | Улучшение диагностируемости | +| Unit-тесты comparator | Обязательная проверка надёжности | +| Integration-тест на одном sample | Обязательная проверка эквивалентности | +| Новый production-модуль | Не создавался | +| Изменение legacy-кода | Отсутствует | +| Изменение новой acquisition pipeline | Отсутствует | +| Изменение runtime-поведения | Отсутствует | + +--- + +## Итоговые проверки + +| Проверка | Результат | +|---|---| +| Unit-тесты comparator | `11 passed in 0.02s` | +| Integration-тест реального sample | `1 passed in 0.10s` | +| `py_compile` | Успешно | +| Полный `pytest` | `192 passed in 0.15s` | +| Отсутствие production-интеграции comparator | Подтверждено | + +--- + +## Условие завершения Build 013 + +Все условия выполнены: + +```text +[✓] Comparator обнаруживает категории различий. + +[✓] Legacy и new implementations получают один и тот же документ. + +[✓] Legacy parser запускается без REST-запроса и кэша. + +[✓] New pipeline запускается через реальный DzengiInstrumentDocumentHandler. + +[✓] Дубликаты проверяются до сравнения полей. + +[✓] Проверяются все 11 общих полей. + +[✓] Реальный exchangeInfo sample проходит без mismatches. + +[✓] Unit-тесты comparator проходят. + +[✓] Integration-тест проходит. + +[✓] Синтаксическая проверка проходит. + +[✓] Полный набор тестов проекта проходит. + +[✓] Production-код не изменён. + +[✓] Обратная совместимость полностью сохранена. +``` + +--- + +## Результат + +```text +BUILD 013 — COMPLETE +``` + +Проверка подтвердила эквивалентность существующей legacy-реализации и новой Instrument Reference Data pipeline на одном и том же реальном документе `exchangeInfo`. + +Можно переходить к следующему этапу утверждённого Migration Plan: + +```text +Build 014 — Compatibility mapper Instrument → ExchangeSymbol +``` \ No newline at end of file diff --git a/docs/migrations/build_014.md b/docs/migrations/build_014.md new file mode 100644 index 0000000..87bfce2 --- /dev/null +++ b/docs/migrations/build_014.md @@ -0,0 +1,890 @@ +# Dzentra — Instrument Reference Data Migration — Build 014 + +> Статус: Завершён + +## Название + +**Compatibility mapper Instrument → ExchangeSymbol** + +--- + +## Цель + +Создать временный compatibility-слой, преобразующий новую внутреннюю модель: + +```text +Instrument +``` + +в существующую legacy-модель: + +```text +ExchangeSymbol +``` + +Целевая цепочка: + +```text +new acquisition pipeline + ↓ +tuple[Instrument, ...] + ↓ +compatibility mapper + ↓ +list[ExchangeSymbol] +``` + +Compatibility mapper необходим для последующего переключения: + +```text +ExchangeService.get_exchange_symbols() +``` + +на новую Instrument Reference Data pipeline без изменения существующего публичного контракта: + +```python +def get_exchange_symbols(self) -> list[ExchangeSymbol]: + ... +``` + +Build 014 создаёт только compatibility-границу. + +Переключение `ExchangeService.get_exchange_symbols()` в рамках этого Build не выполняется. + +--- + +## Причина создания compatibility-слоя + +К началу Build 014 завершены: + +```text +Build 001–012 + ↓ +создана новая независимая Instrument Reference Data acquisition pipeline + +Build 013 + ↓ +доказана эквивалентность legacy- и новой реализации +на одном и том же реальном exchangeInfo sample +``` + +Новая pipeline возвращает: + +```python +tuple[Instrument, ...] +``` + +Существующий legacy-контракт возвращает: + +```python +list[ExchangeSymbol] +``` + +Существующие production-потребители продолжают ожидать: + +```text +ExchangeSymbol +``` + +Поэтому прямое переключение невозможно без временного преобразования: + +```text +Instrument + ↓ +ExchangeSymbol +``` + +--- + +## Реализованные файлы + +Создан production-файл: + +```text +app/src/market_data/acquisition/compatibility.py +``` + +Создан unit-тест: + +```text +app/tests/unit/market_data/acquisition/test_compatibility.py +``` + +Другие файлы в рамках Build 014 не изменялись. + +--- + +## Размещение compatibility mapper + +Compatibility mapper размещён в: + +```text +app/src/market_data/acquisition/compatibility.py +``` + +Он намеренно не размещён в: + +```text +app/src/market_data/acquisition/adapters/dzengi/ +``` + +поскольку преобразование: + +```text +Instrument → ExchangeSymbol +``` + +не зависит от формата Dzengi. + +Он также не размещён в: + +```text +app/src/integrations/exchange/ +``` + +поскольку новый миграционный код не должен расширять legacy-подсистему. + +Архитектурная граница имеет следующий вид: + +```text +new acquisition model + ↓ +compatibility.py + ↓ +legacy integration model +``` + +Compatibility mapper является временным слоем и должен быть удалён после полного перевода production-потребителей: + +```text +Build 024 — Удаление compatibility-слоя +``` + +--- + +## Реализованные функции + +Созданы две функции: + +```python +def map_instrument_to_exchange_symbol( + instrument: Instrument, +) -> ExchangeSymbol: + ... +``` + +и: + +```python +def map_instruments_to_exchange_symbols( + instruments: tuple[Instrument, ...], +) -> list[ExchangeSymbol]: + ... +``` + +Первая функция преобразует один объект: + +```text +Instrument + ↓ +ExchangeSymbol +``` + +Вторая преобразует полный immutable-набор: + +```text +tuple[Instrument, ...] + ↓ +list[ExchangeSymbol] +``` + +--- + +## Архитектурные зависимости + +Compatibility mapper сознательно зависит от обеих моделей: + +```python +from src.integrations.exchange.models import ExchangeSymbol +from src.market_data.acquisition.models.instrument import Instrument +``` + +Это допустимая временная зависимость: + +```text +новая acquisition-подсистема + ↓ +compatibility boundary + ↓ +legacy contract +``` + +Никакие другие компоненты новой acquisition pipeline не изменялись для добавления зависимости от `ExchangeSymbol`. + +Re-export через: + +```text +app/src/market_data/acquisition/__init__.py +``` + +не добавлялся. + +--- + +## Соответствие полей + +Compatibility mapper переносит все 11 полей, общих для `Instrument` и `ExchangeSymbol`: + +```text +Instrument.symbol + → ExchangeSymbol.symbol + +Instrument.name + → ExchangeSymbol.name + +Instrument.status + → ExchangeSymbol.status + +Instrument.base_asset + → ExchangeSymbol.base_asset + +Instrument.quote_asset + → ExchangeSymbol.quote_asset + +Instrument.market_modes + → ExchangeSymbol.market_modes + +Instrument.market_type + → ExchangeSymbol.market_type + +Instrument.tick_size + → ExchangeSymbol.tick_size + +Instrument.step_size + → ExchangeSymbol.step_size + +Instrument.min_qty + → ExchangeSymbol.min_qty + +Instrument.min_notional + → ExchangeSymbol.min_notional +``` + +Никакая повторная предметная интерпретация данных в compatibility mapper не выполняется. + +--- + +## Поля новой модели, не представленные в legacy-модели + +Модель `Instrument` содержит дополнительные поля: + +```text +asset_type +order_types +base_asset_precision +quote_asset_precision +tick_value +max_qty +country +sector +industry +trading_hours +``` + +Эти поля отсутствуют в legacy-модели: + +```text +ExchangeSymbol +``` + +Поэтому они намеренно не переносятся через compatibility boundary. + +Это ожидаемая потеря расширенной информации: + +```text +полная новая модель Instrument + ↓ +ограниченный legacy-контракт ExchangeSymbol +``` + +Compatibility mapper не: + +```text +расширяет ExchangeSymbol; +создаёт дополнительные атрибуты; +переносит данные в несоответствующие поля; +изменяет модель Instrument. +``` + +--- + +## Преобразование Decimal → float + +Новая модель `Instrument` использует: + +```python +Decimal | None +``` + +Legacy-модель `ExchangeSymbol` использует: + +```python +float | None +``` + +Преобразованию подлежат: + +```text +tick_size +step_size +min_qty +min_notional +``` + +Правило преобразования: + +```text +None + ↓ +None +``` + +и: + +```text +Decimal("0.0001") + ↓ +0.0001 +``` + +Для этого реализован внутренний helper: + +```python +def _decimal_to_float( + value: Decimal | None, +) -> float | None: + if value is None: + return None + + return float(value) +``` + +Переход к `float` выполняется только на compatibility-границе. + +Внутри новой Instrument Reference Data pipeline точное десятичное представление через `Decimal` сохраняется. + +--- + +## Преобразование market_modes + +Новая модель использует: + +```python +tuple[str, ...] +``` + +Legacy-модель использует: + +```python +list[str] +``` + +Compatibility mapper выполняет: + +```python +list(instrument.market_modes) +``` + +Например: + +```text +Instrument: +("REGULAR", "CLOSE_ONLY") + + ↓ + +ExchangeSymbol: +["REGULAR", "CLOSE_ONLY"] +``` + +Порядок значений сохраняется. + +Для каждого результата создаётся новый независимый список. + +Изменение: + +```python +exchange_symbol.market_modes.append("ADDED_IN_LEGACY") +``` + +не изменяет: + +```python +instrument.market_modes +``` + +и не влияет на другие объекты `ExchangeSymbol`, созданные из того же `Instrument`. + +--- + +## Преобразование полного набора инструментов + +Функция: + +```python +map_instruments_to_exchange_symbols() +``` + +принимает: + +```python +tuple[Instrument, ...] +``` + +и возвращает: + +```python +list[ExchangeSymbol] +``` + +Порядок инструментов сохраняется. + +Например: + +```text +( + BTC/USD_LEVERAGE, + ETH/USD_LEVERAGE, + XRP/USD_LEVERAGE, +) +``` + +преобразуется в: + +```text +[ + BTC/USD_LEVERAGE, + ETH/USD_LEVERAGE, + XRP/USD_LEVERAGE, +] +``` + +Для пустого входного набора: + +```python +() +``` + +возвращается: + +```python +[] +``` + +--- + +## Обработка ошибок + +Новый тип исключения в рамках Build 014 не создавался. + +Compatibility mapper получает уже построенную и проверенную модель: + +```text +Instrument +``` + +и выполняет только: + +```text +чтение полей; +Decimal → float; +tuple → list; +создание ExchangeSymbol. +``` + +Существующая ошибка: + +```text +InstrumentReferenceMappingError +``` + +не переиспользуется, поскольку она относится к другому направлению преобразования: + +```text +Dzengi raw model + ↓ +Instrument +``` + +Создание отдельной категории ошибки без конкретного реального сценария не требуется. + +--- + +## Unit-тесты + +Создан файл: + +```text +app/tests/unit/market_data/acquisition/test_compatibility.py +``` + +Реализовано 12 тестов. + +Проверяются: + +1. преобразование одного `Instrument` в `ExchangeSymbol`; +2. перенос всех 11 общих legacy-полей; +3. преобразование `Decimal → float`; +4. сохранение `None` для отсутствующих числовых значений; +5. преобразование `tuple[str, ...] → list[str]` для `market_modes`; +6. сохранение порядка `market_modes`; +7. создание независимого списка `market_modes`; +8. преобразование нескольких инструментов; +9. сохранение порядка инструментов; +10. пустой `tuple` преобразуется в пустой `list`; +11. исходный `Instrument` не изменяется; +12. результат compatibility mapper эквивалентен исходным `Instrument` по comparator Build 013. + +--- + +## Round-trip проверка через comparator Build 013 + +Для дополнительной проверки используется уже созданный в Build 013 comparator: + +```text +tuple[Instrument, ...] + ↓ +map_instruments_to_exchange_symbols() + ↓ +list[ExchangeSymbol] + ↓ +compare_instrument_reference_data() + ↓ +InstrumentReferenceEquivalenceReport +``` + +Основная проверка: + +```python +legacy_symbols = map_instruments_to_exchange_symbols( + instruments +) + +report = compare_instrument_reference_data( + legacy_symbols, + instruments, +) + +assert report.is_equivalent, report.format() +``` + +Это подтверждает, что compatibility mapper воспроизводит все 11 общих полей legacy-контракта. + +Comparator остаётся только в тестовом контуре. + +Production-код от comparator не зависит. + +--- + +## Результат unit-тестов + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/test_compatibility.py \ + -q +``` + +Результат: + +```text +12 passed in 0.02s +``` + +Все тесты compatibility mapper успешно пройдены. + +--- + +## Проверка синтаксиса + +Выполнена команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/compatibility.py \ + tests/unit/market_data/acquisition/test_compatibility.py +``` + +Результат: + +```text +успешно +``` + +Ошибок синтаксиса не обнаружено. + +--- + +## Полный регрессионный прогон + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +204 passed in 0.19s +``` + +До Build 014 полный набор содержал: + +```text +192 passed +``` + +В Build 014 добавлено: + +```text +12 новых тестов +``` + +Итого: + +```text +192 + 12 = 204 +``` + +Все предыдущие тесты продолжают проходить. + +Регрессий существующего проекта не обнаружено. + +--- + +## Проверка отсутствия преждевременного production-использования + +Выполнена команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols" \ + src tests +``` + +Результат подтвердил, что функции compatibility mapper используются только в: + +```text +src/market_data/acquisition/compatibility.py +tests/unit/market_data/acquisition/test_compatibility.py +``` + +Других production-потребителей нет. + +В частности: + +```text +ExchangeService.get_exchange_symbols() +``` + +ещё не использует compatibility mapper. + +Это подтверждает отсутствие преждевременного переключения production runtime. + +--- + +## Изменения production-кода + +В рамках Build 014 создан только один новый production-файл: + +```text +app/src/market_data/acquisition/compatibility.py +``` + +Не изменялись: + +```text +app/src/integrations/exchange/service.py +app/src/integrations/exchange/models.py + +app/src/market_data/acquisition/service.py +app/src/market_data/acquisition/registry.py +app/src/market_data/acquisition/protocol.py +app/src/market_data/acquisition/exceptions.py +app/src/market_data/acquisition/models/instrument.py + +app/src/market_data/acquisition/adapters/dzengi/ +app/src/market_data/acquisition/handlers/instrument_handler.py +app/src/market_data/acquisition/feeds/instrument_feed.py + +app/src/telegram/ +app/src/trading/ +``` + +--- + +## Обратная совместимость + +Полностью сохранены: + +```text +ExchangeService.get_exchange_symbols() +ExchangeService.validate_symbol() +ExchangeService.get_symbol_runtime_status() +``` + +Также не изменены: + +```text +сигнатуры существующих production-методов; +legacy imports; +формат runtime-ошибок; +Telegram UI; +автоторговля; +существующее runtime-поведение; +exchange symbols cache. +``` + +--- + +## Что Build 014 намеренно не делает + +Build 014 не выполняет: + +```text +переключение ExchangeService.get_exchange_symbols(); +подключение InstrumentAcquisitionService к ExchangeService; +создание production composition; +изменение exchange symbols cache; +изменение validate_symbol(); +изменение get_symbol_runtime_status(); +изменение normalize_symbol(); +изменение symbol_candidates(); +изменение legacy parser; +удаление legacy parser; +изменение ExchangeSymbol; +изменение Instrument; +изменение Telegram UI; +изменение AutoTrade. +``` + +Переключение: + +```text +ExchangeService.get_exchange_symbols() +``` + +относится строго к следующему этапу: + +```text +Build 015 — Переключение get_exchange_symbols() +``` + +--- + +## Классификация изменений + +| Изменение | Классификация | +|---|---| +| Compatibility mapper `Instrument → ExchangeSymbol` | Обязательное архитектурное изменение | +| `Decimal → float` на legacy-границе | Обратная совместимость | +| `tuple → list` для `market_modes` | Обратная совместимость | +| Batch mapper | Обязательное архитектурное изменение | +| Unit-тесты | Улучшение надёжности | +| Round-trip проверка через Build 013 comparator | Миграционная проверка | +| Новый exception | Не создавался | +| Изменение legacy runtime | Отсутствует | +| Изменение поведения | Отсутствует | + +--- + +## Итоговые проверки + +| Проверка | Результат | +|---|---| +| Unit-тесты Compatibility Mapper | `12 passed in 0.02s` | +| `py_compile` | Успешно | +| Полный `pytest` | `204 passed in 0.19s` | +| Round-trip эквивалентность | Подтверждена | +| Независимость `market_modes` | Подтверждена | +| Сохранение порядка инструментов | Подтверждено | +| Отсутствие преждевременного production-использования | Подтверждено | + +--- + +## Условие завершения Build 014 + +Все условия выполнены: + +```text +[✓] Создан compatibility mapper Instrument → ExchangeSymbol. + +[✓] Переносятся все 11 общих legacy-полей. + +[✓] Decimal корректно преобразуется в float только на legacy-границе. + +[✓] None сохраняется. + +[✓] market_modes преобразуется из tuple в независимый list. + +[✓] Порядок market_modes сохраняется. + +[✓] Batch mapper сохраняет порядок инструментов. + +[✓] Пустой tuple преобразуется в пустой list. + +[✓] Исходные Instrument не изменяются. + +[✓] Round-trip проверка через comparator Build 013 проходит. + +[✓] 12 unit-тестов проходят. + +[✓] Синтаксическая проверка проходит. + +[✓] Полный набор из 204 тестов проходит. + +[✓] ExchangeService ещё не использует compatibility mapper. + +[✓] Production runtime не переключён преждевременно. + +[✓] Обратная совместимость полностью сохранена. +``` + +--- + +## Результат + +```text +BUILD 014 — COMPLETE +``` + +Создана временная compatibility-граница: + +```text +Instrument + ↓ +ExchangeSymbol +``` + +Она позволяет на следующем этапе переключить: + +```text +ExchangeService.get_exchange_symbols() +``` + +на новую Instrument Reference Data acquisition pipeline без изменения существующего публичного контракта: + +```python +def get_exchange_symbols(self) -> list[ExchangeSymbol]: + ... +``` + +Следующий этап утверждённого Migration Plan: + +```text +Build 015 — Переключение get_exchange_symbols() +``` \ No newline at end of file diff --git a/docs/migrations/build_015.md b/docs/migrations/build_015.md new file mode 100644 index 0000000..c031624 --- /dev/null +++ b/docs/migrations/build_015.md @@ -0,0 +1,552 @@ +# Build 015 — Переключение `get_exchange_symbols()` на новый Acquisition Pipeline + +## Статус + +**COMPLETE** + +--- + +## Цель + +Переключить существующий публичный метод: + +```python +ExchangeService.get_exchange_symbols() +``` + +с прямого legacy-получения и обработки `exchangeInfo` на новый стандартизированный Instrument Reference Data acquisition pipeline, сохранив при этом существующий внешний контракт и работоспособность старого бота. + +--- + +## Исходное состояние + +До Build 015 метод: + +```python +ExchangeService.get_exchange_symbols() +``` + +самостоятельно выполнял весь цикл обработки `exchangeInfo`: + +1. создавал `ExchangeRestClient`; +2. выполнял прямой REST-запрос: + + ```text + /api/v1/exchangeInfo + ``` + +3. извлекал массив `symbols`; +4. преобразовывал каждый элемент в legacy-модель `ExchangeSymbol`; +5. сохранял результат в class-level cache: + + ```python + _exchange_symbols_cache + ``` + +Таким образом, transport, validation, parsing, mapping и compatibility logic были сосредоточены внутри legacy `ExchangeService`. + +--- + +## Реализованное изменение + +Метод: + +```python +ExchangeService.get_exchange_symbols() +``` + +переключён на новый Instrument Reference Data acquisition pipeline. + +Теперь production-путь использует следующую цепочку: + +```text +ExchangeService.get_exchange_symbols() + │ + ▼ +_load_exchange_symbols_via_acquisition() + │ + ▼ +DzengiInstrumentDocumentSource + │ + ▼ +DzengiInstrumentDocumentHandler + │ + ▼ +InstrumentFeed + │ + ▼ +InstrumentFeedRegistry + │ + ▼ +InstrumentAcquisitionService + │ + ▼ +Instrument + │ + ▼ +map_instruments_to_exchange_symbols() + │ + ▼ +ExchangeSymbol +``` + +--- + +## Новый production-путь + +В `ExchangeService` используется отдельный compatibility bridge: + +```python +def _load_exchange_symbols_via_acquisition( + self, +) -> list[ExchangeSymbol]: +``` + +Его задача: + +1. создать источник Instrument Reference Data для Dzengi; +2. создать обработчик документа; +3. собрать `InstrumentFeed`; +4. зарегистрировать feed; +5. выполнить acquisition через `InstrumentAcquisitionService`; +6. получить канонические модели `Instrument`; +7. преобразовать их в legacy-модели `ExchangeSymbol`. + +Это позволяет старому боту продолжать использовать существующий контракт: + +```python +list[ExchangeSymbol] +``` + +при том, что фактическим источником данных уже является новая архитектура `market_data/acquisition`. + +--- + +## Сохранённый публичный контракт + +Сигнатура метода не изменилась: + +```python +def get_exchange_symbols(self) -> list[ExchangeSymbol]: +``` + +Это принципиально важно для безопасной поэтапной миграции. + +Существующие потребители не требуют немедленного изменения и продолжают работать через прежний API. + +В частности, существующий UI продолжает использовать: + +```python +exchange_service.get_exchange_symbols() +``` + +без знания о внутреннем переходе на новый acquisition pipeline. + +--- + +## Сохранение cache semantics + +Сохранён существующий class-level cache: + +```python +_exchange_symbols_cache: list[ExchangeSymbol] | None = None +``` + +Поведение осталось прежним: + +```text +Первый вызов + │ + ▼ +Новый acquisition pipeline + │ + ▼ +Compatibility mapping + │ + ▼ +_exchange_symbols_cache + │ + ▼ +list[ExchangeSymbol] +``` + +Последующие вызовы: + +```text +_exchange_symbols_cache + │ + ▼ +list[ExchangeSymbol] +``` + +без повторного обращения к acquisition pipeline. + +Cache заполняется только после успешной загрузки данных. + +При ошибке acquisition cache остаётся незаполненным. + +--- + +## Поведение при отключённой бирже + +Сохранено прежнее поведение: + +```python +if not self.settings.exchange_enabled: + return [] +``` + +Новый acquisition pipeline в этом случае не вызывается. + +--- + +## Обработка ошибок + +Ошибки нового acquisition pipeline проходят через существующую систему `ExchangeService`. + +При ошибке: + +1. ошибка логируется через: + + ```python + self._log_exchange_error(...) + ``` + +2. используется legacy endpoint identifier: + + ```text + exchangeInfo + ``` + +3. вызывающему коду возвращается совместимая `ExchangeError`. + +Это сохраняет существующее поведение старого бота и его журналирования. + +--- + +## Удаление прямого legacy REST-пути + +После Build 015 метод: + +```python +get_exchange_symbols() +``` + +больше не выполняет прямой вызов: + +```python +ExchangeRestClient().get_json("/api/v1/exchangeInfo") +``` + +Фактический REST transport теперь инкапсулирован в: + +```text +src/market_data/acquisition/adapters/dzengi/rest.py +``` + +через: + +```python +DzengiInstrumentDocumentSource +``` + +и константу: + +```python +_EXCHANGE_INFO_PATH = "/api/v1/exchangeInfo" +``` + +Таким образом, ownership получения Instrument Reference Data перенесён из: + +```text +integrations/exchange +``` + +в: + +```text +market_data/acquisition +``` + +--- + +## Legacy helpers + +В `ExchangeService` временно остаются legacy helpers: + +```python +_extract_exchange_symbols_raw() +_parse_exchange_symbol() +_parse_exchange_symbol_status() +_parse_market_modes() +_extract_filter_value() +``` + +Они больше не являются частью нового production-пути `get_exchange_symbols()`. + +Их немедленное удаление не выполнялось в Build 015, поскольку миграция проводится поэтапно и без ненужного расширения scope текущего Build. + +Удаление legacy helpers должно выполняться отдельным контролируемым этапом после подтверждения отсутствия production-зависимостей и завершения необходимых migration/equivalence проверок. + +--- + +## Добавленные тесты + +Создан файл: + +```text +tests/unit/integrations/exchange/test_service_exchange_symbols.py +``` + +Тестами проверяются: + +- возврат пустого списка при отключённой бирже; +- отсутствие вызова acquisition pipeline при отключённой бирже; +- возврат существующего cache; +- отсутствие повторного acquisition при наличии cache; +- загрузка через новый acquisition pipeline; +- заполнение `_exchange_symbols_cache`; +- повторное использование cache; +- сохранение legacy-типа `ExchangeSymbol`; +- сохранение порядка инструментов; +- корректное распространение ошибок; +- отсутствие заполнения cache при ошибке; +- сохранение существующего error logging; +- отсутствие прямого legacy REST-вызова из `get_exchange_symbols()`; +- корректная сборка нового acquisition pipeline; +- использование compatibility mapper; +- корректное поведение пустого результата. + +--- + +## Исправление статической типизации теста + +После первоначального завершения Build 015 в файле: + +```text +tests/unit/integrations/exchange/test_service_exchange_symbols.py +``` + +были обнаружены две ошибки статической типизации Pylance. + +### Типизация yield-fixture + +Исходная аннотация: + +```python +@pytest.fixture(autouse=True) +def reset_exchange_symbols_cache() -> None: +``` + +была некорректна, поскольку функция содержит `yield` и является генератором. + +Исправлено на: + +```python +@pytest.fixture(autouse=True) +def reset_exchange_symbols_cache() -> Iterator[None]: + ExchangeService._exchange_symbols_cache = None + + yield + + ExchangeService._exchange_symbols_cache = None +``` + +Добавлен импорт: + +```python +from collections.abc import Iterator +``` + +### Типизация тестовых settings + +Тестовый helper создаёт `ExchangeService` без вызова его конструктора: + +```python +service = object.__new__(ExchangeService) +``` + +Для изоляции теста используется `SimpleNamespace`, тогда как production-атрибут: + +```python +service.settings +``` + +типизирован как `Settings`. + +Для явного обозначения тестовой границы применён `cast`: + +```python +service.settings = cast( + Settings, + _settings( + exchange_enabled=exchange_enabled, + ), +) +``` + +Таким образом: + +- production-код не изменялся; +- тестовая изоляция сохранена; +- `# type: ignore` не использовался; +- ошибки Pylance устранены. + +--- + +## Результаты окончательной проверки + +### Проверка компиляции + +Команда: + +```bash +python -m py_compile \ + src/integrations/exchange/service.py \ + tests/unit/integrations/exchange/test_service_exchange_symbols.py +``` + +Результат: + +```text +Ошибок нет. +``` + +--- + +### Unit-тесты Build 015 + +Команда: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_exchange_symbols.py \ + -q +``` + +Результат: + +```text +16 passed in 0.07s +``` + +--- + +### Полный regression suite + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +220 passed in 0.15s +``` + +--- + +## Проверка production-пути + +Выполнен поиск: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "get_exchange_symbols|_exchange_symbols_cache|_load_exchange_symbols_via_acquisition|DzengiInstrumentDocumentSource|InstrumentAcquisitionService|map_instruments_to_exchange_symbols|ExchangeRestClient.*exchangeInfo|exchangeInfo" \ + src tests +``` + +Проверка подтвердила: + +- `get_exchange_symbols()` использует `_load_exchange_symbols_via_acquisition()`; +- новый production-путь использует `DzengiInstrumentDocumentSource`; +- используется `InstrumentAcquisitionService`; +- используется compatibility mapper `map_instruments_to_exchange_symbols()`; +- class-level cache `_exchange_symbols_cache` сохранён; +- прямой legacy REST-вызов `exchangeInfo` удалён из `get_exchange_symbols()`; +- существующие внешние потребители продолжают работать через прежний публичный контракт. + +--- + +## Архитектурный результат + +До Build 015: + +```text +Legacy consumer + │ + ▼ +ExchangeService.get_exchange_symbols() + │ + ▼ +ExchangeRestClient + │ + ▼ +exchangeInfo + │ + ▼ +Legacy parsing + │ + ▼ +ExchangeSymbol +``` + +После Build 015: + +```text +Legacy consumer + │ + ▼ +ExchangeService.get_exchange_symbols() + │ + ▼ +Instrument Acquisition Pipeline + │ + ▼ +Canonical Instrument + │ + ▼ +Compatibility Mapper + │ + ▼ +ExchangeSymbol +``` + +Таким образом: + +- новый `market_data/acquisition` стал фактическим production-владельцем получения Instrument Reference Data; +- legacy `ExchangeService` сохраняет прежний публичный API; +- существующий бот продолжает работать без массового изменения потребителей; +- создан безопасный compatibility boundary между новой и старой архитектурой; +- переход выполнен без регрессий. + +--- + +## Итог + +```text +BUILD 015 — COMPLETE +``` + +Build 015 завершён. + +`ExchangeService.get_exchange_symbols()` успешно переключён на новый Instrument Reference Data acquisition pipeline с сохранением: + +- существующего публичного контракта; +- legacy-модели `ExchangeSymbol`; +- cache semantics; +- обработки ошибок; +- журналирования; +- существующих потребителей старого бота. + +Окончательные результаты проверки: + +```text +py_compile — успешно +16 passed in 0.07s +220 passed in 0.15s +``` \ No newline at end of file diff --git a/docs/migrations/build_016.md b/docs/migrations/build_016.md new file mode 100644 index 0000000..7c86d9a --- /dev/null +++ b/docs/migrations/build_016.md @@ -0,0 +1,544 @@ +# Build 016 — Перевод `normalize_symbol()` / `symbol_candidates()` + +## Статус + +**COMPLETE** + +--- + +## Цель Build + +Перенести каноническую реализацию функций нормализации и формирования кандидатов торгового символа из legacy-подсистемы: + +```text +src/integrations/exchange/symbol_utils.py +``` + +в новую подсистему Market Data Acquisition: + +```text +src/market_data/acquisition/symbols.py +``` + +при этом: + +- полностью сохранить существующее поведение; +- не нарушить работу legacy-кода; +- сохранить старый import path; +- исключить дублирование реализации; +- обеспечить постепенную миграцию без остановки работающего бота. + +--- + +## Исходное состояние + +До Build 016 функции: + +```python +normalize_symbol() +symbol_candidates() +``` + +были реализованы непосредственно в: + +```text +src/integrations/exchange/symbol_utils.py +``` + +Их использовал: + +```text +src/integrations/exchange/service.py +``` + +В частности, функции участвовали в: + +- нормализации запрошенного торгового символа; +- проверке существования инструмента; +- формировании альтернативных представлений символа; +- сопоставлении символов с данными `exchangeInfo`; +- поиске торговой комиссии для инструмента. + +Legacy-зависимость выглядела следующим образом: + +```text +ExchangeService + │ + ▼ +src.integrations.exchange.symbol_utils + │ + ├── normalize_symbol() + └── symbol_candidates() +``` + +--- + +## Целевая архитектура + +После Build 016 каноническая реализация находится в: + +```text +src/market_data/acquisition/symbols.py +``` + +Legacy-модуль: + +```text +src/integrations/exchange/symbol_utils.py +``` + +сохранён как compatibility facade. + +Итоговая зависимость: + +```text +ExchangeService + │ + ▼ +src.integrations.exchange.symbol_utils + │ + │ compatibility facade + ▼ +src.market_data.acquisition.symbols + │ + ├── normalize_symbol() + └── symbol_candidates() +``` + +Это позволяет сохранить существующий production-код без массового изменения импортов и одновременно установить новую каноническую точку владения логикой. + +--- + +## Созданный файл + +Создан: + +```text +src/market_data/acquisition/symbols.py +``` + +Он содержит единственную каноническую реализацию: + +```python +def normalize_symbol(raw_symbol: str) -> str: + ... + + +def symbol_candidates(raw_symbol: str) -> list[str]: + ... +``` + +--- + +## Изменённый legacy-модуль + +Файл: + +```text +src/integrations/exchange/symbol_utils.py +``` + +больше не содержит собственной реализации алгоритмов. + +Он импортирует функции непосредственно из: + +```text +src.market_data.acquisition.symbols +``` + +и предоставляет их через прежний import path. + +Таким образом: + +```python +legacy_normalize_symbol is new_normalize_symbol +``` + +и: + +```python +legacy_symbol_candidates is new_symbol_candidates +``` + +возвращают: + +```text +True +``` + +Это подтверждает отсутствие копирования или дублирования функций. + +--- + +## Зафиксированный контракт `normalize_symbol()` + +Функция сохраняет существующее legacy-поведение. + +### Нормализация регистра + +Пример: + +```text +btc/usd +``` + +преобразуется в: + +```text +BTC/USD +``` + +### Удаление внешних пробелов + +Пример: + +```text + btc/usd +``` + +преобразуется в: + +```text +BTC/USD +``` + +### Внутренние пробелы не удаляются + +Функция `normalize_symbol()` не выполняет удаление внутренних пробелов. + +### `%2F` не декодируется + +Пример: + +```text +BTC%2FUSD +``` + +остаётся: + +```text +BTC%2FUSD +``` + +Декодирование разделителя выполняется только на этапе формирования кандидатов. + +### Суффикс `_LEVERAGE` не добавляется автоматически + +Функция не модифицирует семантику инструмента и не добавляет leverage-суффикс. + +### Существующий `_LEVERAGE` сохраняется + +Если суффикс уже присутствует в исходном символе, он сохраняется. + +--- + +## Зафиксированный контракт `symbol_candidates()` + +Функция формирует упорядоченный список допустимых представлений символа. + +### Пустое значение + +Для пустого значения возвращается: + +```python +[] +``` + +### Первый кандидат + +Первым всегда является результат: + +```python +normalize_symbol(raw_symbol) +``` + +### Декодирование `%2F` + +Если символ содержит: + +```text +%2F +``` + +добавляется кандидат с: + +```text +/ +``` + +### Удаление обычных внутренних пробелов + +После декодирования разделителя формируется вариант без обычных пробелов. + +### Порядок преобразований сохраняется + +Порядок кандидатов является частью compatibility-контракта: + +```text +1. Нормализованное исходное значение. +2. Значение после замены %2F на /. +3. Значение после удаления обычных пробелов. +``` + +### Дубликаты не добавляются + +Если очередное преобразование не изменило значение, новый кандидат не создаётся. + +### Каждый вызов возвращает новый список + +Результат не переиспользует mutable list между вызовами. + +### Исходная строка не изменяется + +Функция не мутирует входное значение. + +### Табуляция не удаляется + +Удаляются только обычные пробелы: + +```text +" " +``` + +Внутренняя табуляция сохраняется. + +### Перевод строки не удаляется + +Внутренний символ новой строки сохраняется. + +### `_LEVERAGE` сохраняется + +Функция не изменяет существующий leverage-суффикс. + +--- + +## Добавленные тесты + +Создан: + +```text +tests/unit/market_data/acquisition/test_symbols.py +``` + +Тесты фиксируют канонический контракт новой реализации. + +Результат: + +```text +29 passed in 0.02s +``` + +Также создан: + +```text +tests/unit/integrations/exchange/test_symbol_utils.py +``` + +Этот набор тестов проверяет compatibility facade и эквивалентность legacy и новой реализации. + +Результат: + +```text +22 passed in 0.01s +``` + +--- + +## Проверка identity compatibility + +Отдельно подтверждено, что legacy facade возвращает непосредственно те же функции: + +```python +assert legacy_normalize_symbol is new_normalize_symbol +assert legacy_symbol_candidates is new_symbol_candidates +``` + +Следовательно: + +- отдельной legacy-реализации больше нет; +- wrapper-функции отсутствуют; +- поведение не может разойтись из-за двух независимых реализаций. + +--- + +## Проверка компиляции + +Выполнена команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/symbols.py \ + src/integrations/exchange/symbol_utils.py \ + tests/unit/market_data/acquisition/test_symbols.py \ + tests/unit/integrations/exchange/test_symbol_utils.py +``` + +Результат: + +```text +Успешно. +Синтаксических ошибок нет. +``` + +--- + +## Полный regression suite + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +271 passed in 0.17s +``` + +До Build 016 полный набор проекта содержал: + +```text +220 passed +``` + +Build 016 добавил: + +```text +51 test +``` + +Итог: + +```text +220 + 51 = 271 +``` + +Все тесты проекта проходят успешно. + +--- + +## Финальная проверка зависимостей + +Выполнена команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "normalize_symbol|symbol_candidates|def normalize_symbol|def symbol_candidates" \ + src tests +``` + +Проверка подтвердила: + +- `normalize_symbol()` реализована только в: + +```text +src/market_data/acquisition/symbols.py +``` + +- `symbol_candidates()` реализована только там же; +- `src/integrations/exchange/symbol_utils.py` содержит только compatibility imports и exports; +- `ExchangeService` продолжает использовать существующий стабильный legacy import path; +- дублирование реализации отсутствует. + +--- + +## Состояние `ExchangeService` + +В рамках Build 016 файл: + +```text +src/integrations/exchange/service.py +``` + +не требовал изменения import path. + +Он продолжает использовать: + +```python +from src.integrations.exchange.symbol_utils import ( + normalize_symbol, + symbol_candidates, +) +``` + +Это сделано намеренно. + +Legacy import path остаётся стабильным, а фактическая реализация уже принадлежит новой подсистеме: + +```text +src.market_data.acquisition +``` + +Такой подход соответствует принятой стратегии постепенной миграции: + +```text +сначала новая каноническая реализация + ↓ +затем compatibility facade + ↓ +старый production-код продолжает работать + ↓ +последующая миграция потребителей выполняется отдельно +``` + +--- + +## Что не изменялось + +В рамках Build 016 намеренно не изменялись: + +- публичный контракт `ExchangeService`; +- сигнатуры `normalize_symbol()`; +- сигнатуры `symbol_candidates()`; +- логика `validate_symbol()`; +- формат `SymbolValidationResult`; +- порядок кандидатов; +- алгоритм сопоставления символов; +- поведение mock mode; +- production-поведение работающего бота. + +--- + +## Архитектурный результат + +После Build 016 ответственность распределена следующим образом: + +```text +market_data/acquisition/symbols.py + │ + └── каноническая логика нормализации + и формирования кандидатов символа + +integrations/exchange/symbol_utils.py + │ + └── compatibility facade для legacy-кода + +integrations/exchange/service.py + │ + └── существующий production consumer +``` + +Таким образом, новая подсистема получила владение логикой идентификации торговых символов без нарушения существующих зависимостей. + +--- + +## Итоговый статус + +Все критерии Build 016 выполнены: + +- каноническая реализация перенесена в `market_data`; +- legacy import path сохранён; +- дублирование реализации устранено; +- существующий контракт зафиксирован тестами; +- compatibility facade проверен; +- identity функций подтверждена; +- компиляция успешна; +- полный regression suite успешен; +- production-поведение не изменено. + +```text +BUILD 016 — COMPLETE +``` \ No newline at end of file diff --git a/docs/migrations/build_017.md b/docs/migrations/build_017.md new file mode 100644 index 0000000..ce68ace --- /dev/null +++ b/docs/migrations/build_017.md @@ -0,0 +1,564 @@ +# Build 017 — Переключение `validate_symbol()` на новый механизм разрешения символов + +## Статус + +**COMPLETE** + +--- + +## Цель Build + +Перевести существующий метод `ExchangeService.validate_symbol()` с legacy-алгоритма поиска инструмента на новый канонический механизм разрешения символов, расположенный в подсистеме `src/market_data/acquisition/`. + +При этом необходимо сохранить без изменений существующий внешний контракт legacy-системы `SymbolValidationResult` и обеспечить полную обратную совместимость существующего бота. + +--- + +## Архитектурный контекст + +До Build 017 метод `ExchangeService.validate_symbol()` самостоятельно выполнял разрешение символа через `symbol_candidates()` и вложенный цикл по результату `get_exchange_symbols()`. + +Фактически внутри `ExchangeService` находилась собственная логика поиска соответствующего торгового инструмента. + +После завершения Build 016 канонические функции `normalize_symbol()` и `symbol_candidates()` уже были перенесены в: + + src/market_data/acquisition/symbols.py + +Build 017 продолжает этот переход и выносит непосредственно алгоритм разрешения символа в новую подсистему Market Data Acquisition. + +--- + +## Реализованная архитектура + +Каноническая логика работы с идентификаторами инструментов теперь находится в: + + src/market_data/acquisition/symbols.py + +Файл содержит: + + normalize_symbol() + symbol_candidates() + resolve_symbol_index() + +Распределение ответственности: + + normalize_symbol() + │ + ▼ + Нормализация входного идентификатора + │ + ▼ + symbol_candidates() + │ + ▼ + Формирование упорядоченного набора кандидатов + │ + ▼ + resolve_symbol_index() + │ + ▼ + Поиск первого подходящего символа + в доступной последовательности + │ + ▼ + ExchangeService.validate_symbol() + │ + ▼ + Формирование legacy SymbolValidationResult + +Таким образом: + +- `market_data/acquisition/symbols.py` отвечает за каноническую логику разрешения идентификатора; +- `ExchangeService.validate_symbol()` отвечает за сохранение legacy API и формирование `SymbolValidationResult`; +- `get_exchange_symbols()` остаётся compatibility boundary между новой моделью `Instrument` и legacy-моделью `ExchangeSymbol`. + +--- + +## Новый канонический resolver + +В файле: + + src/market_data/acquisition/symbols.py + +добавлена функция: + + def resolve_symbol_index( + raw_symbol: str, + available_symbols: Sequence[str], + ) -> int | None: + +Её ответственность: + +1. Получить исходный идентификатор инструмента. +2. Сформировать кандидаты через `symbol_candidates()`. +3. Последовательно проверить кандидатов. +4. Последовательно проверить доступные символы. +5. Вернуть индекс первого совпавшего символа. +6. Вернуть `None`, если соответствие отсутствует. + +Возврат индекса, а не самого объекта, позволяет resolver оставаться независимым от: + + ExchangeSymbol + Instrument + SymbolValidationResult + ExchangeService + +и работать только со строковыми идентификаторами. + +--- + +## Сохранённый порядок разрешения + +Build 017 сохраняет существующую семантику legacy-реализации. + +Приоритет определяется в следующем порядке: + + 1. Порядок кандидатов из symbol_candidates() + 2. Порядок available_symbols + 3. Первое найденное совпадение + +Это означает, что resolver сохраняет: + +- приоритет исходного нормализованного значения; +- приоритет декодированного варианта `%2F`; +- приоритет варианта без внутренних пробелов; +- порядок инструментов источника; +- возврат первого совпадения при наличии дубликатов. + +--- + +## Изменение `ExchangeService.validate_symbol()` + +Метод: + + ExchangeService.validate_symbol() + +сохранил существующий публичный контракт: + + def validate_symbol( + self, + raw_symbol: str, + ) -> SymbolValidationResult: + +Сохраняются все существующие сценарии результата: + +### Пустой символ + +Возвращается: + + SymbolValidationResult( + requested_symbol=requested, + normalized_symbol="", + is_valid=False, + message="Символ пустой.", + symbol_info=None, + ) + +### Mock mode + +При отключённой реальной бирже возвращается успешный результат без `symbol_info`: + + SymbolValidationResult( + requested_symbol=requested, + normalized_symbol=requested, + is_valid=True, + message="Mock mode active.", + symbol_info=None, + ) + +### Найденный символ + +При успешном разрешении возвращается исходный объект `ExchangeSymbol` из списка `get_exchange_symbols()`: + + SymbolValidationResult( + requested_symbol=requested, + normalized_symbol=normalize_symbol(symbol_info.symbol), + is_valid=True, + message="Символ найден в exchangeInfo.", + symbol_info=symbol_info, + ) + +### Символ не найден + +Возвращается прежний отрицательный результат: + + SymbolValidationResult( + requested_symbol=requested, + normalized_symbol=requested, + is_valid=False, + message=f"Символ '{requested}' не найден в exchangeInfo.", + symbol_info=None, + ) + +Таким образом, потребители `validate_symbol()` не требуют изменений. + +--- + +## Новый поток данных + +После Build 017 полный путь разрешения символа выглядит следующим образом: + + raw_symbol + │ + ▼ + ExchangeService.validate_symbol() + │ + ▼ + normalize_symbol() + │ + ▼ + ExchangeService.get_exchange_symbols() + │ + ▼ + tuple[Instrument, ...] + │ + ▼ + Compatibility mapper + │ + ▼ + list[ExchangeSymbol] + │ + ▼ + resolve_symbol_index() + │ + ▼ + matched index + │ + ▼ + исходный ExchangeSymbol + │ + ▼ + SymbolValidationResult + +При этом `validate_symbol()`: + +- не обращается напрямую к acquisition adapter; +- не создаёт `InstrumentAcquisitionService`; +- не выполняет REST-запрос; +- не выполняет parsing; +- не выполняет validation входного `exchangeInfo`; +- не выполняет mapping `Instrument → ExchangeSymbol`; +- не управляет cache; +- использует только публичный legacy boundary `get_exchange_symbols()`. + +--- + +## Сохранение compatibility boundary + +Build 017 намеренно не переводит `validate_symbol()` на прямую работу с `Instrument`. + +Текущий compatibility boundary остаётся следующим: + + Instrument Acquisition + │ + ▼ + tuple[Instrument, ...] + │ + ▼ + compatibility.py + │ + ▼ + list[ExchangeSymbol] + │ + ▼ + ExchangeService.get_exchange_symbols() + │ + ▼ + ExchangeService.validate_symbol() + │ + ▼ + SymbolValidationResult + +Это позволяет продолжать поэтапную миграцию без нарушения работы существующего бота. + +--- + +## Сохранение `SymbolValidationResult` + +Legacy-модель: + + src/integrations/exchange/models.py + +остаётся без изменений. + +Контракт: + + class SymbolValidationResult: + requested_symbol: str + normalized_symbol: str + is_valid: bool + message: str + symbol_info: ExchangeSymbol | None + +сохранён полностью. + +Это важно, поскольку `validate_symbol()` используется существующими runtime-компонентами, включая: + +- получение статуса торгового инструмента; +- получение комиссии; +- получение свечей; +- получение цены; +- market snapshot; +- execution snapshot; +- свежий REST snapshot; +- market stream; +- market data runner; +- Telegram handlers. + +--- + +## Сохранение object identity + +При успешном разрешении символа `validate_symbol()` возвращает тот же экземпляр `ExchangeSymbol`, который находится в результате `get_exchange_symbols()`. + +То есть сохраняется условие: + + result.symbol_info is symbol + +Это предотвращает: + +- создание лишних копий legacy-моделей; +- изменение object identity; +- расхождение между cache и результатом validation; +- скрытые изменения поведения существующих потребителей. + +--- + +## Изменённые файлы + +### `src/market_data/acquisition/symbols.py` + +Добавлена каноническая функция: + + resolve_symbol_index() + +Функция выполняет независимое разрешение строкового идентификатора по последовательности доступных символов. + +### `src/integrations/exchange/service.py` + +Метод: + + validate_symbol() + +переключён со встроенного двойного цикла на: + + resolve_symbol_index() + +При этом сохранены: + +- вызов `get_exchange_symbols()`; +- `SymbolValidationResult`; +- тексты сообщений; +- mock mode; +- порядок разрешения; +- возврат исходного объекта `ExchangeSymbol`. + +### `tests/unit/market_data/acquisition/test_symbols.py` + +Добавлены unit-тесты для `resolve_symbol_index()`. + +### `tests/unit/integrations/exchange/test_service_validate_symbol.py` + +Добавлен отдельный набор unit-тестов для публичного legacy-контракта `ExchangeService.validate_symbol()`. + +--- + +## Тестовое покрытие + +### Канонический symbol resolver + +Выполнена команда: + + python -m pytest \ + tests/unit/market_data/acquisition/test_symbols.py \ + -q + +Результат: + + 41 passed in 0.02s + +Проверены: + +- точное совпадение; +- регистронезависимое совпадение; +- внешние пробелы; +- `%2F`; +- внутренние пробелы; +- отсутствующий символ; +- пустой запрос; +- пустая последовательность доступных символов; +- приоритет кандидатов; +- порядок доступных символов; +- первый дубликат; +- отсутствие изменения входной последовательности. + +--- + +## Тестирование `ExchangeService.validate_symbol()` + +Выполнена команда: + + python -m pytest \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + -q + +Результат: + + 16 passed in 0.08s + +Проверены: + +- отклонение пустого символа; +- mock mode; +- точное совпадение; +- регистронезависимость; +- внешние пробелы; +- encoded separator `%2F`; +- внутренние пробелы; +- отсутствующий символ; +- возврат исходного `ExchangeSymbol`; +- нормализация фактически найденного символа; +- сохранение success message; +- единственный вызов `get_exchange_symbols()`; +- отсутствие прямого обращения к acquisition; +- сохранение candidate priority; +- возврат первого duplicate; +- сохранение типа `SymbolValidationResult`. + +--- + +## Проверка компиляции + +Выполнена команда: + + python -m py_compile \ + src/market_data/acquisition/symbols.py \ + src/integrations/exchange/service.py \ + tests/unit/market_data/acquisition/test_symbols.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py + +Результат: + + Успешно. + Ошибок компиляции нет. + +--- + +## Полный regression suite + +Выполнена команда: + + python -m pytest -q + +Результат: + + 299 passed in 0.17s + +Регрессий в существующем проекте не обнаружено. + +--- + +## Финальная архитектурная проверка + +Выполнен поиск: + + grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "validate_symbol|resolve_symbol_index|symbol_candidates|normalize_symbol|SymbolValidationResult|symbol_info" \ + src tests + +Проверка подтвердила: + +- каноническая реализация `normalize_symbol()` находится в `src/market_data/acquisition/symbols.py`; +- каноническая реализация `symbol_candidates()` находится там же; +- `resolve_symbol_index()` находится там же; +- `ExchangeService.validate_symbol()` использует `resolve_symbol_index()`; +- старого двойного цикла внутри `validate_symbol()` больше нет; +- `ExchangeService` использует канонические функции новой подсистемы; +- legacy facade `src/integrations/exchange/symbol_utils.py` сохранён; +- `SymbolValidationResult` сохранён; +- `symbol_info: ExchangeSymbol | None` сохранён; +- новый cache не добавлен; +- прямого обращения `validate_symbol()` к acquisition adapter нет; +- дублирования алгоритма разрешения символа не обнаружено. + +--- + +## Архитектурные гарантии после Build 017 + +После завершения Build 017 выполняются следующие гарантии: + +1. Каноническая логика разрешения символов принадлежит `market_data/acquisition`. +2. `ExchangeService.validate_symbol()` больше не содержит собственного алгоритма поиска совпадения. +3. `validate_symbol()` продолжает использовать `get_exchange_symbols()` как compatibility boundary. +4. Публичный контракт `SymbolValidationResult` не изменён. +5. `symbol_info` продолжает иметь тип `ExchangeSymbol | None`. +6. Object identity найденного `ExchangeSymbol` сохраняется. +7. Порядок кандидатов сохраняется. +8. Порядок доступных символов сохраняется. +9. При дубликатах возвращается первый найденный объект. +10. Новый cache не введён. +11. Существующий `_exchange_symbols_cache` продолжает работать без изменений. +12. Legacy facade `symbol_utils.py` сохранён. +13. Все существующие тесты проекта проходят. + +--- + +## Что намеренно не входит в Build 017 + +Build 017 не выполняет: + +- удаление `SymbolValidationResult`; +- перевод всех потребителей на `Instrument`; +- удаление `ExchangeSymbol`; +- удаление compatibility mapper; +- удаление `_exchange_symbols_cache`; +- прямую работу `validate_symbol()` с `tuple[Instrument, ...]`; +- создание нового instrument cache; +- изменение публичного API `ExchangeService`; +- изменение runtime-логики бота; +- изменение торговой логики. + +Эти изменения должны выполняться отдельными контролируемыми Build-шагами. + +--- + +## Итог + +Build 017 завершён успешно. + +Реализовано переключение: + + ExchangeService.validate_symbol() + │ + ▼ + legacy inline symbol matching + +на: + + ExchangeService.validate_symbol() + │ + ▼ + market_data.acquisition.resolve_symbol_index() + +При этом полностью сохранены: + +- публичный legacy-контракт; +- `SymbolValidationResult`; +- `ExchangeSymbol`; +- `symbol_info`; +- object identity; +- порядок разрешения; +- mock mode; +- compatibility boundary; +- cache; +- работа существующего бота. + +Финальный результат: + + BUILD 017 — COMPLETE + +Полный regression suite: + + 299 passed in 0.17s \ No newline at end of file diff --git a/docs/migrations/build_018.md b/docs/migrations/build_018.md new file mode 100644 index 0000000..f9c3943 --- /dev/null +++ b/docs/migrations/build_018.md @@ -0,0 +1,589 @@ +# Build 018 — Переключение `get_symbol_runtime_status()` + +## Статус + +**COMPLETE** + +--- + +## Цель + +Перевести определение торгового состояния инструмента, используемое методом: + +```python +ExchangeService.get_symbol_runtime_status() +``` + +на каноническую классификацию статусов из подсистемы: + +```text +src/market_data/acquisition/ +``` + +при полном сохранении существующего внешнего контракта `ExchangeRuntimeStatus`, поведения legacy-кода, UI и runtime-потребителей. + +--- + +## Исходное состояние + +До Build 018 классификация биржевых статусов инструмента находилась непосредственно в legacy exchange-слое: + +```text +src/integrations/exchange/status.py +``` + +и основывалась на локальных наборах: + +```python +OPEN_STATUSES +BREAK_STATUSES +``` + +Метод: + +```python +build_market_status_from_symbol_status() +``` + +самостоятельно определял одно из состояний: + +```text +OPEN +NOT_TRADABLE +BREAK +UNKNOWN +``` + +Это означало, что предметная классификация торгового состояния инструмента оставалась внутри legacy exchange integration layer. + +--- + +## Архитектурное решение + +Каноническая классификация статуса инструмента перенесена в: + +```text +src/market_data/acquisition/models/status.py +``` + +Добавлены следующие сущности: + +```python +InstrumentTradingState +InstrumentStatusClassification +classify_instrument_status() +``` + +Теперь архитектурная цепочка имеет вид: + +```text +raw exchange status + ↓ +classify_instrument_status() + ↓ +InstrumentStatusClassification + ↓ +build_market_status_from_symbol_status() + ↓ +ExchangeRuntimeStatus + ↓ +legacy runtime / UI / trading consumers +``` + +Таким образом: + +- `market_data/acquisition` отвечает за предметную классификацию состояния инструмента; +- `integrations/exchange/status.py` сохраняет compatibility-функцию преобразования результата в существующий `ExchangeRuntimeStatus`; +- существующие runtime-потребители продолжают работать без изменения публичного контракта. + +--- + +## Добавленный файл + +```text +src/market_data/acquisition/models/status.py +``` + +Файл содержит каноническую модель классификации торгового состояния инструмента. + +Основные сущности: + +```python +class InstrumentTradingState(StrEnum): + OPEN = "OPEN" + NOT_TRADABLE = "NOT_TRADABLE" + BREAK = "BREAK" + UNKNOWN = "UNKNOWN" +``` + +```python +@dataclass(frozen=True, slots=True) +class InstrumentStatusClassification: + state: InstrumentTradingState + normalized_status: str | None +``` + +Основная функция: + +```python +def classify_instrument_status( + raw_status: str | None, +) -> InstrumentStatusClassification: +``` + +Она выполняет: + +1. нормализацию входного статуса; +2. классификацию открытого рынка; +3. классификацию неторгуемого инструмента; +4. классификацию временной остановки торгов; +5. возврат `UNKNOWN` для неизвестного или отсутствующего статуса. + +--- + +## Канонические состояния + +Подсистема `market_data/acquisition` различает четыре предметных состояния: + +```text +OPEN +NOT_TRADABLE +BREAK +UNKNOWN +``` + +### `OPEN` + +Инструмент доступен для торговли. + +Поддерживаются существующие legacy-статусы: + +```text +TRADING +OPEN +ACTIVE +ENABLED +ONLINE +``` + +### `NOT_TRADABLE` + +Инструмент существует, но недоступен для обычной торговли. + +Поддерживаются: + +```text +NOT_TRADABLE +TRADING_DISABLED +MARKET_DISABLED +UNAVAILABLE_FOR_TRADING +CLOSE_ONLY +REDUCE_ONLY +VIEW_ONLY +``` + +### `BREAK` + +Торги временно остановлены или приостановлены. + +Поддерживаются: + +```text +BREAK +CLOSED +HALT +HALTED +PAUSED +SUSPENDED +DISABLED +SETTLING +POST_ONLY +``` + +### `UNKNOWN` + +Используется для: + +- неизвестного статуса; +- пустой строки; +- `None`; +- значения, отсутствующего в известных классификационных наборах. + +--- + +## Изменение legacy exchange-слоя + +Из файла: + +```text +src/integrations/exchange/status.py +``` + +удалена собственная предметная классификация через публичные наборы: + +```python +OPEN_STATUSES +BREAK_STATUSES +``` + +Вместо неё используются: + +```python +from src.market_data.acquisition.models.status import ( + InstrumentTradingState, + classify_instrument_status, +) +``` + +Функция: + +```python +build_market_status_from_symbol_status() +``` + +теперь сначала вызывает: + +```python +classification = classify_instrument_status(raw_status) +``` + +а затем преобразует каноническое состояние в существующий legacy-контракт: + +```text +InstrumentTradingState.OPEN + ↓ +ExchangeRuntimeStatus(code=OPEN) + +InstrumentTradingState.NOT_TRADABLE + ↓ +ExchangeRuntimeStatus(code=BREAK, reason=market_not_tradable) + +InstrumentTradingState.BREAK + ↓ +ExchangeRuntimeStatus(code=BREAK, reason=market_break) + +InstrumentTradingState.UNKNOWN + ↓ +ExchangeRuntimeStatus(code=UNKNOWN, reason=market_status_unknown) +``` + +--- + +## Сохранённый публичный контракт + +Build 018 не изменяет структуру: + +```python +ExchangeRuntimeStatus +``` + +Сохранены поля: + +```text +code +is_open +is_available +is_auth_ok +title +message +ui_line +reason +symbol +raw_status +raw_error +``` + +Также сохранён compatibility-метод: + +```python +ExchangeRuntimeStatus.as_dict() +``` + +Это позволяет не изменять существующие runtime-, UI- и trading-потребители. + +--- + +## Поведение `get_symbol_runtime_status()` + +Метод: + +```python +ExchangeService.get_symbol_runtime_status() +``` + +сохранил существующий внешний контракт и последовательность обработки. + +Архитектурно поток остаётся следующим: + +```text +requested symbol + ↓ +validate_symbol() + ↓ +ExchangeSymbol + ↓ +raw symbol status + ↓ +build_market_status_from_symbol_status() + ↓ +classify_instrument_status() + ↓ +ExchangeRuntimeStatus +``` + +Для открытого рынка дополнительно сохраняется проверка свежести рыночного snapshot: + +```text +OPEN + ↓ +get_fresh_market_snapshot() + ↓ +age_seconds > 60 + ↓ +STALE_MARKET_DATA / BREAK +``` + +Stale threshold сохранён: + +```text +60 секунд +``` + +Проверка stale market data выполняется только для рынка, первоначально классифицированного как `OPEN`. + +--- + +## Сохранённое поведение + +Build 018 сохраняет следующие legacy-сценарии: + +- mock exchange; +- явный `symbol`; +- использование `default_symbol`, если аргумент равен `None`; +- invalid symbol; +- ошибка при validation; +- `OPEN`; +- `NOT_TRADABLE`; +- `BREAK`; +- `UNKNOWN`; +- stale market data; +- отсутствие `age_seconds`; +- ошибка получения snapshot; +- нормализованный matched symbol; +- существующий `ExchangeRuntimeStatus`; +- существующие `reason`; +- существующие UI-тексты; +- существующие `raw_status`. + +--- + +## Кэширование и сетевые запросы + +Build 018 не добавляет: + +- новый cache; +- новый REST-запрос; +- дополнительную загрузку `exchangeInfo`; +- дополнительный acquisition service; +- отдельный status feed runtime. + +`get_symbol_runtime_status()` продолжает использовать существующий путь: + +```text +validate_symbol() + ↓ +get_exchange_symbols() + ↓ +existing ExchangeService cache + ↓ +Instrument Acquisition path +``` + +Таким образом, миграция не создаёт параллельного источника Instrument Reference Data. + +--- + +## Пустые status feed-файлы + +На момент Build 018 следующие файлы существуют, но остаются пустыми: + +```text +src/market_data/acquisition/models/status.py +src/market_data/acquisition/handlers/status_handler.py +src/market_data/acquisition/feeds/status_feed.py +``` + +После Build 018 файл: + +```text +src/market_data/acquisition/models/status.py +``` + +получил реализацию канонической классификации торгового состояния инструмента. + +Файлы: + +```text +src/market_data/acquisition/handlers/status_handler.py +src/market_data/acquisition/feeds/status_feed.py +``` + +в рамках Build 018 намеренно не реализуются. + +Причина: текущая задача не создаёт отдельный Status Feed и не должна вводить новый источник сетевых запросов или параллельный runtime-путь. + +--- + +## Добавленные тесты + +Добавлен тестовый файл: + +```text +tests/unit/market_data/acquisition/models/test_instrument_status.py +``` + +Он проверяет каноническую классификацию: + +- все открытые статусы; +- все неторгуемые статусы; +- все break-статусы; +- регистронезависимость; +- удаление внешних пробелов; +- неизвестный статус; +- пустой статус; +- `None`; +- immutable-контракт результата. + +Также расширено покрытие: + +```text +tests/unit/integrations/exchange/test_status.py +``` + +для проверки compatibility-преобразования: + +```text +InstrumentStatusClassification + ↓ +ExchangeRuntimeStatus +``` + +Целевой regression-набор метода находится в: + +```text +tests/unit/integrations/exchange/test_service_symbol_runtime_status.py +``` + +Дублирующий тестовый файл: + +```text +tests/unit/integrations/exchange/test_service_runtime_status.py +``` + +удалён. + +--- + +## Проверка целевого runtime-контракта + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ + -q +``` + +Результат: + +```text +34 passed in 0.09s +``` + +--- + +## Проверка компиляции + +Выполнена команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/models/status.py \ + src/integrations/exchange/status.py \ + src/integrations/exchange/service.py \ + tests/unit/market_data/acquisition/models/test_instrument_status.py \ + tests/unit/integrations/exchange/test_status.py \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py +``` + +Результат: + +```text +Успешно. +Ошибок компиляции нет. +``` + +--- + +## Полный regression suite + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +387 passed in 0.20s +``` + +--- + +## Архитектурный результат + +После Build 018 ответственность разделена следующим образом: + +```text +market_data/acquisition/models/status.py + ↓ +каноническая предметная классификация статуса инструмента + +integrations/exchange/status.py + ↓ +compatibility mapping в legacy ExchangeRuntimeStatus + +integrations/exchange/service.py + ↓ +runtime orchestration, validation и stale market data check + +runtime / UI / trading consumers + ↓ +продолжают использовать существующий ExchangeRuntimeStatus +``` + +В результате: + +- предметная классификация статуса инструмента больше не принадлежит legacy exchange-слою; +- дублирование `OPEN_STATUSES` / `BREAK_STATUSES` устранено; +- `ExchangeRuntimeStatus` сохранён как compatibility boundary; +- существующие потребители не требуют массового рефакторинга; +- новый REST-путь не создан; +- новый cache не создан; +- поведение работающего бота сохранено. + +--- + +## Итог + +```text +BUILD 018 — COMPLETE +``` + +Build 018 завершён при полном прохождении целевых тестов, компиляции и общего regression suite: + +```text +34 targeted tests passed +387 total tests passed +``` \ No newline at end of file diff --git a/docs/migrations/build_019.md b/docs/migrations/build_019.md new file mode 100644 index 0000000..e6ffc24 --- /dev/null +++ b/docs/migrations/build_019.md @@ -0,0 +1,618 @@ +# Build 019 — Подготовка переноса кэша в Storage + +## Статус + +**COMPLETE** + +--- + +## Цель + +Подготовить независимый storage-контракт для хранения канонического справочника инструментов перед последующим переносом legacy-кэша: + +```python +ExchangeService._exchange_symbols_cache +``` + +из слоя: + +```text +src/integrations/exchange +``` + +в слой: + +```text +src/storage +``` + +без изменения текущего production-пути и без нарушения работы существующего бота. + +--- + +## Архитектурный принцип + +До Build 019 кэш справочника торговых инструментов находился непосредственно внутри legacy-интеграционного сервиса: + +```python +ExchangeService._exchange_symbols_cache +``` + +Это создаёт архитектурную связь между: + +- получением Instrument Reference Data; +- legacy-моделью `ExchangeSymbol`; +- интеграционным слоем биржи; +- runtime-хранением загруженного справочника. + +В новой архитектуре ответственность разделяется: + +```text +Market Data Acquisition + │ + ▼ +tuple[Instrument, ...] + │ + ▼ +Storage + │ + ▼ +Compatibility Layer + │ + ▼ +Legacy ExchangeSymbol +``` + +Build 019 создаёт только новый storage-контракт и его in-memory реализацию. + +Переключение production-кода на новый store в рамках Build 019 не выполняется. + +--- + +## Созданные файлы + +```text +app/src/storage/exceptions.py +app/src/storage/instrument_store.py +app/tests/unit/storage/test_instrument_store.py +``` + +--- + +## `src/storage/exceptions.py` + +Создана базовая иерархия ошибок storage-слоя: + +```text +Exception + │ + ▼ +StorageError + │ + ▼ +InstrumentStoreError +``` + +### `StorageError` + +Базовая ошибка storage-слоя. + +### `InstrumentStoreError` + +Специализированная ошибка операций и нарушений контракта хранилища справочника инструментов. + +--- + +## `src/storage/instrument_store.py` + +Созданы: + +```text +InstrumentStoreProtocol +InMemoryInstrumentStore +``` + +### `InstrumentStoreProtocol` + +Определяет независимый контракт runtime-хранилища канонического справочника инструментов. + +Поддерживаемые операции: + +```python +get( + source_name: str, +) -> tuple[Instrument, ...] | None +``` + +```python +set( + source_name: str, + instruments: tuple[Instrument, ...], +) -> None +``` + +```python +clear( + source_name: str | None = None, +) -> None +``` + +Контракт не зависит от: + +- `ExchangeService`; +- `ExchangeSymbol`; +- Dzengi REST API; +- PostgreSQL; +- Redis; +- Telegram UI; +- legacy compatibility mapper. + +--- + +## Семантика `get()` + +Метод: + +```python +get(source_name) +``` + +возвращает: + +```text +None +``` + +если данные для источника никогда не сохранялись. + +Это означает: + +```text +cache miss +``` + +Если в store был успешно сохранён пустой справочник: + +```python +() +``` + +метод возвращает именно: + +```python +() +``` + +Таким образом: + +```text +None != () +``` + +и состояния: + +```text +данные отсутствуют +``` + +и: + +```text +успешно загружен пустой справочник +``` + +не смешиваются. + +--- + +## Семантика `set()` + +Метод принимает исключительно: + +```python +tuple[Instrument, ...] +``` + +Это сохраняет immutable-контракт новой модели Instrument Reference Data. + +Store не выполняет: + +- копирование tuple; +- сортировку; +- преобразование элементов; +- mapping в `ExchangeSymbol`; +- нормализацию `Instrument`; +- изменение порядка элементов. + +Сохраняется исходный объект tuple. + +Следовательно: + +```python +store.set("dzengi", instruments) + +assert store.get("dzengi") is instruments +``` + +--- + +## Семантика `clear()` + +Поддерживаются два режима. + +Очистка конкретного источника: + +```python +store.clear("dzengi") +``` + +Полная очистка store: + +```python +store.clear() +``` + +Очистка неизвестного источника является идемпотентной и не вызывает ошибку. + +--- + +## Изоляция источников + +Store поддерживает независимое хранение нескольких источников: + +```text +dzengi +secondary +other-source +``` + +Например: + +```text +InMemoryInstrumentStore +├── dzengi +│ └── tuple[Instrument, ...] +│ +└── secondary + └── tuple[Instrument, ...] +``` + +Изменение или очистка одного источника не влияет на остальные. + +--- + +## Нормализация имени источника + +Внешние пробелы удаляются: + +```text +" dzengi " +``` + +нормализуется в: + +```text +"dzengi" +``` + +Регистр сохраняется. + +Следовательно: + +```text +dzengi +``` + +и: + +```text +DZENGI +``` + +являются разными ключами. + +Пустые имена источников запрещены: + +```text +"" +" " +" " +"\t" +"\n" +``` + +и приводят к: + +```python +InstrumentStoreError +``` + +--- + +## Runtime-валидация + +`InMemoryInstrumentStore.set()` проверяет: + +1. что набор передан как `tuple`; +2. что каждый элемент является экземпляром `Instrument`. + +Нарушение контракта приводит к: + +```python +InstrumentStoreError +``` + +--- + +## Что намеренно не изменялось + +Build 019 не изменяет: + +```text +app/src/integrations/exchange/service.py +app/src/market_data/acquisition/service.py +app/src/market_data/acquisition/compatibility.py +app/src/storage/session.py +app/src/storage/schema.py +app/src/storage/models.py +app/src/storage/repositories/* +app/src/storage/__init__.py +``` + +Не добавлялись: + +- PostgreSQL-таблицы; +- Redis; +- новый database repository; +- dependency injection в `ExchangeService`; +- production singleton store; +- глобальный storage registry. + +--- + +## Legacy-кэш + +После Build 019 legacy-кэш остаётся на прежнем месте: + +```python +class ExchangeService: + _exchange_symbols_cache: list[ExchangeSymbol] | None = None +``` + +Текущий production-путь остаётся неизменным: + +```text +ExchangeService.get_exchange_symbols() + │ + ├── cache hit + │ │ + │ ▼ + │ _exchange_symbols_cache + │ + └── cache miss + │ + ▼ + _load_exchange_symbols_via_acquisition() + │ + ▼ + InstrumentAcquisitionService + │ + ▼ + tuple[Instrument, ...] + │ + ▼ + map_instruments_to_exchange_symbols() + │ + ▼ + list[ExchangeSymbol] + │ + ▼ + _exchange_symbols_cache +``` + +Новый `InMemoryInstrumentStore` в этот production-путь пока не подключён. + +--- + +## Тестовое покрытие + +Создан файл: + +```text +app/tests/unit/storage/test_instrument_store.py +``` + +Проверены: + +- соответствие `InstrumentStoreProtocol`; +- cache miss; +- сохранение и получение данных; +- сохранение identity исходного tuple; +- различие между `None` и пустым tuple; +- изоляция разных источников; +- очистка одного источника; +- полная очистка store; +- замена ранее сохранённого значения; +- нормализация внешних пробелов имени источника; +- сохранение регистра имени источника; +- отклонение пустых имён источников; +- отклонение списка вместо tuple; +- отклонение объектов, не являющихся `Instrument`; +- сохранение порядка инструментов; +- отсутствие изменения входного tuple; +- изоляция разных экземпляров store; +- идемпотентная очистка неизвестного источника; +- наследование `InstrumentStoreError` от `StorageError`. + +--- + +## Результаты проверок + +### Unit-тесты нового store + +Команда: + +```bash +python -m pytest \ + tests/unit/storage/test_instrument_store.py \ + -q +``` + +Результат: + +```text +32 passed in 0.02s +``` + +--- + +### Проверка компиляции + +Команда: + +```bash +python -m py_compile \ + src/storage/exceptions.py \ + src/storage/instrument_store.py \ + tests/unit/storage/test_instrument_store.py +``` + +Результат: + +```text +успешно +``` + +--- + +### Полный regression suite + +Команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +419 passed in 0.21s +``` + +--- + +## Архитектурная проверка + +Выполнена команда: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "InstrumentStoreProtocol|InMemoryInstrumentStore|InstrumentStoreError|StorageError|_exchange_symbols_cache|instrument_store" \ + src tests +``` + +Проверка подтвердила: + +- `InstrumentStoreProtocol` определён в `src/storage/instrument_store.py`; +- `InMemoryInstrumentStore` определён в `src/storage/instrument_store.py`; +- `StorageError` и `InstrumentStoreError` находятся в `src/storage/exceptions.py`; +- новый store используется только собственными unit-тестами; +- production-код на новый store не переключён; +- `_exchange_symbols_cache` остаётся в `ExchangeService`; +- существующие legacy-тесты кэша продолжают работать. + +--- + +## Итоговая архитектура после Build 019 + +```text +External Exchange API + │ + ▼ +Market Data Acquisition + │ + ▼ +tuple[Instrument, ...] + │ + ├──────────────────────────────┐ + │ │ + ▼ ▼ +InMemoryInstrumentStore Compatibility Layer + │ │ + │ ▼ + │ list[ExchangeSymbol] + │ │ + │ ▼ + │ ExchangeService._exchange_symbols_cache + │ + ▼ +готов к будущему подключению +в production-путь +``` + +На текущем этапе новый store существует независимо от legacy-кэша. + +--- + +## Граница Build 019 + +Build 019 считается завершённым, потому что: + +1. создан независимый storage-контракт для `Instrument`; +2. создана in-memory реализация store; +3. сохранена семантика immutable `tuple[Instrument, ...]`; +4. определено различие между cache miss и пустым справочником; +5. обеспечена изоляция источников; +6. добавлена специализированная иерархия storage-ошибок; +7. production-код не изменён; +8. legacy-кэш не удалён; +9. полный regression suite проходит успешно. + +--- + +## Следующий шаг + +Следующий логический этап: + +```text +Build 020 — Подключение Instrument Store к production-пути +``` + +Цель следующего этапа: + +```text +переключить хранение канонического tuple[Instrument, ...] +с legacy-кэша ExchangeService +на InMemoryInstrumentStore +``` + +при сохранении внешнего legacy-контракта: + +```python +ExchangeService.get_exchange_symbols() -> list[ExchangeSymbol] +``` + +и без нарушения работы существующего бота. + +На Build 020 необходимо отдельно определить: + +- где создаётся production-экземпляр `InMemoryInstrumentStore`; +- как `ExchangeService` получает доступ к нему; +- сохраняется ли временно `_exchange_symbols_cache` как compatibility-кэш; +- в какой точке выполняется mapping `Instrument -> ExchangeSymbol`; +- как сохранить существующую identity-семантику `get_exchange_symbols()`; +- как обеспечить безопасный rollback без изменения внешнего API. + +--- + +## Итог + +**Build 019 завершён успешно.** + +Новый storage-контракт создан и полностью покрыт unit-тестами. + +Существующий бот продолжает использовать прежний production-путь без изменений. + +Следующий этап — **Build 020: безопасное подключение `InstrumentStore` к production-пути с сохранением legacy-совместимости**. \ No newline at end of file diff --git a/docs/migrations/build_020.md b/docs/migrations/build_020.md new file mode 100644 index 0000000..9488cfd --- /dev/null +++ b/docs/migrations/build_020.md @@ -0,0 +1,863 @@ +# Build 020 — Перенос кэша инструментов в Storage Layer + +**Engineering Migration Record** + +--- + +## Контроль документа + +| Свойство | Значение | +|---|---| +| Документ | Build 020 — Перенос кэша инструментов в Storage Layer | +| Тип документа | Engineering Migration Record | +| Статус | **Complete** | +| Проект | Dzentra | +| Подсистема | Instrument Reference Data | +| Build | 020 | +| Язык | Русский | +| Целевой файл | `docs/migrations/instrument_reference_data/build_020.md` | + +--- + +## 1. Цель Build 020 + +Цель Build 020 — удалить каноническое хранение справочных данных инструментов из legacy-кэша `ExchangeService._exchange_symbols_cache` и перенести его в специализированный Storage Layer. + +До выполнения Build 020 `ExchangeService` одновременно отвечал за: + +- получение справочных данных инструментов; +- запуск acquisition pipeline; +- преобразование канонических моделей `Instrument` в legacy-модели `ExchangeSymbol`; +- хранение результата в собственном class-level cache. + +Это создавало архитектурную зависимость канонических справочных данных от legacy exchange layer. + +После Build 020 канонические данные инструментов хранятся в специализированном: + +```text +InMemoryInstrumentStore +``` + +в виде: + +```text +tuple[Instrument, ...] +``` + +Legacy-модели `ExchangeSymbol` больше не являются каноническим представлением справочных данных. + +--- + +## 2. Архитектурное состояние до Build 020 + +До миграции `ExchangeService` содержал: + +```python +_exchange_symbols_cache: list[ExchangeSymbol] | None = None +``` + +Метод: + +```python +get_exchange_symbols() +``` + +работал по следующей схеме: + +```text +get_exchange_symbols() + ↓ +_exchange_symbols_cache + ↓ cache miss +_load_exchange_symbols_via_acquisition() + ↓ +Instrument Acquisition Pipeline + ↓ +tuple[Instrument, ...] + ↓ +map_instruments_to_exchange_symbols() + ↓ +list[ExchangeSymbol] + ↓ +_exchange_symbols_cache +``` + +Таким образом, результат новой Instrument Reference Data pipeline сразу преобразовывался в legacy-модель, и именно legacy-представление сохранялось как основной кэш. + +--- + +## 3. Архитектурная проблема + +Старый подход имел несколько принципиальных недостатков. + +### 3.1. Legacy-модель использовалась как каноническое хранилище + +Каноническая модель: + +```python +Instrument +``` + +содержит полные справочные данные инструмента. + +Legacy-модель: + +```python +ExchangeSymbol +``` + +является сокращённой compatibility-проекцией и содержит только часть этих данных. + +Хранение только `ExchangeSymbol` означало потерю полноценного канонического представления после завершения acquisition pipeline. + +### 3.2. Exchange Layer владел состоянием справочных данных + +Поле: + +```python +ExchangeService._exchange_symbols_cache +``` + +делало `ExchangeService` владельцем справочных данных инструментов. + +Это противоречило целевой архитектуре: + +```text +Acquisition Layer + ↓ +Canonical Instrument Models + ↓ +Storage Layer + ↓ +Consumers / Compatibility Projections +``` + +### 3.3. Канонические данные и legacy-проекция были объединены + +Результат acquisition pipeline немедленно преобразовывался: + +```text +Instrument + ↓ +ExchangeSymbol +``` + +и сохранялся только после преобразования. + +Это не позволяло независимо использовать полный `Instrument` в будущих подсистемах Dzentra. + +--- + +## 4. Реализованное архитектурное решение + +В Build 020 введено разделение между: + +1. каноническим хранилищем инструментов; +2. legacy compatibility projection cache. + +Теперь `ExchangeService` использует: + +```python +_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore() +``` + +для хранения канонических моделей: + +```python +tuple[Instrument, ...] +``` + +и отдельный: + +```python +_exchange_symbols_projection_cache: list[ExchangeSymbol] | None = None +``` + +для временного кэширования legacy-проекции. + +Итоговая схема: + +```text +DzengiInstrumentDocumentSource + ↓ +InstrumentFeed + ↓ +DzengiInstrumentDocumentHandler + ↓ +InstrumentAcquisitionService + ↓ +tuple[Instrument, ...] + ↓ +InMemoryInstrumentStore + ↓ +Canonical Instrument Reference Data + ↓ +map_instruments_to_exchange_symbols() + ↓ +list[ExchangeSymbol] + ↓ +Legacy Compatibility Projection Cache +``` + +--- + +## 5. Каноническое хранилище + +Канонические справочные данные теперь находятся в: + +```python +ExchangeService._instrument_store +``` + +Тип зависимости: + +```python +InstrumentStoreProtocol +``` + +Текущая реализация: + +```python +InMemoryInstrumentStore +``` + +Store хранит: + +```python +tuple[Instrument, ...] +``` + +Ключ источника: + +```text +dzengi +``` + +Таким образом, канонические справочные данные больше не зависят от legacy-модели `ExchangeSymbol`. + +--- + +## 6. Новый production flow + +Метод: + +```python +get_exchange_symbols() +``` + +сохраняет прежний внешний контракт: + +```python +list[ExchangeSymbol] +``` + +Это необходимо для сохранения работоспособности существующего бота во время поэтапной миграции. + +Внутренний flow теперь выглядит следующим образом: + +```text +get_exchange_symbols() + ↓ +Проверка exchange_enabled + ↓ +Проверка _exchange_symbols_projection_cache + ↓ cache miss +Чтение InstrumentStore + ↓ store miss +_load_instruments_via_acquisition() + ↓ +tuple[Instrument, ...] + ↓ +Сохранение в InstrumentStore + ↓ +map_instruments_to_exchange_symbols() + ↓ +list[ExchangeSymbol] + ↓ +Сохранение в _exchange_symbols_projection_cache + ↓ +Возврат legacy-результата +``` + +--- + +## 7. Разделение канонического кэша и compatibility projection cache + +После Build 020 существуют два разных уровня состояния. + +### 7.1. Каноническое состояние + +```python +_instrument_store +``` + +Хранит: + +```python +tuple[Instrument, ...] +``` + +Назначение: + +- хранение полной справочной модели инструмента; +- повторное использование канонических данных; +- основа для будущих market intelligence consumers; +- независимость от legacy exchange models. + +### 7.2. Compatibility projection cache + +```python +_exchange_symbols_projection_cache +``` + +Хранит: + +```python +list[ExchangeSymbol] | None +``` + +Назначение: + +- сохранить старый контракт `get_exchange_symbols()`; +- не выполнять повторный compatibility mapping при каждом вызове; +- обеспечить безопасную постепенную миграцию legacy-кода. + +`_exchange_symbols_projection_cache` не является источником истины. + +Источником истины является: + +```text +InstrumentStore +``` + +--- + +## 8. Изменение acquisition loader + +Старый метод: + +```python +_load_exchange_symbols_via_acquisition() +``` + +удалён. + +Он одновременно: + +- запускал acquisition pipeline; +- получал `Instrument`; +- выполнял compatibility mapping; +- возвращал `ExchangeSymbol`. + +Вместо него используется: + +```python +_load_instruments_via_acquisition() +``` + +Новый метод возвращает: + +```python +tuple[Instrument, ...] +``` + +и не выполняет: + +```python +map_instruments_to_exchange_symbols() +``` + +Это обеспечивает чистую архитектурную границу: + +```text +Acquisition Pipeline + ↓ +Canonical Instrument Models +``` + +Compatibility mapping выполняется отдельно только там, где действительно требуется legacy-контракт. + +--- + +## 9. Сохранение обратной совместимости + +Build 020 не меняет публичный контракт: + +```python +ExchangeService.get_exchange_symbols() +``` + +Он по-прежнему возвращает: + +```python +list[ExchangeSymbol] +``` + +Благодаря этому продолжают работать существующие consumers, включая: + +```text +validate_symbol() +currency_ui.py +legacy exchange UI +существующие unit tests +``` + +Миграция выполнена без обязательного одновременного переписывания всех legacy consumers. + +--- + +## 10. Поведение при отключённой бирже + +При: + +```python +exchange_enabled = False +``` + +метод: + +```python +get_exchange_symbols() +``` + +возвращает: + +```python +[] +``` + +При этом он не должен: + +- читать `InstrumentStore`; +- использовать существующий projection cache; +- запускать acquisition pipeline; +- обращаться к реальной бирже. + +Это сохраняет прежнее поведение mock/disabled режима. + +--- + +## 11. Поведение при cache hit + +Если существует: + +```python +_exchange_symbols_projection_cache +``` + +метод возвращает существующий объект legacy-проекции без: + +- чтения acquisition source; +- повторной обработки документа; +- повторного compatibility mapping. + +Если projection cache отсутствует, но канонические данные уже существуют в: + +```python +InstrumentStore +``` + +то acquisition pipeline не запускается повторно. + +Вместо этого выполняется: + +```text +InstrumentStore + ↓ +tuple[Instrument, ...] + ↓ +Compatibility Mapper + ↓ +list[ExchangeSymbol] +``` + +Пустой канонический набор: + +```python +() +``` + +также считается валидным cache hit и не должен ошибочно интерпретироваться как отсутствие данных. + +--- + +## 12. Поведение при cache miss + +Если одновременно отсутствуют: + +```text +_exchange_symbols_projection_cache +InstrumentStore entry for "dzengi" +``` + +выполняется полный production flow: + +```text +Instrument Source + ↓ +Instrument Handler + ↓ +Instrument Acquisition Service + ↓ +tuple[Instrument, ...] + ↓ +InstrumentStore.set(...) + ↓ +Compatibility Mapper + ↓ +list[ExchangeSymbol] +``` + +После успешной загрузки: + +- канонический `tuple[Instrument, ...]` сохраняется в `InstrumentStore`; +- legacy-проекция создаётся отдельно; +- legacy-проекция сохраняется в `_exchange_symbols_projection_cache`. + +--- + +## 13. Поведение при ошибках + +Если acquisition pipeline завершается ошибкой: + +- ошибка логируется как exchange request error; +- endpoint сохраняется как: + +```text +exchangeInfo +``` + +- исходная ошибка становится причиной `ExchangeError`; +- канонический store не должен получать частичные или некорректные данные; +- projection cache не должен заполняться. + +Таким образом, ошибка acquisition не создаёт ложное успешное состояние. + +Если ошибка возникает на этапе compatibility mapping: + +- канонические данные уже могут находиться в `InstrumentStore`; +- projection cache не должен заполняться некорректным результатом; +- следующий вызов может повторно построить legacy-проекцию из сохранённых канонических данных без повторного запуска acquisition pipeline. + +--- + +## 14. Инварианты Build 020 + +После завершения Build 020 действуют следующие обязательные инварианты. + +1. `ExchangeService._exchange_symbols_cache` отсутствует в production-коде. + +2. Канонические справочные данные хранятся как: + +```python +tuple[Instrument, ...] +``` + +3. Владельцем канонического состояния является: + +```text +InstrumentStore +``` + +4. Текущей реализацией store является: + +```python +InMemoryInstrumentStore +``` + +5. `ExchangeService` зависит от абстракции: + +```python +InstrumentStoreProtocol +``` + +6. Старый loader: + +```python +_load_exchange_symbols_via_acquisition() +``` + +отсутствует. + +7. Новый loader: + +```python +_load_instruments_via_acquisition() +``` + +возвращает только: + +```python +tuple[Instrument, ...] +``` + +8. Новый loader не вызывает: + +```python +map_instruments_to_exchange_symbols() +``` + +9. Compatibility mapping выполняется после получения канонических моделей из store или acquisition pipeline. + +10. `_exchange_symbols_projection_cache` не является каноническим источником данных. + +11. Публичный контракт: + +```python +get_exchange_symbols() -> list[ExchangeSymbol] +``` + +сохранён для обратной совместимости. + +12. `validate_symbol()` продолжает работать через `get_exchange_symbols()`. + +13. При отключённой бирже store, projection cache и acquisition pipeline не используются. + +14. Ошибка acquisition не заполняет канонический store. + +15. Ошибка acquisition не заполняет projection cache. + +16. Пустой `tuple[Instrument, ...]` является валидным сохранённым значением и должен отличаться от отсутствия записи в store. + +--- + +## 15. Изменённые production-файлы + +Основные изменения Build 020 выполнены в: + +```text +app/src/integrations/exchange/service.py +``` + +Используются ранее подготовленные компоненты Storage Layer: + +```text +app/src/storage/instrument_store.py +app/src/storage/exceptions.py +``` + +Используется compatibility mapper: + +```text +app/src/market_data/acquisition/compatibility.py +``` + +--- + +## 16. Тестовое покрытие + +Основные проверки миграции находятся в: + +```text +app/tests/unit/integrations/exchange/test_service_exchange_symbols.py +``` + +Дополнительно проверена совместимость: + +```text +app/tests/unit/integrations/exchange/test_service_validate_symbol.py +``` + +И отдельно сохраняется тестовое покрытие самого store: + +```text +app/tests/unit/storage/test_instrument_store.py +``` + +Проверены следующие сценарии: + +- отключённая биржа возвращает пустой список; +- отключённая биржа не читает существующий `InstrumentStore`; +- cache hit legacy-проекции не запускает acquisition pipeline; +- store hit не запускает acquisition pipeline; +- store miss запускает acquisition pipeline; +- загруженные `Instrument` сохраняются в store; +- пустой tuple корректно обрабатывается как cache hit; +- compatibility mapping получает именно канонические `Instrument`; +- legacy-проекция сохраняет прежний тип `list[ExchangeSymbol]`; +- сохраняется порядок инструментов; +- acquisition error оборачивается в `ExchangeError`; +- acquisition error логируется с endpoint `exchangeInfo`; +- acquisition error не заполняет store; +- acquisition error не заполняет projection cache; +- acquisition loader использует реальную processing pipeline; +- acquisition loader использует registry key `dzengi`; +- acquisition loader возвращает `tuple[Instrument, ...]`; +- acquisition loader не вызывает compatibility mapper; +- `validate_symbol()` сохраняет прежнее поведение. + +--- + +## 17. Результаты проверок + +Целевая проверка `get_exchange_symbols()`: + +```text +23 passed in 0.12s +``` + +Целевая проверка `validate_symbol()`: + +```text +16 passed in 0.07s +``` + +Проверка синтаксической компиляции: + +```text +py_compile — успешно +``` + +Полный regression suite: + +```text +426 passed in 0.23s +``` + +Финальная архитектурная проверка подтвердила: + +```text +старый _exchange_symbols_cache отсутствует в production-коде; +старый _load_exchange_symbols_via_acquisition отсутствует; +канонический store подключён через InstrumentStoreProtocol; +текущая реализация store — InMemoryInstrumentStore; +legacy projection cache отделён от канонического store; +compatibility mapper не вызывается внутри acquisition loader; +новый acquisition loader возвращает канонические Instrument. +``` + +--- + +## 18. Архитектурный результат + +До Build 020: + +```text +Exchange API + ↓ +Acquisition Pipeline + ↓ +Instrument + ↓ +Compatibility Mapper + ↓ +ExchangeSymbol + ↓ +ExchangeService class-level cache +``` + +После Build 020: + +```text +Exchange API + ↓ +Acquisition Pipeline + ↓ +Instrument + ↓ +InstrumentStore + ↓ +Canonical Instrument Reference Data + ↓ +Compatibility Mapper + ↓ +ExchangeSymbol + ↓ +Temporary Legacy Projection Cache +``` + +Ключевое изменение: + +```text +ExchangeSymbol больше не является канонически сохраняемой моделью Instrument Reference Data. +``` + +Каноническим представлением теперь является: + +```python +Instrument +``` + +а владельцем его состояния является: + +```text +Storage Layer +``` + +--- + +## 19. Ограничения текущего этапа + +Build 020 намеренно не выполняет полную миграцию всех consumers на каноническую модель `Instrument`. + +На текущем этапе сохраняются: + +```python +get_exchange_symbols() -> list[ExchangeSymbol] +``` + +и: + +```python +_exchange_symbols_projection_cache +``` + +Они являются compatibility-механизмами переходного периода. + +Также Build 020 не переносит в новый `InstrumentStore`: + +- market price cache; +- execution price cache; +- balance snapshots; +- journal storage; +- другие runtime caches. + +Build 020 касается исключительно хранения канонических Instrument Reference Data. + +--- + +## 20. Критерии завершения + +Build 020 считается завершённым, если одновременно выполняются следующие условия: + +- [x] созданный ранее `InstrumentStore` используется production-кодом; +- [x] канонические данные хранятся как `tuple[Instrument, ...]`; +- [x] старый `_exchange_symbols_cache` удалён; +- [x] старый `_load_exchange_symbols_via_acquisition()` удалён; +- [x] новый `_load_instruments_via_acquisition()` возвращает канонические модели; +- [x] acquisition loader не выполняет compatibility mapping; +- [x] legacy API `get_exchange_symbols()` сохранён; +- [x] `validate_symbol()` сохраняет прежнее поведение; +- [x] ошибки acquisition не создают ложное состояние кэша; +- [x] целевые тесты проходят; +- [x] `py_compile` проходит; +- [x] полный regression suite проходит; +- [x] финальная архитектурная grep-проверка выполнена. + +--- + +## 21. Итоговый статус + +```text +BUILD 020 — COMPLETE +``` + +Build 020 завершает фактический перенос канонического кэша Instrument Reference Data из legacy `ExchangeService._exchange_symbols_cache` в специализированный Storage Layer. + +Существующий бот продолжает работать через сохранённый compatibility-контракт: + +```python +get_exchange_symbols() -> list[ExchangeSymbol] +``` + +При этом новая архитектура уже располагает полноценным каноническим хранилищем: + +```text +Instrument Acquisition Pipeline + ↓ +tuple[Instrument, ...] + ↓ +InstrumentStore +``` + +Это создаёт основу для дальнейшего поэтапного перевода consumers с legacy `ExchangeSymbol` на каноническую модель `Instrument` без нарушения работоспособности существующего бота. \ No newline at end of file diff --git a/docs/migrations/build_021.md b/docs/migrations/build_021.md new file mode 100644 index 0000000..b6cb8f7 --- /dev/null +++ b/docs/migrations/build_021.md @@ -0,0 +1,814 @@ +# Build 021 — Перевод первой группы потребителей на канонический Instrument API + +## Статус + +**Завершён** + +--- + +## Цель Build + +Перевести первую группу production-потребителей справочника инструментов с legacy-модели: + +```python +ExchangeSymbol +``` + +и legacy API: + +```python +ExchangeService.get_exchange_symbols() +``` + +на каноническую модель: + +```python +Instrument +``` + +и новый публичный API: + +```python +ExchangeService.get_instruments() +``` + +При этом необходимо: + +- сохранить работоспособность существующего бота; +- не удалять legacy API преждевременно; +- не нарушить существующие runtime-контуры; +- сохранить прежнее поведение потребителей; +- продолжить постепенный переход к каноническому Instrument Reference Data pipeline; +- не создавать параллельный источник истины для справочника инструментов. + +--- + +## Исходное состояние + +После завершения Build 020 канонический справочник инструментов уже сохранялся в: + +```python +InstrumentStoreProtocol +``` + +с текущей in-memory реализацией: + +```python +InMemoryInstrumentStore +``` + +В `ExchangeService` существовали два уровня представления данных: + +```text +Instrument + ↓ +Instrument Store + ↓ +get_exchange_symbols() + ↓ +ExchangeSymbol + ↓ +legacy consumers +``` + +Каноническая модель: + +```python +Instrument +``` + +уже являлась источником полных reference data инструмента, однако production-потребители продолжали использовать legacy-модель: + +```python +ExchangeSymbol +``` + +Первым выбранным потребителем стал: + +```text +app/src/telegram/ui/currency_ui.py +``` + +До миграции он получал список инструментов через: + +```python +exchange_service.get_exchange_symbols() +``` + +и работал с: + +```python +ExchangeSymbol +``` + +--- + +## Принятое архитектурное решение + +В `ExchangeService` добавлен публичный канонический API: + +```python +def get_instruments(self) -> tuple[Instrument, ...]: + ... +``` + +Теперь новые потребители должны получать reference data через: + +```python +ExchangeService.get_instruments() +``` + +Legacy API: + +```python +ExchangeService.get_exchange_symbols() +``` + +сохраняется как compatibility API для ещё не переведённых потребителей. + +Целевая архитектура на текущем этапе: + +```text +Dzengi exchangeInfo + ↓ +DzengiInstrumentDocumentSource + ↓ +InstrumentFeed + ↓ +DzengiInstrumentDocumentHandler + ↓ +InstrumentAcquisitionService + ↓ +tuple[Instrument, ...] + ↓ +Instrument Store + ↓ +ExchangeService.get_instruments() + ├──→ new consumers + │ + └──→ get_exchange_symbols() + ↓ + map_instruments_to_exchange_symbols() + ↓ + list[ExchangeSymbol] + ↓ + legacy consumers +``` + +Таким образом: + +- `Instrument` является канонической моделью; +- `Instrument Store` является runtime-хранилищем канонического справочника; +- `get_instruments()` является публичным API для новых и мигрированных потребителей; +- `get_exchange_symbols()` является временным compatibility API; +- `ExchangeSymbol` не является источником истины. + +--- + +## Реализованные изменения + +### 1. Добавлен публичный `get_instruments()` + +В файле: + +```text +app/src/integrations/exchange/service.py +``` + +добавлен публичный метод: + +```python +def get_instruments(self) -> tuple[Instrument, ...]: + ... +``` + +Метод обеспечивает единый доступ к каноническому справочнику инструментов. + +Его поведение: + +```text +exchange disabled + ↓ +return () + +exchange enabled + ↓ +Instrument Store lookup + ↓ + ┌── cache hit ──→ return tuple[Instrument, ...] + │ + └── cache miss + ↓ + acquisition pipeline + ↓ + tuple[Instrument, ...] + ↓ + Instrument Store + ↓ + return instruments +``` + +--- + +### 2. Сохранено поведение при выключенной бирже + +Если: + +```python +exchange_enabled is False +``` + +метод: + +```python +get_instruments() +``` + +возвращает: + +```python +() +``` + +При этом: + +- `Instrument Store` не читается; +- acquisition pipeline не запускается; +- внешние запросы к бирже не выполняются. + +--- + +### 3. Реализовано чтение из Instrument Store + +При вызове: + +```python +get_instruments() +``` + +сначала проверяется каноническое хранилище: + +```python +ExchangeService._instrument_store +``` + +Если данные уже присутствуют, метод возвращает сохранённый: + +```python +tuple[Instrument, ...] +``` + +без повторного запуска acquisition pipeline. + +Это устраняет повторную загрузку reference data и сохраняет единый runtime-источник канонических инструментов. + +--- + +### 4. Реализована загрузка при отсутствии данных в Store + +Если `Instrument Store` не содержит данных для текущего источника, `get_instruments()` запускает существующий acquisition pipeline: + +```text +DzengiInstrumentDocumentSource + ↓ +InstrumentFeed + ↓ +DzengiInstrumentDocumentHandler + ↓ +InstrumentAcquisitionService + ↓ +tuple[Instrument, ...] +``` + +Полученные канонические модели: + +```python +Instrument +``` + +сохраняются в: + +```python +Instrument Store +``` + +и затем возвращаются вызывающему коду. + +--- + +### 5. `get_exchange_symbols()` переведён на канонический `get_instruments()` + +Legacy API: + +```python +get_exchange_symbols() +``` + +больше не должен самостоятельно загружать справочник инструментов. + +Теперь его роль ограничена compatibility projection: + +```text +get_instruments() + ↓ +tuple[Instrument, ...] + ↓ +map_instruments_to_exchange_symbols() + ↓ +list[ExchangeSymbol] +``` + +Таким образом, оба API используют один канонический источник данных: + +```text +Instrument Store +``` + +а параллельная загрузка reference data отсутствует. + +--- + +### 6. Переведён первый production-потребитель + +Файл: + +```text +app/src/telegram/ui/currency_ui.py +``` + +переведён с: + +```python +ExchangeSymbol +``` + +на: + +```python +Instrument +``` + +До миграции использовался resolver: + +```python +_resolve_asset_quote_symbol() +``` + +После миграции используется: + +```python +_resolve_asset_quote_instrument() +``` + +До миграции: + +```python +symbols = exchange_service.get_exchange_symbols() +``` + +После миграции: + +```python +instruments = exchange_service.get_instruments() +``` + +Теперь `currency_ui.py` больше не зависит от: + +```python +ExchangeSymbol +``` + +и: + +```python +get_exchange_symbols() +``` + +--- + +## Поведение `currency_ui.py` после миграции + +Логика выбора торгового инструмента сохранена. + +Для заданного актива: + +```python +BTC +``` + +resolver получает: + +```python +tuple[Instrument, ...] +``` + +и выбирает кандидатов, у которых: + +```text +base_asset == BTC +``` + +и: + +```text +quote_asset ∈ {USD, USDT} +``` + +Затем кандидаты сортируются по прежним приоритетам: + +```text +1. quote asset +2. trading status +3. market type +4. symbol +``` + +Приоритет котируемой валюты: + +```text +USD → 3 +USDT → 2 +other → 0 +``` + +Приоритет статуса: + +```text +TRADING → 2 +other status → 1 +HALT/BREAK → 0 +``` + +Приоритет типа рынка: + +```text +SPOT → 3 +LEVERAGE → 2 +other → 1 +``` + +Таким образом, поведение выбора инструмента осталось совместимым с предыдущей реализацией. + +--- + +## Получение USD-оценки актива + +Функция: + +```python +get_asset_usd_rate() +``` + +теперь использует канонический `Instrument`. + +Последовательность: + +```text +currency + ↓ +USD / USDT? + ├── yes → 1.0 + │ + └── no + ↓ + price cache hit? + ├── yes → cached rate + │ + └── no + ↓ + _resolve_asset_quote_instrument() + ↓ + Instrument | None + ↓ + exchange_service.get_price(instrument.symbol) + ↓ + rate +``` + +При этом существующая логика: + +```python +USDT ~= USD +``` + +сохранена без изменения. + +--- + +## Обработка ошибок + +Если: + +```python +exchange_service.get_instruments() +``` + +вызывает: + +```python +ExchangeError +``` + +resolver возвращает: + +```python +None +``` + +Если подходящий инструмент отсутствует: + +```python +None +``` + +Если получение цены вызывает: + +```python +ExchangeError +``` + +в price cache сохраняется: + +```python +None +``` + +и функция возвращает: + +```python +None +``` + +Таким образом, прежняя отказоустойчивая семантика `currency_ui.py` сохранена. + +--- + +## Добавленные тесты + +Добавлен отдельный тестовый файл: + +```text +app/tests/unit/integrations/exchange/test_service_instruments.py +``` + +Он проверяет канонический API: + +```python +ExchangeService.get_instruments() +``` + +В том числе: + +- возврат пустого tuple при выключенной бирже; +- отсутствие чтения Store при выключенной бирже; +- отсутствие запуска acquisition pipeline при выключенной бирже; +- чтение существующих инструментов из Store; +- загрузку через acquisition pipeline при cache miss; +- сохранение загруженных инструментов в Store; +- повторное использование Store; +- общее состояние Store между экземплярами `ExchangeService`; +- сохранение ошибок acquisition pipeline; +- отсутствие fallback на legacy REST path; +- использование `get_instruments()` внутри `get_exchange_symbols()`; +- преобразование канонических `Instrument` в legacy `ExchangeSymbol`; +- сохранение identity compatibility projection cache. + +--- + +## Добавлены тесты для первого production-потребителя + +Добавлен файл: + +```text +app/tests/unit/telegram/ui/test_currency_ui.py +``` + +Тестами зафиксировано, что `currency_ui.py`: + +- использует `get_instruments()`; +- не использует `get_exchange_symbols()`; +- работает с `Instrument`; +- выбирает инструмент с `USD` раньше `USDT`; +- учитывает статус инструмента; +- учитывает тип рынка; +- сохраняет детерминированную сортировку; +- возвращает `None`, если подходящего инструмента нет; +- возвращает `None` при ошибке получения справочника; +- не выполняет instrument lookup при наличии цены в cache; +- сохраняет прежнее поведение USD/USDT; +- корректно получает цену через `instrument.symbol`; +- сохраняет `None` в cache при ошибке получения цены; +- корректно рассчитывает USD-оценку баланса. + +--- + +## Результаты тестирования + +Проверка канонического API: + +```text +14 passed in 0.11s +``` + +Проверка первого production-потребителя: + +```text +20 passed in 0.08s +``` + +Совместная проверка затронутого migration-контура: + +```text +73 passed in 0.11s +``` + +Полный regression suite: + +```text +460 passed in 0.26s +``` + +Все тесты проходят успешно. + +--- + +## Проверка фактического состояния кода + +После завершения Build 021 файл: + +```text +app/src/telegram/ui/currency_ui.py +``` + +содержит: + +```python +from src.market_data.acquisition.models.instrument import Instrument +``` + +и использует: + +```python +exchange_service.get_instruments() +``` + +Legacy-зависимости в этом production-потребителе отсутствуют: + +```text +ExchangeSymbol +get_exchange_symbols() +_resolve_asset_quote_symbol() +``` + +В `ExchangeService` одновременно существуют: + +```python +def get_instruments(self) -> tuple[Instrument, ...]: + ... +``` + +и: + +```python +def get_exchange_symbols(self) -> list[ExchangeSymbol]: + ... +``` + +Это ожидаемое промежуточное состояние миграции. + +--- + +## Что намеренно не сделано в Build 021 + +В рамках этого Build не удалялись: + +```python +ExchangeSymbol +``` + +```python +SymbolValidationResult +``` + +```python +get_exchange_symbols() +``` + +```python +validate_symbol() +``` + +```python +map_instruments_to_exchange_symbols() +``` + +```python +_exchange_symbols_projection_cache +``` + +Они остаются необходимыми для ещё не переведённых legacy-потребителей. + +Также не переводились следующие runtime-контуры: + +```text +ExchangeService internal runtime methods +market_stream.py +market_data_runner.py +telegram/handlers/market.py +``` + +Их миграция должна выполняться отдельными Build с собственными regression tests. + +--- + +## Архитектурный результат + +До Build 021: + +```text +Instrument + ↓ +Instrument Store + ↓ +ExchangeSymbol projection + ↓ +all production consumers +``` + +После Build 021: + +```text + Instrument + ↓ + Instrument Store + ↓ + ExchangeService.get_instruments() + ┌─────┴─────┐ + ↓ ↓ + new consumers compatibility + ↓ ↓ + currency_ui get_exchange_symbols() + ↓ + ExchangeSymbol + ↓ + legacy consumers +``` + +Первый production-потребитель полностью переведён на канонический `Instrument API`. + +--- + +## Критерии завершения Build 021 + +Build считается завершённым, поскольку выполнены все критерии: + +- [x] добавлен публичный `ExchangeService.get_instruments()`; +- [x] `get_instruments()` использует `Instrument Store`; +- [x] при cache miss используется acquisition pipeline; +- [x] при выключенной бирже не читается Store и не запускается acquisition; +- [x] `get_exchange_symbols()` получает данные через `get_instruments()`; +- [x] первый production-потребитель переведён на `Instrument`; +- [x] `currency_ui.py` больше не использует `ExchangeSymbol`; +- [x] `currency_ui.py` больше не вызывает `get_exchange_symbols()`; +- [x] legacy API сохранён для остальных потребителей; +- [x] добавлены unit tests для `get_instruments()`; +- [x] добавлены unit tests для `currency_ui.py`; +- [x] migration-контур проходит `73` теста; +- [x] полный regression suite проходит `460` тестов; +- [x] существующий бот остаётся работоспособным. + +--- + +## Итог + +**Build 021 завершён успешно.** + +В проекте появился публичный канонический API: + +```python +ExchangeService.get_instruments() +``` + +Первый production-потребитель: + +```text +app/src/telegram/ui/currency_ui.py +``` + +переведён с legacy-модели: + +```python +ExchangeSymbol +``` + +на каноническую: + +```python +Instrument +``` + +При этом legacy compatibility layer сохранён для остальных потребителей, а полный regression suite подтверждает отсутствие регрессий: + +```text +460 passed in 0.26s +``` \ No newline at end of file diff --git a/docs/migrations/build_022.md b/docs/migrations/build_022.md new file mode 100644 index 0000000..94fc05e --- /dev/null +++ b/docs/migrations/build_022.md @@ -0,0 +1,1169 @@ +# Build 022 — Перевод валидации торгового символа на канонический `Instrument` + +**Engineering Build Record** + +--- + +## Контроль документа + +| Свойство | Значение | +|---|---| +| Документ | Build 022 — Перевод валидации торгового символа на канонический `Instrument` | +| Тип документа | Engineering Build Record | +| Build | 022 | +| Статус | **Завершён** | +| Подсистема | Market Data Acquisition / Exchange Integration | +| Проект | Dzentra | +| Язык | Русский | +| Предыдущий Build | Build 021 — Перевод первой группы потребителей | +| Следующий этап | Build 023+ — Поэтапный перевод остальных потребителей | + +--- + +## 1. Назначение Build + +Build 022 продолжает поэтапную миграцию Dzentra с legacy-модели биржевого инструмента `ExchangeSymbol` на каноническую модель: + +```text +Instrument +``` + +Основная задача Build 022: + +> Перевести механизм валидации торгового символа `ExchangeService.validate_symbol()` и непосредственно связанный с ним runtime-status с legacy-модели `ExchangeSymbol` на канонический справочник `Instrument`, сохранив существующий внешний контракт и работоспособность старого бота. + +После завершения Build 022 метод: + +```python +ExchangeService.validate_symbol() +``` + +больше не использует: + +```python +get_exchange_symbols() +``` + +для поиска и валидации торгового инструмента. + +Каноническим источником данных теперь является: + +```python +get_instruments() +``` + +который возвращает: + +```python +tuple[Instrument, ...] +``` + +--- + +## 2. Архитектурный контекст + +До начала миграции в системе существовала следующая цепочка: + +```text +Exchange API + ↓ +exchangeInfo + ↓ +ExchangeService.get_exchange_symbols() + ↓ +list[ExchangeSymbol] + ↓ +ExchangeService.validate_symbol() + ↓ +SymbolValidationResult.symbol_info + ↓ +ExchangeSymbol | None +``` + +После внедрения нового Market Data Acquisition Layer в системе появился канонический pipeline: + +```text +Exchange API + ↓ +Instrument Document Source + ↓ +Instrument Document Handler + ↓ +Instrument Feed + ↓ +Instrument Feed Registry + ↓ +Instrument Acquisition Service + ↓ +Instrument Store + ↓ +tuple[Instrument, ...] +``` + +На предыдущих этапах был создан новый канонический API: + +```python +ExchangeService.get_instruments() -> tuple[Instrument, ...] +``` + +Build 021 перевёл первого внешнего потребителя: + +```text +src/telegram/ui/currency_ui.py +``` + +с: + +```python +get_exchange_symbols() +ExchangeSymbol +``` + +на: + +```python +get_instruments() +Instrument +``` + +Build 022 продолжает миграцию на более глубоком уровне и переводит на `Instrument` механизм валидации торговых символов. + +--- + +## 3. Состояние до Build 022 + +До Build 022 метод: + +```python +ExchangeService.validate_symbol() +``` + +использовал: + +```python +symbols = self.get_exchange_symbols() +``` + +и выполнял поиск среди: + +```python +list[ExchangeSymbol] +``` + +Результат валидации имел контракт: + +```python +@dataclass(slots=True) +class SymbolValidationResult: + requested_symbol: str + normalized_symbol: str + + is_valid: bool + message: str + + symbol_info: ExchangeSymbol | None +``` + +Таким образом, несмотря на наличие канонического `Instrument Store`, валидация торгового символа всё ещё зависела от legacy-проекции: + +```text +Instrument Store + ↓ +Instrument + ↓ +compatibility mapper + ↓ +ExchangeSymbol + ↓ +validate_symbol() +``` + +Это создавало лишний промежуточный слой: + +```text +Instrument → ExchangeSymbol → validation +``` + +вместо прямой канонической цепочки: + +```text +Instrument → validation +``` + +--- + +## 4. Целевое состояние Build 022 + +После Build 022 архитектура валидации выглядит следующим образом: + +```text +Exchange API + ↓ +Market Data Acquisition + ↓ +Instrument Store + ↓ +ExchangeService.get_instruments() + ↓ +tuple[Instrument, ...] + ↓ +ExchangeService.validate_symbol() + ↓ +SymbolValidationResult + ↓ +Instrument | None +``` + +Главное архитектурное правило Build 022: + +> `validate_symbol()` обязан работать непосредственно с каноническим справочником `Instrument` и не должен использовать legacy-метод `get_exchange_symbols()`. + +--- + +## 5. Изменённые компоненты + +В рамках Build 022 были изменены следующие файлы: + +```text +app/src/integrations/exchange/models.py + +app/src/integrations/exchange/service.py + +app/tests/unit/integrations/exchange/test_service_validate_symbol.py + +app/tests/unit/integrations/exchange/test_service_symbol_runtime_status.py +``` + +--- + +## 6. Изменение `SymbolValidationResult` + +### До Build 022 + +Поле: + +```python +symbol_info +``` + +содержало: + +```python +ExchangeSymbol | None +``` + +Контракт: + +```python +@dataclass(slots=True) +class SymbolValidationResult: + requested_symbol: str + normalized_symbol: str + + is_valid: bool + message: str + + symbol_info: ExchangeSymbol | None +``` + +### После Build 022 + +Поле: + +```python +symbol_info +``` + +содержит каноническую модель: + +```python +Instrument | None +``` + +Итоговый контракт: + +```python +@dataclass(slots=True) +class SymbolValidationResult: + requested_symbol: str + normalized_symbol: str + + is_valid: bool + message: str + + symbol_info: Instrument | None +``` + +Для type checking используется импорт: + +```python +if TYPE_CHECKING: + from src.market_data.acquisition.models.instrument import Instrument +``` + +Это позволяет избежать ненужной runtime-зависимости модели exchange integration от реализации acquisition model при сохранении корректной типизации. + +--- + +## 7. Изменение `validate_symbol()` + +### До Build 022 + +Метод получал legacy-справочник: + +```python +symbols = self.get_exchange_symbols() +``` + +и выполнял поиск среди объектов: + +```python +ExchangeSymbol +``` + +Архитектурная цепочка имела вид: + +```text +validate_symbol() + ↓ +get_exchange_symbols() + ↓ +compatibility projection + ↓ +ExchangeSymbol +``` + +### После Build 022 + +Метод получает канонический справочник: + +```python +instruments = self.get_instruments() +``` + +и выполняет поиск непосредственно среди: + +```python +Instrument +``` + +Новая цепочка: + +```text +validate_symbol() + ↓ +get_instruments() + ↓ +Instrument Store + ↓ +Instrument +``` + +Legacy compatibility projection больше не участвует в валидации торгового символа. + +--- + +## 8. Сохранённая семантика валидации + +Build 022 не меняет пользовательскую и runtime-семантику `validate_symbol()`. + +Сохранены следующие сценарии. + +### 8.1. Пустой символ + +Вход: + +```text +" " +``` + +Результат: + +```python +SymbolValidationResult( + requested_symbol="", + normalized_symbol="", + is_valid=False, + message="Символ пустой.", + symbol_info=None, +) +``` + +--- + +### 8.2. Mock mode + +При: + +```python +exchange_enabled=False +``` + +символ принимается без обращения к каноническому справочнику. + +Пример результата: + +```python +SymbolValidationResult( + requested_symbol="BTC/USD_LEVERAGE", + normalized_symbol="BTC/USD_LEVERAGE", + is_valid=True, + message="Mock mode active.", + symbol_info=None, +) +``` + +--- + +### 8.3. Регистронезависимый поиск + +Следующие значения считаются эквивалентными: + +```text +BTC/USD_LEVERAGE +btc/usd_leverage +``` + +--- + +### 8.4. Игнорирование внешних пробелов + +Следующие значения считаются эквивалентными: + +```text +BTC/USD_LEVERAGE + btc/usd_leverage +``` + +--- + +### 8.5. Поддержка encoded separator + +Поддерживается представление: + +```text +BTC%2FUSD_LEVERAGE +``` + +наряду с: + +```text +BTC/USD_LEVERAGE +``` + +--- + +### 8.6. Поддержка внутренних пробелов + +Поддерживается нормализация значения: + +```text +btc / usd_leverage +``` + +до: + +```text +BTC/USD_LEVERAGE +``` + +--- + +### 8.7. Отсутствующий инструмент + +Если инструмент отсутствует в каноническом справочнике, возвращается неуспешный результат: + +```python +SymbolValidationResult( + requested_symbol="XRP/USD_LEVERAGE", + normalized_symbol="XRP/USD_LEVERAGE", + is_valid=False, + message=( + "Символ 'XRP/USD_LEVERAGE' " + "не найден в exchangeInfo." + ), + symbol_info=None, +) +``` + +Сообщение сохранено для обратной совместимости. + +--- + +### 8.8. Сохранение приоритета кандидатов + +Если разные исходные значения после нормализации могут соответствовать одному символу, сохраняется существующий порядок выбора кандидатов. + +В частности, точное исходное представление продолжает иметь приоритет в предусмотренных legacy-сценариях. + +--- + +### 8.9. Сохранение первого дубликата + +Если канонический справочник содержит несколько эквивалентных инструментов, возвращается первый подходящий объект согласно существующему порядку справочника. + +--- + +## 9. Канонический объект в результате валидации + +После Build 022 успешный результат: + +```python +validation = service.validate_symbol( + "BTC/USD_LEVERAGE" +) +``` + +содержит: + +```python +validation.symbol_info +``` + +типа: + +```python +Instrument +``` + +а не: + +```python +ExchangeSymbol +``` + +Дополнительно сохранена identity-семантика. + +Если канонический справочник содержит объект: + +```python +instrument +``` + +то после успешной валидации выполняется: + +```python +validation.symbol_info is instrument +``` + +То есть `validate_symbol()`: + +- не создаёт копию `Instrument`; +- не создаёт `ExchangeSymbol`; +- не выполняет compatibility mapping; +- возвращает исходный канонический объект из справочника. + +--- + +## 10. Перевод runtime-status + +Метод: + +```python +ExchangeService.get_symbol_runtime_status() +``` + +использует: + +```python +validation = self.validate_symbol(symbol_to_use) +``` + +и получает статус торгового инструмента через: + +```python +validation.symbol_info.status +``` + +После изменения контракта: + +```python +SymbolValidationResult.symbol_info +``` + +runtime-status теперь фактически получает канонический: + +```python +Instrument +``` + +вместо legacy: + +```python +ExchangeSymbol +``` + +При этом внешний контракт runtime-status не изменён. + +Сохраняются: + +```text +ExchangeRuntimeStatus +ExchangeStatusCode +get_symbol_market_status() +``` + +и существующая классификация состояний рынка. + +--- + +## 11. Сохранённая классификация runtime-status + +Build 022 сохраняет существующую классификацию биржевых статусов. + +### Открытый рынок + +Следующие статусы классифицируются как открытый рынок: + +```text +TRADING +OPEN +ACTIVE +ENABLED +ONLINE +``` + +Результат: + +```text +ExchangeStatusCode.OPEN +reason = "market_open" +``` + +--- + +### Перерыв или остановка торгов + +Следующие статусы классифицируются как перерыв: + +```text +BREAK +CLOSED +HALT +HALTED +PAUSED +SUSPENDED +DISABLED +SETTLING +POST_ONLY +``` + +Результат: + +```text +ExchangeStatusCode.BREAK +reason = "market_break" +``` + +--- + +### Инструмент недоступен для торговли + +Следующие статусы классифицируются как неторгуемые: + +```text +NOT_TRADABLE +TRADING_DISABLED +MARKET_DISABLED +UNAVAILABLE_FOR_TRADING +CLOSE_ONLY +REDUCE_ONLY +VIEW_ONLY +``` + +Результат: + +```text +ExchangeStatusCode.BREAK +reason = "market_not_tradable" +``` + +--- + +### Неизвестный статус + +Неизвестные значения классифицируются как: + +```text +ExchangeStatusCode.UNKNOWN +reason = "market_status_unknown" +``` + +--- + +## 12. Сохранение проверки freshness + +Build 022 не изменяет существующую проверку свежести рыночных данных. + +Для открытого рынка runtime-status продолжает проверять: + +```python +get_fresh_market_snapshot() +``` + +Если данные устарели, возвращается: + +```text +ExchangeStatusCode.BREAK +reason = "market_data_stale" +raw_status = "STALE_MARKET_DATA" +``` + +Проверка freshness выполняется только для инструмента, который сначала классифицирован как открытый для торговли. + +Для закрытого или приостановленного рынка получение snapshot не выполняется. + +--- + +## 13. Сохранение legacy runtime-контракта + +Метод: + +```python +get_symbol_market_status() +``` + +продолжает возвращать legacy dictionary contract. + +Пример: + +```python +{ + "code": "BREAK", + "status": "BREAK", + "symbol": "BTC/USD", + "is_open": False, + "is_available": True, + "is_auth_ok": True, + "title": "Перерыв в торгах", + "message": "Торги временно остановлены.", + "ui_line": "⏸️ Перерыв в торгах", + "reason": "market_break", + "raw_status": "BREAK", + "raw_error": None, +} +``` + +Таким образом, Build 022 меняет внутренний источник reference data, но не ломает существующий внешний runtime-контракт старого бота. + +--- + +## 14. Архитектурные ограничения, закреплённые тестами + +Build 022 вводит и закрепляет следующие архитектурные правила. + +### Правило 1 + +`validate_symbol()` обязан использовать: + +```python +get_instruments() +``` + +--- + +### Правило 2 + +`validate_symbol()` не должен использовать: + +```python +get_exchange_symbols() +``` + +--- + +### Правило 3 + +`validate_symbol()` не должен напрямую вызывать: + +```python +_load_instruments_via_acquisition() +``` + +Правильная цепочка: + +```text +validate_symbol() + ↓ +get_instruments() + ↓ +Instrument Store + ↓ +Acquisition Pipeline при необходимости +``` + +Неправильная цепочка: + +```text +validate_symbol() + ↓ +_load_instruments_via_acquisition() +``` + +--- + +### Правило 4 + +Успешная валидация должна возвращать исходный канонический: + +```python +Instrument +``` + +--- + +### Правило 5 + +Legacy API: + +```python +get_exchange_symbols() +``` + +может временно существовать только как compatibility layer для ещё не переведённых потребителей. + +Он больше не является каноническим источником данных для `validate_symbol()`. + +--- + +## 15. Тестовое покрытие `validate_symbol()` + +После Build 022 тесты подтверждают следующие сценарии: + +```text +пустой символ; +mock mode; +точное совпадение Instrument; +регистронезависимый поиск; +игнорирование внешних пробелов; +поддержка encoded separator; +поддержка внутренних пробелов; +отсутствующий инструмент; +возврат исходного объекта Instrument; +нормализация фактически найденного символа; +сохранение success message; +единственный вызов get_instruments(); +запрет использования legacy get_exchange_symbols(); +запрет прямого вызова acquisition; +сохранение приоритета кандидатов; +возврат первого дубликата; +сохранение типа SymbolValidationResult; +возврат канонического Instrument. +``` + +Результат: + +```text +18 passed in 0.11s +``` + +--- + +## 16. Тестовое покрытие runtime-status + +Тесты runtime-status были переведены с: + +```python +ExchangeSymbol +``` + +на: + +```python +Instrument +``` + +и подтверждают сохранение существующей runtime-семантики. + +Проверяются: + +```text +mock status; +explicit symbol; +default symbol; +invalid symbol; +exchange unavailable; +классификация открытого рынка; +классификация остановленного рынка; +классификация неторгуемого инструмента; +неизвестные статусы; +freshness только для открытого рынка; +stale market data; +граница freshness threshold; +отсутствующий age; +ошибка получения snapshot; +использование нормализованного символа; +сохранение legacy dictionary contract. +``` + +Результат: + +```text +34 passed in 0.09s +``` + +--- + +## 17. Проверка компиляции + +Выполнена команда: + +```bash +python -m py_compile \ + src/integrations/exchange/models.py \ + src/integrations/exchange/service.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py +``` + +Результат: + +```text +Ошибок компиляции нет. +``` + +--- + +## 18. Полный regression suite + +После завершения Build 022 выполнен полный набор тестов: + +```bash +python -m pytest -q +``` + +Результат: + +```text +462 passed in 0.24s +``` + +Это подтверждает, что переход: + +```text +validate_symbol() + ↓ +ExchangeSymbol → Instrument +``` + +не нарушил существующую функциональность проекта. + +--- + +## 19. Состояние legacy-слоя после Build 022 + +После Build 022 legacy-компоненты всё ещё существуют: + +```python +ExchangeSymbol +get_exchange_symbols() +map_instruments_to_exchange_symbols() +_exchange_symbols_projection_cache +``` + +Это ожидаемое состояние. + +Они сохраняются для ещё не переведённых потребителей и не должны удаляться до завершения их поэтапной миграции. + +Текущая архитектура: + +```text + ┌─────────────────────┐ + │ Instrument Store │ + └──────────┬──────────┘ + │ + ▼ + ┌─────────────────────┐ + │ get_instruments() │ + └──────────┬──────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ │ + ▼ ▼ + ┌─────────────────────┐ ┌─────────────────────┐ + │ Canonical consumers │ │ Compatibility layer │ + └─────────────────────┘ └──────────┬──────────┘ + │ │ + │ ▼ + │ ┌─────────────────────┐ + │ │ ExchangeSymbol │ + │ └──────────┬──────────┘ + │ │ + ▼ ▼ + ┌─────────────────────┐ ┌─────────────────────┐ + │ currency_ui.py │ │ Legacy consumers │ + │ validate_symbol() │ │ not migrated yet │ + │ runtime-status │ │ │ + └─────────────────────┘ └─────────────────────┘ +``` + +--- + +## 20. Что не входило в Build 022 + +Build 022 намеренно не выполняет полное удаление legacy-слоя. + +В рамках этого Build не удаляются: + +```python +ExchangeSymbol +get_exchange_symbols() +map_instrument_to_exchange_symbol() +map_instruments_to_exchange_symbols() +_exchange_symbols_projection_cache +``` + +Также Build 022 не выполняет массовый перевод всех оставшихся потребителей. + +Не входят в scope Build 022 отдельные runtime-потребители: + +```text +src/integrations/exchange/market_stream.py + +src/integrations/exchange/market_data_runner.py + +src/telegram/handlers/market.py +``` + +Их миграция должна выполняться поэтапно в следующих Build с отдельной проверкой контрактов и regression suite. + +--- + +## 21. Итоговое состояние после Build 022 + +После завершения Build 022 достигнуто следующее состояние: + +```text +Instrument Acquisition Pipeline + ↓ +Instrument Store + ↓ +get_instruments() + ↓ + ├── currency_ui.py + │ + ├── validate_symbol() + │ ↓ + │ SymbolValidationResult + │ ↓ + │ Instrument | None + │ + └── runtime-status +``` + +Ключевой результат: + +> Валидация торгового символа больше не зависит от legacy-модели `ExchangeSymbol` и работает непосредственно с каноническим справочником `Instrument`. + +При этом: + +```text +старый бот продолжает работать; +внешний контракт validate_symbol() сохранён; +runtime-status сохранён; +legacy dictionary contract сохранён; +полный regression suite проходит; +legacy compatibility layer остаётся доступен для ещё не переведённых потребителей. +``` + +--- + +## 22. Критерии завершения Build 022 + +Build 022 считается завершённым, поскольку выполнены все критерии: + +- [x] `SymbolValidationResult.symbol_info` переведён на `Instrument | None`. +- [x] `validate_symbol()` использует `get_instruments()`. +- [x] `validate_symbol()` больше не использует `get_exchange_symbols()`. +- [x] `validate_symbol()` не вызывает acquisition pipeline напрямую. +- [x] Успешная валидация возвращает исходный канонический `Instrument`. +- [x] Сохранена существующая нормализация торговых символов. +- [x] Сохранена семантика mock mode. +- [x] Сохранена семантика invalid symbol. +- [x] Сохранён runtime-status. +- [x] Сохранена классификация биржевых статусов. +- [x] Сохранена freshness-проверка. +- [x] Сохранён legacy dictionary contract. +- [x] Целевые тесты `validate_symbol()` проходят: `18 passed`. +- [x] Целевые тесты runtime-status проходят: `34 passed`. +- [x] `py_compile` проходит без ошибок. +- [x] Полный regression suite проходит: `462 passed`. + +--- + +## 23. Следующий этап + +Следующий этап: + +```text +Build 023+ — Поэтапный перевод остальных потребителей +``` + +Перед началом следующего Build необходимо определить очередную минимальную группу потребителей, которая всё ещё зависит от legacy reference data: + +```python +ExchangeSymbol +get_exchange_symbols() +SymbolValidationResult.symbol_info +``` + +Рекомендуемый принцип дальнейшей миграции: + +```text +один логически связанный набор потребителей + ↓ +перевод на Instrument + ↓ +целевые тесты + ↓ +полный regression suite + ↓ +grep-аудит + ↓ +документ Build + ↓ +следующий этап +``` + +Полное удаление: + +```python +ExchangeSymbol +get_exchange_symbols() +map_instruments_to_exchange_symbols() +_exchange_symbols_projection_cache +``` + +должно выполняться только после того, как последний реальный потребитель будет переведён на канонический `Instrument`. + +--- + +## 24. Финальный результат Build 022 + +```text +Build 022: ЗАВЕРШЁН + +Канонический справочник: +Instrument Store + ↓ +get_instruments() + ↓ +Instrument + +Переведённые потребители: + ├── currency_ui.py + ├── validate_symbol() + └── runtime-status через validate_symbol() + +Legacy compatibility: + ├── ExchangeSymbol + ├── get_exchange_symbols() + ├── compatibility mapper + └── projection cache + +Статус legacy compatibility: +Временно сохранён для ещё не переведённых потребителей. + +Regression: +462 passed. +``` \ No newline at end of file diff --git a/docs/migrations/build_023.md b/docs/migrations/build_023.md new file mode 100644 index 0000000..58ee273 --- /dev/null +++ b/docs/migrations/build_023.md @@ -0,0 +1,335 @@ +# Build 023 — Миграция runtime-потребителей на канонический Instrument Reference + +**Статус:** ✅ Завершён +**Дата:** 2026-07-12 +**Подсистема:** Exchange Integration +**Этап:** Instrument Reference Migration + +--- + +# Цель Build + +Перевести runtime-компоненты получения рыночных данных на использование канонического справочника инструментов (`Instrument Reference`), полностью отказавшись от любых зависимостей от legacy-проекций `ExchangeSymbol`. + +Build является логическим продолжением Build 022 и завершает миграцию всех runtime-потребителей ExchangeService. + +--- + +# Причина изменений + +После завершения Build 022 основная логика ExchangeService уже использовала новый механизм: + +``` +Instrument Store + ↓ +get_instruments() + ↓ +validate_symbol() +``` + +Однако требовалось убедиться, что runtime-компоненты также используют исключительно новый контракт и не имеют скрытых зависимостей от legacy-моделей. + +Проверке подлежали: + +- `market_stream.py` +- `market_data_runner.py` + +--- + +# Выполненный анализ + +Проведён аудит файлов: + +``` +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Проверено использование: + +- validate_symbol() +- normalized_symbol +- symbol_info +- get_exchange_symbols() +- ExchangeSymbol +- Instrument + +--- + +# Результаты анализа + +Установлено следующее. + +## Market Stream + +Используется: + +```python +validation = service.validate_symbol(...) +``` + +После успешной проверки используется: + +```python +validation.normalized_symbol +``` + +Объект `symbol_info` нигде не читается. + +Модель `ExchangeSymbol` не используется. + +Получение списка инструментов напрямую отсутствует. + +--- + +## Market Data Runner + +Используется: + +```python +validation = ExchangeService().validate_symbol(...) +``` + +После успешной проверки используется: + +```python +validation.normalized_symbol +``` + +При ошибке валидации сохраняется безопасный fallback: + +```text +исходный symbol +``` + +Обращений к: + +- symbol_info +- ExchangeSymbol +- get_exchange_symbols() + +не обнаружено. + +--- + +# Изменения Production-кода + +Изменения Production-кода не потребовались. + +Build подтвердил, что оба runtime-компонента уже полностью соответствуют новой архитектуре Instrument Reference. + +--- + +# Добавлены Unit-тесты + +Создан: + +``` +tests/unit/integrations/exchange/test_market_stream.py +``` + +Проверяются следующие сценарии. + +### Использование normalized_symbol + +Проверяется, что WebSocket запускается для канонического символа. + +--- + +### Отсутствие зависимости от symbol_info + +Проверяется отсутствие чтения: + +```python +validation.symbol_info +``` + +--- + +### Обработка невалидного символа + +Проверяется, что: + +- WebSocket не создаётся; +- используется результат validate_symbol(). + +--- + +### Exchange Disabled + +Проверяется, что при отключённой бирже: + +- ExchangeService не создаётся; +- runtime сразу завершается. + +--- + +Использованы синхронные тесты через: + +```python +asyncio.run(...) +``` + +что позволило избежать зависимости проекта от `pytest-asyncio`. + +--- + +# Проверка Market Data Runner + +Подтверждены существующие тесты: + +``` +tests/unit/integrations/exchange/test_market_data_runner.py +``` + +Проверяется: + +- использование validate_symbol(); +- использование normalized_symbol; +- отсутствие зависимости от symbol_info; +- fallback при ошибке; +- fallback при invalid symbol. + +--- + +# Проверка компиляции + +Выполнено: + +```bash +python -m py_compile \ + src/integrations/exchange/market_stream.py \ + src/integrations/exchange/market_data_runner.py \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py +``` + +Результат: + +``` +Ошибок нет. +``` + +--- + +# Проверка целевого набора тестов + +Выполнено: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ + -q +``` + +Результат: + +``` +61 passed +``` + +--- + +# Полный Regression Suite + +Выполнено: + +```bash +python -m pytest -q +``` + +Результат: + +``` +471 passed +``` + +--- + +# Финальный аудит зависимостей + +Выполнено: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "validate_symbol|symbol_info|get_instruments|get_exchange_symbols|ExchangeSymbol|Instrument" \ + src/integrations/exchange/market_stream.py \ + src/integrations/exchange/market_data_runner.py \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py +``` + +Подтверждено: + +Production-код использует только: + +``` +validate_symbol() +``` + +через: + +``` +normalized_symbol +``` + +Прямых зависимостей от: + +- ExchangeSymbol +- get_exchange_symbols() +- symbol_info +- Instrument + +не обнаружено. + +--- + +# Архитектурный итог + +После Build 023 runtime-компоненты полностью используют канонический путь работы с инструментами: + +```text +Instrument Reference + │ + ▼ +Instrument Store + │ + ▼ +get_instruments() + │ + ▼ +validate_symbol() + │ + ▼ +normalized_symbol + │ + ▼ +Market Stream +Market Data Runner +``` + +Legacy-проекция `ExchangeSymbol` больше не участвует в работе runtime-компонентов. + +--- + +# Итог Build + +Build 023 полностью завершён. + +Подтверждено: + +- runtime-компоненты используют канонический Instrument Reference; +- используется единая точка валидации символов через `validate_symbol()`; +- используется только `normalized_symbol`; +- отсутствуют зависимости от `ExchangeSymbol` и `symbol_info`; +- Production-код соответствует новой архитектуре без дополнительных изменений; +- добавлены Unit-тесты для `market_stream.py`; +- подтверждена корректность `market_data_runner.py`; +- полный Regression Suite успешно пройден (**471 passed**). + +**Статус Build:** ✅ Завершён. \ No newline at end of file diff --git a/docs/migrations/build_024.md b/docs/migrations/build_024.md new file mode 100644 index 0000000..5641dc7 --- /dev/null +++ b/docs/migrations/build_024.md @@ -0,0 +1,290 @@ +# Build 024 — Удаление legacy Telegram Market Handler + +**Статус:** ✅ Завершён +**Дата:** 2026-07-12 +**Подсистема:** Telegram UI +**Этап:** Legacy Cleanup + +--- + +# Цель Build + +Полностью удалить устаревший экран **Market** из Telegram UI после подтверждения, что он больше не используется работающим приложением. + +Build является этапом очистки архитектуры после перехода на новую концепцию Dzentra, где: + +- экран **«Автоторговля»** является основным рабочим интерфейсом; +- диагностика рынка встроена непосредственно в AutoTrade; +- отдельный экран **«Рынок»** больше не существует. + +--- + +# Предпосылки + +Ранее были удалены: + +- меню Market; +- переходы на Market; +- экран Monitoring; +- интеграция диагностики рынка в отдельный экран. + +Однако файл: + +```text +src/telegram/handlers/market.py +``` + +оставался в проекте. + +Требовалось убедиться, что он действительно является неиспользуемым legacy-компонентом перед его физическим удалением. + +--- + +# Выполненный аудит + +Проверены регистрации Telegram Router. + +Файл: + +```text +src/telegram/routers.py +``` + +Подключает только: + +```text +start +home +portfolio +auto +journal +debug_auto +debug +system +``` + +Router Market отсутствует. + +--- + +Проверены: + +```text +src/main.py +``` + +```text +src/telegram/handlers/__init__.py +``` + +Импортов: + +```python +src.telegram.handlers.market +``` + +не обнаружено. + +--- + +# Поиск скрытых зависимостей + +Выполнен аудит проекта. + +Проверены: + +- import market handler; +- include_router(); +- callback_data; +- screen="market"; +- open_market(); +- open_market_from_monitoring(); +- runtime-события. + +Выполнены проверки: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + --exclude="market.py" \ + -E "open_market|open_market_from_monitoring|market:retry|market_open_requested|market_open_success|market_open_error|screen=['\"]market['\"]|router.*market|market_router" \ + src tests +``` + +и + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + --exclude="market.py" \ + -E "src\.telegram\.handlers\.market|telegram\.handlers\.market|handlers\.market" \ + src tests +``` + +Результат: + +``` +Внешних зависимостей не обнаружено. +``` + +Единственное совпадение: + +```text +test_get_symbol_runtime_status_checks_freshness_only_for_open_market +``` + +оказалось названием unit-теста и не связано с Telegram Market Handler. + +--- + +# Выполненные изменения + +Удалён файл: + +```text +src/telegram/handlers/market.py +``` + +После удаления очищены Python cache: + +```bash +find src tests \ + -type d \ + -name "__pycache__" \ + -prune \ + -exec rm -rf {} + +``` + +--- + +# Проверка компиляции + +Выполнено: + +```bash +python -m py_compile \ + src/telegram/routers.py \ + src/main.py +``` + +Результат: + +``` +Ошибок нет. +``` + +--- + +# Полный Regression Suite + +Выполнено: + +```bash +python -m pytest -q +``` + +Результат: + +``` +471 passed +``` + +--- + +# Финальный аудит + +Выполнено: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "src\.telegram\.handlers\.market|telegram\.handlers\.market|handlers\.market|screen=['\"]market['\"]|market:retry|market_open_requested|market_open_success|market_open_error|open_market_from_monitoring" \ + src tests +``` + +Результат: + +``` +Совпадений не найдено. +``` + +Дополнительно подтверждено отсутствие файла: + +```bash +test ! -f src/telegram/handlers/market.py \ + && echo "legacy market handler removed" +``` + +Получен результат: + +```text +legacy market handler removed +``` + +--- + +# Архитектурный итог + +После завершения Build 024 структура Telegram UI окончательно соответствует новой архитектуре Dzentra. + +Активные Router: + +```text +Telegram +│ +├── start +├── home +├── portfolio +├── auto +├── journal +├── debug_auto +├── debug +└── system +``` + +Legacy Market Handler полностью исключён из проекта. + +--- + +# Удалённые legacy-компоненты + +Полностью устранены: + +```text +src/telegram/handlers/market.py + +screen="market" + +market:retry + +market_open_requested + +market_open_success + +market_open_error + +open_market_from_monitoring +``` + +Никаких ссылок на экран Market в проекте больше не существует. + +--- + +# Итог Build + +Build 024 полностью завершён. + +Подтверждено: + +- выполнён полный аудит подключений Telegram Router; +- подтверждено отсутствие использования Market Handler; +- безопасно удалён `src/telegram/handlers/market.py`; +- удалены все остаточные ссылки на экран Market; +- проект успешно проходит компиляцию; +- полный Regression Suite успешно пройден (**471 passed**); +- архитектура Telegram UI очищена от legacy-компонентов. + +**Статус Build:** ✅ Завершён. \ No newline at end of file diff --git a/docs/migrations/build_025.md b/docs/migrations/build_025.md new file mode 100644 index 0000000..60cb2dd --- /dev/null +++ b/docs/migrations/build_025.md @@ -0,0 +1,599 @@ +# Build 025 — Удаление legacy `ExchangeSymbol` compatibility layer + +**Статус:** ✅ Завершён +**Дата:** 2026-07-12 +**Подсистема:** Instrument Reference / Exchange Integration +**Этап:** Завершение миграции на каноническую модель `Instrument` + +--- + +# Цель Build + +Полностью удалить временный compatibility layer, использовавшийся во время поэтапного перехода от legacy-модели: + +```text +ExchangeSymbol +``` + +к канонической модели: + +```text +Instrument +``` + +После завершения Build все production-потребители работают через единый канонический контур: + +```text +Instrument Acquisition + ↓ +Instrument Store + ↓ +get_instruments() + ↓ +validate_symbol() + ↓ +Instrument +``` + +--- + +# Предпосылки + +На предыдущих этапах были выполнены: + +- создание канонической модели `Instrument`; +- построение Instrument Acquisition pipeline; +- создание `InstrumentStoreProtocol`; +- реализация `InMemoryInstrumentStore`; +- перенос кэша справочника инструментов в Storage; +- добавление `ExchangeService.get_instruments()`; +- перевод `validate_symbol()` на канонический справочник; +- перевод Telegram UI и runtime-потребителей на `Instrument`; +- удаление неиспользуемого legacy Market Handler. + +После этого legacy-контур сохранялся только как временная проекция: + +```text +Instrument + ↓ +compatibility.py + ↓ +ExchangeSymbol + ↓ +get_exchange_symbols() +``` + +Реальных production-потребителей этого контура больше не осталось. + +--- + +# Предварительный аудит + +Выполнен поиск production-зависимостей: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "ExchangeSymbol|get_exchange_symbols|map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols|_exchange_symbols_projection_cache" \ + src +``` + +Установлено, что все найденные элементы находились только внутри самого legacy-контура: + +```text +src/market_data/acquisition/compatibility.py +src/integrations/exchange/models.py +src/integrations/exchange/service.py +``` + +Канонические production-потребители уже использовали: + +```text +get_instruments() +validate_symbol() +Instrument +``` + +--- + +# Аудит legacy parser helpers + +Проверены методы: + +```text +_extract_exchange_symbols_raw() +_parse_exchange_symbol() +_parse_exchange_symbol_status() +_parse_market_modes() +_extract_filter_value() +``` + +Выполнено: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "_extract_exchange_symbols_raw|_parse_exchange_symbol\(|_parse_exchange_symbol_status|_parse_market_modes|_extract_filter_value" \ + src tests +``` + +Подтверждено, что эти методы использовались только устаревшим equivalence-тестом и больше не требовались production-коду. + +--- + +# Подготовка тестового контура + +Перед удалением production compatibility layer были обновлены канонические тесты. + +## Обновлён файл + +```text +tests/unit/integrations/exchange/test_service_instruments.py +``` + +Из него удалены тесты legacy-проекции: + +```text +test_get_exchange_symbols_uses_get_instruments +test_get_exchange_symbols_maps_canonical_instruments +test_get_exchange_symbols_projection_cache_preserves_identity +``` + +Сохранены все тесты канонического поведения: + +- отключённая биржа; +- Instrument Store hit; +- Instrument Store miss; +- загрузка через acquisition; +- сохранение результата в Store; +- повторное использование Store; +- поддержка пустого immutable-набора; +- общий Store между экземплярами `ExchangeService`; +- обработка acquisition errors; +- логирование ошибок; +- отсутствие заполнения Store при ошибке. + +--- + +## Обновлён файл + +```text +tests/unit/integrations/exchange/test_service_validate_symbol.py +``` + +Legacy-проверка через monkeypatch метода: + +```text +get_exchange_symbols() +``` + +заменена проверкой прямого использования: + +```text +get_instruments() +``` + +Дополнительно добавлен отрицательный архитектурный тест: + +```python +def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None: + assert not hasattr( + ExchangeService, + "get_exchange_symbols", + ) +``` + +--- + +## Обновлён файл + +```text +tests/unit/telegram/ui/test_currency_ui.py +``` + +Legacy-проверка отсутствия вызова `get_exchange_symbols()` заменена прямой проверкой использования канонического: + +```text +get_instruments() +``` + +--- + +# Проверка подготовительного пакета + +Выполнена компиляция: + +```bash +python -m py_compile \ + tests/unit/integrations/exchange/test_service_instruments.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + tests/unit/telegram/ui/test_currency_ui.py +``` + +Результат: + +```text +Ошибок нет. +``` + +Выполнены целевые тесты: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_instruments.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + tests/unit/telegram/ui/test_currency_ui.py \ + -q +``` + +Результат: + +```text +49 passed +``` + +--- + +# Удалённые migration-only файлы + +Полностью удалены: + +```text +src/market_data/acquisition/compatibility.py + +tests/unit/market_data/acquisition/test_compatibility.py +tests/unit/market_data/acquisition/test_equivalence_comparator.py + +tests/unit/integrations/exchange/test_service_exchange_symbols.py + +tests/integration/market_data/acquisition/test_instrument_reference_equivalence.py +tests/support/instrument_reference_equivalence.py +``` + +Эти файлы обслуживали только временный compatibility/equivalence-контур и завершили свою миграционную задачу. + +--- + +# Изменения в `models.py` + +Из файла: + +```text +src/integrations/exchange/models.py +``` + +полностью удалена legacy-модель: + +```python +@dataclass(slots=True) +class ExchangeSymbol: + ... +``` + +Модель: + +```python +SymbolValidationResult +``` + +сохраняет канонический контракт: + +```python +symbol_info: Instrument | None +``` + +Импорт `Instrument` выполняется через: + +```python +TYPE_CHECKING +``` + +что исключает runtime-cycle и сохраняет корректную типизацию. + +--- + +# Изменения в `service.py` + +Из файла: + +```text +src/integrations/exchange/service.py +``` + +удалены следующие элементы. + +## Legacy imports + +Удалены: + +```python +ExchangeSymbol +``` + +и: + +```python +from src.market_data.acquisition.compatibility import ( + map_instruments_to_exchange_symbols, +) +``` + +--- + +## Legacy projection cache + +Удалено поле: + +```python +_exchange_symbols_projection_cache +``` + +Теперь `ExchangeService` хранит только канонический Store: + +```python +_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore() +``` + +--- + +## Legacy API + +Полностью удалён метод: + +```python +get_exchange_symbols() +``` + +Единственным публичным API справочника инструментов остаётся: + +```python +get_instruments() +``` + +--- + +## Legacy parser helpers + +Удалены методы: + +```text +_extract_exchange_symbols_raw() +_parse_exchange_symbol() +_parse_exchange_symbol_status() +_parse_market_modes() +_extract_filter_value() +``` + +Их функции полностью заменены новой pipeline: + +```text +Dzengi REST document + ↓ +Dzengi parser + ↓ +validation + ↓ +mapper + ↓ +Instrument +``` + +--- + +## Сохранённый helper + +Метод: + +```python +_safe_str() +``` + +сохранён, так как продолжает использоваться обработкой trading fee payload. + +--- + +# Финальный production-контур + +После удаления compatibility layer работа со справочником инструментов выполняется так: + +```text +DzengiInstrumentDocumentSource + ↓ +DzengiInstrumentDocumentHandler + ↓ +InstrumentFeed + ↓ +InstrumentFeedRegistry + ↓ +InstrumentAcquisitionService + ↓ +tuple[Instrument, ...] + ↓ +Instrument Store + ↓ +ExchangeService.get_instruments() +``` + +Проверка пользовательского символа выполняется через: + +```text +validate_symbol() + ↓ +SymbolValidationResult + ↓ +symbol_info: Instrument | None +``` + +--- + +# Архитектурные отрицательные тесты + +Добавлены проверки физического отсутствия legacy API. + +## Отсутствие legacy-метода + +```python +def test_exchange_service_has_no_legacy_get_exchange_symbols() -> None: + assert not hasattr( + ExchangeService, + "get_exchange_symbols", + ) +``` + +## Отсутствие legacy projection cache + +```python +def test_exchange_service_has_no_legacy_projection_cache() -> None: + assert not hasattr( + ExchangeService, + "_exchange_symbols_projection_cache", + ) +``` + +--- + +# Финальный аудит + +Выполнено: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "ExchangeSymbol|get_exchange_symbols|map_instrument_to_exchange_symbol|map_instruments_to_exchange_symbols|_exchange_symbols_projection_cache|market_data\.acquisition\.compatibility" \ + src tests +``` + +Остались только ожидаемые упоминания в отрицательных архитектурных тестах: + +```text +test_exchange_service_has_no_legacy_get_exchange_symbols +test_exchange_service_has_no_legacy_projection_cache +``` + +Других production- или test-зависимостей не обнаружено. + +--- + +# Проверка компиляции + +Выполнено: + +```bash +python -m py_compile \ + src/integrations/exchange/models.py \ + src/integrations/exchange/service.py \ + tests/unit/integrations/exchange/test_service_instruments.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + tests/unit/telegram/ui/test_currency_ui.py +``` + +Результат: + +```text +Ошибок нет. +``` + +--- + +# Целевой Regression Suite + +Выполнено: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_instruments.py \ + tests/unit/integrations/exchange/test_service_validate_symbol.py \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py \ + tests/unit/telegram/ui/test_currency_ui.py \ + -q +``` + +Результат: + +```text +94 passed +``` + +--- + +# Полный Regression Suite + +Выполнено: + +```bash +python -m pytest -q +``` + +Результат: + +```text +423 passed +``` + +Снижение общего числа тестов относительно предыдущего Build является ожидаемым, поскольку были удалены временные compatibility- и equivalence-тесты вместе с соответствующим legacy-кодом. + +--- + +# Архитектурный итог + +До Build 025: + +```text +Instrument + ├── canonical consumers + └── compatibility mapper + ↓ + ExchangeSymbol + ↓ + legacy projection cache +``` + +После Build 025: + +```text +Instrument + ↓ +Instrument Store + ↓ +canonical consumers +``` + +В проекте больше не существует: + +```text +ExchangeSymbol +compatibility.py +get_exchange_symbols() +_exchange_symbols_projection_cache +Instrument → ExchangeSymbol mapping +legacy exchangeInfo parser helpers +migration equivalence framework +``` + +--- + +# Итог Build + +Build 025 полностью завершён. + +Подтверждено: + +- все production-потребители переведены на `Instrument`; +- удалена legacy-модель `ExchangeSymbol`; +- удалён временный compatibility mapper; +- удалён legacy-метод `get_exchange_symbols()`; +- удалён projection cache; +- удалены неиспользуемые parser helpers; +- удалены migration-only и equivalence-тесты; +- сохранены и усилены канонические тесты Instrument Store; +- добавлены архитектурные тесты отсутствия legacy API; +- компиляция проходит без ошибок; +- целевой Regression Suite успешно пройден — **94 passed**; +- полный Regression Suite успешно пройден — **423 passed**. + +**Статус Build:** ✅ Завершён. diff --git a/docs/migrations/build_026.md b/docs/migrations/build_026.md new file mode 100644 index 0000000..77a7d60 --- /dev/null +++ b/docs/migrations/build_026.md @@ -0,0 +1,799 @@ +# Build 026 — Аудит текущего контура Quotes Feed + +**Статус:** Завершён +**Подсистема:** Market Data Acquisition +**Функциональный модуль:** Quotes Feed +**Проект:** Dzentra + +--- + +## 1. Цель Build + +Провести полный аудит существующего контура получения, обработки, кэширования и потребления текущих рыночных котировок перед началом миграции в целевую подсистему: + +```text +src/market_data/acquisition/ +``` + +Основная задача Build — определить: + +- где сейчас реализовано получение котировок; +- какие REST- и WebSocket-источники используются; +- какие модели представляют котировку; +- где выполняются parsing, validation и mapping; +- как работает оперативный кэш котировок; +- какие компоненты являются фактическими потребителями ценовых данных; +- какие обязанности относятся непосредственно к Quotes Feed; +- какие обязанности должны остаться за пределами Acquisition; +- в какой последовательности выполнять безопасную миграцию без нарушения работы существующего бота. + +--- + +## 2. Итог аудита + +Текущий бот уже имеет функционально работающий контур получения и использования котировок. + +Котировки поступают из двух источников: + +1. REST API; +2. WebSocket depth stream. + +При этом архитектурно логика распределена между: + +```text +src/integrations/exchange/service.py +src/integrations/exchange/rest_client.py +src/integrations/exchange/ws_client.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +src/integrations/exchange/market_cache.py +src/integrations/exchange/models.py +``` + +Единой канонической модели `Quote` в production-контуре пока нет. + +Одна и та же концепция текущей рыночной котировки представлена несколькими различными контрактами: + +```text +TickerPrice +ExecutionPriceSnapshot +MarketPriceSnapshot +dict[str, object] +``` + +Это подтверждает необходимость поэтапной миграции в каноническую модель Quotes Feed. + +--- + +## 3. Текущий REST-контур котировок + +Основная реализация находится в: + +```text +src/integrations/exchange/service.py +``` + +Используются следующие методы: + +```python +refresh_price_cache() +refresh_market_snapshot_cache() +get_price() +get_market_snapshot() +get_execution_snapshot() +get_fresh_market_snapshot() +_get_real_price() +``` + +Основной источник данных: + +```text +GET /api/v1/ticker/24hr +``` + +Из ответа используются поля: + +```text +lastPrice +bidPrice +askPrice +``` + +Текущая цепочка выглядит следующим образом: + +```text +ExchangeService.get_fresh_market_snapshot() + ↓ +ExchangeRestClient.get_json() + ↓ +GET /api/v1/ticker/24hr + ↓ +lastPrice / bidPrice / askPrice + ↓ +legacy dict snapshot +``` + +REST-транспорт реализован в: + +```text +src/integrations/exchange/rest_client.py +``` + +--- + +## 4. Текущий WebSocket-контур котировок + +В проекте существуют две реализации обработки WebSocket/depth-данных: + +```text +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +WebSocket-транспорт находится в: + +```text +src/integrations/exchange/ws_client.py +``` + +Для получения данных используется: + +```python +ExchangeWebSocketClient.stream_depth() +``` + +Из depth payload извлекаются: + +```text +best bid +best ask +``` + +После чего рассчитывается: + +```text +midpoint = (best_bid + best_ask) / 2 +``` + +Результат записывается в: + +```text +MarketPriceCache +``` + +--- + +## 5. Текущие модели котировок + +### 5.1. TickerPrice + +Находится в: + +```text +src/integrations/exchange/models.py +``` + +Текущий контракт: + +```python +@dataclass(slots=True) +class TickerPrice: + symbol: str + price: float + source: str + updated_at: str +``` + +Используется как упрощённое представление текущей цены инструмента. + +--- + +### 5.2. ExecutionPriceSnapshot + +Находится в: + +```text +src/integrations/exchange/models.py +``` + +Содержит: + +```text +symbol +last_price +bid_price +ask_price +updated_at +source +is_fresh +age_seconds +freshness_status +spread_percent +``` + +Эта модель относится прежде всего к execution layer и не должна становиться канонической моделью Quotes Feed. + +--- + +### 5.3. MarketPriceSnapshot + +Находится в: + +```text +src/integrations/exchange/market_cache.py +``` + +Содержит: + +```text +symbol +price +bid_price +ask_price +updated_at +source +runtime_key +received_monotonic +``` + +Одновременно выполняет роль: + +- модели записи кэша; +- контейнера рыночной цены; +- источника информации о возрасте записи. + +--- + +### 5.4. Словарные snapshot-контракты + +Ряд методов `ExchangeService` возвращает: + +```python +dict[str, object] +``` + +с ключами: + +```text +symbol +last_price +bid_price +ask_price +updated_at +source +age_seconds +``` + +Такие словарные контракты используются многими существующими потребителями и должны быть удалены только после их полного перевода на новые типизированные контракты. + +--- + +## 6. Основные архитектурные проблемы + +### 6.1. Отсутствует единая каноническая модель Quote + +Файл: + +```text +src/market_data/acquisition/models/quote.py +``` + +существует в целевой структуре, но текущий production-контур ещё не использует единую каноническую модель `Quote`. + +Вместо неё используются: + +```text +TickerPrice +ExecutionPriceSnapshot +MarketPriceSnapshot +dict[str, object] +``` + +Целевая архитектура должна иметь одну внутреннюю каноническую модель котировки. + +--- + +### 6.2. ExchangeService перегружен обязанностями + +В текущем состоянии `ExchangeService` одновременно: + +- вызывает REST API; +- получает ticker response; +- разбирает поля ответа; +- проверяет значения; +- создаёт snapshot; +- читает кэш; +- обновляет кэш; +- оценивает freshness; +- создаёт execution snapshot; +- поддерживает legacy API для существующих потребителей. + +Эти обязанности должны быть постепенно разделены между: + +```text +adapters/dzengi/ +validation/ +models/ +handlers/ +feeds/ +service.py +storage/ +execution/ +``` + +--- + +### 6.3. WebSocket parsing дублируется + +Сходная логика присутствует одновременно в: + +```text +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Дублируются следующие операции: + +- извлечение вложенного payload; +- извлечение `bids`; +- извлечение `asks`; +- получение первой цены; +- преобразование значения в `float`; +- проверка положительности цены; +- расчёт midpoint. + +Эта логика должна быть централизована в Dzengi adapter: + +```text +src/market_data/acquisition/adapters/dzengi/parser.py +src/market_data/acquisition/adapters/dzengi/mapper.py +``` + +--- + +### 6.4. Quotes Feed и Order Book Feed частично смешаны + +Метод: + +```python +stream_depth() +``` + +получает depth-сообщение, относящееся к данным стакана. + +Однако текущие потребители используют из него только: + +```text +best bid +best ask +``` + +Для Quotes Feed это допустимый источник Level I quote. + +При этом полный depth не должен переноситься в Quotes Feed, поскольку полный стакан относится к отдельной будущей подсистеме: + +```text +Order Book Feed +``` + +Таким образом, Quotes Feed должен получать из depth только необходимую информацию верхнего уровня: + +```text +best bid +best ask +``` + +и формировать из неё канонический `Quote`. + +--- + +### 6.5. Кэш расположен в integration layer + +Текущий кэш находится в: + +```text +src/integrations/exchange/market_cache.py +``` + +Он отвечает одновременно за: + +- модель snapshot; +- хранение; +- runtime partitioning; +- возраст записи; +- форматирование локального времени. + +В целевой архитектуре хранение котировок не должно принадлежать Acquisition или exchange integration layer. + +Канонический Quote Store должен находиться в storage layer. + +--- + +### 6.6. Внутреннее время представлено UI-строкой + +Текущее представление: + +```text +DD.MM.YYYY HH:MM:SS +``` + +например: + +```text +10.07.2026 12:00:00 +``` + +является человекочитаемым UI-представлением, а не подходящим внутренним временным контрактом. + +Каноническая модель должна хранить машинное время, например: + +```text +exchange_timestamp_ms +received_timestamp_ms +``` + +или timezone-aware `datetime`. + +Форматирование времени для пользователя должно происходить только на UI-границе. + +--- + +### 6.7. REST client содержит дублирование + +В: + +```text +src/integrations/exchange/rest_client.py +``` + +существуют два метода: + +```python +get_payload() +get_json() +``` + +которые в значительной степени дублируют транспортную реализацию. + +Исправление этого дублирования не является задачей первого этапа Quotes Feed. + +Однако при дальнейшем развитии Dzengi REST adapter не следует создавать дополнительное дублирование транспорта. + +--- + +### 6.8. Текущий WebSocket не является обычной push-subscription + +Метод: + +```python +stream_depth() +``` + +работает следующим образом: + +```text +открыть постоянное WebSocket-соединение + ↓ +отправить новый request + ↓ +получить один response + ↓ +сделать sleep + ↓ +повторить request +``` + +Таким образом, текущая реализация ближе к polling поверх постоянного WebSocket-соединения, чем к классической push-subscription. + +Кроме того, запуск WebSocket stream из: + +```text +src/main.py +``` + +временно отключён, поскольку runtime probe не подтвердил рабочий endpoint с WebSocket Upgrade 101. + +Поэтому на текущем этапе архитектурно зафиксировано: + +```text +REST — рабочий основной источник котировок +WebSocket — сохраняемый экспериментальный или резервный транспорт +``` + +Первая версия нового Quotes Feed не должна зависеть от гарантированной доступности WebSocket. + +--- + +## 7. Граница ответственности канонической модели Quote + +Каноническая модель должна представлять непосредственно полученную рыночную котировку. + +В неё должны входить данные уровня: + +```text +symbol +last_price +bid_price +ask_price +exchange_timestamp +received_timestamp +source +``` + +Дополнительно могут быть предусмотрены: + +```text +sequence_id +event_id +``` + +но только если соответствующий источник Dzengi действительно предоставляет такие значения. + +--- + +## 8. Что не должно входить в базовую модель Quote + +В каноническую модель не следует помещать: + +```text +runtime_key +age_seconds +is_fresh +freshness_status +spread_percent +execution side +entry price +UI-formatted updated_at +``` + +Причины: + +| Поле | Правильная ответственность | +|---|---| +| `runtime_key` | Storage | +| `age_seconds` | Storage / Access layer | +| `is_fresh` | Политика конкретного потребителя | +| `freshness_status` | Runtime / consumer policy | +| `spread_percent` | Производная метрика | +| `execution side` | Execution layer | +| `entry price` | Execution layer | +| `updated_at` в UI-формате | UI formatting | + +--- + +## 9. Фактические потребители котировок + +### 9.1. Потребители `get_price()` + +```text +src/telegram/ui/currency_ui.py +src/telegram/handlers/auto/ui.py +src/trading/auto/execution_quality.py +``` + +--- + +### 9.2. Потребители `get_market_snapshot()` + +```text +src/telegram/handlers/auto/ui.py +src/telegram/handlers/debug_auto/ui.py +src/trading/auto/signal_runtime.py +src/trading/auto/execution_quality.py +src/trading/strategies/trend.py +src/trading/strategies/scalp.py +src/trading/diagnostics/snapshot.py +``` + +--- + +### 9.3. Потребители `get_execution_snapshot()` + +```text +src/trading/execution/pricing.py +src/telegram/handlers/debug_auto/ui.py +``` + +--- + +### 9.4. Потребители `get_fresh_market_snapshot()` + +```text +src/integrations/exchange/service.py +src/trading/debug/execution.py +``` + +Кроме того, этот метод используется внутри runtime-проверки статуса инструмента. + +--- + +## 10. Текущий MarketPriceCache + +Реализация находится в: + +```text +src/integrations/exchange/market_cache.py +``` + +Основные операции: + +```python +MarketPriceCache.set_price() +MarketPriceCache.get_price() +MarketPriceCache.clear() +``` + +Ключ записи: + +```text +(runtime_key, symbol) +``` + +Кэш используется из: + +```text +src/integrations/exchange/service.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +На текущем этапе `MarketPriceCache` нельзя удалять, поскольку он является частью рабочего production-контура. + +Он будет заменён только после появления канонического Quote Store и перевода всех производителей и потребителей. + +--- + +## 11. Целевая архитектурная цепочка REST Quotes Feed + +```text +Dzengi GET /api/v1/ticker/24hr + ↓ +adapters/dzengi/rest.py + ↓ +adapters/dzengi/models.py + ↓ +adapters/dzengi/parser.py + ↓ +validation/schema.py + ↓ +validation/values.py + ↓ +adapters/dzengi/mapper.py + ↓ +models/quote.py + ↓ +handlers/quotes_handler.py + ↓ +feeds/quotes_feed.py + ↓ +acquisition/service.py + ↓ +legacy ExchangeService facade + ↓ +существующие потребители бота +``` + +--- + +## 12. Целевая архитектурная цепочка WebSocket Quotes Feed + +```text +Dzengi WebSocket depth message + ↓ +adapters/dzengi/websocket.py + ↓ +adapters/dzengi/parser.py + ↓ +извлечение best bid / best ask + ↓ +validation/ + ↓ +adapters/dzengi/mapper.py + ↓ +models/quote.py + ↓ +handlers/quotes_handler.py + ↓ +feeds/quotes_feed.py + ↓ +Quote Store + ↓ +runtime consumers +``` + +Полный order book при этом не является частью Quotes Feed и должен в будущем обрабатываться отдельной подсистемой: + +```text +Order Book Feed +``` + +--- + +## 13. Принцип безопасной миграции + +Миграция должна выполняться без одномоментной замены рабочего контура. + +Основной принцип: + +```text +новая реализация создаётся параллельно + ↓ +покрывается тестами + ↓ +подключается под существующий facade + ↓ +потребители переводятся поэтапно + ↓ +legacy удаляется только после подтверждения отсутствия потребителей +``` + +На переходном этапе сохраняются: + +```text +ExchangeService.get_price() +ExchangeService.get_market_snapshot() +ExchangeService.get_execution_snapshot() +ExchangeService.get_fresh_market_snapshot() +MarketPriceCache +TickerPrice +ExecutionPriceSnapshot +``` + +Удаление допускается только в соответствующих поздних Build после полного перевода потребителей. + +--- + +## 14. Утверждённый план миграции Quotes Feed + +```text +Build 026 — Аудит текущего контура Quotes Feed +Build 027 — Каноническая модель Quote и специализированные контракты +Build 028 — Dzengi REST quote models, parser и validation +Build 029 — Dzengi mapper и Quotes Handler +Build 030 — Quotes Feed и регистрация в Acquisition Service +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +Положение WebSocket-этапа после рабочего REST-контура является намеренным. + +Бот должен сохранить гарантированный рабочий способ получения котировок даже при отсутствии подтверждённого production WebSocket endpoint. + +--- + +## 15. Результат Build 026 + +В результате Build 026: + +- полностью определён существующий REST-контур котировок; +- полностью определён существующий WebSocket/depth-контур; +- найдены все текущие модели ценовых данных; +- определены прямые производители и потребители котировок; +- проанализирован `MarketPriceCache`; +- обнаружено дублирование WebSocket parsing; +- определена граница между Quotes Feed и Order Book Feed; +- определена граница между Acquisition, Storage, Execution и UI; +- подтверждена необходимость сохранения legacy facade на время миграции; +- определена безопасная последовательность Build 027–040. + +--- + +## 16. Статус завершения + +**Build 026 завершён полностью.** + +Дополнительных изменений кода в рамках Build 026 не требуется. + +Следующий этап: + +```text +Build 027 — Каноническая модель Quote и специализированные контракты +``` \ No newline at end of file diff --git a/docs/migrations/build_027.md b/docs/migrations/build_027.md new file mode 100644 index 0000000..8556ec8 --- /dev/null +++ b/docs/migrations/build_027.md @@ -0,0 +1,700 @@ +# Build 027 — Каноническая модель Quote и специализированные контракты + +## Статус + +**Завершён** + +--- + +## Цель Build + +Создать каноническую внутреннюю модель текущей рыночной котировки `Quote` и специализированные контракты подсистемы `Quotes Feed`. + +Build должен сформировать независимую от конкретной биржи модель рыночной котировки и определить архитектурные границы между: + +- источником сырого документа котировки; +- обработчиком документа; +- готовым потоком котировок; +- потребителями `Market Data Acquisition`. + +При этом существующий legacy-контур получения и использования цен не должен изменяться. + +--- + +## Место в плане миграции + +Build 027 является вторым этапом миграции подсистемы `Quotes Feed`. + +Полный утверждённый план: + +```text +Build 026 — Аудит текущего контура Quotes Feed +Build 027 — Каноническая модель Quote и специализированные контракты +Build 028 — Dzengi REST quote models, parser и validation +Build 029 — Dzengi mapper и Quotes Handler +Build 030 — Quotes Feed и регистрация в Acquisition Service +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +--- + +## Архитектурная граница Build + +Build 027 ограничен двумя задачами: + +1. создание канонической модели `Quote`; +2. создание специализированных контрактов `Quotes Feed`. + +В рамках Build не реализуются: + +- REST-запрос котировки Dzengi; +- модели REST-ответа Dzengi; +- parsing ответа `ticker/24hr`; +- schema validation ответа Dzengi; +- value validation полей котировки; +- mapping модели Dzengi в `Quote`; +- `QuotesHandler`; +- `QuotesFeed`; +- регистрация потока в `Acquisition Service`; +- хранение котировок; +- WebSocket parsing; +- изменение `ExchangeService`; +- изменение `MarketPriceCache`; +- изменение market runtime; +- перевод UI-потребителей; +- перевод execution-потребителей. + +Эти изменения относятся к следующим Build. + +--- + +## Изменённые файлы + +```text +src/market_data/acquisition/ +├── models/ +│ └── quote.py +└── protocol.py +``` + +Всего изменено: + +- **2 файла**. + +--- + +## 1. Каноническая модель Quote + +Файл: + +```text +src/market_data/acquisition/models/quote.py +``` + +Создана независимая от конкретной биржи immutable-модель: + +```python +@dataclass(frozen=True, slots=True) +class Quote: + symbol: str + + last_price: Decimal + bid_price: Decimal + ask_price: Decimal + + exchange_timestamp: datetime | None + received_at: datetime + + source: str +``` + +### Назначение модели + +`Quote` представляет текущий рыночный факт о котировке одного инструмента. + +Модель является внутренней моделью слоя: + +```text +Market Data + ↓ +Market Data Acquisition + ↓ +Quotes Feed + ↓ +Quote +``` + +Она не зависит от: + +- API Dzengi; +- формата `ticker/24hr`; +- WebSocket-сообщений; +- legacy-моделей `integrations/exchange`; +- UI; +- Execution; +- конкретного способа хранения данных. + +--- + +## 2. Поля модели Quote + +### `symbol` + +```python +symbol: str +``` + +Каноническое обозначение инструмента. + +Пример: + +```text +BTC/USD +``` + +Поле не должно содержать транспортное или биржевое представление, специфичное для конкретного API, если оно отличается от канонического обозначения Dzentra. + +--- + +### `last_price` + +```python +last_price: Decimal +``` + +Последняя известная цена инструмента, полученная от источника. + +--- + +### `bid_price` + +```python +bid_price: Decimal +``` + +Лучшая доступная цена покупки. + +--- + +### `ask_price` + +```python +ask_price: Decimal +``` + +Лучшая доступная цена продажи. + +--- + +### `exchange_timestamp` + +```python +exchange_timestamp: datetime | None +``` + +Время рыночного события на стороне источника данных. + +Поле является optional, поскольку конкретный источник или endpoint может не предоставлять достоверный timestamp события. + +--- + +### `received_at` + +```python +received_at: datetime +``` + +Время получения рыночных данных системой Dzentra. + +Это позволяет независимо от наличия `exchange_timestamp` фиксировать момент поступления данных в систему. + +--- + +### `source` + +```python +source: str +``` + +Идентификатор источника рыночных данных. + +Пример: + +```text +dzengi +``` + +Модель не фиксирует конкретный набор допустимых источников на уровне класса `Quote`. + +--- + +## 3. Использование Decimal + +Для канонических цен используется: + +```python +Decimal +``` + +а не: + +```python +float +``` + +Это позволяет избежать привязки новой внутренней модели к ограничениям legacy-кода и уменьшает риск потери точности при работе с денежными значениями. + +Legacy-потребители при необходимости смогут получать преобразованное значение `float` через compatibility/facade-слой на следующих этапах миграции. + +--- + +## 4. Immutable-модель + +Модель объявлена как: + +```python +@dataclass(frozen=True, slots=True) +``` + +Это означает: + +- экземпляр `Quote` не изменяется после создания; +- исключается случайная мутация рыночного факта; +- модель имеет компактное представление через `slots`; +- объект подходит для передачи между слоями системы как immutable value object. + +Такой подход соответствует уже принятому направлению построения канонических моделей `Market Data Acquisition`. + +--- + +## 5. Что сознательно не включено в Quote + +В каноническую модель не включены поля: + +```text +is_fresh +age_seconds +freshness_status +spread_percent +runtime_key +received_monotonic +``` + +Причина: эти значения не являются исходным фактом котировки. + +Они относятся к другим обязанностям системы. + +### Freshness + +```text +is_fresh +age_seconds +freshness_status +``` + +Это runtime-оценка актуальности данных. + +Она должна вычисляться на основании времени получения или хранения котировки, а не быть частью исходного объекта `Quote`. + +### Spread + +```text +spread_percent +``` + +Это производное значение: + +```text +ask_price - bid_price +``` + +или его процентное представление. + +Оно может быть вычислено отдельным processing/runtime-компонентом. + +### Runtime identity + +```text +runtime_key +``` + +Это идентификатор runtime-контекста, а не свойство рыночной котировки. + +### Monotonic clock + +```text +received_monotonic +``` + +Это внутренний технический механизм runtime/storage-слоя для измерения возраста данных. + +Он не должен загрязнять каноническую модель рыночного факта. + +--- + +## 6. Специализированные контракты Quotes Feed + +В файл: + +```text +src/market_data/acquisition/protocol.py +``` + +добавлены три специализированных контракта: + +```text +QuoteDocumentSource +QuoteDocumentHandler +QuoteFeedProtocol +``` + +Архитектурная цепочка: + +```text +QuoteDocumentSource + ↓ +сырой документ + ↓ +QuoteDocumentHandler + ↓ +Quote + ↓ +QuoteFeedProtocol + ↓ +Acquisition Service +``` + +--- + +## 7. QuoteDocumentSource + +Контракт: + +```python +@runtime_checkable +class QuoteDocumentSource(Protocol): + def fetch_quote_document( + self, + symbol: str, + ) -> object: + """ + Получить декодированный транспортный документ текущей котировки. + + Источник не выполняет schema validation, parsing, value validation + или mapping во внутреннюю модель Quote. + """ + ... +``` + +### Ответственность + +`QuoteDocumentSource` отвечает только за получение сырого декодированного транспортного документа. + +Он не должен: + +- проверять схему; +- проверять значения; +- выполнять mapping; +- создавать `Quote`; +- хранить котировку; +- вычислять freshness; +- обслуживать UI или Execution. + +Для Dzengi конкретная реализация будет создана на следующих этапах. + +--- + +## 8. QuoteDocumentHandler + +Контракт: + +```python +@runtime_checkable +class QuoteDocumentHandler(Protocol): + def handle_quote_document( + self, + document: object, + ) -> Quote: + """ + Преобразовать сырой документ в проверенную внутреннюю модель Quote. + """ + ... +``` + +### Ответственность + +`QuoteDocumentHandler` определяет границу между сырым внешним документом и проверенной канонической моделью `Quote`. + +Конкретная реализация должна организовать последовательность: + +```text +сырой документ + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +Quote +``` + +Сам контракт не зависит от конкретной биржи. + +--- + +## 9. QuoteFeedProtocol + +Контракт: + +```python +@runtime_checkable +class QuoteFeedProtocol(Protocol): + def load_quote( + self, + symbol: str, + ) -> Quote: + """ + Получить внутреннюю модель текущей котировки инструмента. + """ + ... +``` + +### Ответственность + +`QuoteFeedProtocol` представляет готовый поток получения канонической текущей котировки для `Acquisition Service`. + +Потребитель этого контракта не должен знать: + +- какая биржа является источником; +- используется REST или другой транспорт; +- как устроен внешний payload; +- как выполняется parsing; +- как выполняется validation; +- как выполняется mapping. + +Для потребителя существует только операция: + +```text +symbol → Quote +``` + +--- + +## 10. Соответствие паттерну Instrument Reference Data + +Build 027 продолжает архитектурный подход, уже реализованный для `Instrument Reference Data`. + +### Instrument Reference Data + +```text +InstrumentDocumentSource + ↓ +InstrumentDocumentHandler + ↓ +InstrumentFeedProtocol + ↓ +Instrument +``` + +### Quotes Feed + +```text +QuoteDocumentSource + ↓ +QuoteDocumentHandler + ↓ +QuoteFeedProtocol + ↓ +Quote +``` + +Таким образом, новая вертикаль `Quotes Feed` строится в соответствии с уже принятой архитектурой `Market Data Acquisition`, без создания альтернативного или параллельного архитектурного подхода. + +--- + +## 11. Legacy-контур + +Build 027 не изменяет существующие legacy-компоненты: + +```text +src/integrations/exchange/models.py +src/integrations/exchange/service.py +src/integrations/exchange/market_cache.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +src/integrations/exchange/ws_client.py +``` + +Продолжают работать без изменений: + +```text +TickerPrice +ExecutionPriceSnapshot +MarketPriceSnapshot +MarketPriceCache +ExchangeService.get_price() +ExchangeService.get_market_snapshot() +ExchangeService.get_execution_snapshot() +ExchangeService.get_fresh_market_snapshot() +``` + +На данном этапе новая модель `Quote` существует параллельно legacy-контуру и ещё не используется работающим ботом. + +Это соответствует утверждённой стратегии безопасной миграции: + +```text +создать новый контур + ↓ +проверить новый контур + ↓ +подключить его под legacy facade + ↓ +поэтапно перевести потребителей + ↓ +удалить legacy только после полного переключения +``` + +--- + +## 12. Обратная совместимость + +Build 027 полностью обратно совместим с существующим ботом. + +Не изменены: + +- публичные методы `ExchangeService`; +- форматы legacy snapshot; +- `MarketPriceCache`; +- market runtime; +- Telegram UI; +- trading strategies; +- Execution; +- существующие модели интеграционного слоя. + +Новая модель и контракты пока не участвуют в runtime работающего приложения. + +--- + +## 13. Проверка синтаксиса + +Выполнена команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/models/quote.py \ + src/market_data/acquisition/protocol.py +``` + +Результат: + +```text +Успешно. +Ошибок синтаксиса и импортов не обнаружено. +``` + +--- + +## 14. Полная регрессия + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +423 passed in 0.24s +``` + +Все существующие тесты проекта проходят. + +Регрессий не обнаружено. + +--- + +## 15. Критерии завершения + +Build 027 считается завершённым, поскольку выполнены все его критерии: + +- [x] создана каноническая модель `Quote`; +- [x] модель не зависит от Dzengi; +- [x] цены представлены через `Decimal`; +- [x] модель immutable; +- [x] разделены `exchange_timestamp` и `received_at`; +- [x] runtime-поля не включены в каноническую модель; +- [x] создан `QuoteDocumentSource`; +- [x] создан `QuoteDocumentHandler`; +- [x] создан `QuoteFeedProtocol`; +- [x] сохранён архитектурный паттерн существующей вертикали `Instrument`; +- [x] legacy-контур не изменён; +- [x] синтаксическая проверка проходит; +- [x] полная регрессия проходит; +- [x] `423` теста проходят успешно. + +--- + +## Итог + +В рамках Build 027 создан фундамент канонической вертикали `Quotes Feed`. + +Теперь архитектура содержит независимое представление текущей рыночной котировки: + +```text +Quote +``` + +и три специализированных контракта: + +```text +QuoteDocumentSource +QuoteDocumentHandler +QuoteFeedProtocol +``` + +Целевая архитектурная цепочка сформирована как: + +```text +Внешний источник + ↓ +QuoteDocumentSource + ↓ +сырой транспортный документ + ↓ +QuoteDocumentHandler + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +Quote + ↓ +QuoteFeedProtocol + ↓ +Acquisition Service +``` + +Build 027 завершён без изменения поведения работающего бота и без преждевременного вмешательства в legacy-контур. + +Следующий этап: + +```text +Build 028 — Dzengi REST quote models, parser и validation +``` \ No newline at end of file diff --git a/docs/migrations/build_028.md b/docs/migrations/build_028.md new file mode 100644 index 0000000..7e0dfb4 --- /dev/null +++ b/docs/migrations/build_028.md @@ -0,0 +1,591 @@ +# Build 028 — Dzengi REST Quote Models, Parser и Validation + +## Статус + +**Завершён** + +--- + +## 1. Цель Build 028 + +Цель Build 028 — реализовать специализированный слой приёма, разбора и первичной проверки REST-ответа Dzengi для текущей рыночной котировки инструмента, не изменяя существующее поведение работающего бота и не подключая новую реализацию к production runtime до следующих этапов миграции. + +Build является частью поэтапной миграции подсистемы: + +**Quotes Feed** + +в новую архитектуру: + +```text +src/market_data/acquisition/ +``` + +На данном этапе реализованы: + +- модель сырого REST-ответа Dzengi; +- parser REST-ответа `/api/v1/ticker/24hr`; +- проверка структуры входящего payload; +- проверка допустимости значений котировки; +- специализированные ошибки обработки quote payload. + +Подключение mapper, handler, feed, service, store и перевод runtime-потребителей в данный Build не входят. + +--- + +## 2. Место Build 028 в плане миграции Quotes Feed + +Утверждённая последовательность: + +```text +Build 026 — Аудит текущего контура Quotes Feed +Build 027 — Каноническая модель Quote и специализированные контракты +Build 028 — Dzengi REST quote models, parser и validation +Build 029 — Dzengi mapper и Quotes Handler +Build 030 — Quotes Feed и регистрация в Acquisition Service +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +Build 028 продолжает фундамент, созданный в Build 027. + +Целевая цепочка после завершения следующих этапов: + +```text +Dzengi REST /api/v1/ticker/24hr + ↓ +adapters/dzengi/rest.py + ↓ +adapters/dzengi/parser.py + ↓ +adapters/dzengi/models.py + ↓ +validation/schema.py + ↓ +validation/values.py + ↓ +adapters/dzengi/mapper.py + ↓ +handlers/quotes_handler.py + ↓ +feeds/quotes_feed.py + ↓ +acquisition/service.py + ↓ +Quote Store + ↓ +потребители платформы +``` + +--- + +## 3. Исходные данные + +Для проектирования реализации использован реальный успешный ответ Dzengi: + +```json +{ + "askPrice": "64159.55", + "bidPrice": "64159.45", + "closeTime": 1783887270312, + "highPrice": "64261.45", + "lastPrice": "64159.45", + "lastQty": "5.0", + "lowPrice": "63590.7", + "openPrice": "63785.75", + "openTime": 1783814400000, + "prevClosePrice": "63785.75", + "priceChange": "368.85", + "priceChangePercent": "0.57822", + "quoteVolume": "616402.92146", + "symbol": "BTC/USD_LEVERAGE", + "volume": "9.6002", + "weightedAvgPrice": "64159.50" +} +``` + +Для базовой модели текущей котировки используются поля: + +```text +symbol +lastPrice +bidPrice +askPrice +closeTime +``` + +Остальные поля ответа `/api/v1/ticker/24hr` относятся к расширенной 24-часовой статистике рынка и не включаются в базовую модель `Quote`. + +Это сохраняет правильное разделение ответственностей между: + +- текущей котировкой; +- рыночной статистикой; +- OHLCV; +- trades; +- order book; +- другими специализированными типами рыночных данных. + +--- + +## 4. Реализованные компоненты + +В рамках Build 028 изменены следующие файлы: + +```text +src/market_data/acquisition/adapters/dzengi/models.py +src/market_data/acquisition/adapters/dzengi/parser.py +src/market_data/acquisition/validation/schema.py +src/market_data/acquisition/validation/values.py +src/market_data/acquisition/exceptions.py +``` + +Добавлены специализированные тесты: + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py +tests/unit/market_data/acquisition/validation/test_quote_schema.py +tests/unit/market_data/acquisition/validation/test_quote_values.py +``` + +--- + +## 5. Dzengi REST Quote Model + +В файле: + +```text +src/market_data/acquisition/adapters/dzengi/models.py +``` + +реализована модель сырой котировки Dzengi. + +Её ответственность: + +- представить уже разобранные поля ответа Dzengi; +- сохранить биржевую семантику полей; +- не зависеть от legacy-моделей `TickerPrice` и `MarketPriceSnapshot`; +- не выполнять бизнес-интерпретацию; +- не выполнять преобразование во внутреннюю каноническую модель `Quote`. + +Архитектурная граница: + +```text +Dzengi API payload + ↓ +Dzengi REST quote model + ↓ +mapper + ↓ +canonical Quote +``` + +Модель адаптера является специфичной для Dzengi и не должна использоваться напрямую верхними слоями платформы. + +--- + +## 6. REST Quote Parser + +В файле: + +```text +src/market_data/acquisition/adapters/dzengi/parser.py +``` + +реализован специализированный parser REST-котировки. + +Его ответственность: + +1. принять необработанный ответ API; +2. определить фактический quote payload; +3. поддержать прямую структуру ответа; +4. поддержать wrapped payload; +5. проверить структуру через schema validation; +6. извлечь необходимые поля; +7. проверить значения через value validation; +8. вернуть специализированную Dzengi quote model. + +Поддерживаемые формы payload: + +```text +Прямой payload +``` + +```json +{ + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + "bidPrice": "64159.45", + "askPrice": "64159.55", + "closeTime": 1783887270312 +} +``` + +и wrapped payload: + +```json +{ + "payload": { + "symbol": "BTC/USD_LEVERAGE", + "lastPrice": "64159.45", + "bidPrice": "64159.45", + "askPrice": "64159.55", + "closeTime": 1783887270312 + } +} +``` + +Parser не создаёт канонический `Quote`. Это ответственность mapper, реализуемого в Build 029. + +--- + +## 7. Schema Validation + +В файле: + +```text +src/market_data/acquisition/validation/schema.py +``` + +реализована проверка структуры quote payload. + +Проверяются обязательные поля: + +```text +symbol +lastPrice +bidPrice +askPrice +closeTime +``` + +Schema validation отвечает только на вопрос: + +> Имеет ли входящее сообщение необходимую структуру для дальнейшей обработки? + +Она не должна: + +- преобразовывать значения; +- вычислять midpoint; +- определять freshness; +- создавать `Quote`; +- обращаться к сети; +- обращаться к store; +- зависеть от `ExchangeService`. + +--- + +## 8. Value Validation + +В файле: + +```text +src/market_data/acquisition/validation/values.py +``` + +реализована проверка допустимости значений REST-котировки. + +Контролируются следующие инварианты: + +```text +symbol != empty +last_price > 0 +bid_price > 0 +ask_price > 0 +close_time >= 0 +bid_price <= ask_price +``` + +Проверка: + +```text +bid_price <= ask_price +``` + +является важным базовым инвариантом котировки. + +Payload, в котором: + +```text +bid_price > ask_price +``` + +не должен бесконтрольно попадать в канонический слой платформы. + +--- + +## 9. Исключения + +В файле: + +```text +src/market_data/acquisition/exceptions.py +``` + +используются специализированные исключения Acquisition layer для ошибок обработки рыночных данных. + +Ошибки quote parsing и validation не должны зависеть от: + +```text +src/integrations/exchange/exceptions.py +``` + +Это необходимо для соблюдения направления зависимостей: + +```text +market_data/acquisition + X +integrations/exchange legacy layer +``` + +Новая подсистема Acquisition не должна архитектурно зависеть от legacy `ExchangeService`. + +--- + +## 10. Архитектурные решения Build 028 + +### 10.1. Каноническая модель не зависит от формата Dzengi + +Поля API: + +```text +lastPrice +bidPrice +askPrice +closeTime +``` + +существуют только внутри Dzengi adapter layer. + +Во внутренних слоях платформы используются канонические имена: + +```text +last_price +bid_price +ask_price +source_timestamp_ms +``` + +Преобразование между ними является ответственностью mapper. + +--- + +### 10.2. Parser не выполняет mapping + +Разделение сохраняется строго: + +```text +parser + ↓ +разбирает внешний payload + +validation + ↓ +проверяет структуру и значения + +mapper + ↓ +преобразует adapter model в canonical model +``` + +Это предотвращает смешивание: + +- API-specific parsing; +- validation; +- domain mapping. + +--- + +### 10.3. REST quote не зависит от legacy TickerPrice + +Новая цепочка не использует: + +```text +src.integrations.exchange.models.TickerPrice +``` + +`TickerPrice` остаётся временной legacy-моделью и будет удалён только после перевода всех потребителей согласно плану миграции. + +--- + +### 10.4. REST quote не зависит от MarketPriceCache + +Build 028 не изменяет: + +```text +src/integrations/exchange/market_cache.py +``` + +и не записывает данные в: + +```text +MarketPriceCache +``` + +Миграция хранения выполняется отдельно: + +```text +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +``` + +--- + +### 10.5. Runtime-поведение бота не изменено + +На этапе Build 028: + +- новый parser не подключён к production runtime; +- `ExchangeService` продолжает работать по прежнему интерфейсу; +- `MarketPriceCache` не изменён; +- `MarketDataRunner` не изменён; +- execution-потребители не изменены; +- UI-потребители не изменены. + +Таким образом, Build 028 является безопасным additive-этапом миграции. + +--- + +## 11. Что намеренно не реализовано + +В Build 028 не входят: + +```text +Dzengi mapper +Quotes Handler +Quotes Feed +регистрация Quotes Feed +подключение к Acquisition Service +подключение к ExchangeService facade +Quote Store +перенос MarketPriceCache +WebSocket quote parser +перевод MarketDataRunner +перевод UI-потребителей +перевод execution-потребителей +удаление TickerPrice +удаление market snapshot dict layer +удаление MarketPriceCache +``` + +Эти изменения выполняются только в соответствующих последующих Build. + +--- + +## 12. Проверки + +Выполнена синтаксическая проверка: + +```bash +python -m py_compile \ + src/market_data/acquisition/adapters/dzengi/models.py \ + src/market_data/acquisition/adapters/dzengi/parser.py \ + src/market_data/acquisition/validation/schema.py \ + src/market_data/acquisition/validation/values.py \ + src/market_data/acquisition/exceptions.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \ + tests/unit/market_data/acquisition/validation/test_quote_schema.py \ + tests/unit/market_data/acquisition/validation/test_quote_values.py +``` + +Результат: + +```text +Успешно. +``` + +Выполнены специализированные тесты Build 028: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \ + tests/unit/market_data/acquisition/validation/test_quote_schema.py \ + tests/unit/market_data/acquisition/validation/test_quote_values.py \ + -q +``` + +Результат: + +```text +22 passed in 0.02s +``` + +Выполнена полная регрессия проекта: + +```bash +python -m pytest -q +``` + +Результат: + +```text +445 passed in 0.27s +``` + +Регрессий не обнаружено. + +--- + +## 13. Критерии завершения Build 028 + +Build 028 считается завершённым, поскольку выполнены все необходимые условия: + +- [x] получен реальный успешный ответ `/api/v1/ticker/24hr`; +- [x] определён минимальный набор полей текущей котировки; +- [x] реализована специализированная Dzengi REST quote model; +- [x] реализован REST quote parser; +- [x] поддержан прямой payload; +- [x] поддержан wrapped payload; +- [x] реализована schema validation; +- [x] реализована value validation; +- [x] проверяются положительные цены; +- [x] проверяется временная метка; +- [x] проверяется инвариант `bid_price <= ask_price`; +- [x] новая реализация не зависит от legacy `TickerPrice`; +- [x] новая реализация не зависит от `MarketPriceCache`; +- [x] production runtime не изменён; +- [x] специализированные тесты проходят; +- [x] полная регрессия проходит. + +--- + +## 14. Итог + +В результате Build 028 создан специализированный входной контур для REST-котировок Dzengi: + +```text +Dzengi /api/v1/ticker/24hr + ↓ +raw payload + ↓ +schema validation + ↓ +Dzengi quote parser + ↓ +value validation + ↓ +Dzengi REST quote model +``` + +При этом сохранены ключевые архитектурные свойства миграции: + +- новая реализация добавлена параллельно legacy-контуру; +- работающий бот не сломан; +- публичное поведение `ExchangeService` не изменено; +- отсутствует зависимость новой Acquisition subsystem от legacy quote models; +- parsing, validation и будущий mapping разделены по ответственности; +- сохранена возможность безопасного поэтапного переключения потребителей. + +**Build 028 завершён.** + +Следующий этап: + +```text +Build 029 — Dzengi mapper и Quotes Handler +``` \ No newline at end of file diff --git a/docs/migrations/build_029.md b/docs/migrations/build_029.md new file mode 100644 index 0000000..8b90a32 --- /dev/null +++ b/docs/migrations/build_029.md @@ -0,0 +1,717 @@ +# Build 029 — Dzengi Mapper и Quotes Handler + +## Статус + +**Завершён** + +--- + +## 1. Цель Build 029 + +Цель Build 029 — реализовать преобразование специализированной модели REST-котировки Dzengi в каноническую модель `Quote` и создать обработчик полного цикла преобразования сырого REST-документа в проверенную внутреннюю модель котировки. + +Build является частью поэтапной миграции подсистемы: + +**Quotes Feed** + +в новую архитектуру: + +```text +src/market_data/acquisition/ +``` + +На данном этапе реализованы: + +- специализированный Dzengi quote mapper; +- преобразование `DzengiTicker24hrResponse` в канонический `Quote`; +- преобразование цен в `Decimal`; +- преобразование биржевого timestamp в timezone-aware UTC `datetime`; +- фиксация времени получения котировки; +- специализированный `QuotesHandler`; +- полная handler-цепочка обработки сырого REST-документа. + +Подключение `Quotes Feed`, registry, `Acquisition Service`, `Quote Store`, `ExchangeService` facade и runtime-потребителей в данный Build не входит. + +--- + +## 2. Место Build 029 в плане миграции Quotes Feed + +Утверждённая последовательность: + +```text +Build 026 — Аудит текущего контура Quotes Feed +Build 027 — Каноническая модель Quote и специализированные контракты +Build 028 — Dzengi REST quote models, parser и validation +Build 029 — Dzengi mapper и Quotes Handler +Build 030 — Quotes Feed и регистрация в Acquisition Service +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +Build 029 продолжает фундамент, созданный в Builds 027–028. + +После его завершения сформирована цепочка: + +```text +raw Dzengi REST document + ↓ +schema validation + ↓ +Dzengi quote parser + ↓ +value validation + ↓ +DzengiTicker24hrResponse + ↓ +Dzengi quote mapper + ↓ +canonical Quote +``` + +--- + +## 3. Исходное состояние перед Build 029 + +До начала Build 029 уже были реализованы: + +### Build 027 + +```text +src/market_data/acquisition/models/quote.py +src/market_data/acquisition/protocol.py +``` + +Были определены: + +- каноническая модель `Quote`; +- контракт источника сырого quote-документа; +- контракт обработчика quote-документа; +- контракт готового `Quotes Feed`. + +### Build 028 + +```text +src/market_data/acquisition/adapters/dzengi/models.py +src/market_data/acquisition/adapters/dzengi/parser.py +src/market_data/acquisition/validation/schema.py +src/market_data/acquisition/validation/values.py +src/market_data/acquisition/exceptions.py +``` + +Были реализованы: + +- модель REST-ответа `/api/v1/ticker/24hr`; +- parser quote payload; +- schema validation; +- value validation; +- специализированные ошибки Acquisition layer. + +Отсутствовал слой, преобразующий проверенную Dzengi-specific модель в канонический `Quote`, а также единая точка оркестрации всей цепочки обработки сырого документа. + +--- + +## 4. Изменённые и добавленные файлы + +В рамках Build 029 изменены только два исходных файла: + +```text +src/market_data/acquisition/adapters/dzengi/mapper.py +src/market_data/acquisition/handlers/quotes_handler.py +``` + +Добавлены два специализированных файла тестов: + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py +tests/unit/market_data/acquisition/handlers/test_quotes_handler.py +``` + +Другие файлы в рамках фактически применённого Build 029 не изменялись. + +--- + +## 5. Dzengi Quote Mapper + +В файле: + +```text +src/market_data/acquisition/adapters/dzengi/mapper.py +``` + +реализовано преобразование: + +```text +DzengiTicker24hrResponse + ↓ +Quote +``` + +Mapper является архитектурной границей между: + +```text +exchange-specific adapter model +``` + +и: + +```text +canonical Acquisition model +``` + +Его ответственность: + +- принять проверенную модель `DzengiTicker24hrResponse`; +- преобразовать биржевые значения цен в канонический тип; +- преобразовать биржевой timestamp; +- определить источник данных; +- зафиксировать время получения котировки; +- создать канонический `Quote`. + +Mapper не должен: + +- выполнять REST-запрос; +- разбирать сырой JSON payload; +- выполнять schema validation сырого документа; +- управлять store или cache; +- обращаться к `ExchangeService`; +- содержать UI-логику; +- содержать execution-логику. + +--- + +## 6. Преобразование модели Dzengi в канонический Quote + +Исходная модель адаптера содержит данные, соответствующие REST-ответу Dzengi: + +```text +symbol +lastPrice +bidPrice +askPrice +closeTime +``` + +После parsing и validation эти данные представлены специализированной моделью: + +```text +DzengiTicker24hrResponse +``` + +Mapper преобразует её в: + +```text +Quote +``` + +с канонической семантикой: + +```text +symbol +last_price +bid_price +ask_price +source_timestamp +received_at +source +``` + +Таким образом, API-specific имена: + +```text +lastPrice +bidPrice +askPrice +closeTime +``` + +не выходят за пределы Dzengi adapter layer. + +--- + +## 7. Использование Decimal для цен + +Цены преобразуются в `Decimal`. + +Целевая семантика: + +```text +last_price: Decimal +bid_price: Decimal +ask_price: Decimal +``` + +Это решение исключает ненужную потерю точности при преобразовании рыночных цен через бинарный `float`. + +Архитектурная цепочка: + +```text +Dzengi string price + ↓ +Decimal + ↓ +canonical Quote +``` + +Например: + +```text +"64159.45" + ↓ +Decimal("64159.45") +``` + +Mapper не должен сначала преобразовывать строку в `float`, а затем создавать `Decimal`, поскольку такой путь способен внести артефакты двоичного представления числа. + +--- + +## 8. Преобразование биржевого timestamp + +Поле Dzengi: + +```text +closeTime +``` + +содержит Unix timestamp в миллисекундах. + +Mapper преобразует его в timezone-aware UTC `datetime`. + +Семантика преобразования: + +```text +closeTime milliseconds + ↓ +UTC datetime + ↓ +Quote.source_timestamp +``` + +Использование timezone-aware значения необходимо для однозначного представления времени рыночного события и последующих операций: + +- freshness calculation; +- sequence validation; +- event ordering; +- диагностика задержек; +- сопоставление данных из нескольких источников. + +--- + +## 9. Время получения котировки + +Помимо биржевого времени события, канонический `Quote` содержит время фактического получения данных платформой: + +```text +received_at +``` + +Разделение двух временных характеристик принципиально: + +```text +source_timestamp +``` + +означает время, указанное источником данных; + +```text +received_at +``` + +означает время, когда котировка была преобразована во внутреннюю модель платформы. + +Это создаёт фундамент для последующего определения: + +- возраста котировки; +- сетевой задержки; +- freshness; +- stale data; +- задержки между биржей и локальной системой. + +--- + +## 10. Источник котировки + +Канонический `Quote` получает идентификатор источника: + +```text +dzengi +``` + +Это позволяет внутренней модели не зависеть от конкретного adapter-класса, сохраняя при этом происхождение рыночных данных. + +Целевая модель допускает дальнейшую работу с несколькими источниками: + +```text +Dzengi +Binance +Coinbase +другие источники +``` + +При этом приоритетным источником для торговых решений остаётся биржа исполнения. + +--- + +## 11. Quotes Handler + +В файле: + +```text +src/market_data/acquisition/handlers/quotes_handler.py +``` + +реализован специализированный обработчик quote-документа. + +Его ответственность — оркестрировать существующие специализированные стадии обработки: + +```text +raw document + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +Quote +``` + +Handler является единой точкой преобразования: + +```text +object → Quote +``` + +Он не должен самостоятельно дублировать внутреннюю реализацию: + +- schema validation; +- parsing; +- value validation; +- mapping. + +Вместо этого handler координирует специализированные компоненты. + +--- + +## 12. Полная цепочка обработки + +После Build 029 полный путь REST-документа выглядит следующим образом: + +```text +{ + "askPrice": "64159.55", + "bidPrice": "64159.45", + "closeTime": 1783887270312, + "lastPrice": "64159.45", + "symbol": "BTC/USD_LEVERAGE" +} + ↓ +schema validation + ↓ +Dzengi REST quote parser + ↓ +DzengiTicker24hrResponse + ↓ +value validation + ↓ +Dzengi quote mapper + ↓ +Quote( + symbol=..., + last_price=..., + bid_price=..., + ask_price=..., + source_timestamp=..., + received_at=..., + source=... +) +``` + +Таким образом, верхние слои платформы больше не обязаны знать формат ответа Dzengi. + +--- + +## 13. Архитектурные решения Build 029 + +### 13.1. Mapper изолирует специфику Dzengi + +Только adapter layer знает о: + +```text +DzengiTicker24hrResponse +lastPrice +bidPrice +askPrice +closeTime +``` + +После mapping верхние слои работают исключительно с: + +```text +Quote +``` + +--- + +### 13.2. Handler не зависит от ExchangeService + +Новый `QuotesHandler` не использует: + +```text +src.integrations.exchange.service.ExchangeService +``` + +Направление зависимостей остаётся правильным: + +```text +external Dzengi payload + ↓ +Acquisition adapter + ↓ +Acquisition handler + ↓ +canonical Quote +``` + +Обратной зависимости новой подсистемы от legacy integration layer нет. + +--- + +### 13.3. Handler не является Feed + +`QuotesHandler` отвечает только за преобразование документа: + +```text +object → Quote +``` + +Он не отвечает за получение документа от биржи. + +Получение данных будет ответственностью: + +```text +src/market_data/acquisition/feeds/quotes_feed.py +``` + +на следующем этапе миграции. + +--- + +### 13.4. Handler не является Store + +`QuotesHandler` не сохраняет котировки. + +Хранение будет реализовано отдельно: + +```text +Build 032 — Канонический Quote Store +``` + +Такое разделение предотвращает смешивание: + +```text +acquisition +processing +storage +``` + +--- + +### 13.5. Build не изменяет production runtime + +В Build 029 не изменены: + +```text +src/integrations/exchange/service.py +src/integrations/exchange/market_cache.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Не переведены: + +```text +UI consumers +execution consumers +strategy consumers +diagnostics consumers +``` + +Работающий бот продолжает использовать прежний runtime-контур. + +--- + +## 14. Что намеренно не реализовано + +В Build 029 не входят: + +```text +Quotes Feed +регистрация Quotes Feed +подключение к Acquisition Service +подключение нового REST Quotes Feed к ExchangeService facade +Quote Store +перенос MarketPriceCache на Quote Store +WebSocket quote parsing +WebSocket quote adapter +перевод market runtime +перевод read-only потребителей +перевод UI-потребителей +перевод execution-потребителей +удаление TickerPrice +удаление market snapshot dict layer +удаление legacy quote parsing +удаление MarketPriceCache +``` + +Каждая из этих задач выполняется только в соответствующем последующем Build. + +--- + +## 15. Проверки + +Выполнена синтаксическая проверка: + +```bash +python -m py_compile \ + src/market_data/acquisition/adapters/dzengi/mapper.py \ + src/market_data/acquisition/handlers/quotes_handler.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \ + tests/unit/market_data/acquisition/handlers/test_quotes_handler.py +``` + +Результат: + +```text +Успешно. +``` + +Выполнены специализированные тесты Build 029: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_quote_mapper.py \ + tests/unit/market_data/acquisition/handlers/test_quotes_handler.py \ + -q +``` + +Результат: + +```text +12 passed in 0.03s +``` + +Выполнена полная регрессия проекта: + +```bash +python -m pytest -q +``` + +Результат: + +```text +457 passed in 0.26s +``` + +Регрессий не обнаружено. + +Количество тестов увеличилось: + +```text +После Build 028: 445 passed +После Build 029: 457 passed +``` + +Добавлено: + +```text +12 специализированных тестов +``` + +--- + +## 16. Критерии завершения Build 029 + +Build 029 считается завершённым, поскольку выполнены все необходимые условия: + +- [x] реализован специализированный Dzengi quote mapper; +- [x] `DzengiTicker24hrResponse` преобразуется в канонический `Quote`; +- [x] API-specific имена не выходят за пределы adapter layer; +- [x] цены преобразуются в `Decimal`; +- [x] не используется промежуточное преобразование цен через `float`; +- [x] `closeTime` преобразуется в timezone-aware UTC `datetime`; +- [x] фиксируется `received_at`; +- [x] сохраняется источник котировки; +- [x] реализован специализированный `QuotesHandler`; +- [x] handler оркестрирует полный цикл обработки сырого документа; +- [x] handler не дублирует ответственность parser; +- [x] handler не дублирует ответственность validation; +- [x] handler не дублирует ответственность mapper; +- [x] новая реализация не зависит от `ExchangeService`; +- [x] новая реализация не зависит от `MarketPriceCache`; +- [x] production runtime не изменён; +- [x] специализированные тесты проходят; +- [x] полная регрессия проходит. + +--- + +## 17. Итог + +В результате Build 029 завершён слой преобразования REST-котировки Dzengi во внутреннюю каноническую модель платформы: + +```text +Dzengi REST payload + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +DzengiTicker24hrResponse + ↓ +mapper + ↓ +canonical Quote +``` + +Также создан единый специализированный обработчик: + +```text +QuotesHandler +``` + +который предоставляет операцию: + +```text +raw document → canonical Quote +``` + +При этом сохранены ключевые архитектурные свойства миграции: + +- новая реализация развивается параллельно legacy-контуру; +- работающий бот не сломан; +- `ExchangeService` не изменён; +- `MarketPriceCache` не изменён; +- runtime-потребители не изменены; +- Dzengi-specific формат изолирован внутри adapter layer; +- верхние слои получают каноническую модель `Quote`; +- mapping и orchestration разделены по ответственности; +- сохранена возможность безопасного поэтапного переключения системы. + +**Build 029 завершён.** + +Следующий этап: + +```text +Build 030 — Quotes Feed и регистрация в Acquisition Service +``` \ No newline at end of file diff --git a/docs/migrations/build_030.md b/docs/migrations/build_030.md new file mode 100644 index 0000000..b81556c --- /dev/null +++ b/docs/migrations/build_030.md @@ -0,0 +1,630 @@ +# Build 030 — Quotes Feed и регистрация в Acquisition Service + +**Статус:** Завершён +**Подсистема:** Market Data Acquisition +**Вертикаль:** Quotes Feed +**Проект:** Dzentra +**Тип изменения:** Архитектурная миграция без изменения поведения legacy runtime + +--- + +## 1. Цель Build + +Цель Build 030 — собрать ранее реализованные компоненты Quotes Feed в завершённую прикладную цепочку получения канонической котировки и зарегистрировать эту цепочку в слое Acquisition Service. + +Build должен обеспечить следующий поток данных: + +```text +Dzengi REST /api/v1/ticker/24hr + ↓ +DzengiQuoteDocumentSource + ↓ +DzengiQuoteDocumentHandler + ↓ +QuotesFeed + ↓ +QuoteFeedRegistry + ↓ +QuoteAcquisitionService + ↓ +Quote +``` + +На данном этапе новый Quotes Feed существует параллельно с legacy-контуром и ещё не подключается к `ExchangeService`, `MarketPriceCache`, market runtime, UI или Execution. + +--- + +## 2. Предпосылки + +К началу Build 030 были завершены предыдущие этапы: + +```text +Build 027 — Каноническая модель Quote и специализированные контракты +Build 028 — Dzengi REST quote models, parser и validation +Build 029 — Dzengi mapper и Quotes Handler +``` + +В результате уже существовали: + +- каноническая модель `Quote`; +- контракт `QuoteDocumentSource`; +- контракт `QuoteDocumentHandler`; +- контракт `QuoteFeedProtocol`; +- транспортная модель ответа Dzengi; +- schema validation; +- parser; +- value validation; +- mapper; +- `DzengiQuoteDocumentHandler`; +- специализированные исключения Quotes Feed. + +Не хватало orchestration-слоя, связывающего эти компоненты в завершённый pipeline. + +--- + +## 3. Границы Build + +В Build 030 изменены следующие production-файлы: + +```text +src/market_data/acquisition/adapters/dzengi/rest.py +src/market_data/acquisition/feeds/quotes_feed.py +src/market_data/acquisition/registry.py +src/market_data/acquisition/service.py +``` + +Добавлен новый файл тестов: + +```text +tests/unit/market_data/acquisition/feeds/test_quotes_feed.py +``` + +Расширены существующие тесты: + +```text +tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py +tests/unit/market_data/acquisition/test_registry.py +tests/unit/market_data/acquisition/test_service.py +``` + +Следующие компоненты намеренно не изменялись: + +```text +src/integrations/exchange/service.py +src/integrations/exchange/market_cache.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Также не изменялись: + +- UI-потребители; +- Execution-потребители; +- торговые стратегии; +- runtime-контур; +- Quote Store; +- legacy market snapshot dict layer. + +Эти изменения относятся к следующим Build. + +--- + +## 4. Реализованная архитектура + +### 4.1. REST source + +В файле: + +```text +src/market_data/acquisition/adapters/dzengi/rest.py +``` + +реализован специализированный источник: + +```python +DzengiQuoteDocumentSource +``` + +Его ответственность ограничена получением сырого транспортного документа текущей котировки. + +Целевая операция: + +```python +fetch_quote_document(symbol: str) -> object +``` + +Источник выполняет запрос: + +```text +GET /api/v1/ticker/24hr +``` + +с параметрами: + +```python +{ + "symbol": symbol, +} +``` + +REST source: + +- принимает торговый символ; +- передаёт его REST-клиенту без изменения; +- получает декодированный транспортный документ; +- возвращает исходный payload; +- преобразует транспортные ошибки в специализированную ошибку Quotes Feed. + +REST source не выполняет: + +- schema validation; +- parsing; +- value validation; +- mapping; +- кэширование; +- retry; +- нормализацию торгового символа. + +--- + +## 5. Quotes Feed + +В файле: + +```text +src/market_data/acquisition/feeds/quotes_feed.py +``` + +реализован: + +```python +QuotesFeed +``` + +Основная операция: + +```python +load_quote(symbol: str) -> Quote +``` + +Внутренняя последовательность: + +```text +symbol + ↓ +QuoteDocumentSource.fetch_quote_document(symbol) + ↓ +raw document + ↓ +QuoteDocumentHandler.handle_quote_document(document) + ↓ +Quote +``` + +`QuotesFeed` является orchestration-компонентом и не дублирует обязанности других слоёв. + +Он не выполняет: + +- транспортные запросы самостоятельно; +- schema validation; +- parsing; +- value validation; +- mapping; +- нормализацию символа; +- retry; +- кэширование; +- сохранение в Store; +- обращение к `ExchangeService`. + +Ошибки source и handler не переоборачиваются повторно. + +--- + +## 6. Quote Feed Registry + +В файле: + +```text +src/market_data/acquisition/registry.py +``` + +добавлен отдельный реестр: + +```python +QuoteFeedRegistry +``` + +Существующий: + +```python +InstrumentFeedRegistry +``` + +сохранён без архитектурного объединения с Quotes Feed. + +Это позволяет: + +- не изменять стабильный Instrument Reference Data contour; +- сохранить изоляцию вертикалей Acquisition; +- минимизировать область регрессии; +- избежать преждевременной универсализации registry. + +`QuoteFeedRegistry` обеспечивает: + +- регистрацию `QuoteFeedProtocol`; +- получение зарегистрированного Feed по имени источника; +- нормализацию внешних пробелов имени источника; +- запрет пустого имени; +- запрет повторной регистрации; +- runtime-проверку соответствия `QuoteFeedProtocol`; +- сохранение identity зарегистрированного объекта. + +Ошибки registry представлены специализированным типом: + +```python +QuoteFeedRegistryError +``` + +--- + +## 7. Quote Acquisition Service + +В файле: + +```text +src/market_data/acquisition/service.py +``` + +добавлен отдельный прикладной сервис: + +```python +QuoteAcquisitionService +``` + +Основная операция: + +```python +load_quote( + source_name: str, + symbol: str, +) -> Quote +``` + +Внутренняя последовательность: + +```text +source_name + ↓ +QuoteFeedRegistry.get(source_name) + ↓ +QuoteFeedProtocol + ↓ +load_quote(symbol) + ↓ +Quote +``` + +Сервис: + +- выбирает Feed через registry; +- передаёт `symbol` выбранному Feed без изменения; +- возвращает канонический `Quote`; +- не копирует полученную модель; +- не выполняет retry; +- не перехватывает и не переоборачивает ошибки registry или Feed. + +Существующий: + +```python +InstrumentAcquisitionService +``` + +не изменяет свою ответственность и продолжает обслуживать Instrument Reference Data. + +--- + +## 8. Dependency Injection + +В Build 030 сохранён уже применяемый в Instrument Reference Data подход явной сборки зависимостей. + +Пример архитектурной сборки: + +```python +source = DzengiQuoteDocumentSource(...) +handler = DzengiQuoteDocumentHandler(...) +feed = QuotesFeed( + source=source, + handler=handler, +) + +registry = QuoteFeedRegistry() +registry.register("dzengi", feed) + +service = QuoteAcquisitionService( + registry=registry, +) +``` + +В Build намеренно не добавлены: + +- глобальный singleton registry; +- автоматическая регистрация при импорте; +- скрытая сборка production pipeline внутри `QuoteAcquisitionService`; +- глобальное mutable-состояние для Feed. + +Такое решение сохраняет: + +- dependency injection; +- тестируемость; +- явные зависимости; +- изоляцию composition root от application service. + +Фактическое подключение production pipeline к legacy facade отложено до Build 031. + +--- + +## 9. Ответственности компонентов + +| Компонент | Ответственность | +|---|---| +| `DzengiQuoteDocumentSource` | Получение сырого REST-документа котировки | +| `DzengiQuoteDocumentHandler` | Полная обработка документа до канонической модели | +| `QuotesFeed` | Оркестрация source → handler | +| `QuoteFeedRegistry` | Регистрация и выбор Quotes Feed | +| `QuoteAcquisitionService` | Прикладная точка получения `Quote` через выбранный Feed | +| `Quote` | Каноническое внутреннее представление текущей котировки | + +--- + +## 10. Полная цепочка обработки + +После завершения Build 030 REST Quotes Feed имеет следующую структуру: + +```text +GET /api/v1/ticker/24hr + ↓ +DzengiQuoteDocumentSource + ↓ +raw object + ↓ +DzengiQuoteDocumentHandler + ↓ +validate_dzengi_quote_schema() + ↓ +parse_dzengi_quote_document() + ↓ +DzengiQuotePayload + ↓ +validate_dzengi_quote_values() + ↓ +map_dzengi_quote() + ↓ +Quote + ↓ +QuotesFeed + ↓ +QuoteFeedRegistry + ↓ +QuoteAcquisitionService +``` + +Таким образом, транспортный формат Dzengi полностью изолирован от внешних потребителей Acquisition. + +--- + +## 11. Архитектурные ограничения + +Build 030 намеренно не реализует следующие функции: + +```text +ExchangeService facade integration +Quote Store +MarketPriceCache migration +WebSocket quote parsing +market runtime migration +read-only consumer migration +UI consumer migration +Execution consumer migration +legacy TickerPrice removal +legacy market snapshot dict removal +MarketPriceCache removal +``` + +Они относятся к следующим этапам: + +```text +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +--- + +## 12. Тестовое покрытие + +Build 030 покрывает следующие сценарии. + +### 12.1. REST source + +Проверяется: + +- использование endpoint `/api/v1/ticker/24hr`; +- передача `symbol` в query parameters; +- возврат исходного payload; +- однократный вызов REST-клиента; +- поддержка dependency injection REST-клиента; +- создание стандартного REST-клиента при отсутствии injected client; +- преобразование транспортной ошибки в `QuoteTransportError`; +- сохранение исходной ошибки через `__cause__`. + +### 12.2. Quotes Feed + +Проверяется: + +- соответствие `QuoteFeedProtocol`; +- однократный вызов source; +- передача `symbol` без изменения; +- однократный вызов handler; +- передача исходного документа handler без изменения; +- возврат `Quote` без копирования; +- отсутствие retry; +- отсутствие повторного переоборачивания ошибок. + +### 12.3. Quote Feed Registry + +Проверяется: + +- регистрация корректного Feed; +- получение Feed по имени; +- нормализация внешних пробелов имени; +- запрет пустого имени; +- запрет повторной регистрации; +- проверка соответствия `QuoteFeedProtocol`; +- сохранение identity объекта; +- специализированные ошибки registry. + +### 12.4. Quote Acquisition Service + +Проверяется: + +- передача `source_name` registry; +- передача `symbol` Feed без изменения; +- однократное обращение к registry; +- однократный вызов Feed; +- возврат `Quote` без копирования; +- сохранение ошибок registry; +- сохранение ошибок Feed; +- отсутствие retry. + +--- + +## 13. Проверка компиляции + +Выполнена команда: + +```bash +python -m py_compile \ + src/market_data/acquisition/adapters/dzengi/rest.py \ + src/market_data/acquisition/feeds/quotes_feed.py \ + src/market_data/acquisition/registry.py \ + src/market_data/acquisition/service.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \ + tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \ + tests/unit/market_data/acquisition/test_registry.py \ + tests/unit/market_data/acquisition/test_service.py +``` + +Результат: + +```text +Успешно. +Ошибок компиляции нет. +``` + +--- + +## 14. Специализированные тесты + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py \ + tests/unit/market_data/acquisition/feeds/test_quotes_feed.py \ + tests/unit/market_data/acquisition/test_registry.py \ + tests/unit/market_data/acquisition/test_service.py \ + -q +``` + +Результат: + +```text +89 passed in 0.06s +``` + +--- + +## 15. Полная регрессия + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +498 passed in 0.25s +``` + +Регрессий не обнаружено. + +--- + +## 16. Результат Build + +Build 030 завершён полностью. + +Создана завершённая и протестированная вертикаль REST Quotes Feed: + +```text +Dzengi REST API + ↓ +DzengiQuoteDocumentSource + ↓ +DzengiQuoteDocumentHandler + ↓ +QuotesFeed + ↓ +QuoteFeedRegistry + ↓ +QuoteAcquisitionService + ↓ +Quote +``` + +Новая вертикаль пока работает независимо от legacy runtime, что обеспечивает безопасную поэтапную миграцию без изменения поведения работающего торгового бота. + +--- + +## 17. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +``` + +Его цель — переключить REST-получение текущей котировки внутри существующего `ExchangeService` на новый канонический Quotes Feed, сохранив текущие публичные интерфейсы и поведение legacy-потребителей. + +Целевая переходная схема: + +```text +Legacy consumer + ↓ +ExchangeService facade + ↓ +QuoteAcquisitionService + ↓ +QuotesFeed + ↓ +DzengiQuoteDocumentSource + ↓ +Dzengi /api/v1/ticker/24hr + ↓ +Quote + ↓ +legacy-compatible projection + ↓ +Legacy consumer +``` + +До завершения последующих этапов `ExchangeService` остаётся совместимым фасадом между новой архитектурой Market Data Acquisition и существующими потребителями работающего бота. \ No newline at end of file diff --git a/docs/migrations/build_031.md b/docs/migrations/build_031.md new file mode 100644 index 0000000..f8a7f89 --- /dev/null +++ b/docs/migrations/build_031.md @@ -0,0 +1,894 @@ +# Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade + +## Статус + +**Завершён.** + +--- + +## Цель + +Подключить новый канонический контур **Quotes Feed** как внутренний источник свежих REST-котировок для существующего `ExchangeService`, сохранив без изменений его публичные legacy-контракты и поведение существующих потребителей. + +Основная архитектурная цель Build 031: + +```text +Dzengi GET /api/v1/ticker/24hr + ↓ +DzengiQuoteDocumentSource + ↓ +DzengiQuoteDocumentHandler + ↓ +QuotesFeed + ↓ +QuoteAcquisitionService + ↓ +Quote + ↓ +ExchangeService legacy facade + ↓ +существующие потребители +``` + +После Build 031 `ExchangeService` больше не должен самостоятельно: + +- выполнять прямой REST-запрос к `/api/v1/ticker/24hr`; +- знать транспортные поля `lastPrice`, `bidPrice`, `askPrice`, `closeTime`; +- разбирать сырой ответ ticker endpoint; +- выполнять собственный parsing котировки Dzengi. + +Эти обязанности переданы специализированной подсистеме `market_data/acquisition`. + +--- + +## Исходное состояние + +До Build 031 метод: + +```python +ExchangeService.get_fresh_market_snapshot() +``` + +самостоятельно выполнял полный legacy-процесс: + +```text +ExchangeService + ↓ +ExchangeRestClient + ↓ +GET /api/v1/ticker/24hr + ↓ +ручное чтение lastPrice / bidPrice / askPrice / closeTime + ↓ +legacy dict snapshot +``` + +В результате `ExchangeService` одновременно отвечал за: + +- транспорт; +- знание конкретного endpoint Dzengi; +- знание транспортной схемы Dzengi; +- parsing значений; +- формирование внутреннего представления котировки; +- формирование legacy snapshot; +- обработку freshness. + +Это нарушало архитектурное разделение ответственности. + +К моменту начала Build 031 новый канонический Quotes Feed уже был реализован: + +```text +DzengiQuoteDocumentSource + ↓ +DzengiQuoteDocumentHandler + ↓ +QuotesFeed + ↓ +QuoteFeedRegistry + ↓ +QuoteAcquisitionService + ↓ +Quote +``` + +Задачей Build 031 стало подключение этого контура под существующий `ExchangeService` facade. + +--- + +## Объём изменений + +### Изменён production-файл + +```text +src/integrations/exchange/service.py +``` + +### Добавлен тестовый файл + +```text +tests/unit/integrations/exchange/test_service_quotes_facade.py +``` + +### Не изменялись + +```text +src/integrations/exchange/models.py +src/integrations/exchange/market_cache.py +src/integrations/exchange/mock_data.py + +src/market_data/acquisition/adapters/dzengi/rest.py +src/market_data/acquisition/feeds/quotes_feed.py +src/market_data/acquisition/handlers/quotes_handler.py +src/market_data/acquisition/models/quote.py +src/market_data/acquisition/registry.py +src/market_data/acquisition/service.py +src/market_data/acquisition/exceptions.py +``` + +Новый Acquisition-контур уже содержал всю необходимую функциональность и не потребовал дополнительных изменений. + +--- + +## Реализованная архитектура + +После Build 031 получение свежей REST-котировки выполняется по следующей цепочке: + +```text +ExchangeService.get_fresh_market_snapshot() + ↓ +QuoteAcquisitionService + ↓ +QuoteFeedRegistry + ↓ +QuotesFeed + ↓ +DzengiQuoteDocumentSource + ↓ +GET /api/v1/ticker/24hr + ↓ +DzengiQuoteDocumentHandler + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +canonical Quote + ↓ +legacy-compatible snapshot dict +``` + +Таким образом, граница ответственности теперь выглядит следующим образом: + +```text +market_data/acquisition + │ + │ отвечает за получение, проверку, + │ parsing и mapping котировки + ↓ +canonical Quote + │ + │ временная compatibility boundary + ↓ +ExchangeService facade + │ + │ сохраняет старые публичные контракты + ↓ +legacy consumers +``` + +--- + +## Подключение Quote Acquisition pipeline + +В `ExchangeService` добавлен внутренний путь получения канонической котировки через уже реализованные компоненты Quotes Feed. + +Используемая цепочка: + +```text +DzengiQuoteDocumentSource + ↓ +DzengiQuoteDocumentHandler + ↓ +QuotesFeed + ↓ +QuoteFeedRegistry + ↓ +QuoteAcquisitionService + ↓ +Quote +``` + +`ExchangeService` теперь получает готовую каноническую модель: + +```python +Quote +``` + +вместо сырого ответа Dzengi: + +```python +dict[str, object] +``` + +Это устраняет зависимость facade от транспортной схемы `ticker/24hr`. + +--- + +## Изменение get_fresh_market_snapshot() + +До Build 031 метод самостоятельно выполнял: + +```text +создание ExchangeRestClient + ↓ +вызов /api/v1/ticker/24hr + ↓ +чтение lastPrice + ↓ +чтение bidPrice + ↓ +чтение askPrice + ↓ +чтение closeTime / eventTime + ↓ +преобразование значений + ↓ +формирование snapshot +``` + +После Build 031 метод получает: + +```python +quote = self._load_quote_via_acquisition( + validation.normalized_symbol, +) +``` + +После чего выполняет только временную legacy-проекцию: + +```text +Quote + ↓ +legacy-compatible dict snapshot +``` + +Таким образом, `get_fresh_market_snapshot()` больше не является parser транспортного ответа Dzengi. + +--- + +## Удалённый legacy parsing + +Из REST quote-пути `ExchangeService` удалено прямое знание следующих транспортных полей: + +```text +lastPrice +bidPrice +askPrice +closeTime +eventTime +``` + +Также удалён прямой вызов: + +```text +GET /api/v1/ticker/24hr +``` + +из `ExchangeService`. + +Теперь endpoint и его транспортная схема принадлежат исключительно адаптеру: + +```text +src/market_data/acquisition/adapters/dzengi/ +``` + +Это соответствует утверждённой архитектуре Acquisition. + +--- + +## Legacy-compatible projection + +Build 031 намеренно не удаляет legacy snapshot layer. + +Каноническая модель: + +```python +Quote +``` + +временно преобразуется обратно в: + +```python +dict[str, object] +``` + +с сохранением прежней структуры: + +```python +{ + "symbol": ..., + "last_price": ..., + "bid_price": ..., + "ask_price": ..., + "updated_at": ..., + "source": "fresh_rest", + "age_seconds": ..., + "is_fresh": ..., +} +``` + +Это необходимо для безопасной поэтапной миграции существующего работающего бота. + +Удаление этого compatibility layer запланировано на: + +```text +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +``` + +--- + +## Сохранение числового контракта + +Каноническая модель `Quote` использует точные числовые значения, представленные через `Decimal`. + +Legacy-потребители ожидают `float`. + +Поэтому на временной границе совместимости выполняется преобразование: + +```text +Quote Decimal + ↓ +ExchangeService compatibility boundary + ↓ +legacy float +``` + +То есть точность сохраняется внутри новой канонической подсистемы, а преобразование выполняется только при передаче данных старым потребителям. + +Это временное решение до полного перевода потребителей на канонический `Quote`. + +--- + +## Сохранение symbol contract + +Перед получением котировки сохраняется существующая проверка символа: + +```python +validation = self.validate_symbol(symbol_to_use) +``` + +В новый Acquisition pipeline передаётся: + +```python +validation.normalized_symbol +``` + +Таким образом: + +- невалидный символ не передаётся в Quotes Feed; +- используется канонически нормализованный символ; +- существующее поведение `ExchangeService` сохраняется. + +--- + +## Сохранение source contract + +Канонический `Quote` содержит источник Acquisition: + +```text +dzengi +``` + +Однако существующий legacy snapshot использует: + +```text +fresh_rest +``` + +В Build 031 сохранено прежнее значение: + +```python +"source": "fresh_rest" +``` + +Это исключает непреднамеренное изменение поведения: + +- UI; +- журналирования; +- диагностики; +- runtime; +- существующих потребителей, потенциально зависящих от значения `source`. + +Переход на каноническую семантику источника должен выполняться отдельно при удалении legacy snapshot layer. + +--- + +## Сохранение timestamp contract + +Канонический `Quote` содержит timezone-aware timestamp. + +На legacy-границе сохраняется прежнее представление: + +```text +Quote.exchange_timestamp + ↓ +timestamp в миллисекундах + ↓ +существующие ExchangeService helpers + ↓ +updated_at +age_seconds +is_fresh +``` + +Благодаря этому существующие потребители не получают изменения временной семантики. + +--- + +## Сохранение freshness contract + +Сохранена существующая логика определения свежести REST-котировки. + +Порог: + +```text +60 секунд +``` + +Результат продолжает содержать: + +```python +"age_seconds": ... +"is_fresh": ... +``` + +Условие остаётся эквивалентным прежнему: + +```python +is_fresh = ( + age_seconds is not None + and age_seconds <= 60 +) +``` + +Build 031 не меняет политику freshness. + +--- + +## Сохранение публичных контрактов ExchangeService + +После Build 031 сохранены без изменения следующие публичные методы: + +```text +get_price() -> TickerPrice + +get_fresh_market_snapshot() -> dict[str, object] + +refresh_price_cache() -> TickerPrice + +refresh_market_snapshot_cache() -> dict[str, object] + +get_market_snapshot() -> dict[str, object] + +get_execution_snapshot() -> ExecutionPriceSnapshot +``` + +Это позволяет существующим потребителям продолжать работу без изменений. + +В частности, не потребовалось изменять: + +```text +src/telegram/ui/currency_ui.py +src/telegram/handlers/auto/ui.py +src/telegram/handlers/debug_auto/ui.py + +src/trading/auto/signal_runtime.py +src/trading/auto/execution_quality.py + +src/trading/strategies/trend.py +src/trading/strategies/scalp.py + +src/trading/execution/pricing.py +src/trading/diagnostics/snapshot.py +src/trading/debug/execution.py +``` + +--- + +## Влияние на существующие методы ExchangeService + +Методы: + +```text +get_price() +get_market_snapshot() +get_execution_snapshot() +refresh_market_snapshot_cache() +refresh_price_cache() +_get_real_price() +get_symbol_runtime_status() +``` + +продолжают работать через существующие публичные и внутренние контракты. + +Поскольку свежая REST-котировка теперь поступает через: + +```text +get_fresh_market_snapshot() + ↓ +Quote Acquisition pipeline +``` + +существующие методы автоматически используют новый канонический REST Quotes Feed без прямого перевода каждого потребителя. + +--- + +## Обработка ошибок + +Новый Acquisition-контур использует специализированные ошибки quote-подсистемы. + +Внешний контракт `ExchangeService` продолжает использовать: + +```python +ExchangeError +``` + +Поэтому на границе facade сохраняется адаптация: + +```text +Acquisition error + ↓ +ExchangeService compatibility boundary + ↓ +ExchangeError +``` + +При этом исходная ошибка сохраняется как: + +```python +__cause__ +``` + +Это обеспечивает одновременно: + +- совместимость существующих потребителей; +- сохранение исходного контекста ошибки; +- возможность диагностики первопричины; +- отсутствие утечки новой модели исключений в legacy-код раньше запланированного этапа миграции. + +--- + +## Сохранение mock-режима + +При отключённой реальной биржевой интеграции: + +```python +exchange_enabled = False +``` + +новый Acquisition pipeline не вызывается. + +Сохраняется прежний mock-контур: + +```text +ExchangeService + ↓ +mock_ticker_price() + ↓ +legacy snapshot +``` + +Build 031 не изменяет поведение mock-режима. + +--- + +## Тестовое покрытие + +Добавлен специализированный тестовый файл: + +```text +tests/unit/integrations/exchange/test_service_quotes_facade.py +``` + +Тесты проверяют границу между: + +```text +canonical Quote Acquisition +``` + +и: + +```text +legacy ExchangeService facade +``` + +Проверяемые свойства включают: + +- использование нового Acquisition pipeline; +- передачу нормализованного символа; +- сохранение legacy snapshot contract; +- преобразование канонических числовых значений в legacy-compatible значения; +- сохранение `source="fresh_rest"`; +- сохранение timestamp contract; +- сохранение freshness contract; +- адаптацию ошибок в `ExchangeError`; +- сохранение исходной ошибки в `__cause__`; +- отсутствие вызова нового Acquisition pipeline в mock-режиме; +- отсутствие прямого ticker REST parsing в новом facade-пути. + +--- + +## Регрессионная проверка runtime status + +Дополнительно выполнен существующий набор тестов: + +```text +tests/unit/integrations/exchange/test_service_symbol_runtime_status.py +``` + +Он подтверждает сохранение внешнего runtime-контракта после переключения внутреннего источника REST-котировки. + +--- + +## Выполненные проверки + +### Проверка синтаксиса + +```bash +python -m py_compile \ + src/integrations/exchange/service.py \ + tests/unit/integrations/exchange/test_service_quotes_facade.py +``` + +Результат: + +```text +успешно +``` + +### Специализированные и регрессионные тесты + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_quotes_facade.py \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ + -q +``` + +Результат: + +```text +38 passed in 0.08s +``` + +### Полная регрессия проекта + +```bash +python -m pytest -q +``` + +Результат: + +```text +502 passed in 0.25s +``` + +--- + +## Рост тестового покрытия + +После предыдущего этапа: + +```text +Build 030 +498 passed +``` + +После завершения Build 031: + +```text +Build 031 +502 passed +``` + +Добавлено: + +```text +4 новых теста +``` + +Полная регрессия остаётся зелёной. + +--- + +## Архитектурный результат + +### До Build 031 + +```text +ExchangeService + ↓ +ExchangeRestClient + ↓ +GET /api/v1/ticker/24hr + ↓ +ручной parsing полей Dzengi + ↓ +legacy dict snapshot + ↓ +потребители +``` + +### После Build 031 + +```text +ExchangeService legacy facade + ↓ +QuoteAcquisitionService + ↓ +QuoteFeedRegistry + ↓ +QuotesFeed + ↓ +DzengiQuoteDocumentSource + ↓ +DzengiQuoteDocumentHandler + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +canonical Quote + ↓ +legacy-compatible dict projection + ↓ +существующие потребители +``` + +--- + +## Архитектурные гарантии после Build 031 + +После завершения этапа выполняются следующие гарантии: + +1. `ExchangeService` больше не выполняет прямой REST-запрос к `/api/v1/ticker/24hr`. + +2. `ExchangeService` больше не знает транспортные поля: + + ```text + lastPrice + bidPrice + askPrice + closeTime + eventTime + ``` + +3. REST-котировка проходит через канонический Quotes Feed. + +4. Внутренним результатом Acquisition является: + + ```python + Quote + ``` + +5. Legacy snapshot создаётся только как временная compatibility projection. + +6. Существующие публичные контракты `ExchangeService` сохранены. + +7. `MarketPriceCache` пока не изменён. + +8. WebSocket-контур пока не изменён. + +9. UI-потребители пока не переведены напрямую на `Quote`. + +10. Execution-потребители пока не переведены напрямую на `Quote`. + +11. Полная регрессия проекта проходит успешно: + + ```text + 502 passed + ``` + +--- + +## Что намеренно не входит в Build 031 + +Build 031 не реализует: + +```text +Quote Store +перенос MarketPriceCache +удаление MarketPriceCache +изменение MarketPriceSnapshot +WebSocket quote parsing +WebSocket quote adapter +перевод market runtime на Quotes Feed +прямой перевод UI на Quote +прямой перевод execution на Quote +удаление TickerPrice +удаление ExecutionPriceSnapshot +удаление legacy market snapshot dict layer +удаление legacy quote compatibility layer +``` + +Эти изменения выполняются последующими Build по утверждённому плану. + +--- + +## Следующий этап + +```text +Build 032 — Канонический Quote Store +``` + +Его задача — создать канонический слой хранения текущих котировок, который станет основой для последующего переноса существующего: + +```text +MarketPriceCache +``` + +на новую архитектуру. + +Последовательность дальнейшей миграции: + +```text +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +--- + +## Итог + +**Build 031 завершён полностью.** + +Новый канонический REST Quotes Feed стал внутренним источником свежих котировок для `ExchangeService`, при этом существующий работающий бот сохранил прежние публичные контракты. + +Ключевой результат: + +```text +Было: + +ExchangeService + ↓ +прямой REST ticker/24hr + ↓ +ручной parsing + ↓ +legacy snapshot +``` + +```text +Стало: + +ExchangeService facade + ↓ +canonical Quotes Feed + ↓ +Quote + ↓ +временная legacy projection + ↓ +существующие потребители +``` + +Это создаёт безопасную архитектурную основу для следующего этапа: + +```text +Build 032 — Канонический Quote Store +``` \ No newline at end of file diff --git a/docs/migrations/build_032.md b/docs/migrations/build_032.md new file mode 100644 index 0000000..dda7eda --- /dev/null +++ b/docs/migrations/build_032.md @@ -0,0 +1,1033 @@ +# Build 032 — Канонический Quote Store + +**Статус:** Завершён +**Результат:** Успешно +**Полная регрессия:** `562 passed` + +--- + +## 1. Назначение Build + +Цель Build 032 — создать каноническое оперативное хранилище текущих котировок `Quote` в Storage layer и подготовить архитектурную основу для последующего переноса legacy-механизма `MarketPriceCache`. + +До выполнения Build 032 в проекте уже существовал канонический контур получения текущей котировки: + +```text +Dzengi REST API + ↓ +DzengiQuoteDocumentSource + ↓ +Dzengi quote parser + ↓ +Dzengi quote value validation + ↓ +Dzengi quote mapper + ↓ +QuotesHandler + ↓ +QuotesFeed + ↓ +QuoteAcquisitionService + ↓ +Quote +``` + +Однако канонического хранилища объектов `Quote` ещё не существовало. + +Текущие runtime-котировки продолжал хранить legacy-компонент: + +```text +MarketPriceCache +``` + +Build 032 вводит новый независимый Storage-компонент: + +```text +canonical Quote + ↓ +InMemoryQuoteStore +``` + +При этом существующий production pipeline не переключается на новое хранилище в рамках данного Build. + +--- + +## 2. Архитектурная граница Build + +Build 032 ограничен созданием канонического Quote Store. + +В рамках Build: + +- добавлен контракт `QuoteStoreProtocol`; +- добавлена in-memory реализация `InMemoryQuoteStore`; +- добавлена специализированная ошибка `QuoteStoreError`; +- реализовано хранение канонических объектов `Quote`; +- реализована изоляция по источнику данных; +- реализована изоляция по runtime-контексту; +- реализована изоляция по торговому инструменту; +- реализована полная, выборочная и комбинированная очистка; +- добавлен полный набор unit-тестов. + +В рамках Build не выполнялись: + +- изменение `ExchangeService`; +- изменение `MarketPriceCache`; +- подключение `QuoteStore` к `ExchangeService`; +- перенос данных из `MarketPriceCache`; +- изменение `QuotesFeed`; +- изменение `QuoteAcquisitionService`; +- изменение WebSocket-контура; +- изменение market runtime; +- изменение UI-потребителей; +- изменение execution-потребителей; +- удаление legacy-компонентов. + +Эти изменения относятся к последующим Build утверждённого плана миграции Quotes Feed. + +--- + +## 3. Изменённые файлы + +### Изменён production-файл + +```text +src/storage/exceptions.py +``` + +Добавлена специализированная ошибка: + +```python +QuoteStoreError +``` + +### Добавлен production-файл + +```text +src/storage/quote_store.py +``` + +Содержит: + +```python +QuoteStoreProtocol +InMemoryQuoteStore +``` + +### Добавлен тестовый файл + +```text +tests/unit/storage/test_quote_store.py +``` + +--- + +## 4. Целевая архитектура + +После Build 032 Storage layer содержит два специализированных канонических хранилища: + +```text +src/storage/ +├── exceptions.py +├── instrument_store.py +└── quote_store.py +``` + +Архитектурно: + +```text +Instrument + ↓ +InMemoryInstrumentStore +``` + +и: + +```text +Quote + ↓ +InMemoryQuoteStore +``` + +`InstrumentStore` хранит канонический справочник инструментов. + +`QuoteStore` хранит канонические текущие котировки. + +--- + +## 5. Контракт Quote Store + +Канонический контракт представлен протоколом: + +```python +QuoteStoreProtocol +``` + +Он определяет три основные операции: + +```text +get() +set() +clear() +``` + +Концептуальный контракт: + +```python +@runtime_checkable +class QuoteStoreProtocol(Protocol): + def get( + self, + source_name: str, + symbol: str, + *, + runtime_key: str = "default", + ) -> Quote | None: + ... + + def set( + self, + source_name: str, + quote: Quote, + *, + runtime_key: str = "default", + ) -> None: + ... + + def clear( + self, + source_name: str | None = None, + symbol: str | None = None, + *, + runtime_key: str | None = None, + ) -> None: + ... +``` + +Контракт не зависит от: + +- Dzengi; +- REST; +- WebSocket; +- `ExchangeService`; +- `MarketPriceCache`; +- UI; +- Execution layer. + +--- + +## 6. Модель хранения + +Quote Store использует составной ключ: + +```text +source_name + runtime_key + symbol +``` + +Внутреннее представление: + +```python +tuple[str, str, str] +``` + +Примеры независимых записей: + +```text +("dzengi", "auto", "BTC/USD_LEVERAGE") + +("dzengi", "debug_auto", "BTC/USD_LEVERAGE") + +("secondary", "auto", "BTC/USD_LEVERAGE") +``` + +Все эти записи независимы друг от друга. + +Такая модель позволяет одновременно хранить: + +- котировки от разных поставщиков; +- котировки для разных runtime-контекстов; +- котировки разных инструментов. + +--- + +## 7. Семантика `source_name` + +`source_name` представляет namespace источника данных в Storage layer. + +Применяются следующие правила: + +```text +внешние пробелы удаляются; +регистр сохраняется; +пустое значение запрещено. +``` + +Пример: + +```text +" dzengi " → "dzengi" +``` + +При этом: + +```text +"DZENGI" != "dzengi" +``` + +Такое поведение соответствует существующей семантике: + +```text +InstrumentRegistry +QuoteRegistry +InstrumentStore +``` + +Quote Store не требует равенства: + +```text +source_name == quote.source +``` + +Это принципиально позволяет использовать алиасы источников: + +```text +dzengi +dzengi-demo +dzengi-prod +dzengi-primary +``` + +при сохранении канонического происхождения самой модели в: + +```python +quote.source +``` + +--- + +## 8. Семантика `runtime_key` + +`runtime_key` разделяет независимые runtime-контексты. + +Примеры: + +```text +default +auto +debug_auto +``` + +Правила нормализации: + +```text +внешние пробелы удаляются; +значение приводится к lowercase; +пустое значение запрещено. +``` + +Пример: + +```text +" AUTO " → "auto" +``` + +Таким образом: + +```text +AUTO +Auto +auto +``` + +адресуют один runtime namespace: + +```text +auto +``` + +Эта семантика соответствует существующему поведению legacy `MarketPriceCache`. + +--- + +## 9. Семантика `symbol` + +Символ используется как третья часть ключа Quote Store. + +Правила нормализации: + +```text +внешние пробелы удаляются; +значение приводится к uppercase; +пустое значение запрещено. +``` + +Пример: + +```text +" btc/usd_leverage " +``` + +преобразуется в ключ: + +```text +BTC/USD_LEVERAGE +``` + +Нормализация применяется только к ключу хранения. + +Сам объект `Quote` не изменяется. + +--- + +## 10. Семантика `set()` + +Метод: + +```python +set() +``` + +сохраняет канонический объект `Quote`. + +Основные гарантии: + +- принимается объект `Quote`; +- используется `quote.symbol`; +- объект сохраняется без копирования; +- `Decimal` не преобразуется в `float`; +- `datetime` не преобразуется в строку; +- timestamps не изменяются; +- существующая запись с тем же ключом заменяется; +- содержимое `Quote` не нормализуется повторно. + +Пример: + +```python +store.set( + "dzengi", + quote, + runtime_key="auto", +) +``` + +Если для ключа: + +```text +("dzengi", "auto", "BTC/USD_LEVERAGE") +``` + +уже существует запись, она заменяется новой. + +Quote Store сохраняет identity объекта: + +```python +store.get( + "dzengi", + "BTC/USD_LEVERAGE", + runtime_key="auto", +) is quote +``` + +--- + +## 11. Семантика `get()` + +Метод: + +```python +get() +``` + +возвращает: + +```python +Quote | None +``` + +Если запись существует: + +```text +возвращается исходный сохранённый объект Quote +``` + +Если запись отсутствует: + +```python +None +``` + +Store не: + +- создаёт копию; +- выполняет сетевой запрос; +- обращается к Acquisition layer; +- вычисляет freshness; +- выполняет fallback. + +--- + +## 12. Семантика `clear()` + +Метод: + +```python +clear() +``` + +поддерживает полную, выборочную и комбинированную очистку. + +### 12.1. Полная очистка + +```python +store.clear() +``` + +Удаляет все сохранённые котировки. + +--- + +### 12.2. Очистка по источнику + +```python +store.clear( + source_name="dzengi", +) +``` + +Удаляет все котировки указанного источника независимо от: + +- символа; +- runtime-контекста. + +--- + +### 12.3. Очистка по символу + +```python +store.clear( + symbol="BTC/USD_LEVERAGE", +) +``` + +Удаляет указанный символ у всех: + +- источников; +- runtime-контекстов. + +--- + +### 12.4. Очистка по runtime + +```python +store.clear( + runtime_key="auto", +) +``` + +Удаляет все котировки указанного runtime-контекста. + +--- + +### 12.5. Точная очистка + +```python +store.clear( + source_name="dzengi", + symbol="BTC/USD_LEVERAGE", + runtime_key="auto", +) +``` + +Удаляет только одну конкретную запись. + +--- + +### 12.6. Комбинированная очистка + +Поддерживаются комбинации фильтров. + +Например: + +```python +store.clear( + source_name="dzengi", + runtime_key="auto", +) +``` + +Удаляет все котировки источника `dzengi` только из runtime: + +```text +auto +``` + +Остальные записи сохраняются. + +--- + +### 12.7. Идемпотентность + +Очистка отсутствующей записи не является ошибкой. + +Например: + +```python +store.clear( + source_name="unknown", + symbol="UNKNOWN", + runtime_key="unknown", +) +``` + +завершается без исключения. + +--- + +## 13. Специализированная ошибка + +В Storage layer добавлена ошибка: + +```python +QuoteStoreError +``` + +Иерархия: + +```text +Exception + ↓ +StorageError + ↓ +QuoteStoreError +``` + +Она используется для нарушений контракта Quote Store. + +Примеры: + +- пустой `source_name`; +- пустой `runtime_key`; +- пустой `symbol`; +- передача объекта неправильного типа. + +Это позволяет отличать ошибки хранения котировок от: + +- ошибок Acquisition; +- ошибок адаптера биржи; +- транспортных ошибок; +- ошибок Exchange facade; +- ошибок Execution layer. + +--- + +## 14. Сохранение канонической модели + +Quote Store хранит непосредственно: + +```python +Quote +``` + +Хранилище не создаёт промежуточные представления типа: + +```text +MarketPriceSnapshot +dict[str, object] +TickerPrice +``` + +Архитектурно: + +```text +Quote + ↓ +Quote Store +``` + +а не: + +```text +Quote + ↓ +legacy dict + ↓ +MarketPriceSnapshot + ↓ +Store +``` + +Это принципиально для дальнейшего устранения legacy quote representations. + +--- + +## 15. Сохранение точности чисел + +Числовые поля канонического `Quote` используют: + +```python +Decimal +``` + +Quote Store сохраняет их без преобразования. + +Не выполняется: + +```text +Decimal → float +``` + +Таким образом сохраняются: + +- точность котировок; +- исходная числовая семантика; +- единый канонический тип данных. + +--- + +## 16. Сохранение временной семантики + +Quote Store сохраняет временные поля модели без преобразования. + +Не выполняется: + +```text +datetime → str +``` + +Store не: + +- форматирует timestamps; +- переводит время в локальную строку; +- вычисляет возраст записи; +- определяет freshness. + +Временная семантика остаётся частью канонической модели `Quote`. + +--- + +## 17. Изоляция экземпляров + +Разные экземпляры: + +```python +InMemoryQuoteStore() +``` + +имеют независимое состояние. + +Пример: + +```text +first_store + ↓ +собственные записи + +second_store + ↓ +собственные записи +``` + +Запись в одном экземпляре не появляется в другом. + +Это отличает Quote Store от legacy `MarketPriceCache`, использующего class-level storage. + +--- + +## 18. Ответственность Quote Store + +Quote Store отвечает только за: + +```text +хранение уже созданных канонических Quote +``` + +Quote Store не отвечает за: + +- получение данных; +- REST-запросы; +- WebSocket-соединения; +- parsing; +- schema validation; +- value validation; +- sequence validation; +- mapping; +- retry; +- reconnect; +- freshness; +- вычисление возраста; +- выбор REST или WebSocket; +- market status; +- execution pricing. + +Эти обязанности принадлежат другим компонентам архитектуры. + +--- + +## 19. Тестовое покрытие + +Добавлен файл: + +```text +tests/unit/storage/test_quote_store.py +``` + +Проверены: + +- соответствие `InMemoryQuoteStore` протоколу `QuoteStoreProtocol`; +- получение отсутствующей записи; +- сохранение `Quote`; +- получение сохранённого `Quote`; +- сохранение identity объекта; +- замена существующей записи; +- изоляция разных источников; +- изоляция разных runtime-контекстов; +- изоляция разных символов; +- нормализация внешних пробелов `source_name`; +- сохранение регистра `source_name`; +- lowercase-нормализация `runtime_key`; +- uppercase-нормализация символа; +- запрет пустого `source_name`; +- запрет пустого `runtime_key`; +- запрет пустого символа; +- запрет объекта неправильного типа; +- полная очистка; +- очистка по источнику; +- очистка по runtime; +- очистка по символу; +- точечная очистка; +- комбинированная очистка; +- идемпотентность очистки; +- независимость экземпляров Store; +- сохранение `Decimal`; +- сохранение `datetime`; +- наследование `QuoteStoreError` от `StorageError`. + +--- + +## 20. Результаты проверки + +Проверка синтаксиса: + +```bash +python -m py_compile \ + src/storage/exceptions.py \ + src/storage/quote_store.py \ + tests/unit/storage/test_quote_store.py +``` + +Результат: + +```text +успешно +``` + +Специализированные тесты: + +```bash +python -m pytest \ + tests/unit/storage/test_quote_store.py \ + tests/unit/storage/test_instrument_store.py \ + -q +``` + +Результат: + +```text +92 passed in 0.05s +``` + +Полная регрессия: + +```bash +python -m pytest -q +``` + +Результат: + +```text +562 passed in 0.28s +``` + +--- + +## 21. Изменение количества тестов + +До Build 032: + +```text +502 passed +``` + +После Build 032: + +```text +562 passed +``` + +Добавлено: + +```text +60 тестов +``` + +Полная регрессия осталась зелёной. + +--- + +## 22. Состояние архитектуры после Build 032 + +После завершения Build 032 существуют два параллельных контура. + +Канонический REST Quotes Feed: + +```text +Dzengi REST API + ↓ +DzengiQuoteDocumentSource + ↓ +Dzengi quote parser + ↓ +Dzengi quote value validation + ↓ +Dzengi quote mapper + ↓ +QuotesHandler + ↓ +QuotesFeed + ↓ +QuoteAcquisitionService + ↓ +Quote +``` + +Каноническое хранилище: + +```text +Quote + ↓ +InMemoryQuoteStore +``` + +При этом legacy runtime-контур пока продолжает использовать: + +```text +MarketPriceCache +``` + +То есть на момент завершения Build 032: + +```text +QuoteAcquisitionService + ↓ + Quote + + и + +InMemoryQuoteStore +``` + +существуют как канонические компоненты, но production pipeline ещё не переключён на новый Store. + +--- + +## 23. Что не изменилось + +Build 032 не изменил поведение работающего бота. + +Не изменялись: + +```text +src/integrations/exchange/service.py +src/integrations/exchange/market_cache.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +src/market_data/acquisition/service.py +``` + +Также не изменялись: + +- Telegram UI; +- AutoTrade runtime; +- стратегии; +- diagnostics; +- debug runtime; +- execution pricing. + +Это соответствует принятому принципу миграции: + +```text +сначала создать новый канонический компонент + ↓ +проверить его изолированно + ↓ +подключить под существующие facade-контракты + ↓ +перевести потребителей + ↓ +удалить legacy только после полного переключения +``` + +--- + +## 24. Итог Build 032 + +Build 032 завершён успешно. + +Создан канонический Storage-компонент для текущих котировок: + +```text +QuoteStoreProtocol + ↓ +InMemoryQuoteStore + ↓ +Quote +``` + +Достигнуты следующие архитектурные свойства: + +```text +каноническая модель хранения +изоляция источников +изоляция runtime-контекстов +изоляция символов +сохранение Decimal +сохранение datetime +отсутствие зависимости от биржи +отсутствие зависимости от Acquisition +отсутствие зависимости от ExchangeService +отсутствие зависимости от legacy MarketPriceCache +полная тестовая изоляция +``` + +Build завершён с полной зелёной регрессией: + +```text +562 passed +``` + +--- + +## 25. Следующий этап + +Следующий этап утверждённого плана: + +```text +Build 033 — Перенос MarketPriceCache на Quote Store +``` + +Его задача — начать интеграцию канонического `QuoteStore` в существующий runtime-контур котировок без нарушения обратной совместимости работающего бота. + +Целевая переходная схема: + +```text +legacy consumers + ↓ +ExchangeService facade + ↓ +MarketPriceCache compatibility layer + ↓ +Quote Store + ↓ +canonical Quote +``` + +После Build 033 `MarketPriceCache` должен перестать быть самостоятельным владельцем quote state и стать временным compatibility layer над каноническим `QuoteStore`. \ No newline at end of file diff --git a/docs/migrations/build_033.md b/docs/migrations/build_033.md new file mode 100644 index 0000000..e6cd1f7 --- /dev/null +++ b/docs/migrations/build_033.md @@ -0,0 +1,864 @@ +# Build 033 — Перенос MarketPriceCache на Quote Store + +**Engineering Build Record** + +--- + +## Контроль документа + +| Свойство | Значение | +|---|---| +| Документ | Build 033 — Перенос MarketPriceCache на Quote Store | +| Тип документа | Engineering Build Record | +| Статус | **Completed** | +| Подсистема | Market Data / Storage / Legacy Exchange Integration | +| Проект | Dzentra | +| Язык | Русский | +| Предыдущий этап | Build 032 — Канонический Quote Store | +| Следующий этап | Build 034 — Dzengi WebSocket quote parsing и адаптер | + +--- + +## 1. Назначение Build + +Цель Build 033 — перевести legacy-компонент `MarketPriceCache` с собственного внутреннего хранилища котировок на канонический `Quote Store`, сохранив полную обратную совместимость с существующими потребителями. + +До Build 033 `MarketPriceCache` самостоятельно владел runtime-состоянием котировок: + +```python +_prices: dict[tuple[str, str], MarketPriceSnapshot] = {} +``` + +Это создавало отдельный контур хранения рыночных цен параллельно с введённым в Build 032 каноническим `Quote Store`. + +После Build 033 единственным владельцем состояния котировок, доступных через `MarketPriceCache`, становится канонический `Quote Store`. + +Целевая переходная архитектура: + +```text +Legacy consumers + │ + ▼ +MarketPriceCache +compatibility facade + │ + ▼ +Canonical Quote + │ + ▼ +InMemoryQuoteStore +``` + +Сам `MarketPriceCache` сохраняется временно как compatibility facade до его окончательного удаления на Build 039. + +--- + +## 2. Архитектурный контекст + +Build 033 является частью последовательного перехода Quotes Feed на новую архитектуру Market Data Acquisition: + +```text +Build 026 — Аудит текущего контура Quotes Feed +Build 027 — Каноническая модель Quote и специализированные контракты +Build 028 — Dzengi REST quote models, parser и validation +Build 029 — Dzengi mapper и Quotes Handler +Build 030 — Quotes Feed и регистрация в Acquisition Service +Build 031 — Подключение нового REST Quotes Feed под legacy ExchangeService facade +Build 032 — Канонический Quote Store +Build 033 — Перенос MarketPriceCache на Quote Store +Build 034 — Dzengi WebSocket quote parsing и адаптер +Build 035 — Перевод market runtime на Quotes Feed +Build 036 — Перевод read-only и UI-потребителей +Build 037 — Перевод execution-потребителей +Build 038 — Удаление legacy TickerPrice и market snapshot dict layer +Build 039 — Удаление legacy quote parsing и MarketPriceCache +Build 040 — Финальная архитектурная проверка Quotes Feed +``` + +Build 033 не переводит непосредственных потребителей `MarketPriceCache` на новые API. Эта миграция выполняется последующими Build. + +Задача текущего этапа — устранить независимое legacy-хранилище котировок без нарушения работы существующего бота. + +--- + +## 3. Исходное состояние + +До Build 033 класс: + +```text +src/integrations/exchange/market_cache.py +``` + +содержал собственное class-level хранилище: + +```python +class MarketPriceCache: + _prices: dict[tuple[str, str], MarketPriceSnapshot] = {} +``` + +Ключ записи формировался из: + +```text +(runtime_key, symbol) +``` + +`MarketPriceCache` самостоятельно выполнял: + +- запись текущей цены; +- хранение `bid_price`; +- хранение `ask_price`; +- хранение `updated_at`; +- хранение фактического источника данных; +- изоляцию по `runtime_key`; +- вычисление возраста snapshot; +- очистку записей по символу и runtime. + +При этом после Build 032 уже существовал канонический: + +```text +InMemoryQuoteStore +``` + +работающий с моделью: + +```text +Quote +``` + +Таким образом, существовали два отдельных механизма хранения котировок: + +```text +MarketPriceCache + │ + └── собственный dict[tuple[str, str], MarketPriceSnapshot] + +Quote Store + │ + └── каноническое хранилище Quote +``` + +Build 033 устранил это дублирование для контура `MarketPriceCache`. + +--- + +## 4. Выполненные изменения + +### 4.1. Изменённый исходный файл + +Изменён: + +```text +src/integrations/exchange/market_cache.py +``` + +### 4.2. Добавленный тестовый файл + +Добавлен: + +```text +tests/unit/integrations/exchange/test_market_cache.py +``` + +### 4.3. Файлы, не потребовавшие изменений + +В рамках Build 033 не изменялись: + +```text +src/storage/quote_store.py +src/storage/exceptions.py +src/market_data/acquisition/models/quote.py +tests/unit/storage/test_quote_store.py +src/integrations/exchange/service.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Это подтверждает сохранение существующих публичных контрактов и минимальный scope миграции. + +--- + +## 5. Новая роль MarketPriceCache + +После Build 033 `MarketPriceCache` больше не является самостоятельным владельцем runtime-состояния котировок. + +Его новая роль: + +```text +Legacy compatibility facade +``` + +Он обеспечивает совместимость между существующими legacy-потребителями и канонической моделью хранения котировок. + +Логика записи: + +```text +Legacy caller + │ + ▼ +MarketPriceCache.set_price(...) + │ + ▼ +Canonical Quote + │ + ▼ +QuoteStoreProtocol.set(...) +``` + +Логика чтения: + +```text +Legacy caller + │ + ▼ +MarketPriceCache.get_price(...) + │ + ▼ +QuoteStoreProtocol.get(...) + │ + ▼ +Canonical Quote + │ + ▼ +MarketPriceSnapshot + │ + ▼ +Legacy caller +``` + +Таким образом, `MarketPriceSnapshot` остаётся только временной compatibility model. + +--- + +## 6. Устранение собственного хранилища MarketPriceCache + +До Build 033: + +```python +_prices: dict[tuple[str, str], MarketPriceSnapshot] = {} +``` + +После Build 033 `MarketPriceCache` использует канонический контракт: + +```text +QuoteStoreProtocol +``` + +и реализацию: + +```text +InMemoryQuoteStore +``` + +Собственное независимое хранилище `_prices` устранено. + +Это является главным архитектурным результатом Build 033. + +--- + +## 7. Преобразование legacy-входа в канонический Quote + +Публичный legacy-контракт записи сохранён: + +```python +MarketPriceCache.set_price( + symbol=..., + price=..., + bid_price=..., + ask_price=..., + updated_at=..., + source=..., + runtime_key=..., +) +``` + +Внутри compatibility facade эти данные преобразуются в каноническую модель: + +```text +Quote +``` + +Основное соответствие полей: + +| Legacy `MarketPriceCache` | Канонический `Quote` | +|---|---| +| `symbol` | `symbol` | +| `price` | `last_price` | +| `bid_price` | `bid_price` | +| `ask_price` | `ask_price` | +| `updated_at` | каноническое timestamp-представление | +| `source` | `source` | +| время получения | `received_at` | + +На legacy-границе сохраняется использование `float`. + +Внутри канонической модели используются точные числовые значения `Decimal`. + +Таким образом, преобразование имеет вид: + +```text +Legacy float values + │ + ▼ +MarketPriceCache + │ + ▼ +Decimal values + │ + ▼ +Canonical Quote +``` + +--- + +## 8. Чтение через MarketPriceSnapshot + +Существующие потребители ожидают от: + +```python +MarketPriceCache.get_price(...) +``` + +объект: + +```text +MarketPriceSnapshot +``` + +Поэтому Build 033 не удаляет эту модель. + +При чтении выполняется обратное compatibility-преобразование: + +```text +QuoteStore + │ + ▼ +Quote + │ + ▼ +MarketPriceSnapshot +``` + +Сохраняются legacy-поля: + +```text +symbol +price +bid_price +ask_price +updated_at +source +runtime_key +``` + +Также сохранены методы: + +```python +age_seconds() +has_bid_ask() +``` + +Благодаря этому существующие потребители не потребовали изменений. + +--- + +## 9. Сохранение семантики свежести + +Legacy-потребители используют: + +```python +cached_price.age_seconds() +``` + +для определения возраста котировки. + +Build 033 сохраняет этот публичный контракт. + +Возраст snapshot определяется на основе канонической информации о времени получения котировки. + +Таким образом, freshness-семантика больше не требует отдельного независимого хранилища состояния внутри `MarketPriceCache`. + +Существующие вызовы: + +```python +cached_price.age_seconds() +``` + +продолжают работать без изменений. + +--- + +## 10. Сохранение семантики bid/ask + +Legacy-модель предоставляет: + +```python +has_bid_ask() +``` + +Этот контракт сохранён. + +Он продолжает использоваться существующими execution-потребителями для проверки наличия корректных положительных значений: + +```text +bid_price +ask_price +``` + +Build 033 не требует изменения существующих потребителей этой проверки. + +--- + +## 11. Изоляция runtime_key + +Сохранена существующая изоляция котировок по: + +```text +runtime_key +``` + +Например: + +```text +auto +debug_auto +default +``` + +Котировки одного инструмента в разных runtime остаются независимыми. + +Концептуальный ключ хранения: + +```text +source_name + + +runtime_key + + +symbol +``` + +Это позволяет одновременно хранить: + +```text +BTC/USD_LEVERAGE + auto +BTC/USD_LEVERAGE + debug_auto +BTC/USD_LEVERAGE + default +``` + +как независимые runtime-записи. + +--- + +## 12. Нормализация runtime_key и symbol + +Сохранено существующее поведение нормализации. + +Символ нормализуется в uppercase: + +```text +btc/usd_leverage + ↓ +BTC/USD_LEVERAGE +``` + +`runtime_key` нормализуется в lowercase: + +```text +AUTO + ↓ +auto +``` + +Это сохраняет прежнюю семантику `MarketPriceCache`. + +--- + +## 13. Разделение source_name и Quote.source + +Build 033 сохраняет архитектурное различие между: + +```text +source_name +``` + +и: + +```text +Quote.source +``` + +`source_name` определяет namespace хранения. + +`Quote.source` определяет фактическое происхождение котировки. + +Например: + +```text +Storage namespace: +legacy-market-price-cache + +Actual quote source: +ws_depth:auto +``` + +или: + +```text +Storage namespace: +legacy-market-price-cache + +Actual quote source: +market-polling +``` + +Это предотвращает смешивание: + +- идентичности storage namespace; +- provenance рыночных данных. + +--- + +## 14. Сохранение семантики clear() + +Полностью сохранены существующие варианты очистки. + +### Полная очистка facade namespace + +```python +MarketPriceCache.clear() +``` + +Очищает все записи, принадлежащие `MarketPriceCache`. + +### Очистка символа во всех runtime + +```python +MarketPriceCache.clear("BTC/USD_LEVERAGE") +``` + +Очищает указанный символ во всех runtime внутри namespace facade. + +### Очистка runtime по всем символам + +```python +MarketPriceCache.clear(runtime_key="auto") +``` + +Очищает все символы указанного runtime. + +### Точечная очистка + +```python +MarketPriceCache.clear( + "BTC/USD_LEVERAGE", + runtime_key="auto", +) +``` + +Очищает только конкретную запись. + +--- + +## 15. Изоляция от других владельцев Quote Store + +Критически важное требование Build 033: + +```text +MarketPriceCache.clear() +``` + +не должен удалять котировки, записанные другими владельцами или источниками в канонический `Quote Store`. + +Поэтому операции facade ограничиваются собственным storage namespace. + +Архитектурно: + +```text +Quote Store +├── legacy-market-price-cache +│ ├── auto +│ ├── debug_auto +│ └── default +│ +└── other-source + └── ... +``` + +Очистка: + +```python +MarketPriceCache.clear() +``` + +затрагивает только: + +```text +legacy-market-price-cache +``` + +и не изменяет данные других namespace. + +--- + +## 16. Обратная совместимость + +Build 033 не изменил публичные вызовы: + +```python +MarketPriceCache.set_price(...) +MarketPriceCache.get_price(...) +MarketPriceCache.clear(...) +``` + +Не изменены существующие production-потребители: + +```text +src/integrations/exchange/service.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Также сохранены legacy-контракты: + +```python +MarketPriceSnapshot.age_seconds() +MarketPriceSnapshot.has_bid_ask() +``` + +Это позволило выполнить архитектурную миграцию без изменения поведения работающего бота. + +--- + +## 17. Тестовое покрытие + +Добавлен специализированный тестовый файл: + +```text +tests/unit/integrations/exchange/test_market_cache.py +``` + +Тестами проверяются: + +- соответствие `MarketPriceCache` каноническому `QuoteStoreProtocol`; +- запись канонического `Quote`; +- чтение через legacy `MarketPriceSnapshot`; +- сохранение `symbol`; +- сохранение `price`; +- сохранение `bid_price`; +- сохранение `ask_price`; +- сохранение `source`; +- сохранение `runtime_key`; +- нормализация символа; +- нормализация `runtime_key`; +- изоляция разных runtime; +- изоляция разных символов; +- замена предыдущей котировки новой; +- полная очистка facade namespace; +- очистка по символу; +- очистка по runtime; +- точечная очистка; +- вычисление возраста snapshot; +- legacy-проверка `has_bid_ask()`; +- преобразование timestamp; +- защита внешних записей другого `source_name` от очистки через `MarketPriceCache`. + +--- + +## 18. Проверка компиляции + +Выполнена команда: + +```bash +python -m py_compile \ + src/integrations/exchange/market_cache.py \ + tests/unit/integrations/exchange/test_market_cache.py +``` + +Результат: + +```text +SUCCESS +``` + +Ошибок компиляции нет. + +--- + +## 19. Специализированные тесты + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_market_cache.py \ + tests/unit/storage/test_quote_store.py \ + -q +``` + +Результат: + +```text +76 passed in 0.04s +``` + +Все специализированные тесты успешно пройдены. + +--- + +## 20. Регрессионная проверка потребителей + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_quotes_facade.py \ + tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py \ + -q +``` + +Результат: + +```text +47 passed in 0.13s +``` + +Регрессионный контур существующих потребителей полностью сохранён. + +--- + +## 21. Полная регрессионная проверка проекта + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +578 passed in 0.28s +``` + +Все тесты проекта успешно пройдены. + +Регрессий не обнаружено. + +--- + +## 22. Архитектурный результат + +До Build 033: + +```text +Legacy consumers + │ + ▼ +MarketPriceCache + │ + ▼ +Private _prices dict + │ + ▼ +MarketPriceSnapshot +``` + +Параллельно существовал: + +```text +Canonical Quote + │ + ▼ +Quote Store +``` + +После Build 033: + +```text +Legacy consumers + │ + ▼ +MarketPriceCache +compatibility facade + │ + ▼ +Canonical Quote + │ + ▼ +Quote Store +``` + +При чтении: + +```text +Quote Store + │ + ▼ +Canonical Quote + │ + ▼ +MarketPriceSnapshot +compatibility model + │ + ▼ +Legacy consumer +``` + +Таким образом, независимое legacy-хранилище котировок устранено. + +--- + +## 23. Что намеренно не входит в Build 033 + +Build 033 не выполняет: + +- удаление `MarketPriceCache`; +- удаление `MarketPriceSnapshot`; +- перевод WebSocket parsing на новый Dzengi quote adapter; +- перевод `MarketDataRunner` на `Quotes Feed`; +- перевод UI-потребителей на канонический `Quote`; +- перевод execution-потребителей на канонический `Quote`; +- удаление `TickerPrice`; +- удаление legacy market snapshot dict layer; +- удаление legacy quote parsing. + +Эти изменения выполняются последующими этапами утверждённого плана. + +--- + +## 24. Условия завершения + +Build 033 считается завершённым, поскольку выполнены все обязательные условия: + +- [x] `MarketPriceCache` больше не владеет собственным `_prices` dict. +- [x] Канонический `Quote Store` используется для хранения котировок facade. +- [x] `set_price()` преобразует legacy-вход в канонический `Quote`. +- [x] `get_price()` возвращает совместимый `MarketPriceSnapshot`. +- [x] Сохранён контракт `age_seconds()`. +- [x] Сохранён контракт `has_bid_ask()`. +- [x] Сохранена изоляция по `runtime_key`. +- [x] Сохранена нормализация символа. +- [x] Сохранена семантика `clear()`. +- [x] Очистка facade не затрагивает другие storage namespace. +- [x] Production-потребители не потребовали изменений. +- [x] Специализированные тесты успешно пройдены. +- [x] Регрессионные тесты потребителей успешно пройдены. +- [x] Полный набор тестов проекта успешно пройден. +- [x] Обратная совместимость работающего бота сохранена. + +--- + +## 25. Статус Build + +**Build 033 — Completed.** + +Канонический `Quote Store` теперь является владельцем состояния котировок, доступных через legacy `MarketPriceCache`. + +`MarketPriceCache` сохранён только как временный compatibility facade для существующих потребителей. + +Следующий этап: + +```text +Build 034 — Dzengi WebSocket quote parsing и адаптер +``` \ No newline at end of file diff --git a/docs/migrations/build_034.md b/docs/migrations/build_034.md new file mode 100644 index 0000000..b888d99 --- /dev/null +++ b/docs/migrations/build_034.md @@ -0,0 +1,594 @@ +# Build 034 — Dzengi WebSocket quote parsing и адаптер + +**Статус:** Завершён +**Подсистема:** Market Data +**Контур:** Market Data Acquisition / Quotes Feed +**Проект:** Dzentra +**Язык документации:** Русский + +--- + +## 1. Цель Build + +Цель Build 034 — создать специализированный контур обработки WebSocket-сообщений котировок Dzengi и преобразования их в каноническую модель `Quote`. + +Build должен был изолировать знание транспортных форматов Dzengi WebSocket от канонического слоя Market Data и подготовить архитектурную основу для последующего перевода market runtime на новый Quotes Feed. + +В рамках Build реализована цепочка: + +```text +Dzengi WebSocket message + ↓ +schema validation + ↓ +parser + ↓ +DzengiWebSocketQuoteResponse + ↓ +value validation + ↓ +mapper + ↓ +DzengiWebSocketQuoteAdapter + ↓ +canonical Quote +``` + +--- + +## 2. Архитектурный результат + +После Build 034 обработка WebSocket-котировок Dzengi получила специализированный адаптерный контур внутри: + +```text +src/market_data/acquisition/ +``` + +Транспортные особенности Dzengi WebSocket больше не должны распространяться на каноническую модель `Quote` и будущих потребителей Quotes Feed. + +Архитектурная граница имеет следующий вид: + +```text +Dzengi-specific transport formats + ↓ +adapters/dzengi + ↓ +canonical Quote + ↓ +Quotes Feed / Quote Store / consumers +``` + +Каноническая модель: + +```text +src/market_data/acquisition/models/quote.py +``` + +остаётся независимой от: + +```text +payload +Payload +symbolName +bid +ask +ofr +bids +asks +price +p +bidPrice +askPrice +``` + +Эти имена являются особенностями внешнего транспорта Dzengi и обрабатываются внутри адаптерного слоя. + +--- + +## 3. Изменённые файлы + +В рамках Build 034 изменены следующие файлы: + +```text +src/market_data/acquisition/adapters/dzengi/models.py +src/market_data/acquisition/adapters/dzengi/parser.py +src/market_data/acquisition/adapters/dzengi/mapper.py +src/market_data/acquisition/validation/schema.py +src/market_data/acquisition/validation/values.py +``` + +Добавлен специализированный WebSocket-адаптер: + +```text +src/market_data/acquisition/adapters/dzengi/websocket.py +``` + +--- + +## 4. Добавленные тесты + +Добавлены следующие специализированные тестовые файлы: + +```text +tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py +tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py +tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py +tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py +tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py +``` + +--- + +## 5. Поддерживаемые WebSocket-форматы + +Новый адаптерный контур поддерживает транспортные варианты, существовавшие в legacy-реализации Dzengi WebSocket. + +### 5.1. Оболочки сообщения + +Поддерживаются: + +```text +payload +Payload +``` + +а также вложенная двойная оболочка. + +Примеры допустимой структуры: + +```json +{ + "payload": { + "symbolName": "BTC/USD_LEVERAGE", + "bid": "64159.45", + "ask": "64159.55" + } +} +``` + +и: + +```json +{ + "Payload": { + "Payload": { + "symbolName": "BTC/USD_LEVERAGE", + "bids": [ + ["64159.45", "1.0"] + ], + "asks": [ + ["64159.55", "1.0"] + ] + } + } +} +``` + +--- + +## 6. Поддерживаемые поля символа + +Адаптер поддерживает следующие транспортные имена символа: + +```text +symbolName +symbol +``` + +После обработки внешнее представление преобразуется в каноническое поле: + +```text +Quote.symbol +``` + +--- + +## 7. Поддерживаемые представления bid и ask + +Поддерживаются прямые поля: + +```text +bid +ask +``` + +вариант Dzengi: + +```text +bid +ofr +``` + +а также depth-представление: + +```text +bids +asks +``` + +Для элементов depth поддерживаются представления в виде: + +```text +list +dict +``` + +Поддерживаемые имена поля цены внутри depth-элементов: + +```text +price +p +bidPrice +askPrice +``` + +--- + +## 8. Семантика last_price для depth-сообщений + +Для WebSocket depth-сообщений, содержащих лучшие цены bid и ask, сохранена legacy-семантика: + +```text +last_price = midpoint(best_bid, best_ask) +``` + +То есть каноническое значение `Quote.last_price` определяется как середина между лучшей ценой покупки и лучшей ценой продажи. + +Это решение сохраняет обратную совместимость с существующим поведением market runtime до его последующего архитектурного перевода. + +--- + +## 9. Обработка timestamp + +WebSocket timestamp является необязательным. + +Если транспортное сообщение содержит допустимый timestamp биржи, он преобразуется в: + +```text +Quote.exchange_timestamp +``` + +Если timestamp отсутствует, каноническая модель допускает: + +```text +exchange_timestamp = None +``` + +Время фактического получения и обработки котировки фиксируется отдельно: + +```text +Quote.received_at +``` + +Таким образом, сохраняется разделение двух временных характеристик: + +```text +exchange_timestamp + время события по данным биржи + +received_at + время получения котировки системой Dzentra +``` + +--- + +## 10. Schema validation + +Schema validation отвечает исключительно за структурную корректность WebSocket-документа. + +На этом этапе проверяется возможность извлечения необходимых частей сообщения без переноса бизнес-логики в транспортный слой. + +Schema validation не должна: + +```text +создавать canonical Quote +выполнять mapping +управлять runtime +записывать данные в Quote Store +обращаться к MarketPriceCache +``` + +--- + +## 11. Parser + +Parser преобразует структурно проверенный WebSocket-документ в специализированную raw-модель Dzengi: + +```text +DzengiWebSocketQuoteResponse +``` + +Parser сохраняет границу между: + +```text +сырой внешний документ +``` + +и: + +```text +типизированное транспортное представление Dzengi +``` + +Parser не создаёт канонический `Quote`. + +--- + +## 12. Value validation + +Value validation проверяет семантическую допустимость извлечённых значений. + +В частности, контур должен обеспечивать корректность значений, необходимых для построения канонической котировки: + +```text +symbol +bid price +ask price +timestamp, если присутствует +``` + +Проверка значений выполняется до mapping в каноническую модель. + +--- + +## 13. Mapper + +Mapper преобразует проверенную raw-модель Dzengi WebSocket в: + +```text +Quote +``` + +На этой границе происходит переход: + +```text +Dzengi-specific representation + ↓ +canonical Dzentra representation +``` + +После mapping потребитель не должен зависеть от исходного формата WebSocket-сообщения. + +--- + +## 14. DzengiWebSocketQuoteAdapter + +Специализированный адаптер инкапсулирует полный конвейер обработки одного WebSocket-сообщения: + +```text +raw document + ↓ +schema validation + ↓ +parsing + ↓ +value validation + ↓ +mapping + ↓ +Quote +``` + +Результатом успешной обработки является канонический объект: + +```text +Quote +``` + +Адаптер не отвечает за: + +```text +поддержание WebSocket-соединения +reconnect +runtime lifecycle +регистрацию market runtime +запись в Quote Store +legacy MarketPriceCache facade +``` + +Эти обязанности принадлежат другим архитектурным слоям. + +--- + +## 15. Что намеренно не изменялось + +В Build 034 не изменялись runtime-файлы: + +```text +src/integrations/exchange/ws_client.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +Также Build 034 не выполнял переключение: + +```text +market runtime → Quotes Feed +``` + +и не удалял legacy-механизмы. + +Это принципиальная граница Build. + +Build 034 создаёт новый специализированный адаптерный контур, но не переключает на него существующий runtime. + +Перевод runtime предусмотрен следующим этапом: + +```text +Build 035 — Перевод market runtime на Quotes Feed +``` + +--- + +## 16. Обратная совместимость + +В Build 034 сохранены существующие транспортные варианты legacy WebSocket-контура: + +```text +payload / Payload +двойная оболочка +symbolName / symbol +bid + ask +bid + ofr +bids + asks +depth item list +depth item dict +price / p / bidPrice / askPrice +необязательный timestamp +``` + +Для depth-сообщений сохранено существующее правило: + +```text +last_price = midpoint(best_bid, best_ask) +``` + +Таким образом, Build не требует одномоментного удаления legacy runtime и подготавливает безопасный переход к новой архитектуре. + +--- + +## 17. Проверка компиляции + +Выполнена проверка: + +```bash +python -m py_compile \ + src/market_data/acquisition/adapters/dzengi/models.py \ + src/market_data/acquisition/adapters/dzengi/parser.py \ + src/market_data/acquisition/adapters/dzengi/mapper.py \ + src/market_data/acquisition/adapters/dzengi/websocket.py \ + src/market_data/acquisition/validation/schema.py \ + src/market_data/acquisition/validation/values.py \ + tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py \ + tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py +``` + +Результат: + +```text +успешно +``` + +--- + +## 18. Специализированные тесты + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/market_data/acquisition/validation/test_websocket_quote_schema.py \ + tests/unit/market_data/acquisition/validation/test_websocket_quote_values.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_mapper.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py \ + -q +``` + +Результат: + +```text +24 passed in 0.04s +``` + +--- + +## 19. Полная регрессия + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +602 passed in 0.30s +``` + +Количество тестов до Build 034: + +```text +578 passed +``` + +Количество тестов после Build 034: + +```text +602 passed +``` + +Добавлено: + +```text +24 теста +``` + +Полная регрессия подтверждает отсутствие обнаруженных регрессий в существующем коде проекта. + +--- + +## 20. Итог Build + +Build 034 завершён полностью. + +Реализованы: + +```text +специализированная WebSocket raw-модель Dzengi +WebSocket schema validation +WebSocket parser +WebSocket value validation +WebSocket mapper +DzengiWebSocketQuoteAdapter +преобразование WebSocket-сообщения в canonical Quote +поддержка legacy-вариантов формата Dzengi +24 специализированных теста +``` + +Не выполнялись: + +```text +переключение market runtime +изменение ws_client.py +изменение market_stream.py +изменение market_data_runner.py +удаление legacy WebSocket parsing +удаление MarketPriceCache +``` + +Архитектурный результат: + +```text +Dzengi WebSocket transport + ↓ +Dzengi-specific validation / parsing / mapping + ↓ +canonical Quote +``` + +--- + +## 21. Следующий Build + +Следующий этап: + +```text +Build 035 — Перевод market runtime на Quotes Feed +``` + +Его задача — подключить существующий market runtime к новому каноническому контуру котировок, используя созданные ранее: + +```text +Quote +Quote Store +Quotes Feed +Dzengi REST Quotes Feed +Dzengi WebSocket quote adapter +``` + +При этом переход должен выполняться без преждевременного удаления legacy-механизмов и с сохранением работоспособности существующего бота до завершения последующих этапов миграции. \ No newline at end of file diff --git a/docs/migrations/build_035.md b/docs/migrations/build_035.md new file mode 100644 index 0000000..86182f9 --- /dev/null +++ b/docs/migrations/build_035.md @@ -0,0 +1,898 @@ +# Build 035 — Перевод market runtime на Quotes Feed + +**Статус:** Завершён +**Подсистема:** Market Data +**Контур:** Market Data Acquisition / Quotes Feed / Market Runtime +**Проект:** Dzentra +**Язык документации:** Русский + +--- + +## 1. Цель Build + +Цель Build 035 — перевести существующий WebSocket market runtime с самостоятельного legacy parsing рыночных сообщений на канонический контур обработки котировок, созданный в предыдущих Build. + +До Build 035 runtime самостоятельно извлекал цены из WebSocket-сообщений Dzengi и передавал примитивные значения в legacy facade: + +```text +Dzengi WebSocket message + ↓ +legacy runtime parsing + ↓ +float price / bid / ask + ↓ +MarketPriceCache.set_price() + ↓ +Quote Store +``` + +После Build 035 рабочий runtime-путь использует специализированный WebSocket-адаптер и каноническую модель `Quote`: + +```text +Dzengi WebSocket message + ↓ +ExchangeWebSocketClient + ↓ +DzengiWebSocketQuoteAdapter + ↓ +canonical Quote + ↓ +MarketPriceCache.set_quote() + ↓ +Quote Store +``` + +Таким образом, WebSocket runtime больше не выполняет собственное преобразование транспортного формата Dzengi в набор примитивных ценовых значений. + +--- + +## 2. Предпосылки + +Build 035 опирается на результаты предыдущих этапов миграции. + +### Build 027 + +Создана каноническая модель: + +```text +Quote +``` + +### Build 028 + +Созданы: + +```text +REST quote schema validation +REST quote parser +REST quote value validation +``` + +### Build 029 + +Созданы: + +```text +REST quote mapper +DzengiQuoteDocumentHandler +``` + +### Build 030 + +Создан полный REST Quotes Feed: + +```text +Dzengi REST ticker/24hr + ↓ +DzengiQuoteDocumentSource + ↓ +QuotesFeed + ↓ +QuoteAcquisitionService + ↓ +canonical Quote +``` + +### Build 031 + +Новый REST Quotes Feed подключён под legacy `ExchangeService` facade. + +### Build 032 + +Создан канонический: + +```text +Quote Store +``` + +### Build 033 + +`MarketPriceCache` переведён на использование `Quote Store` как внутреннего хранилища. + +### Build 034 + +Создан специализированный контур обработки WebSocket-котировок: + +```text +Dzengi WebSocket message + ↓ +schema validation + ↓ +parser + ↓ +value validation + ↓ +mapper + ↓ +DzengiWebSocketQuoteAdapter + ↓ +canonical Quote +``` + +Build 035 подключает этот контур к существующему market runtime. + +--- + +## 3. Архитектурная проблема до Build 035 + +До Build 035 существовало несколько независимых путей обработки котировок. + +REST-контур уже использовал каноническую модель: + +```text +REST ticker/24hr + ↓ +Quotes Feed + ↓ +Quote + ↓ +Quote Store +``` + +Но WebSocket runtime продолжал самостоятельно разбирать транспортные сообщения. + +### `market_stream.py` + +Рабочий путь имел вид: + +```text +WebSocket message + ↓ +_payload_from_message() + ↓ +_extract_market_event() + ↓ +float price / bid / ask + ↓ +MarketPriceCache.set_price() +``` + +### `market_data_runner.py` + +Рабочий путь имел вид: + +```text +WebSocket message + ↓ +_extract_depth_payload() + ↓ +_extract_best_price() + ↓ +float midpoint + ↓ +MarketPriceCache.set_price() +``` + +Таким образом, логика понимания формата Dzengi WebSocket существовала одновременно: + +```text +в новом DzengiWebSocketQuoteAdapter +в market_stream.py +в market_data_runner.py +``` + +Это нарушало архитектурную границу: + +```text +transport-specific parsing + ↓ +только adapter layer +``` + +--- + +## 4. Архитектурный результат + +После Build 035 оба WebSocket runtime-пути используют единый канонический адаптер: + +```text +ExchangeWebSocketClient + ↓ +decoded WebSocket message + ↓ +DzengiWebSocketQuoteAdapter + ↓ +canonical Quote + ↓ +MarketPriceCache compatibility facade + ↓ +Quote Store +``` + +Runtime больше не должен самостоятельно знать: + +```text +как устроены payload / Payload +как извлекается symbolName +как извлекаются bids / asks +какие варианты depth item поддерживает Dzengi +как вычисляется canonical last_price +как преобразуется timestamp +``` + +Эта ответственность принадлежит: + +```text +src/market_data/acquisition/adapters/dzengi/websocket.py +``` + +и связанному с ним контуру: + +```text +schema validation +parser +value validation +mapper +``` + +--- + +## 5. Изменённые production-файлы + +В рамках Build 035 изменены: + +```text +src/integrations/exchange/market_cache.py +src/integrations/exchange/market_stream.py +src/integrations/exchange/market_data_runner.py +``` + +--- + +## 6. Изменённые тестовые файлы + +Расширены существующие тесты: + +```text +tests/unit/integrations/exchange/test_market_cache.py +tests/unit/integrations/exchange/test_market_stream.py +tests/unit/integrations/exchange/test_market_data_runner.py +``` + +Новые тестовые файлы не создавались. + +--- + +## 7. Расширение MarketPriceCache + +В `MarketPriceCache` добавлен новый метод: + +```python +@classmethod +def set_quote( + cls, + quote: Quote, + *, + runtime_key: str = "default", +) -> None: + ... +``` + +Его задача — принять уже готовый канонический объект: + +```text +Quote +``` + +и записать его в существующее внутреннее хранилище через compatibility facade. + +Цепочка: + +```text +canonical Quote + ↓ +MarketPriceCache.set_quote() + ↓ +Quote Store +``` + +--- + +## 8. Сохранение канонического Quote без повторного mapping + +До Build 035 при наличии уже готового `Quote` потенциально мог возникнуть лишний цикл: + +```text +Quote + ↓ +float values + ↓ +MarketPriceCache.set_price() + ↓ +создание нового Quote + ↓ +Quote Store +``` + +После Build 035 используется прямой путь: + +```text +Quote + ↓ +MarketPriceCache.set_quote() + ↓ +Quote Store +``` + +При этом не требуется: + +```text +преобразовывать Decimal в float +повторно создавать Quote +повторно вычислять received_at +повторно преобразовывать exchange_timestamp +изменять source +``` + +Таким образом, сохраняется исходный канонический объект. + +--- + +## 9. Сохранение legacy set_price() + +Существующий публичный метод: + +```text +MarketPriceCache.set_price() +``` + +не удалён. + +Это необходимо для сохранения обратной совместимости существующего бота и legacy-потребителей. + +После Build 035 его архитектурная роль: + +```text +legacy primitive values + ↓ +MarketPriceCache.set_price() + ↓ +canonical Quote + ↓ +MarketPriceCache.set_quote() + ↓ +Quote Store +``` + +Таким образом, `set_price()` остаётся compatibility entry point, а непосредственная запись готового канонического объекта выполняется через: + +```text +set_quote() +``` + +--- + +## 10. Перевод market_stream.py + +Рабочий WebSocket-путь `market_stream.py` переведён на: + +```text +ExchangeWebSocketClient.stream_depth() + ↓ +DzengiWebSocketQuoteAdapter.map_message() + ↓ +Quote + ↓ +MarketPriceCache.set_quote() +``` + +Новый runtime-путь больше не использует legacy-функцию: + +```text +_extract_market_event() +``` + +для обработки рабочих WebSocket-сообщений. + +--- + +## 11. Проверка символа в market_stream.py + +После получения канонического `Quote` выполняется проверка соответствия символа ожидаемому инструменту. + +Концептуально: + +```text +requested symbol + ↕ +Quote.symbol +``` + +Если сообщение относится к другому инструменту, оно не должно записываться в runtime namespace. + +Это предотвращает сохранение чужой котировки в контексте текущего WebSocket-потока. + +--- + +## 12. Обработка невалидных сообщений в market_stream.py + +Ошибка обработки отдельного WebSocket-сообщения не должна немедленно завершать весь market stream. + +Ошибки канонического Acquisition-контура отдельного сообщения обрабатываются внутри цикла: + +```text +invalid WebSocket message + ↓ +DzengiWebSocketQuoteAdapter + ↓ +MarketDataAcquisitionError + ↓ +сообщение пропускается + ↓ +stream продолжает работу +``` + +При этом сетевые, transport и connection errors не маскируются этим механизмом и продолжают обрабатываться существующим reconnect-контуром. + +--- + +## 13. Перевод MarketDataRunner + +Основной WebSocket runtime в: + +```text +MarketDataRunner._run_websocket() +``` + +переведён с самостоятельного извлечения: + +```text +best_bid +best_ask +``` + +на канонический путь: + +```text +raw WebSocket payload + ↓ +DzengiWebSocketQuoteAdapter.map_message() + ↓ +Quote +``` + +После успешного mapping готовый объект записывается: + +```text +Quote + ↓ +MarketPriceCache.set_quote( + runtime_key=context.runtime_key +) + ↓ +Quote Store +``` + +--- + +## 14. Runtime isolation + +Сохраняется существующая изоляция runtime-контекстов через: + +```text +runtime_key +``` + +Примеры существующих runtime: + +```text +auto +debug_auto +default +``` + +При записи через: + +```text +MarketPriceCache.set_quote() +``` + +используется соответствующий: + +```text +context.runtime_key +``` + +Таким образом, котировки разных runtime не смешиваются. + +--- + +## 15. Bid и ask после перехода на Quote + +До Build 035 `MarketDataRunner` самостоятельно извлекал: + +```text +best_bid +best_ask +``` + +из сырого WebSocket payload. + +После Build 035 эти значения берутся из уже проверенного канонического объекта: + +```text +Quote.bid_price +Quote.ask_price +``` + +Для legacy logging boundary при необходимости допускается преобразование: + +```text +Decimal → float +``` + +Но внутренний канонический объект остаётся основанным на: + +```text +Decimal +``` + +--- + +## 16. Last price для depth-сообщений + +Согласно контракту Build 034 для WebSocket depth-сообщений: + +```text +last_price = midpoint(best_bid, best_ask) +``` + +После Build 035 runtime больше не должен самостоятельно повторять этот расчёт. + +Он получает готовое значение: + +```text +Quote.last_price +``` + +из `DzengiWebSocketQuoteAdapter`. + +Таким образом, правило определения `last_price` имеет одну каноническую реализацию. + +--- + +## 17. Сохранение invalid payload semantics + +До Build 035 `MarketDataRunner` поддерживал счётчик последовательных невалидных WebSocket-сообщений. + +Существующая семантика сохранена: + +```text +invalid message + ↓ +invalid_payload_count += 1 +``` + +Успешный `Quote`: + +```text +valid Quote + ↓ +invalid_payload_count = 0 +``` + +После пяти последовательных невалидных сообщений: + +```text +5 consecutive invalid messages + ↓ +RuntimeError + ↓ +существующий fallback-контур +``` + +Таким образом, Build 035 не изменяет существующую политику деградации runtime. + +--- + +## 18. REST fallback + +REST fallback не потребовал изменения. + +К моменту Build 035 REST-путь уже использует новый Quotes Feed: + +```text +Dzengi REST ticker/24hr + ↓ +DzengiQuoteDocumentSource + ↓ +QuotesFeed + ↓ +QuoteAcquisitionService + ↓ +canonical Quote + ↓ +MarketPriceCache facade + ↓ +Quote Store +``` + +После Build 035 WebSocket и REST-пути сходятся на одной канонической модели: + +```text + WebSocket + ↓ + DzengiWebSocketQuoteAdapter + ↓ + Quote + ↓ + Quote Store + ↑ + Quote + ↑ + REST Quotes Feed + ↑ + REST +``` + +--- + +## 19. Что намеренно не изменялось + +В Build 035 не изменялись: + +```text +src/integrations/exchange/ws_client.py +src/market_data/acquisition/adapters/dzengi/websocket.py +src/market_data/acquisition/models/quote.py +src/storage/quote_store.py +``` + +Причина: + +- `ws_client.py` уже имеет достаточный transport-only контракт; +- WebSocket-адаптер завершён в Build 034; +- каноническая модель `Quote` не требует расширения; +- `Quote Store` уже предоставляет необходимый контракт хранения. + +--- + +## 20. Legacy parsing helpers + +После перевода рабочего runtime-пути некоторые legacy helper-функции больше не являются частью основного пути обработки котировок. + +В частности: + +```text +market_stream.py: + _extract_market_event() + +market_data_runner.py: + _extract_best_price() +``` + +Они не удалены в Build 035. + +Это намеренное решение. + +Окончательная очистка legacy parsing относится к последующему этапу: + +```text +Build 039 — Удаление legacy quote parsing и MarketPriceCache +``` + +Build 035 меняет рабочий runtime-путь, но не выполняет преждевременную очистку compatibility layer. + +--- + +## 21. Обратная совместимость + +Build 035 сохраняет работоспособность существующего бота. + +Не удалены: + +```text +MarketPriceCache +MarketPriceCache.set_price() +legacy read APIs +runtime_key isolation +REST fallback +существующие runtime lifecycle contracts +``` + +Это соответствует принятому принципу миграции Dzentra: + +```text +новый контур создаётся + ↓ +существующие потребители постепенно переключаются + ↓ +legacy удаляется только после завершения миграции +``` + +--- + +## 22. Проверка компиляции + +Выполнена команда: + +```bash +python -m py_compile \ + src/integrations/exchange/market_cache.py \ + src/integrations/exchange/market_stream.py \ + src/integrations/exchange/market_data_runner.py \ + tests/unit/integrations/exchange/test_market_cache.py \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py +``` + +Результат: + +```text +успешно +``` + +--- + +## 23. Специализированные тесты + +Выполнена команда: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_market_cache.py \ + tests/unit/integrations/exchange/test_market_stream.py \ + tests/unit/integrations/exchange/test_market_data_runner.py \ + tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_adapter.py \ + -q +``` + +Результат: + +```text +33 passed in 0.13s +``` + +--- + +## 24. Полная регрессия + +Выполнена команда: + +```bash +python -m pytest -q +``` + +Результат: + +```text +608 passed in 0.29s +``` + +Количество тестов до Build 035: + +```text +602 passed +``` + +Количество тестов после Build 035: + +```text +608 passed +``` + +Добавлено: + +```text +6 тестов +``` + +Полная регрессия подтверждает отсутствие обнаруженных регрессий в существующем коде проекта. + +--- + +## 25. Итог Build + +Build 035 завершён полностью. + +Реализованы: + +```text +подключение DzengiWebSocketQuoteAdapter к market_stream +подключение DzengiWebSocketQuoteAdapter к MarketDataRunner +передача canonical Quote непосредственно в compatibility facade +добавление MarketPriceCache.set_quote() +сохранение legacy MarketPriceCache.set_price() +сохранение runtime_key isolation +сохранение invalid payload semantics +сохранение REST fallback +сохранение обратной совместимости +``` + +Подтверждён новый рабочий WebSocket-путь: + +```text +ExchangeWebSocketClient + ↓ +DzengiWebSocketQuoteAdapter + ↓ +canonical Quote + ↓ +MarketPriceCache.set_quote() + ↓ +Quote Store +``` + +--- + +## 26. Архитектурное состояние после Build 035 + +После завершения Build 035 Dzentra имеет два канонических пути получения текущей котировки. + +### WebSocket + +```text +Dzengi WebSocket + ↓ +ExchangeWebSocketClient + ↓ +DzengiWebSocketQuoteAdapter + ↓ +Quote + ↓ +MarketPriceCache compatibility facade + ↓ +Quote Store +``` + +### REST + +```text +Dzengi REST ticker/24hr + ↓ +DzengiQuoteDocumentSource + ↓ +QuotesFeed + ↓ +QuoteAcquisitionService + ↓ +Quote + ↓ +MarketPriceCache compatibility facade + ↓ +Quote Store +``` + +Таким образом, оба transport-пути приводят данные к единой канонической модели: + +```text +Quote +``` + +до передачи их потребителям. + +--- + +## 27. Следующий Build + +Следующий этап: + +```text +Build 036 — Перевод read-only и UI-потребителей +``` + +Его задача — определить и перевести read-only и UI-потребителей текущей рыночной котировки с legacy-представлений на канонический `Quote` и новый контур хранения там, где это архитектурно обосновано. + +Build 036 должен выполняться без изменения execution semantics и без преждевременного удаления legacy compatibility layer. + +Execution-потребители остаются отдельным последующим этапом миграции. \ No newline at end of file diff --git a/docs/migrations/build_036.md b/docs/migrations/build_036.md new file mode 100644 index 0000000..ac05702 --- /dev/null +++ b/docs/migrations/build_036.md @@ -0,0 +1,739 @@ +# Build 036 — Перевод read-only и UI-потребителей + +**Статус:** Завершён +**Результат:** Успешно +**Полная регрессия:** `614 passed` + +--- + +## 1. Назначение Build + +Цель Build 036 — перевести read-only и UI-потребителей рыночной котировки с legacy-представлений: + +- `TickerPrice`; +- `dict[str, object]` из `get_market_snapshot()`; + +на каноническую внутреннюю модель: + +`Quote` + +Build продолжает миграцию подсистемы рыночных данных на целевую архитектуру: + + Market Data + ↓ + Market Intelligence + ↓ + Decision + ↓ + Execution + ↓ + Exchange + +В рамках Build 036 изменяется исключительно read-only контур. + +Execution pricing, торговые стратегии, signal runtime и execution quality не переводятся и не изменяются. + +--- + +## 2. Исходное состояние + +До Build 036 read-only и UI-потребители получали текущую рыночную котировку через несколько legacy-интерфейсов. + +Основные варианты: + + UI / diagnostics + ↓ + ExchangeService.get_price() + ↓ + TickerPrice + +или: + + UI / diagnostics + ↓ + ExchangeService.get_market_snapshot() + ↓ + dict[str, object] + +При этом после предыдущих Build каноническая модель `Quote` уже существовала и использовалась внутри новой инфраструктуры: + + REST Quotes Feed + ↓ + Quote + + WebSocket quote adapter + ↓ + Quote + + MarketPriceCache + ↓ + Quote Store + ↓ + Quote + +Таким образом, read-only потребители продолжали зависеть от compatibility-представлений, несмотря на наличие канонической модели котировки. + +--- + +## 3. Целевое состояние + +После Build 036 read-only и UI-потребители получают канонический объект: + +`Quote` + +через публичный facade: + +`ExchangeService.get_quote()` + +Целевая цепочка чтения: + + read-only / UI consumer + ↓ + ExchangeService.get_quote() + ↓ + MarketPriceCache.get_quote() + ↓ + Quote Store + ↓ + canonical Quote + +При отсутствии свежей котировки используется REST fallback: + + ExchangeService.get_quote() + ↓ + REST Quotes Feed + ↓ + canonical Quote + ↓ + MarketPriceCache.set_quote() + ↓ + Quote Store + ↓ + canonical Quote + +--- + +## 4. Архитектурный принцип + +Read-only и UI-потребители не обращаются напрямую к: + +`Quote Store` + +Доступ выполняется через: + +`ExchangeService.get_quote()` + +Это позволяет сохранить единый facade, отвечающий за: + +- выбор default symbol; +- нормализацию и валидацию символа; +- нормализацию `runtime_key`; +- поддержку mock mode; +- чтение канонической котировки из cache/store; +- проверку свежести; +- REST fallback; +- преобразование внутренних ошибок в `ExchangeError`. + +Целевая граница: + + UI / diagnostics + ↓ + ExchangeService + ↓ + MarketPriceCache + ↓ + Quote Store + +UI не должен самостоятельно знать: + +- структуру ключей Quote Store; +- `source_name`; +- правила `runtime_key`; +- freshness policy; +- правила REST fallback; +- внутреннюю обработку ошибок Acquisition Layer. + +--- + +## 5. Изменённые production-файлы + +В рамках Build 036 изменены: + + src/integrations/exchange/market_cache.py + src/integrations/exchange/service.py + + src/telegram/ui/currency_ui.py + src/telegram/handlers/auto/ui.py + src/telegram/handlers/debug_auto/ui.py + + src/trading/diagnostics/snapshot.py + +--- + +## 6. Изменённые и добавленные тесты + +Изменены: + + tests/unit/integrations/exchange/test_market_cache.py + tests/unit/telegram/ui/test_currency_ui.py + +Добавлен: + + tests/unit/integrations/exchange/test_service_quote.py + +После Build 036 общее количество тестов увеличилось: + + после Build 035: 608 passed + после Build 036: 614 passed + +Добавлено: + + 6 тестов + +--- + +## 7. Изменения в MarketPriceCache + +Файл: + +`src/integrations/exchange/market_cache.py` + +Добавлен канонический read API: + +`MarketPriceCache.get_quote()` + +Его назначение — вернуть непосредственно канонический объект `Quote`, сохранённый в Quote Store. + +Цепочка: + + MarketPriceCache.get_quote() + ↓ + QuoteStore.get() + ↓ + Quote | None + +Метод: + +- нормализует `symbol`; +- нормализует `runtime_key`; +- читает котировку из собственного namespace Quote Store; +- возвращает канонический `Quote`; +- не создаёт `MarketPriceSnapshot`; +- не выполняет преобразование `Decimal` в `float`; +- сохраняет канонический объект котировки. + +Legacy API: + +`MarketPriceCache.get_price()` + +сохранён для compatibility-потребителей, которые ещё не переведены на `Quote`. + +--- + +## 8. Изменения в ExchangeService + +Файл: + +`src/integrations/exchange/service.py` + +Добавлен публичный канонический read API: + +`ExchangeService.get_quote()` + +Метод сохраняет обязанности facade и отвечает за: + +1. выбор default symbol; +2. поддержку mock mode; +3. валидацию символа; +4. нормализацию `runtime_key`; +5. чтение канонической котировки из MarketPriceCache; +6. проверку свежести; +7. REST fallback при cache miss или stale quote; +8. сохранение свежей котировки в Quote Store через MarketPriceCache; +9. возврат канонического `Quote`. + +Целевая цепочка: + + ExchangeService.get_quote() + ↓ + validate_symbol() + ↓ + MarketPriceCache.get_quote() + ↓ + freshness check + ↓ + Quote + +При отсутствии свежей котировки: + + ExchangeService.get_quote() + ↓ + REST Quotes Feed + ↓ + Quote + ↓ + MarketPriceCache.set_quote() + ↓ + Quote Store + +--- + +## 9. Политика свежести + +Для read-only и UI-потребителей сохранена существующая политика свежести рыночной котировки. + +Возраст котировки определяется по: + +`Quote.received_at` + +Если сохранённая котировка достаточно свежая, возвращается существующий канонический объект. + +Если котировка отсутствует или устарела, выполняется REST fallback через новый Quotes Feed. + +Таким образом: + + fresh cached Quote + ↓ + вернуть Quote без REST-запроса + + stale cached Quote + ↓ + REST Quotes Feed + ↓ + сохранить новый Quote + ↓ + вернуть новый Quote + +--- + +## 10. REST fallback + +REST fallback использует новую каноническую цепочку Acquisition Layer: + + Dzengi REST API + ↓ + GET /api/v1/ticker/24hr + ↓ + DzengiQuoteDocumentSource + ↓ + DzengiQuoteDocumentHandler + ↓ + schema validation + ↓ + parsing + ↓ + value validation + ↓ + mapping + ↓ + canonical Quote + +Полученный объект сохраняется: + + Quote + ↓ + MarketPriceCache.set_quote() + ↓ + Quote Store + +Не используется лишний цикл преобразований: + + Quote + ↓ + dict + ↓ + float + ↓ + новый Quote + +Канонический объект остаётся `Quote` на всём новом пути. + +--- + +## 11. Mock mode + +`ExchangeService.get_quote()` сохраняет поддержку режима: + +`exchange_enabled = False` + +В этом режиме потребителю также возвращается канонический: + +`Quote` + +Mock-котировка содержит: + +- `symbol`; +- `last_price`; +- `bid_price`; +- `ask_price`; +- `exchange_timestamp`; +- `received_at`; +- `source`. + +Таким образом, потребители `get_quote()` не должны знать, работает приложение с реальной биржей или в mock mode. + +--- + +## 12. Обработка ошибок + +Новый canonical read API сохраняет существующую границу ошибок ExchangeService. + +Ошибки Acquisition Layer: + +- не передаются напрямую UI-потребителям; +- логируются в контексте `ticker/24hr`; +- преобразуются во внешний `ExchangeError`; +- сохраняют исходную ошибку через `__cause__`. + +Граница остаётся следующей: + + Acquisition error + ↓ + ExchangeService + ↓ + ExchangeError + ↓ + UI / diagnostics consumer + +--- + +## 13. Перевод currency_ui.py + +Файл: + +`src/telegram/ui/currency_ui.py` + +До Build 036 использовался legacy API: + +`ExchangeService.get_price()` + +Возвращаемая модель: + +`TickerPrice` + +Для расчёта использовалось: + +`ticker.price` + +После Build 036 используется: + +`ExchangeService.get_quote()` + +и каноническое поле: + +`quote.last_price` + +Целевая цепочка: + + currency_ui + ↓ + ExchangeService.get_quote() + ↓ + Quote.last_price + +Локальная логика расчёта стоимости баланса, обработка `ExchangeError` и кэширование рассчитанных цен сохранены. + +--- + +## 14. Перевод auto/ui.py + +Файл: + +`src/telegram/handlers/auto/ui.py` + +До Build 036 UI получал legacy market snapshot: + +`ExchangeService.get_market_snapshot()` + +и работал с: + +`dict[str, object]` + +Основные поля: + +- `last_price`; +- `bid_price`; +- `ask_price`. + +После Build 036 UI получает: + +`Quote` + +через: + +`ExchangeService.get_quote()` + +Используются канонические поля: + +- `quote.last_price`; +- `quote.bid_price`; +- `quote.ask_price`. + +В результате UI больше не зависит от legacy market snapshot dict для получения текущей рыночной котировки. + +--- + +## 15. Перевод debug_auto/ui.py + +Файл: + +`src/telegram/handlers/debug_auto/ui.py` + +В debug UI разделены две разные сущности: + +1. текущая рыночная котировка; +2. execution snapshot. + +После Build 036 market-секция использует: + +`Quote` + +и читает: + +- `last_price`; +- `bid_price`; +- `ask_price`; +- `source`; +- `received_at`; +- `exchange_timestamp`. + +Execution-секция продолжает использовать: + +`ExecutionPriceSnapshot` + +Это намеренное разделение. + +Build 036 не изменяет execution semantics. + +Целевая схема: + + Debug UI + ├── Market section + │ ↓ + │ Quote + │ + └── Execution section + ↓ + ExecutionPriceSnapshot + +--- + +## 16. Перевод trading/diagnostics/snapshot.py + +Файл: + +`src/trading/diagnostics/snapshot.py` + +До Build 036 диагностика использовала: + +`ExchangeService.get_market_snapshot()` + +После Build 036 используется: + +`ExchangeService.get_quote()` + +Для выбора диагностической цены сохраняется существующая семантика: + + BUY + ↓ + quote.ask_price + + SELL + ↓ + quote.bid_price + + другое состояние + ↓ + quote.last_price + +Build не изменяет торговые решения и используется только для read-only диагностики. + +--- + +## 17. Сохранённые legacy API + +Build 036 не удаляет: + +- `ExchangeService.get_price()`; +- `ExchangeService.get_market_snapshot()`; +- `ExchangeService.get_execution_snapshot()`; +- `ExchangeService.get_fresh_market_snapshot()`; +- `MarketPriceCache.get_price()`. + +Они сохраняются для ещё не переведённых compatibility-потребителей. + +Удаление legacy API возможно только после полного перевода всех зависимых компонентов и отдельной проверки использования. + +--- + +## 18. Что намеренно не изменялось + +Build 036 не затрагивает: + + src/trading/execution/pricing.py + src/trading/auto/signal_runtime.py + src/trading/auto/execution_quality.py + src/trading/strategies/trend.py + src/trading/strategies/scalp.py + src/trading/debug/execution.py + +Эти компоненты относятся к: + +- execution pricing; +- signal runtime; +- execution quality; +- strategy decisions; +- debug execution semantics. + +Их перевод должен выполняться отдельно. + +--- + +## 19. Что не входит в Build 036 + +В рамках Build 036 не выполнялись: + +- перевод execution pricing; +- перевод signal runtime; +- перевод execution quality; +- перевод торговых стратегий; +- удаление `TickerPrice`; +- удаление `get_price()`; +- удаление `get_market_snapshot()`; +- удаление legacy market snapshot dict layer; +- удаление `ExecutionPriceSnapshot`; +- удаление `MarketPriceCache`; +- удаление legacy parsing helpers. + +--- + +## 20. Проверка синтаксиса + +Выполнена команда: + + python -m py_compile \ + src/integrations/exchange/market_cache.py \ + src/integrations/exchange/service.py \ + src/telegram/ui/currency_ui.py \ + src/telegram/handlers/auto/ui.py \ + src/telegram/handlers/debug_auto/ui.py \ + src/trading/diagnostics/snapshot.py \ + tests/unit/integrations/exchange/test_market_cache.py \ + tests/unit/integrations/exchange/test_service_quote.py \ + tests/unit/telegram/ui/test_currency_ui.py + +Результат: + +`Успешно` + +Ошибок синтаксиса не обнаружено. + +--- + +## 21. Специализированные тесты + +Выполнена команда: + + python -m pytest \ + tests/unit/integrations/exchange/test_market_cache.py \ + tests/unit/integrations/exchange/test_service_quote.py \ + tests/unit/telegram/ui/test_currency_ui.py \ + -q + +Результат: + +`44 passed in 0.29s` + +--- + +## 22. Полная регрессия + +Выполнена команда: + +`python -m pytest -q` + +Результат: + +`614 passed in 0.27s` + +Полная тестовая регрессия проекта успешно пройдена. + +--- + +## 23. Итоговая архитектура после Build 036 + +После завершения Build 036 read-only и UI-контур использует следующую архитектуру: + + ┌──────────────────────────────┐ + │ currency_ui │ + │ auto UI │ + │ debug UI market section │ + │ trading diagnostics │ + └──────────────┬───────────────┘ + ↓ + ExchangeService.get_quote() + ↓ + MarketPriceCache.get_quote() + ↓ + Quote Store + ↓ + canonical Quote + +При cache miss или stale quote: + + ExchangeService.get_quote() + ↓ + REST Quotes Feed + ↓ + canonical Quote + ↓ + MarketPriceCache.set_quote() + ↓ + Quote Store + +--- + +## 24. Результат Build + +Build 036 успешно завершён. + +Достигнуты следующие результаты: + +- добавлен канонический read API `MarketPriceCache.get_quote()`; +- добавлен публичный facade `ExchangeService.get_quote()`; +- сохранены symbol validation, mock mode, freshness policy и REST fallback; +- `currency_ui.py` переведён с `TickerPrice` на `Quote`; +- `auto/ui.py` переведён с legacy market snapshot dict на `Quote`; +- market-секция debug UI переведена на `Quote`; +- execution-секция debug UI оставлена на `ExecutionPriceSnapshot`; +- trading diagnostics переведена на `Quote`; +- execution-контур не изменён; +- legacy API сохранены для последующей миграции; +- специализированные тесты успешно пройдены; +- полная регрессия успешно пройдена. + +Итог: + + До Build 036: + + UI / diagnostics + ↓ + TickerPrice / market snapshot dict + + После Build 036: + + UI / diagnostics + ↓ + ExchangeService.get_quote() + ↓ + canonical Quote + +--- + +## 25. Следующий этап + +Следующий этап миграции: + +**Build 037 — Перевод execution-потребителей** + +Его задача — отдельно проанализировать и перевести execution-sensitive потребителей канонической рыночной котировки без изменения торговой семантики и без преждевременного удаления legacy compatibility API. \ No newline at end of file diff --git a/docs/migrations/build_037.md b/docs/migrations/build_037.md new file mode 100644 index 0000000..f45c3d2 --- /dev/null +++ b/docs/migrations/build_037.md @@ -0,0 +1,608 @@ +# Build 037 — Перевод execution-потребителей на canonical Quote + +**Статус:** завершён +**Тип изменения:** миграция / архитектурный рефакторинг +**Подсистема:** Market Data / Exchange Integration / Trading Execution +**Дата завершения:** 13 июля 2026 + +--- + +## 1. Цель Build + +Цель Build 037 — перевести execution-потребителей с legacy-представлений рыночной котировки на каноническую модель `Quote`, сохранив существующее поведение торговой системы и обратную совместимость переходного периода. + +Build продолжает миграционную последовательность: + +```text +Build 031 — Quotes Feed + ↓ +Build 032 — Канонический Quote Store + ↓ +Build 033 — Перенос MarketPriceCache на Quote Store + ↓ +Build 034 — Dzengi WebSocket quote parsing и адаптер + ↓ +Build 035 — Перевод market runtime на Quotes Feed + ↓ +Build 036 — Перевод read-only и UI-потребителей + ↓ +Build 037 — Перевод execution-потребителей +``` + +После завершения Build 037 execution-контур получает рыночную котировку через канонический `Quote`, а специализированный execution boundary предоставляет типизированное представление `ExecutionPriceSnapshot`. + +--- + +## 2. Архитектурный принцип + +В рамках Build 037 зафиксировано следующее направление зависимости: + +```text +Dzengi REST / WebSocket + ↓ +Market Data Acquisition + ↓ +canonical Quote + ↓ +Quote Store + ↓ +ExchangeService + ↓ +ExecutionPriceSnapshot + ↓ +Execution consumers +``` + +Execution-потребители не должны самостоятельно: + +- разбирать сырой payload биржи; +- знать формат Dzengi REST или WebSocket; +- обращаться напрямую к `Quote Store`; +- зависеть от legacy `MarketPriceSnapshot`; +- использовать dict-based market snapshot там, где необходим типизированный execution-контракт; +- самостоятельно определять источник котировки. + +Канонический объект рыночной котировки: + +```python +Quote +``` + +Типизированное представление для execution-контура: + +```python +ExecutionPriceSnapshot +``` + +--- + +## 3. Область изменений + +В рамках Build 037 изменены следующие production-файлы: + +```text +src/integrations/exchange/service.py +src/trading/auto/execution_quality.py +src/trading/debug/execution.py +``` + +Добавлены специализированные тесты: + +```text +tests/unit/integrations/exchange/test_service_execution_quote.py +tests/unit/trading/auto/test_execution_quality.py +tests/unit/trading/debug/test_execution.py +``` + +Файл: + +```text +src/trading/execution/pricing.py +``` + +был проанализирован, но не потребовал production-изменений, поскольку уже использовал типизированный boundary: + +```python +ExchangeService().get_execution_snapshot(symbol) +``` + +--- + +## 4. Изменения в ExchangeService + +### 4.1. Execution snapshot теперь строится из canonical Quote + +Метод: + +```python +get_execution_snapshot() +``` + +переведён на получение канонической котировки через: + +```python +get_quote() +``` + +Таким образом, execution boundary больше не зависит от legacy `MarketPriceSnapshot`. + +Целевая цепочка: + +```text +Quote Store + ↓ +canonical Quote + ↓ +ExchangeService.get_quote() + ↓ +ExchangeService.get_execution_snapshot() + ↓ +ExecutionPriceSnapshot +``` + +--- + +### 4.2. Сохранён типизированный execution-контракт + +Execution-потребители получают: + +```python +ExecutionPriceSnapshot +``` + +с полями: + +```text +symbol +last_price +bid_price +ask_price +updated_at +source +is_fresh +age_seconds +``` + +Это позволяет execution-слою работать с явным типизированным контрактом вместо произвольного словаря. + +--- + +### 4.3. Сохранена семантика freshness + +При построении `ExecutionPriceSnapshot` сохраняется информация о возрасте котировки: + +```text +age_seconds +``` + +и состоянии актуальности: + +```text +is_fresh +``` + +Execution-контур продолжает использовать существующие ограничения максимального возраста котировки. + +В частности: + +```python +_max_execution_snapshot_age_seconds = 5 +``` + +остаётся execution-policy и не переносится в слой Market Data. + +Это соответствует разделению ответственности: + +```text +Market Data + предоставляет Quote и объективный возраст данных + +Execution + определяет, допустим ли этот возраст для исполнения сделки +``` + +--- + +## 5. Изменения в execution quality + +Файл: + +```text +src/trading/auto/execution_quality.py +``` + +переведён с legacy dict-based market snapshot на типизированный: + +```python +ExecutionPriceSnapshot +``` + +Ранее execution quality зависел от: + +```python +get_market_snapshot() +``` + +и извлекал значения через: + +```python +snapshot.get("bid_price") +snapshot.get("ask_price") +snapshot.get("last_price") +snapshot.get("age_seconds") +snapshot.get("is_fresh") +``` + +После Build 037 используется типизированный execution boundary: + +```python +get_execution_snapshot() +``` + +и атрибуты: + +```python +snapshot.bid_price +snapshot.ask_price +snapshot.last_price +snapshot.age_seconds +snapshot.is_fresh +``` + +Это устраняет зависимость execution quality от legacy dict-based представления котировки. + +--- + +## 6. Устранение legacy fallback через get_price() + +В execution quality существовал fallback через: + +```python +ExchangeService().get_price(...) +``` + +В рамках Build 037 этот путь устранён из execution-потребителя. + +Fallback теперь также проходит через типизированный execution boundary: + +```python +ExchangeService().get_execution_snapshot(...) +``` + +Таким образом, основной и fallback-пути используют единый контракт данных. + +Целевая схема: + +```text +canonical Quote + ↓ +ExecutionPriceSnapshot + ├── основной execution path + └── fallback execution path +``` + +--- + +## 7. Изменения debug execution + +Файл: + +```text +src/trading/debug/execution.py +``` + +переведён с: + +```python +get_fresh_market_snapshot() +``` + +на: + +```python +get_execution_snapshot() +``` + +Debug execution теперь использует тот же типизированный execution boundary, что и production execution. + +Это устраняет архитектурное расхождение: + +```text +production execution → ExecutionPriceSnapshot +debug execution → legacy dict snapshot +``` + +и заменяет его единым подходом: + +```text +production execution → ExecutionPriceSnapshot +debug execution → ExecutionPriceSnapshot +``` + +--- + +## 8. Side-aware execution pricing + +Build 037 сохраняет существующую семантику выбора цены исполнения. + +Для входа в LONG: + +```text +ask_price + ↓ fallback +last_price +``` + +Для входа в SHORT: + +```text +bid_price + ↓ fallback +last_price +``` + +Для выхода из LONG: + +```text +bid_price + ↓ fallback +last_price +``` + +Для выхода из SHORT: + +```text +ask_price + ↓ fallback +last_price +``` + +Для общего получения текущей рыночной цены: + +```text +last_price +``` + +Это поведение не изменялось в рамках Build 037. + +--- + +## 9. Что намеренно не изменялось + +Build 037 ограничен execution-потребителями. + +В рамках данного Build намеренно не переводились: + +```text +src/trading/auto/signal_runtime.py +src/trading/strategies/trend.py +src/trading/strategies/scalp.py +``` + +Эти файлы относятся к следующему этапу: + +```text +Build 038 — Перевод strategy и runtime-потребителей на canonical Quote +``` + +Также не выполнялись: + +- удаление legacy `get_market_snapshot()`; +- удаление legacy `get_fresh_market_snapshot()`; +- удаление `MarketPriceCache`; +- глобальная перестройка `ExchangeService`; +- изменение утверждённой структуры каталогов; +- изменение торговой стратегии; +- изменение execution thresholds; +- изменение логики открытия, закрытия или flip позиции. + +--- + +## 10. Обратная совместимость + +Build 037 выполнен как безопасный миграционный этап. + +Legacy API не удалялись, поскольку некоторые потребители ещё используют переходные методы. + +Сохраняются: + +```text +get_market_snapshot() +get_fresh_market_snapshot() +get_price() +MarketPriceCache +``` + +Их удаление допустимо только после полного перевода всех production-потребителей и отдельного контрольного `grep`. + +--- + +## 11. Тестовое покрытие + +Для Build 037 добавлены специализированные unit-тесты: + +```text +tests/unit/integrations/exchange/test_service_execution_quote.py +tests/unit/trading/auto/test_execution_quality.py +tests/unit/trading/debug/test_execution.py +``` + +Тестами подтверждено: + +- `get_execution_snapshot()` использует canonical `Quote`; +- execution service не зависит от legacy market snapshot; +- execution quality использует `ExecutionPriceSnapshot`; +- legacy `get_market_snapshot()` не используется новым execution quality path; +- debug execution использует `get_execution_snapshot()`; +- legacy `get_fresh_market_snapshot()` не используется новым debug execution path; +- сохраняется корректная передача `last_price`; +- сохраняется корректная передача `bid_price`; +- сохраняется корректная передача `ask_price`; +- сохраняется информация о возрасте котировки; +- сохраняется freshness semantics. + +--- + +## 12. Результаты проверки + +Проверка синтаксиса: + +```bash +python -m py_compile \ + src/integrations/exchange/service.py \ + src/trading/auto/execution_quality.py \ + src/trading/debug/execution.py \ + tests/unit/integrations/exchange/test_service_execution_quote.py \ + tests/unit/trading/auto/test_execution_quality.py \ + tests/unit/trading/debug/test_execution.py +``` + +Результат: + +```text +успешно +``` + +Специализированные тесты: + +```bash +python -m pytest \ + tests/unit/integrations/exchange/test_service_execution_quote.py \ + tests/unit/trading/auto/test_execution_quality.py \ + tests/unit/trading/debug/test_execution.py \ + -q +``` + +Результат: + +```text +4 passed in 0.08s +``` + +Полная регрессия: + +```bash +python -m pytest -q +``` + +Результат: + +```text +618 passed in 0.28s +``` + +--- + +## 13. Изменение количества тестов + +До Build 037: + +```text +614 passed +``` + +После Build 037: + +```text +618 passed +``` + +Добавлено: + +```text +4 теста +``` + +--- + +## 14. Итоговая архитектура после Build 037 + +После завершения Build 037 execution-контур выглядит следующим образом: + +```text +Dzengi REST / WebSocket + ↓ +Market Data Acquisition + ↓ +canonical Quote + ↓ +Quote Store + ↓ +ExchangeService.get_quote() + ↓ +ExchangeService.get_execution_snapshot() + ↓ +ExecutionPriceSnapshot + ├── trading/execution/pricing.py + ├── trading/auto/execution_quality.py + └── trading/debug/execution.py +``` + +Legacy market snapshot больше не является обязательным источником данных для переведённых execution-потребителей. + +--- + +## 15. Критерии завершения Build 037 + +Build 037 считается завершённым, поскольку выполнены все обязательные условия: + +- [x] `get_execution_snapshot()` строится из canonical `Quote`; +- [x] execution quality переведён на `ExecutionPriceSnapshot`; +- [x] debug execution переведён на `ExecutionPriceSnapshot`; +- [x] legacy `get_price()` устранён из изменённого execution fallback path; +- [x] `pricing.py` проверен и уже использует typed execution boundary; +- [x] side-aware pricing сохранён; +- [x] freshness semantics сохранена; +- [x] legacy API не удалены преждевременно; +- [x] специализированные тесты проходят; +- [x] полная регрессия проходит; +- [x] существующая торговая логика не изменена. + +--- + +## 16. Следующий этап + +Следующий этап миграции: + +```text +Build 038 — Перевод strategy и runtime-потребителей на canonical Quote +``` + +Основные кандидаты следующего Build: + +```text +src/trading/auto/signal_runtime.py +src/trading/strategies/trend.py +src/trading/strategies/scalp.py +``` + +Цель Build 038: + +```text +убрать зависимость strategy и runtime-потребителей +от legacy dict-based market snapshot и перевести их +на canonical Quote с сохранением существующей торговой семантики. +``` + +После Build 038 должен быть выполнен новый контрольный `grep` для определения оставшихся production-зависимостей от: + +```text +get_market_snapshot() +get_fresh_market_snapshot() +get_price() +MarketPriceCache +``` + +--- + +## 17. Статус + +```text +Build 037 — ЗАВЕРШЁН +``` + +Следующий Build: + +```text +Build 038 — Перевод strategy и runtime-потребителей на canonical Quote +``` \ No newline at end of file diff --git a/docs/migrations/build_038.md b/docs/migrations/build_038.md new file mode 100644 index 0000000..6c0ff9f --- /dev/null +++ b/docs/migrations/build_038.md @@ -0,0 +1,567 @@ +# Build 038 — Перевод strategy и runtime-потребителей на canonical Quote + +**Dzentra Market Data Migration** + +--- + +## Контроль документа + +| Свойство | Значение | +|---|---| +| Документ | Build 038 — Перевод strategy и runtime-потребителей на canonical Quote | +| Тип документа | Build Record | +| Версия | 1.0 | +| Статус | **Completed** | +| Подсистема | Market Data Acquisition | +| Проект | Dzentra | +| Язык | Русский | +| Предыдущий этап | Build 037 — Перевод execution-потребителей | +| Результат полной регрессии | **626 passed** | + +--- + +## 1. Назначение Build 038 + +Build 038 завершает перевод выбранных strategy- и runtime-потребителей с legacy snapshot API на каноническую модель рыночной котировки: + +```text +Quote +``` + +Цель этапа — исключить использование словарных market snapshot в следующих потребителях: + +```text +src/trading/auto/signal_runtime.py +src/trading/strategies/trend.py +src/trading/strategies/scalp.py +``` + +и перевести их на единый типизированный источник текущей рыночной котировки: + +```text +ExchangeService.get_quote() + ↓ +Quote +``` + +--- + +## 2. Контекст предыдущих Build + +Build 038 является продолжением последовательной миграции Market Data: + +```text +Build 031 + ↓ +Canonical Quote model + +Build 032 + ↓ +Quote acquisition pipeline + +Build 033 + ↓ +Quote Store + +Build 034 + ↓ +Dzengi WebSocket quote parsing и adapter + +Build 035 + ↓ +Market runtime переведён на Quotes Feed + +Build 036 + ↓ +Read-only и UI-потребители переведены на canonical Quote + +Build 037 + ↓ +Execution-потребители переведены на typed execution snapshot + +Build 038 + ↓ +Strategy и runtime-потребители переведены на canonical Quote +``` + +В результате Build 038 каноническая модель `Quote` становится непосредственным источником текущей рыночной котировки для выбранных runtime- и strategy-компонентов. + +--- + +## 3. Каноническая модель Quote + +Используется модель: + +```python +@dataclass(frozen=True, slots=True) +class Quote: + symbol: str + + last_price: Decimal + bid_price: Decimal + ask_price: Decimal + + exchange_timestamp: datetime | None + received_at: datetime + + source: str +``` + +Модель расположена в: + +```text +src/market_data/acquisition/models/quote.py +``` + +Основные свойства модели: + +- независимость от конкретного поставщика рыночных данных; +- типизированные цены через `Decimal`; +- отсутствие словарного доступа к ценовым полям; +- наличие времени биржи; +- наличие времени получения данных; +- явное указание источника. + +--- + +## 4. Изменённые runtime-потребители + +### 4.1. Auto Signal Runtime + +Файл: + +```text +src/trading/auto/signal_runtime.py +``` + +Legacy-путь: + +```text +ExchangeService.get_market_snapshot() + ↓ +dict[str, object] + ↓ +snapshot.get("bid_price") +snapshot.get("ask_price") +snapshot.get("last_price") +``` + +Новый путь: + +```text +ExchangeService.get_quote() + ↓ +Quote + ↓ +quote.bid_price +quote.ask_price +quote.last_price +``` + +В результате runtime больше не зависит от словарного представления текущей рыночной котировки. + +--- + +## 5. Изменённые strategy-потребители + +### 5.1. Trend Strategy + +Файл: + +```text +src/trading/strategies/trend.py +``` + +Стратегия переведена с legacy market snapshot на canonical `Quote`. + +Новый источник: + +```text +ExchangeService.get_quote( + symbol, + runtime_key="auto", +) +``` + +Ценовые данные теперь читаются непосредственно из типизированной модели: + +```text +quote.last_price +quote.bid_price +quote.ask_price +``` + +Сохранена существующая логика выбора цены анализа: + +```text +1. midpoint между bid и ask; +2. last_price; +3. 0.0 при отсутствии пригодной цены. +``` + +Midpoint остаётся предпочтительной ценой анализа: + +```text +(bid + ask) / 2 +``` + +Это позволяет уменьшить зависимость анализа от случайного последнего исполнения сделки. + +--- + +### 5.2. Scalp Strategy + +Файл: + +```text +src/trading/strategies/scalp.py +``` + +Стратегия также переведена на: + +```text +ExchangeService.get_quote( + symbol, + runtime_key="auto", +) +``` + +Ценовые поля теперь получаются через: + +```text +quote.last_price +quote.bid_price +quote.ask_price +``` + +Сохранены: + +- существующая логика определения analysis price; +- midpoint между bid и ask; +- fallback на `last_price`; +- safe fallback; +- существующие strategy payload; +- существующие торговые пороги; +- существующая логика принятия решений. + +--- + +## 6. Обработка Decimal + +Canonical `Quote` использует: + +```text +Decimal +``` + +для полей: + +```text +last_price +bid_price +ask_price +``` + +Существующие strategy helper-функции `_safe_float()` ранее принимали: + +```text +NumericLike | None +``` + +где `Decimal` не входил в контракт `NumericLike`. + +Это приводило к ошибкам статической типизации Pylance: + +```text +Аргумент типа "Decimal" нельзя присвоить параметру "value" +типа "NumericLike | None" +``` + +В рамках Build 038 контракт локальных strategy helper-функций расширен до: + +```python +value: NumericLike | Decimal | None +``` + +Глобальный тип: + +```text +NumericLike +``` + +не изменялся. + +Это сохраняет локальность изменения и не расширяет общий типовой контракт проекта без необходимости. + +Архитектурная граница имеет вид: + +```text +canonical Quote + ↓ +Decimal + ↓ +strategy helper + ↓ +float + ↓ +существующие strategy calculations и payload +``` + +--- + +## 7. Что намеренно не изменялось + +Build 038 не изменяет: + +- структуру canonical `Quote`; +- `QuoteStore`; +- acquisition pipeline; +- WebSocket adapter; +- REST adapter; +- market runtime producer; +- execution pricing semantics; +- торговые пороги; +- логику открытия позиции; +- логику закрытия позиции; +- flip-логику; +- risk management; +- journal payload contracts; +- strategy payload keys; +- Telegram UI; +- структуру каталогов проекта. + +Build 038 является локальным этапом миграции потребителей, а не изменением торговой логики. + +--- + +## 8. Контроль legacy-зависимостей + +После выполнения Build 038 выполнен контрольный поиск: + +```bash +grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "get_market_snapshot\(|get_fresh_market_snapshot\(|get_execution_snapshot\(|get_price\(|snapshot\.get\(|quote\.get\(|runtime_key" \ + src/trading/auto/signal_runtime.py \ + src/trading/strategies/trend.py \ + src/trading/strategies/scalp.py +``` + +Результат: + +```text +src/trading/auto/signal_runtime.py:881: runtime_key="auto", +src/trading/strategies/trend.py:244: runtime_key="auto", +src/trading/strategies/scalp.py:64: runtime_key="auto", +``` + +Legacy-вызовы не обнаружены. + +В проверенных файлах отсутствуют рабочие зависимости от: + +```text +get_market_snapshot() +get_fresh_market_snapshot() +get_execution_snapshot() +get_price() +snapshot.get(...) +quote.get(...) +``` + +Оставшиеся: + +```text +runtime_key="auto" +``` + +являются ожидаемой частью вызова `get_quote()` и не представляют legacy-зависимость. + +--- + +## 9. Проверка синтаксиса + +Выполнена проверка: + +```bash +python -m py_compile \ + src/trading/auto/signal_runtime.py \ + src/trading/strategies/trend.py \ + src/trading/strategies/scalp.py +``` + +Результат: + +```text +успешно +``` + +Ошибки синтаксиса отсутствуют. + +--- + +## 10. Целевые тесты + +После исправления типизации `Decimal` выполнены целевые тесты: + +```bash +python -m pytest \ + tests/unit/trading/strategies/test_trend_quote.py \ + tests/unit/trading/strategies/test_scalp_quote.py \ + -q +``` + +Результат: + +```text +...... [100%] + +6 passed in 0.09s +``` + +--- + +## 11. Полная регрессия + +Выполнена полная проверка проекта: + +```bash +python -m pytest -q +``` + +Результат: + +```text +626 passed in 0.31s +``` + +Регрессий не обнаружено. + +Для сравнения: + +```text +После Build 037: 618 passed +После Build 038: 626 passed +``` + +Количество тестов увеличено на: + +```text +8 +``` + +--- + +## 12. Итоговая архитектура после Build 038 + +После завершения Build 038 путь текущей рыночной котировки для мигрированных strategy- и runtime-потребителей выглядит следующим образом: + +```text +Dzengi WebSocket + ↓ +DzengiWebSocketQuoteAdapter + ↓ +canonical Quote + ↓ +Quote Store + ↓ +ExchangeService.get_quote() + ├── Auto Signal Runtime + ├── Trend Strategy + └── Scalp Strategy +``` + +Для execution-контура сохраняется специализированная типизированная граница, введённая в Build 037: + +```text +canonical Quote + ↓ +ExchangeService + ↓ +ExecutionPriceSnapshot + ↓ +Execution consumers +``` + +Таким образом, после Build 038 разделены два типа потребления: + +```text +Market / Strategy / Runtime + ↓ +canonical Quote + +Execution + ↓ +ExecutionPriceSnapshot +``` + +--- + +## 13. Архитектурный результат + +Build 038 устраняет ещё один слой legacy market snapshot API из рабочего торгового контура. + +До Build 038: + +```text +Market Data + ↓ +legacy dict snapshot + ↓ +snapshot.get(...) + ↓ +runtime / strategies +``` + +После Build 038: + +```text +Market Data + ↓ +canonical Quote + ↓ +typed attributes + ↓ +runtime / strategies +``` + +Это обеспечивает: + +- единый канонический контракт текущей котировки; +- статическую типизацию; +- отказ от строковых ключей для доступа к ценам; +- явную работу с `Decimal`; +- уменьшение зависимости trading layer от legacy exchange representations; +- подготовку к дальнейшему удалению legacy snapshot API. + +--- + +## 14. Критерии завершения + +Build 038 считается завершённым, поскольку выполнены все критерии: + +- [x] `signal_runtime.py` переведён на canonical `Quote`; +- [x] `trend.py` переведён на canonical `Quote`; +- [x] `scalp.py` переведён на canonical `Quote`; +- [x] legacy `get_market_snapshot()` удалён из мигрированных путей; +- [x] словарный доступ `snapshot.get(...)` к текущей котировке удалён; +- [x] типизация `Decimal` обработана явно; +- [x] глобальный `NumericLike` не изменён; +- [x] существующая торговая логика сохранена; +- [x] strategy payload contracts сохранены; +- [x] `py_compile` проходит успешно; +- [x] целевые тесты проходят успешно; +- [x] полная регрессия проходит успешно; +- [x] итоговый результат — **626 passed**. + +--- + +## 15. Статус + +```text +Build 038: COMPLETED +``` + +Build 038 завершён и зафиксирован. + +Система готова к следующему этапу миграции. \ No newline at end of file diff --git a/docs/migrations/greps.txt b/docs/migrations/greps.txt new file mode 100644 index 0000000..0735675 --- /dev/null +++ b/docs/migrations/greps.txt @@ -0,0 +1,246 @@ +((.venv) ) segeba@mbpbsg dzentra_bot % >.... + elif isinstance(data.get("payload"), dict): + payload = data["payload"] + if isinstance(payload.get("symbols"), list): + symbols = payload["symbols"] + +print("Количество symbols:", len(symbols) if symbols is not None else None) + +if symbols: + dict_items = [item for item in symbols if isinstance(item, dict)] + + all_keys = sorted( + { + key + for item in dict_items + for key in item + } + ) + + print("Все ключи symbol items:") + for key in all_keys: + print(f" {key}") + + print("\nПервый symbol item:") + print(json.dumps(dict_items[0], ensure_ascii=False, indent=2)) +PY +Корневой тип: dict +Корневые ключи: ['exchangeFilters', 'rateLimits', 'serverTime', 'symbols', 'timezone'] +Количество symbols: 51 +Все ключи symbol items: + assetType + baseAsset + baseAssetPrecision + country + filters + industry + longRate + marketModes + marketType + maxSLGap + maxTPGap + minSLGap + minTPGap + name + orderTypes + quoteAsset + quoteAssetId + quotePrecision + sector + shortRate + status + swapChargeInterval + symbol + tickSize + tickValue + tradingFee + tradingHours + +Первый symbol item: +{ + "assetType": "CRYPTOCURRENCY", + "baseAsset": "ETH", + "baseAssetPrecision": 3, + "country": "", + "filters": [ + { + "filterType": "LOT_SIZE", + "maxQty": "1000", + "minQty": "0.001", + "stepSize": "0.001" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "2" + } + ], + "industry": "", + "longRate": -0.01, + "marketModes": [ + "REGULAR" + ], + "marketType": "LEVERAGE", + "maxSLGap": 50.0, + "maxTPGap": 50.0, + "minSLGap": 0, + "minTPGap": 0, + "name": "ETH/EUR", + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP" + ], + "quoteAsset": "EUR", + "quoteAssetId": "EUR_LEVERAGE", + "quotePrecision": 3, + "sector": "", + "shortRate": 0.01, + "status": "TRADING", + "swapChargeInterval": 480, + "symbol": "ETH/EUR_LEVERAGE", + "tickSize": 0.01, + "tickValue": 18.3415, + "tradingFee": 0.06, + "tradingHours": "UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 05:00, 07:00 - 21:00, 21:05 -; Sun - 21:00, 21:05 -" +} + + +((.venv) ) segeba@mbpbsg dzentra_bot % >.... +filter_keys: dict[str, set[str]] = {} + +for item in symbols: + if not isinstance(item, dict): + continue + + filters = item.get("filters") + if not isinstance(filters, list): + continue + + for entry in filters: + if not isinstance(entry, dict): + continue + + filter_type = str(entry.get("filterType") or "") + filter_types[filter_type] += 1 + filter_keys.setdefault(filter_type, set()).update( + str(key) for key in entry + ) + +print("Типы filters:") +for filter_type, count in sorted(filter_types.items()): + print(f"{filter_type}: {count}") + print(" keys:", sorted(filter_keys[filter_type])) +PY +Типы filters: +LOT_SIZE: 51 + keys: ['filterType', 'maxQty', 'minQty', 'stepSize'] +MIN_NOTIONAL: 39 + keys: ['filterType', 'minNotional'] + + +python - <<'PY' +import json +from collections import Counter +from pathlib import Path + +path = Path( + "app/tools/dzengi_probe/runtime_samples/rest/exchangeInfo/all.json" +) + +data = json.loads(path.read_text(encoding="utf-8")) + +if isinstance(data, dict) and isinstance(data.get("symbols"), list): + symbols = data["symbols"] +elif ( + isinstance(data, dict) + and isinstance(data.get("payload"), dict) + and isinstance(data["payload"].get("symbols"), list) +): + symbols = data["payload"]["symbols"] +else: + raise SystemExit("symbols не найдены") + +fields = [ + "symbol", + "name", + "status", + "baseAsset", + "quoteAsset", + "marketModes", + "marketType", + "tickSize", + "stepSize", + "minQty", + "minNotional", + "filters", +] + +for field in fields: + present = 0 + non_empty = 0 + types = Counter() + + for item in symbols: + if not isinstance(item, dict): + continue + + if field in item: + present += 1 + value = item[field] + types[type(value).__name__] += 1 + + if value not in (None, "", [], {}): + non_empty += 1 + + print( + f"{field}: present={present}, " + f"non_empty={non_empty}, " + f"types={dict(types)}" + ) +PY +symbol: present=51, non_empty=51, types={'str': 51} +name: present=51, non_empty=51, types={'str': 51} +status: present=51, non_empty=51, types={'str': 51} +baseAsset: present=51, non_empty=51, types={'str': 51} +quoteAsset: present=51, non_empty=51, types={'str': 51} +marketModes: present=51, non_empty=51, types={'list': 51} +marketType: present=51, non_empty=51, types={'str': 51} +tickSize: present=51, non_empty=51, types={'float': 48, 'int': 3} +stepSize: present=0, non_empty=0, types={} +minQty: present=0, non_empty=0, types={} +minNotional: present=0, non_empty=0, types={} +filters: present=51, non_empty=51, types={'list': 51} +((.venv) ) segeba@mbpbsg dzentra_bot % ;2B + + +((.venv) ) segeba@mbpbsg dzentra_bot % ;2Bgrep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "from src\.telegram\.handlers\.market import|import src\.telegram\.handlers\.market|include_router\(.*market|market\.router|handlers\.market" \ + app/src app/tests tests 2>/dev/null + +((.venv) ) segeba@mbpbsg dzentra_bot % grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "include_router|include_routers" \ + app/src \ + | grep -Ei "market|router" +app/src/telegram/routers.py:16: dispatcher.include_router(start_router) +app/src/telegram/routers.py:17: dispatcher.include_router(home_router) +app/src/telegram/routers.py:18: dispatcher.include_router(portfolio_router) +app/src/telegram/routers.py:19: dispatcher.include_router(auto_router) +app/src/telegram/routers.py:20: dispatcher.include_router(journal_router) +app/src/telegram/routers.py:21: dispatcher.include_router(debug_auto_router) +app/src/telegram/routers.py:22: dispatcher.include_router(debug_router) +app/src/telegram/routers.py:23: dispatcher.include_router(system_router) +app/src/telegram/handlers/auto/__init__.py:8:router.include_router(main_router) +app/src/telegram/handlers/auto/__init__.py:9:router.include_router(risk_router) +((.venv) ) segeba@mbpbsg dzentra_bot % + +((.venv) ) segeba@mbpbsg dzentra_bot % grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "from src\.telegram\.ui\.currency_ui import|import src\.telegram\.ui\.currency_ui" \ + app/src app/tests tests 2>/dev/null +app/src/telegram/handlers/market.py:28:from src.telegram.ui.currency_ui import format_usd_amount +app/src/telegram/handlers/portfolio.py:26:from src.telegram.ui.currency_ui import ( \ No newline at end of file diff --git a/docs/migrations/instrument_reference_data_migration.md b/docs/migrations/instrument_reference_data_migration.md new file mode 100644 index 0000000..e7d1743 --- /dev/null +++ b/docs/migrations/instrument_reference_data_migration.md @@ -0,0 +1,201 @@ +# Dzentra --- Instrument Reference Data Migration Plan + +> Статус: Утверждённый базовый план миграции + +## Общая стратегия + +Миграция выполняется постепенно без нарушения работы существующего бота. + +Основные принципы: + +- не переписывать подсистему целиком; +- не удалять legacy-код до полного перевода потребителей; +- сохранять публичные интерфейсы `ExchangeService`; +- не создавать параллельную бизнес-логику без плана удаления; +- разделять Acquisition, Validation, Storage и Processing; +- выполнять миграцию небольшими проверяемыми Build. + +------------------------------------------------------------------------ + +## План Build + + Build Цель Совместимость + ------- ---------------------------------------------------- ------------------------ + 001 Внутренняя модель Instrument Reference Data Без изменения runtime + 002 Raw-модели ответа Dzengi Полная + 003 Структурная валидация exchangeInfo Полная + 004 Parser exchangeInfo Полная + 005 Value validation Полная + 006 Mapper Dzengi → Instrument Полная + 007 Protocol и Exceptions Полная + 008 Dzengi REST Adapter Полная + 009 Instrument Handler Полная + 010 Instrument Feed Полная + 011 Registry Полная + 012 Acquisition Service Полная + 013 Проверка эквивалентности старой и новой реализации Полная + 014 Compatibility mapper Instrument → ExchangeSymbol Полная + 015 Переключение get_exchange_symbols() Полная + 016 Перевод normalize_symbol()/symbol_candidates() Полная + 017 Переключение validate_symbol() Полная + 018 Переключение get_symbol_runtime_status() Полная + 019 Подготовка переноса кэша в Storage Полная + 020 Перенос кэша Полная + 021 Перевод первой группы потребителей Полная + 022 Перевод валидации и runtime-статусов на канонический Instrumen + 023 Перевод рыночных runtime-потребителей на канонический Instrument + 024 Удаление неиспользуемого legacy Market Handler + 025 Удаление ExchangeSymbol compatibility layer и завершение миграции + +------------------------------------------------------------------------ + +## Зависимости + +``` text +001 + ↓ +002 + ↓ +003 + ↓ +004 + ↓ +005 + ↓ +006 + ↓ +007 + ↓ +008 + ↓ +009 + ↓ +010 + ↓ +011 + ↓ +012 + ↓ +013 + ↓ +014 + ↓ +015 + ├──→016→017 + └──→018 + ↓ +019→020 + ↓ +021→022+ + ↓ +023 + ↓ +024 + ↓ +025 +``` + +------------------------------------------------------------------------ + +## Точки обратной совместимости + +До завершения миграции должны сохраняться: + +- `ExchangeService.get_exchange_symbols()` +- `ExchangeService.validate_symbol()` +- `ExchangeService.get_symbol_runtime_status()` + +Также сохраняются: + +- сигнатуры методов; +- старые импорты; +- формат ошибок; +- Telegram UI; +- автоторговля; +- существующее поведение runtime; +- совместимость `ExchangeSymbol` через временный compatibility mapper. + +------------------------------------------------------------------------ + +## Классификация изменений + + Тип Значение + -------------------------------------- ---------------------------------- + Исправление ошибки Исправляет неверную работу + Обязательное архитектурное изменение Необходимо для новой архитектуры + Улучшение надёжности Не меняет наблюдаемое поведение + Изменение поведения Требует отдельного согласования + +------------------------------------------------------------------------ + +## Улучшения допускаются + +Допускается: + +- усиление типизации; +- безопасная обработка неполных данных; +- исправление ошибок parser; +- корректная обработка filters; +- улучшение надёжности REST; +- отделение транспортной логики от предметной; +- тестируемые контракты. + +Недопустимо без согласования: + +- изменение поведения; +- изменение TTL кэша; +- изменение правил валидации; +- изменение логики UI. + +------------------------------------------------------------------------ + +## Build 001 + +### Цель + +Создать независимую внутреннюю модель Instrument Reference Data. + +На этом этапе: + +- не переносится parser; +- не создаётся REST adapter; +- не меняется ExchangeService; +- не меняется runtime; +- не переносится кэш. + +### Для начала Build 001 необходимо получить + +Обязательно: + +1. модель `ExchangeSymbol`; +2. `SymbolValidationResult`; +3. `normalize_symbol()`; +4. `symbol_candidates()`; +5. `validate_symbol()`; +6. `integrations/exchange/service.py`; +7. текущий parser `exchangeInfo`; +8. текущий mapper (если существует); +9. модели ответа `exchangeInfo`; +10. REST-клиент `exchangeInfo`; +11. обработку filters/status/marketModes. + +Также нужны результаты `grep` по использованию этих сущностей и, если +существуют, соответствующие тесты. + +------------------------------------------------------------------------ + +## Обязательные правила + +После каждого Build: + +1. анализ; +2. внесение изменений только текущего этапа; +3. обновление тестов; +4. проверка импортов; +5. проверка синтаксиса; +6. запуск тестов; +7. запуск приложения; +8. проверка обратной совместимости. + +Только после успешного завершения Build допускается переход к следующему +этапу. diff --git a/docs/migrations/Вывод grep по дополнительным полям.txt b/docs/migrations/Вывод grep по дополнительным полям.txt new file mode 100644 index 0000000..cc9d6b8 --- /dev/null +++ b/docs/migrations/Вывод grep по дополнительным полям.txt @@ -0,0 +1,70 @@ +((.venv) ) segeba@mbpbsg dzentra_bot % grep -RIn \ + --exclude-dir="__pycache__" \ + --exclude="*.pyc" \ + -E "pricePrecision|quantityPrecision|baseAssetPrecision|quoteAssetPrecision|maxQty|maxNotional|maxPrice|minPrice|contractSize|lotSize|tradingMode|tradeMode|availableForTrading|isTradingAllowed|tradingAllowed|isTradable|tradable|marketOpen|isOpen|enabled" \ + app/src app/tests tests docs 2>/dev/null +app/src/core/config.py:34: exchange_enabled: bool +app/src/core/config.py:52: debug_enabled: bool +app/src/core/config.py:53: journal_debug_enabled: bool +app/src/core/config.py:94: debug_enabled=_parse_bool(os.getenv("DEBUG_ENABLED", "false")), +app/src/core/config.py:95: journal_debug_enabled=_parse_bool( +app/src/core/config.py:100: exchange_enabled=_parse_bool(os.getenv("EXCHANGE_ENABLED", "false")), +app/src/integrations/exchange/service.py:79: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:145: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:473: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:651: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:687: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:788: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:820: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:879: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:954: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:1017: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:1061: if not self.settings.exchange_enabled: +app/src/integrations/exchange/service.py:1185: "isTradingAllowed", +app/src/integrations/exchange/service.py:1186: "tradingAllowed", +app/src/integrations/exchange/service.py:1187: "availableForTrading", +app/src/integrations/exchange/service.py:1188: "isTradable", +app/src/integrations/exchange/service.py:1189: "tradable", +app/src/integrations/exchange/service.py:1191: "marketOpen", +app/src/integrations/exchange/service.py:1192: "isOpen", +app/src/integrations/exchange/service.py:1193: "enabled", +app/src/integrations/exchange/service.py:1208: for key in ("tradingMode", "tradeMode", "mode", "state"): +app/src/integrations/exchange/service.py:1279: if not self.settings.exchange_enabled: +app/src/integrations/exchange/market_stream.py:134: if not settings.exchange_enabled: +app/src/integrations/exchange/status.py:170: reason="market_not_tradable", +app/src/telegram/handlers/system.py:758:def _journal_debug_enabled() -> bool: +app/src/telegram/handlers/system.py:759: return bool(load_settings().journal_debug_enabled) +app/src/telegram/handlers/system.py:786: if _journal_debug_enabled(): +app/src/telegram/handlers/system.py:818: if _journal_debug_enabled() +app/src/telegram/handlers/system.py:839: enabled = _journal_debug_enabled() +app/src/telegram/handlers/system.py:840: new_value = "false" if enabled else "true" +app/src/telegram/handlers/system.py:849: if enabled +app/src/telegram/handlers/system.py:855: "journal_debug_enabled": not enabled, +app/src/telegram/handlers/system.py:864: "Debug логирование выключено" if enabled else "Debug логирование включено" +app/src/telegram/handlers/auto/ui.py:1104: enabled: list[tuple[str, float]] = [] +app/src/telegram/handlers/auto/ui.py:1107: enabled.append(("SL", sl_value)) +app/src/telegram/handlers/auto/ui.py:1110: enabled.append(("TP", tp_value)) +app/src/telegram/handlers/auto/ui.py:1113: enabled.append(("ML", ml_value)) +app/src/telegram/handlers/auto/ui.py:1115: if len(enabled) == 1: +app/src/telegram/handlers/auto/ui.py:1116: key, value = enabled[0] +app/src/telegram/handlers/debug.py:23:def _debug_enabled() -> bool: +app/src/telegram/handlers/debug.py:24: return bool(load_settings().debug_enabled) +app/src/telegram/handlers/debug.py:62: if not _debug_enabled(): +app/src/telegram/handlers/debug.py:71: if not _debug_enabled(): +app/src/telegram/handlers/debug.py:222: if not _debug_enabled(): +app/src/telegram/handlers/debug.py:318: if not _debug_enabled(): +app/src/telegram/handlers/debug.py:338: if not _debug_enabled(): +app/src/telegram/handlers/debug.py:376: if not _debug_enabled(): +app/src/telegram/handlers/debug.py:397: if not _debug_enabled(): +app/src/trading/journal/service.py:128: if not load_settings().journal_debug_enabled: +app/src/trading/journal/service.py:148: if not load_settings().journal_debug_enabled: +app/src/trading/market_intelligence/common/models.py:47: enabled_by_default: bool = True +docs/stages/stage-03_1-integration_mock.md:25:- exchange_enabled +docs/stages/stage-04_1-storage.md:78:- exchange_enabled +docs/market_intelligence/information/dzengi_market_data_inventory.md:731:| `baseAssetPrecision` | integer | Точность базового актива. | +docs/market_intelligence/information/dzengi_market_data_inventory.md:753:| `maxQty` | string | Максимальное количество. | +docs/market_intelligence/information/dzengi_openapi.json:1:{"swagger":"2.0","info":{"title":"Api Documentation","termsOfService":"https://dzengi.com/agreement"},"host":"https://api-adapter.dzengi.com/ ]\n[ Base demo URL: https://demo-api-adapter.dzengi.com","basePath":"/","tags":[{"name":"rest-api","description":"Rest API"},{"name":"websocket-api","description":"WebSocket API"}],"paths":{"/api/v1/account":{"get":{"tags":["rest-api"],"summary":"accountInfo","description":"Get current account information","operationId":"accountUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"showZeroBalance","in":"query","description":"showZeroBalance","required":false,"type":"boolean"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/AccountResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/aggTrades":{"get":{"tags":["rest-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"aggTradesUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}","schema":{"type":"array","items":{"$ref":"#/definitions/AggTrades"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/closeTradingPosition":{"post":{"tags":["rest-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"closeTradingPositionUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/currencies":{"get":{"tags":["rest-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"getCurrenciesUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}","schema":{"$ref":"#/definitions/CurrencyResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/depositAddress":{"get":{"tags":["rest-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"getDepositAddressUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"coin","in":"query","description":"coin","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/deposits":{"get":{"tags":["rest-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"getDepositsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/depth":{"get":{"tags":["rest-api"],"summary":"orderBook","description":"Order book","operationId":"depthUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}","schema":{"$ref":"#/definitions/DepthResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/exchangeInfo":{"get":{"tags":["rest-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"exchangeInfoUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":false,"type":"string"},{"name":"signature","in":"query","description":"signature","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/fetchOrder":{"get":{"tags":["rest-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"getOrderUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/fundingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"getFundingLimitsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/FundingLimitsDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/klines":{"get":{"tags":["rest-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"klinesUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"interval","in":"query","description":"interval","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"priceType","in":"query","description":"priceType","required":false,"type":"string","default":"bid"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"},{"name":"type","in":"query","description":"type","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/ledger":{"get":{"tags":["rest-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"getLedgerUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/leverageSettings":{"get":{"tags":["rest-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"leverageSettingsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/myTrades":{"get":{"tags":["rest-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"myTradesUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/openOrders":{"get":{"tags":["rest-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"openOrdersUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/order":{"post":{"tags":["rest-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"orderUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"newOrderRespType","in":"query","description":"newOrderRespType in the exchange trading mode for MARKET order RESULT or FULL can be mentioned. MARKET order type default to FULL. LIMIT order type can be only RESULT. For the leverage trading mode only RESULT is available.","required":false,"type":"string","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"type","in":"query","description":"Type MARKET or LIMIT should be mentioned to open an order in the exchange trading mode. Type MARKET, LIMIT or STOP should be mentioned to open an order in the leverage trading mode.","required":true,"type":"string","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"accountId","in":"query","description":"accountId","required":false,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean"},{"name":"leverage","in":"query","description":"leverage","required":false,"type":"integer","format":"int32"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"quantity","in":"query","description":"quantity","required":true,"type":"number"},{"name":"side","in":"query","description":"side","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["rest-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"putEditOrderUsingPUT_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["rest-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"cancelOrderUsingDELETE_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/CancelOrderResponse"}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/api/v1/ticker/24hr":{"get":{"tags":["rest-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"ticker_24hrUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/time":{"get":{"tags":["rest-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"timeUsingGET_1","produces":["*/*"],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}","schema":{"$ref":"#/definitions/ServerTime"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingFees":{"get":{"tags":["rest-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"getTradingFeesUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingFeesResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"getTradingLimitsUsingGET_1","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingLimitsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingPositions":{"get":{"tags":["rest-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"tradingPositionsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionListResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/tradingPositionsHistory":{"get":{"tags":["rest-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"tradingPositionsHistoryUsingGET_1","produces":["*/*"],"parameters":[{"name":"from","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"to","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/transactions":{"get":{"tags":["rest-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"getTransactionsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/updateTradingOrder":{"post":{"tags":["rest-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingOrderUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"newPrice","in":"query","description":"newPrice","required":false,"type":"number"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/updateTradingPosition":{"post":{"tags":["rest-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingPositionUsingPOST_1","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string","format":"uuid"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v1/withdrawals":{"get":{"tags":["rest-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"getWithdrawalsUsingGET_1","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/account":{"get":{"tags":["rest-api"],"summary":"accountInfo","description":"Get current account information","operationId":"accountUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"showZeroBalance","in":"query","description":"showZeroBalance","required":false,"type":"boolean"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/AccountResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/aggTrades":{"get":{"tags":["rest-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"aggTradesUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}","schema":{"type":"array","items":{"$ref":"#/definitions/AggTrades"}}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/closeTradingPosition":{"post":{"tags":["rest-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"closeTradingPositionUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/currencies":{"get":{"tags":["rest-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"getCurrenciesUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}","schema":{"$ref":"#/definitions/CurrencyResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/depositAddress":{"get":{"tags":["rest-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"getDepositAddressUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"coin","in":"query","description":"coin","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/deposits":{"get":{"tags":["rest-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"getDepositsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/depth":{"get":{"tags":["rest-api"],"summary":"orderBook","description":"Order book","operationId":"depthUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}","schema":{"$ref":"#/definitions/DepthResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/exchangeInfo":{"get":{"tags":["rest-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"exchangeInfoUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":false,"type":"string"},{"name":"signature","in":"query","description":"signature","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/fetchOrder":{"get":{"tags":["rest-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"getOrderUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/fundingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"getFundingLimitsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/FundingLimitsDtoResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/klines":{"get":{"tags":["rest-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"klinesUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"interval","in":"query","description":"interval","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"priceType","in":"query","description":"priceType","required":false,"type":"string","default":"bid"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"},{"name":"type","in":"query","description":"type","required":false,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/ledger":{"get":{"tags":["rest-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"getLedgerUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/leverageSettings":{"get":{"tags":["rest-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"leverageSettingsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/myTrades":{"get":{"tags":["rest-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"myTradesUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/openOrders":{"get":{"tags":["rest-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"openOrdersUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/order":{"post":{"tags":["rest-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"orderUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"newOrderRespType","in":"query","description":"newOrderRespType in the exchange trading mode for MARKET order RESULT or FULL can be mentioned. MARKET order type default to FULL. LIMIT order type can be only RESULT. For the leverage trading mode only RESULT is available.","required":false,"type":"string","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"type","in":"query","description":"Type MARKET or LIMIT should be mentioned to open an order in the exchange trading mode. Type MARKET, LIMIT or STOP should be mentioned to open an order in the leverage trading mode.","required":true,"type":"string","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"accountId","in":"query","description":"accountId","required":false,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean"},{"name":"leverage","in":"query","description":"leverage","required":false,"type":"integer","format":"int32"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"quantity","in":"query","description":"quantity","required":true,"type":"number"},{"name":"side","in":"query","description":"side","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"put":{"tags":["rest-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"putEditOrderUsingPUT","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"price","in":"query","description":"price","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}},"delete":{"tags":["rest-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"cancelOrderUsingDELETE","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":true,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}","schema":{"$ref":"#/definitions/CancelOrderResponse"}},"204":{"description":"No Content"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"}}}},"/api/v2/ticker/24hr":{"get":{"tags":["rest-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"ticker_24hrUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}","schema":{"type":"object"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/time":{"get":{"tags":["rest-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"timeUsingGET","produces":["*/*"],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}","schema":{"$ref":"#/definitions/ServerTime"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingFees":{"get":{"tags":["rest-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"getTradingFeesUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingFeesResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingLimits":{"get":{"tags":["rest-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"getTradingLimitsUsingGET","produces":["*/*"],"parameters":[{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TradingLimitsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingPositions":{"get":{"tags":["rest-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"tradingPositionsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionListResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/tradingPositionsHistory":{"get":{"tags":["rest-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"tradingPositionsHistoryUsingGET","produces":["*/*"],"parameters":[{"name":"from","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"symbol","in":"query","description":"Symbol - In order to receive orders within an ‘exchange’ trading mode ‘symbol’ parameter value from the exchangeInfo endpoint: ‘BTC%2FUSD’.\nIn order to mention the right symbolLeverage it should be checked with the ‘symbol’ parameter value from the exchangeInfo endpoint. In case ‘symbol’ has currencies in its name then the following format should be used: ‘BTC%2FUSD_LEVERAGE’. In case ‘symbol’ has only an asset name then for the leverage trading mode the following format is correct: ‘Oil%20-%20Brent.’","required":false,"type":"string","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"to","in":"query","description":"Timestamp in milliseconds, Filtration based on execTimestamp parameter","required":false,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/transactions":{"get":{"tags":["rest-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"getTransactionsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/updateTradingOrder":{"post":{"tags":["rest-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingOrderUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"expireTimestamp","in":"query","description":"expireTimestamp","required":false,"type":"integer","format":"int64"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"newPrice","in":"query","description":"newPrice","required":false,"type":"number"},{"name":"orderId","in":"query","description":"orderId","required":true,"type":"string"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/updateTradingPosition":{"post":{"tags":["rest-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"updateTradingPositionUsingPOST","consumes":["application/json"],"produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"guaranteedStopLoss","in":"query","description":"guaranteedStopLoss","required":false,"type":"boolean","default":false},{"name":"positionId","in":"query","description":"positionId","required":true,"type":"string","format":"uuid"},{"name":"profitDistance","in":"query","description":"profitDistance","required":false,"type":"number"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"stopDistance","in":"query","description":"stopDistance","required":false,"type":"number"},{"name":"stopLoss","in":"query","description":"stopLoss","required":false,"type":"number"},{"name":"takeProfit","in":"query","description":"takeProfit","required":false,"type":"number"},{"name":"trailingStopLoss","in":"query","description":"trailingStopLoss","required":false,"type":"boolean","default":false}],"responses":{"200":{"description":"Example:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}},"201":{"description":"Created"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"/api/v2/withdrawals":{"get":{"tags":["rest-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"getWithdrawalsUsingGET","produces":["*/*"],"parameters":[{"name":"recvWindow","in":"query","description":"recvWindow cannot be greater than 60000","required":false,"type":"integer","default":5000,"format":"int64","allowEmptyValue":false},{"name":"timestamp","in":"query","description":"Timestamp in milliseconds","required":true,"type":"integer","format":"int64","allowEmptyValue":false},{"name":"X-MBX-APIKEY","in":"header","description":"X-MBX-APIKEY","required":true,"type":"string"},{"name":"endTime","in":"query","description":"endTime","required":false,"type":"integer","format":"int64"},{"name":"limit","in":"query","description":"limit","required":false,"type":"integer","default":10,"format":"int32"},{"name":"signature","in":"query","description":"signature","required":true,"type":"string"},{"name":"startTime","in":"query","description":"startTime","required":false,"type":"integer","format":"int64"}],"responses":{"200":{"description":"Example:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}","schema":{"$ref":"#/definitions/TransactionsResponse"}},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Not Found"}}}},"wss:/api/v1/account":{"get":{"tags":["websocket-api"],"summary":"accountInfo","description":"Get current account information","operationId":"websocketmethods_53","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AccountRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/AccountResponse"}}}}},"wss:/api/v1/aggTrades":{"get":{"tags":["websocket-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"websocketmethods_7","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AggTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AggTradesResponse"}}}}},"wss:/api/v1/closeTradingPosition":{"get":{"tags":["websocket-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"websocketmethods_13","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CloseTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}}}}},"wss:/api/v1/currencies":{"get":{"tags":["websocket-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"websocketmethods_20","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/CurrencyResponse"}}}}},"wss:/api/v1/depositAddress":{"get":{"tags":["websocket-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"websocketmethods_3","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BlockchainAddressRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}\n","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}}}}},"wss:/api/v1/deposits":{"get":{"tags":["websocket-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"websocketmethods_4","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v1/depth":{"get":{"tags":["websocket-api"],"summary":"orderBook","description":"Order book","operationId":"websocketmethods_12","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/DepthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/DepthResponse"}}}}},"wss:/api/v1/exchangeInfo":{"get":{"tags":["websocket-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"websocketmethods_22","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/OptionalAuthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/ExchangeInfo"}}}}},"wss:/api/v1/fetchOrder":{"get":{"tags":["websocket-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"websocketmethods","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/GetOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}}}}},"wss:/api/v1/fundingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"websocketmethods_54","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/FundingLimitsDtoResponseWS"}}}}},"wss:/api/v1/klines":{"get":{"tags":["websocket-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"websocketmethods_15","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/KLinesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}\n","schema":{"$ref":"#/definitions/KLinesResponse"}}}}},"wss:/api/v1/ledger":{"get":{"tags":["websocket-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"websocketmethods_2","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v1/leverageSettings":{"get":{"tags":["websocket-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"websocketmethods_19","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/LeverageSettingsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}\n","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}}}}},"wss:/api/v1/myTrades":{"get":{"tags":["websocket-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"websocketmethods_14","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AllMyTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AllMyTradesResponse"}}}}},"wss:/api/v1/openOrders":{"get":{"tags":["websocket-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"websocketmethods_9","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedBySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/OpenOrdersReponse"}}}}},"wss:/api/v1/order/cancel":{"get":{"tags":["websocket-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"websocketmethods_52","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CancelOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/CancelOrderResponse"}}}}},"wss:/api/v1/order/create":{"get":{"tags":["websocket-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"websocketmethods_21","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CreateOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}}}}},"wss:/api/v1/order/edit":{"get":{"tags":["websocket-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"websocketmethods_51","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EditExchangeOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}}}}},"wss:/api/v1/ticker/24hr":{"get":{"tags":["websocket-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"websocketmethods_8","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}\n","schema":{"$ref":"#/definitions/Ticker24HResponse"}}}}},"wss:/api/v1/time":{"get":{"tags":["websocket-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"websocketmethods_17","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EmptyRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}\n","schema":{"$ref":"#/definitions/ServerTime"}}}}},"wss:/api/v1/tradingFees":{"get":{"tags":["websocket-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"websocketmethods_10","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingFeesResponseWS"}}}}},"wss:/api/v1/tradingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"websocketmethods_55","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingLimitsResponseWS"}}}}},"wss:/api/v1/tradingPositions":{"get":{"tags":["websocket-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"websocketmethods_6","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionListResponse"}}}}},"wss:/api/v1/tradingPositionsHistory":{"get":{"tags":["websocket-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"websocketmethods_11","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/PositionHistoryRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}}}}},"wss:/api/v1/transactions":{"get":{"tags":["websocket-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"websocketmethods_5","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v1/updateTradingOrder":{"get":{"tags":["websocket-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_18","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}}}}},"wss:/api/v1/updateTradingPosition":{"get":{"tags":["websocket-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_1","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}}}}},"wss:/api/v1/withdrawals":{"get":{"tags":["websocket-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"websocketmethods_16","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/account":{"get":{"tags":["websocket-api"],"summary":"accountInfo","description":"Get current account information","operationId":"websocketmethods_33","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AccountRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"makerCommission\":0.20,\n \"takerCommission\":0.20,\n \"buyerCommission\":0.20,\n \"sellerCommission\":0.20,\n \"canTrade\":true,\n \"canWithdraw\":true,\n \"canDeposit\":true,\n \"updateTime\":1586935521,\n \"balances\":[\n {\n \"accountId\":\"2376104765040206\",\n \"collateralCurrency\":true,\n \"asset\":\"BYN\",\n \"free\":0.0,\n \"locked\":0.0,\n \"default\":false\n },\n {\n \"accountId\":\"2376109060084932\",\n \"collateralCurrency\":true,\n \"asset\":\"USD\",\n \"free\":515.59092523,\n \"locked\":0.0,\n \"default\":true\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/AccountResponse"}}}}},"wss:/api/v2/aggTrades":{"get":{"tags":["websocket-api"],"summary":"tradesAggregated","description":"Get compressed, aggregate trades. Trades that fill at the same time, from the same order, with the same price will have the quantity aggregated.","operationId":"websocketmethods_46","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AggTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"//a\":\"Aggregate tradeId\",\n \"a\":1582595833,\n \"//p\":\"Price\",\n \"p\":\"8980.4\",\n \"//q\":\"Quantity (should be ignored)\",\n \"q\":\"0.0\",\n \"//T\":\"Timestamp\",\n \"T\":1580204505793,\n \"//m\":\"Was the buyer the maker\",\n \"m\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AggTradesResponse"}}}}},"wss:/api/v2/closeTradingPosition":{"get":{"tags":["websocket-api"],"summary":"tradingPositionClose","description":"Close an active leverage trade.","operationId":"websocketmethods_30","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CloseTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"request\":[\n {\n \"id\":242057,\n \"accountId\":2376109060084932,\n \"instrumentId\":\"45076691096786116\",\n \"rqType\":\"ORDER_NEW\",\n \"state\":\"PROCESSED\",\n \"createdTimestamp\":1587031306969\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionCloseAllResponse"}}}}},"wss:/api/v2/currencies":{"get":{"tags":["websocket-api"],"summary":"ListOfCurrencies","description":"Get all system currencies","operationId":"websocketmethods_37","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"name\": \"US Dollar\",\n \"displaySymbol\": \"USD.cx\",\n \"precision\": 2,\n \"type\": \"FIAT\",\n \"minWithdrawal\": 100,\n \"maxWithdrawal\": 100000000,\n \"commissionMin\": 0.02,\n \"commissionPercent\": 1.5,\n \"minDeposit\": 100\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/CurrencyResponse"}}}}},"wss:/api/v2/depositAddress":{"get":{"tags":["websocket-api"],"summary":"stringOfAddress","description":"Get deposit address by coin","operationId":"websocketmethods_32","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BlockchainAddressRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":{\n \"address\": \"0xa12b8b8157da0e44d3e56cda7ade1d587141c27f\"\n }\n\n}\n","schema":{"$ref":"#/definitions/BlockchainAddressGetResponse"}}}}},"wss:/api/v2/deposits":{"get":{"tags":["websocket-api"],"summary":"ListOfDeposits","description":"Get deposits for user","operationId":"websocketmethods_24","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n\t\t \"id\": 77170270,\n \"balance\": 100000.0,\n \t\"amount\": 100000.0,\n \"currency\": \"BYN\",\n \"type\": \"deposit\",\n \t\"timestamp\": 1647000860502,\n \t\"commission\": 3500.0,\n \t\"paymentMethod\": \"VISA\",\n \t\"status\": \"PROCESSED\"\n \t }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/depth":{"get":{"tags":["websocket-api"],"summary":"orderBook","description":"Order book","operationId":"websocketmethods_49","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/DepthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"lastUpdateId\":1027024,\n \"asks\":[\n [\n \"//Price\",\n \"4.00000200\",\n \"//Qty\",\n \"12.00000000\"\n ]\n ],\n \"bids\":[\n [\n \"// Price\",\n \"4.00000000\",\n \"// Quantity\",\n \"431.00000000\"\n ]\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/DepthResponse"}}}}},"wss:/api/v2/exchangeInfo":{"get":{"tags":["websocket-api"],"summary":"exchangeInfo","description":"Current exchange trading rules and symbol information. When using signature parameter returns the market pairs which are traded under the account's jurisdiction. Also note that when sending an authorized request and using the X-MBX-API-KEY header timestamp and signature parameters are mandatory.","operationId":"websocketmethods_40","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/OptionalAuthRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"timezone\":\"UTC\",\n \"serverTime\":1628193845310,\n \"rateLimits\":[\n ],\n \"exchangeFilters\":[\n ],\n \"symbols\":[\n {\n \"symbol\":\"EVK\",\n \"name\":\"Evonik\",\n \"status\":\"BREAK\",\n \"baseAsset\":\"EVK\",\n \"baseAssetPrecision\":3,\n \"quoteAsset\":\"EUR\",\n \"quoteAssetId\":\"EUR\",\n \"quotePrecision\":3,\n \"orderTypes\":[\n \"LIMIT\",\n \"MARKET\"\n ],\n \"filters\":[\n {\n \"filterType\":\"LOT_SIZE\",\n \"minQty\":\"1\",\n \"maxQty\":\"27000\",\n \"stepSize\":\"1\"\n },\n {\n \"filterType\":\"MIN_NOTIONAL\",\n \"minNotional\":\"29\"\n }\n ],\n \"marketModes\":[\n \"REGULAR\"\n ],\n \"marketType\":\"SPOT\",\n \"country\":\"DE\",\n \"sector\":\"Basic Materials\",\n \"industry\":\"Diversified Chemicals\",\n \"tradingHours\":\"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30\",\n \"tickSize\":0.005,\n \"tickValue\":0.14475,\n \"exchangeFee\":0.05\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/ExchangeInfo"}}}}},"wss:/api/v2/fetchOrder":{"get":{"tags":["websocket-api"],"summary":"Order","description":"Fetch order by symbol and order id","operationId":"websocketmethods_42","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/GetOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"accountId\":19042209961170116,\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\",\n \"quantity\":1.0,\n \"price\":95.0,\n \"timestamp\":1651072423560,\n \"status\":\"CREATED\",\n \"type\":\"LIMIT\",\n \"expireTime\":2208988800000,\n \"timeInForceType\":\"GTC\",\n \"side\":\"BUY\",\n \"guaranteedStopLoss\":true,\n \"margin\":0.05,\n \"takeProfit\":25.0,\n \"takeProfitType\":\"OFFSET\",\n \"stopLoss\":-15.0,\n \"stopLossType\":\"OFFSET\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/GetOrderDtoResponse"}}}}},"wss:/api/v2/fundingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfFundingLimits","description":"Get all system Funding limits","operationId":"websocketmethods_43","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"TOKENISED ASSETS\",\n \t\"minWithdrawal\": \"100 USD equivalent\"\n },\n {\n \t\"paymentOption\": \"CRYPTO\",\n \t\"accountCurrency\": \"BAT\",\n \t\"minWithdrawal\": \"52\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/FundingLimitsDtoResponseWS"}}}}},"wss:/api/v2/klines":{"get":{"tags":["websocket-api"],"summary":"klines","description":"Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.","operationId":"websocketmethods_27","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/KLinesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n [\n \"// Open time\",\n 1499040000000,\n \" // Open\",\n \"0.01634790\",\n \" // High\",\n \"0.80000000\",\n \" // Low\",\n \"0.01575800\",\n \" // Close\",\n \"0.01577100\",\n \" // Volume.\",\n \"148976.11427815\"\n ]\n ]\n\n}\n","schema":{"$ref":"#/definitions/KLinesResponse"}}}}},"wss:/api/v2/ledger":{"get":{"tags":["websocket-api"],"summary":"ListOfLedgers","description":"Get ledger by limit","operationId":"websocketmethods_47","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 77753629,\n\t \"balance\": 20423.49571214,\n\t \"amount\": -0.002601,\n\t \"currency\": \"USD\",\n\t \"type\": \"exchange_commission\",\n\t \"timestamp\": 1647609091989,\n\t \"commission\": 0.002601,\n \"status\": \"PROCESSED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/leverageSettings":{"get":{"tags":["websocket-api"],"summary":"leverageSettings","description":"General leverage settings can be seen.","operationId":"websocketmethods_31","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/LeverageSettingsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"values\":[\n 2,\n 5,\n 10,\n 20,\n 50,\n 100,\n \" // the possible leverage sizes;\"\n ],\n \"//value\":\"depicts a default leverage size which will be set in case you don’t mention the ‘leverage’ parameter in the corresponding requests.\",\n \"value\":20\n }\n\n}\n","schema":{"$ref":"#/definitions/LeverageSettingsResponse"}}}}},"wss:/api/v2/myTrades":{"get":{"tags":["websocket-api"],"summary":"listOfTrades","description":"Get trades for a specific account and symbol.","operationId":"websocketmethods_48","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/AllMyTradesRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0004-0000-00000006f0a2\",\n \"price\":\"9593.2\",\n \"qty\":\"0.1\",\n \"commission\":\"0.20\",\n \"commissionAsset\":\"USD\",\n \"time\":1582192427437,\n \"maker\":false,\n \"buyer\":true,\n \"isBuyer\":true,\n \"isMaker\":false\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/AllMyTradesResponse"}}}}},"wss:/api/v2/openOrders":{"get":{"tags":["websocket-api"],"summary":"listOfOpenOrders","description":"Get all open orders within exchange and leverage trading modes on a symbol. Careful when accessing this with no symbol.","operationId":"websocketmethods_26","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedBySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"NEW\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\",\n \"time\":1586958863147,\n \"updateTime\":1586958863147,\n \"leverage\":false,\n \"working\":true\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/OpenOrdersReponse"}}}}},"wss:/api/v2/order/cancel":{"get":{"tags":["websocket-api"],"summary":"cancelOrder","description":"Cancel an active order within exchange and leverage trading modes.","operationId":"websocketmethods_41","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CancelOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0002-0000-0000000b3302\",\n \"price\":\"6600\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.0\",\n \"status\":\"CANCELED\",\n \"timeInForce\":\"GTC\",\n \"type\":\"LIMIT\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/CancelOrderResponse"}}}}},"wss:/api/v2/order/create":{"get":{"tags":["websocket-api"],"summary":"createOrder","description":"To create a market or limit order in the exchange trading mode, and market, limit or stop order in the leverage trading mode.\nPlease note that to open an order within the ‘leverage’ trading mode symbolLeverage should be used and additional accountId parameter should be mentioned in the request.","operationId":"websocketmethods_44","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/CreateOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00000000-0000-0000-0000-0000000c028d\",\n \"transactTime\":1589879478020,\n \"price\":\"9797.05500000\",\n \"origQty\":\"0.01\",\n \"executedQty\":\"0.01\",\n \"status\":\"FILLED\",\n \"timeInForce\":\"FOK\",\n \"type\":\"MARKET\",\n \"side\":\"BUY\"\n }\n\n}\n","schema":{"$ref":"#/definitions/NewOrderResponseRESULT"}}}}},"wss:/api/v2/order/edit":{"get":{"tags":["websocket-api"],"summary":"Edit exchange order","description":"Edit exchange order expirationTime or price","operationId":"websocketmethods_50","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EditExchangeOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":[\n {\n \"orderId\":\"00a0c503-0079-54c4-0000-0000803400c0\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/EditExchangeOrderResponse"}}}}},"wss:/api/v2/ticker/24hr":{"get":{"tags":["websocket-api"],"summary":"priceChange","description":"24 hour rolling window price change statistics. Careful when accessing this with no symbol.","operationId":"websocketmethods_25","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/BySymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"symbol\":\"LTC/USD\",\n \"priceChange\":\"0.88\",\n \"priceChangePercent\":\"1.49\",\n \"weightedAvgPrice\":\"59.29\",\n \"prevClosePrice\":\"58.37\",\n \"lastPrice\":\"59.25\",\n \"lastQty\":\"220.0\",\n \"bidPrice\":\"59.25\",\n \"askPrice\":\"59.32\",\n \"openPrice\":\"58.37\",\n \"highPrice\":\"61.39\",\n \"lowPrice\":\"58.37\",\n \"volume\":\"22632\",\n \"quoteVolume\":\"440.0\",\n \"openTime\":1580169600000,\n \"closeTime\":1580205307222\n }\n\n}\n","schema":{"$ref":"#/definitions/Ticker24HResponse"}}}}},"wss:/api/v2/time":{"get":{"tags":["websocket-api"],"summary":"serverTime","description":"Test connectivity to the API and get the current server time.","operationId":"websocketmethods_28","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/EmptyRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"3\",\n \"payload\":{\n \"serverTime\":1628195607917\n }\n\n}\n","schema":{"$ref":"#/definitions/ServerTime"}}}}},"wss:/api/v2/tradingFees":{"get":{"tags":["websocket-api"],"summary":"ListOfFees","description":"Get all system fees","operationId":"websocketmethods_38","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"UNI/USD\",\n \"name\": \"UNI/USD\",\n \"fee\": 0.1\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingFeesResponseWS"}}}}},"wss:/api/v2/tradingLimits":{"get":{"tags":["websocket-api"],"summary":"ListOfLimits","description":"Get all system limits","operationId":"websocketmethods_35","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SymbolRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"symbol\": \"EVK\",\n \t\"name\": \"Evonik\",\n \t\"minVolume\": 1.0,\n \t\"maxVolume\": 27000.0,\n \t\t\"minStep\": 1.0,\n \t\"tickSize\": 0.005\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TradingLimitsResponseWS"}}}}},"wss:/api/v2/tradingPositions":{"get":{"tags":["websocket-api"],"summary":"listOfLeverageTrades","description":"Get all open trades within the account.","operationId":"websocketmethods_39","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SignedRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"positions\":[\n {\n \"accountId\":2376109060084932,\n \"id\":\"00a02503-0079-54c4-0000-00004067006b\",\n \"instrumentId\":\"45076691096786116\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004067006a\",\n \"openQuantity\":0.01,\n \"openPrice\":6734.4,\n \"closeQuantity\":0.0,\n \"closePrice\":0,\n \"takeProfit\":7999.15,\n \"stopLoss\":5999.15,\n \"guaranteedStopLoss\":false,\n \"rpl\":0,\n \"rplConverted\":0,\n \"swap\":-0.00335894,\n \"swapConverted\":-0.00335894,\n \"fee\":-0.050508,\n \"dividend\":0,\n \"margin\":0.5,\n \"state\":\"ACTIVE\",\n \"currency\":\"USD\",\n \"createdTimestamp\":1586953061455,\n \"openTimestamp\":1586953061243,\n \"cost\":33.73775,\n \"symbol\":\"BTC/USD_LEVERAGE\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionListResponse"}}}}},"wss:/api/v2/tradingPositionsHistory":{"get":{"tags":["websocket-api"],"summary":"listOfHistoricalPositions","description":"Get all closes postions within the account.","operationId":"websocketmethods_36","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/PositionHistoryRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"history\":[\n {\n \"accountId\":19039018800469188,\n \"accountCurrency\":\"USD\",\n \"positionId\":\"00a18509-0079-54c4-0000-00004062007b\",\n \"currency\":\"USD\",\n \"executionType\":\"IOC\",\n \"quantity\":-0.1,\n \"price\":44.95,\n \"source\":\"USER\",\n \"status\":\"CLOSED\",\n \"rpl\":-0.002,\n \"rplConverted\":-0.002,\n \"fee\":0,\n \"createdTimestamp\":1606999328398,\n \"execTimestamp\":1606999315265,\n \"symbol\":\"Oil - Crude.\"\n }\n ]\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionHistoryResponse"}}}}},"wss:/api/v2/transactions":{"get":{"tags":["websocket-api"],"summary":"ListOfTransactions","description":"Get transactions by limit and sinceTime","operationId":"websocketmethods_23","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:/api/v2/updateTradingOrder":{"get":{"tags":["websocket-api"],"summary":"leverageOrdersEdit","description":"Edit current leverage orders by changing take profit and stop loss levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_29","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingOrderRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":241986,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingOrderUpdateResponse"}}}}},"wss:/api/v2/updateTradingPosition":{"get":{"tags":["websocket-api"],"summary":"leverageTradeEdit","description":"Edit current leverage trade by changing stop loss and take profit levels. Please note that in case guaranteedStopLoss or trailingStopLoss values are not mentioned in the request then they are set to false automatically.","operationId":"websocketmethods_45","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/UpdateTradingPositionRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"requestId\":242040,\n \"state\":\"PROCESSED\"\n }\n\n}\n","schema":{"$ref":"#/definitions/TradingPositionUpdateResponse"}}}}},"wss:/api/v2/withdrawals":{"get":{"tags":["websocket-api"],"summary":"ListOfWithdrawals","description":"Get withdrawals for user","operationId":"websocketmethods_34","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/TransactionsRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n{\n\n \"status\": \"OK\",\n \"correlationId\": \"2\",\n \"payload\":[\n {\n \"id\": 12225003,\n \"balance\": 19759.5292569,\n \"amount\": -100,\n \"currency\": \"dEUR\",\n \"timestamp\": 1562831860753,\n \"commission\": 4.6,\n \"paymentMethod\": \"MASTERCARD\",\n \"status\": \"DECLINED\"\n }\n ]\n\n}\n","schema":{"$ref":"#/definitions/TransactionsResponse"}}}}},"wss:OHLCMarketData.subscribe":{"get":{"tags":["websocket-api"],"summary":"OHLCMarketData","description":"OHLC market data stream","operationId":"websocketmethods_58","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/OHLCSubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"correlationId\":\"2\",\n \"payload\":{\n \"status\":\"OK\",\n \"Destination\":\"ohlc.event\",\n \"Payload\":{\n \"interval\":\"1m\",\n \"symbol\":\"TS\",\n \"T\":1597850100000,\n \"H\":11.89,\n \"L\":11.88,\n \"O\":11.89,\n \"C\":11.89\n }\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}},"wss:depthMarketData.subscribe":{"get":{"tags":["websocket-api"],"summary":"DepthMarketData","description":"Depth market data stream","operationId":"websocketmethods_56","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"Destination\":\"marketdepth.event\",\n \"Payload\":{\n \"Data\":{\n \"ts\":1597849462575,\n \"Bid\":{\n \"2\":25,\n \"1.94\":25.9\n },\n \"Ofr\":{\n \"3.3\":1,\n \"2.627\":6.1\n }\n },\n \"symbol\":\"Natural Gas\"\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}},"wss:marketData.subscribe":{"get":{"tags":["websocket-api"],"summary":"MarketData","description":"Market data stream","operationId":"websocketmethods_57","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"Destination\":\"internal.quote\",\n \"Payload\":{\n \"symbolName\":\"TXN\",\n \"bid\":139.85,\n \"bidQty\":2500,\n \"ofr\":139.92000000000002,\n \"ofrQty\":2500,\n \"timestamp\":1597850971558\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}},"wss:ping":{"get":{"tags":["websocket-api"],"summary":"ping","description":"Ping pong","operationId":"websocketmethods_60","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/PingRequest"}}],"responses":{"200":{"description":"This stream results in the following response:\n {} \n","schema":{"$ref":"#/definitions/PingResponse"}}}}},"wss:trades.subscribe":{"get":{"tags":["websocket-api"],"summary":"Trades","description":"Trades stream","operationId":"websocketmethods_59","parameters":[{"in":"body","name":"request","description":"query parameter","required":true,"schema":{"$ref":"#/definitions/SubscribeRequest"}}],"responses":{"200":{"description":"This subscription produces the following events:\n{\n\n \"status\":\"OK\",\n \"destination\":\"internal.trade\",\n \"payload\":{\n \"price\":11400.95,\n \"size\":0.058,\n \"id\":1616651347,\n \"ts\":1596625079952,\n \"symbol\":\"BTC/USD\",\n \"orderId\":\"00a02503-0079-54c4-0000-00004020316a\",\n \"clientOrderId\":\"00a02503-0079-54c4-0000-482f00003a06\",\n \"buyer\":true\n }\n\n}","schema":{"$ref":"#/definitions/SubscribeResponse"}}}}}},"definitions":{"AccountBalance":{"type":"object","properties":{"accountId":{"type":"string"},"asset":{"type":"string"},"collateralCurrency":{"type":"boolean"},"default":{"type":"boolean"},"free":{"type":"number"},"locked":{"type":"number"}},"title":"AccountBalance"},"AccountRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"showZeroBalance":{"type":"boolean"},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"AccountRequest"},"AccountResponse":{"type":"object","properties":{"affiliateId":{"type":"string"},"balances":{"type":"array","items":{"$ref":"#/definitions/AccountBalance"}},"buyerCommission":{"type":"number"},"canDeposit":{"type":"boolean"},"canTrade":{"type":"boolean"},"canWithdraw":{"type":"boolean"},"makerCommission":{"type":"number"},"sellerCommission":{"type":"number"},"takerCommission":{"type":"number"},"updateTime":{"type":"integer","format":"int64"},"userId":{"type":"integer","format":"int64"}},"title":"AccountResponse"},"AggTrades":{"type":"object","properties":{"T":{"type":"integer","format":"int64"},"a":{"type":"integer","format":"int64"},"m":{"type":"boolean"},"p":{"type":"string"},"q":{"type":"string"}},"title":"AggTrades"},"AggTradesRequest":{"type":"object","required":["symbol"],"properties":{"endTime":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"startTime":{"type":"integer","format":"int64"},"symbol":{"type":"string"}},"title":"AggTradesRequest"},"AggTradesResponse":{"type":"object","properties":{"aggTrades":{"type":"array","items":{"$ref":"#/definitions/AggTrades"}}},"title":"AggTradesResponse"},"AllMyTradesRequest":{"type":"object","required":["apiKey","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"endTime":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"startTime":{"type":"integer","format":"int64"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"AllMyTradesRequest"},"AllMyTradesResponse":{"type":"object","properties":{"myTrades":{"type":"array","items":{"$ref":"#/definitions/MyTradesResponse"}}},"title":"AllMyTradesResponse"},"BlockchainAddressGetResponse":{"type":"object","properties":{"address":{"type":"string"},"addressLegacy":{"type":"string"},"destinationTag":{"type":"string"}},"title":"BlockchainAddressGetResponse"},"BlockchainAddressRequest":{"type":"object","required":["apiKey","coin","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"coin":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"BlockchainAddressRequest"},"BySymbolRequest":{"type":"object","properties":{"symbol":{"type":"string"}},"title":"BySymbolRequest"},"CancelOrderRequest":{"type":"object","required":["apiKey","orderId","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"orderId":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"CancelOrderRequest"},"CancelOrderResponse":{"type":"object","properties":{"executedQty":{"type":"string"},"orderId":{"type":"string"},"origQty":{"type":"string"},"price":{"type":"string"},"side":{"type":"string","enum":["BUY","SELL"]},"status":{"type":"string","enum":["CANCELED","EXPIRED","FILLED","NEW","PARTIALLY_FILLED","PENDING_CANCEL","REJECTED"]},"symbol":{"type":"string"},"timeInForce":{"type":"string","enum":["FOK","GTC","IOC"]},"type":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]}},"title":"CancelOrderResponse"},"CloseTradingPositionRequest":{"type":"object","required":["apiKey","positionId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"positionId":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"CloseTradingPositionRequest"},"CreateOrderRequest":{"type":"object","required":["apiKey","quantity","side","signature","symbol","timestamp","type"],"properties":{"accountId":{"type":"integer","format":"int64"},"apiKey":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"leverage":{"type":"integer","format":"int32"},"newOrderRespType":{"type":"string"},"price":{"type":"number"},"profitDistance":{"type":"number"},"quantity":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"side":{"type":"string"},"signature":{"type":"string"},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string"}},"title":"CreateOrderRequest"},"CurrencyDtoResponse":{"type":"object","properties":{"commissionFixed":{"type":"number"},"commissionMin":{"type":"number"},"commissionPercent":{"type":"number"},"displaySymbol":{"type":"string"},"maxWithdrawal":{"type":"number"},"minDeposit":{"type":"number"},"minWithdrawal":{"type":"number"},"name":{"type":"string"},"precision":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["CRYPTO","EXCHANGE_TOKEN","FIAT","ICO","TOKEN","TOKENISED_SECURITY","UTILITY_TOKENS"]}},"title":"CurrencyDtoResponse"},"CurrencyResponse":{"type":"object","properties":{"currencies":{"type":"array","items":{"$ref":"#/definitions/CurrencyDtoResponse"}}},"title":"CurrencyResponse"},"DepthRequest":{"type":"object","required":["symbol"],"properties":{"limit":{"type":"integer","format":"int32"},"symbol":{"type":"string"}},"title":"DepthRequest"},"DepthResponse":{"type":"object","properties":{"asks":{"type":"array","items":{"type":"array","items":{"type":"number"}}},"bids":{"type":"array","items":{"type":"array","items":{"type":"number"}}},"lastUpdateId":{"type":"integer","format":"int64"}},"title":"DepthResponse"},"EditExchangeOrderRequest":{"type":"object","required":["apiKey","orderId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"orderId":{"type":"string"},"price":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"EditExchangeOrderRequest"},"EditExchangeOrderResponse":{"type":"object","properties":{"orderId":{"type":"string","format":"uuid"}},"title":"EditExchangeOrderResponse"},"EmptyRequest":{"type":"object","title":"EmptyRequest"},"ExchangeFilter":{"type":"object","title":"ExchangeFilter"},"ExchangeInfo":{"type":"object","properties":{"exchangeFilters":{"type":"array","items":{"$ref":"#/definitions/ExchangeFilter"}},"rateLimits":{"type":"array","items":{"$ref":"#/definitions/RateLimits"}},"serverTime":{"type":"integer","format":"int64"},"symbols":{"type":"array","items":{"$ref":"#/definitions/ExchangeSymbolInfo"}},"timezone":{"type":"string"}},"title":"ExchangeInfo"},"ExchangeSymbolInfo":{"type":"object","properties":{"assetType":{"type":"string","enum":["BOND","COMMODITY","CREDIT","CRYPTOCURRENCY","CURRENCY","EQUITY","ICO","INDEX","INTEREST_RATE","OPT_TOKENS","OTHER_ASSET","REAL_ESTATE","UTILITY_TOKENS"]},"baseAsset":{"type":"string"},"baseAssetPrecision":{"type":"integer","format":"int32"},"country":{"type":"string"},"exchangeFee":{"type":"number"},"filters":{"type":"array","items":{"$ref":"#/definitions/SymbolFilter"}},"industry":{"type":"string"},"longRate":{"type":"number","format":"double"},"makerFee":{"type":"number"},"marketModes":{"type":"array","items":{"type":"string","enum":["CLOSED_FOR_CORPORATE_ACTION","CLOSE_ONLY","DELISTING","HOLIDAY","LONG_ONLY","REGULAR","UNKNOWN","VIEW_AND_REQUEST","VIEW_ONLY"]}},"marketType":{"type":"string","enum":["LEVERAGE","SPOT"]},"maxSLGap":{"type":"number"},"maxTPGap":{"type":"number"},"minSLGap":{"type":"number"},"minTPGap":{"type":"number"},"name":{"type":"string"},"orderTypes":{"type":"array","items":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]}},"quoteAsset":{"type":"string"},"quoteAssetId":{"type":"string"},"quotePrecision":{"type":"integer","format":"int32"},"sector":{"type":"string"},"shortRate":{"type":"number","format":"double"},"status":{"type":"string","enum":["AUCTION_MATCH","BREAK","END_OF_DAY","HALT","POST_TRADING","PRE_TRADING","TRADING"]},"swapChargeInterval":{"type":"integer","format":"int64"},"symbol":{"type":"string"},"takerFee":{"type":"number"},"tickSize":{"type":"number"},"tickValue":{"type":"number"},"tradingFee":{"type":"number"},"tradingHours":{"type":"string"}},"title":"ExchangeSymbolInfo"},"FundingLimitsDtoResponse":{"type":"object","properties":{"accountCurrency":{"type":"string"},"minWithdrawal":{"type":"string"},"paymentOption":{"type":"string"}},"title":"FundingLimitsDtoResponse"},"FundingLimitsDtoResponseWS":{"type":"object","properties":{"fundingLimits":{"type":"array","items":{"$ref":"#/definitions/FundingLimitsDtoResponse"}}},"title":"FundingLimitsDtoResponseWS"},"GetOrderDtoResponseReq":{"type":"object","properties":{"accountId":{"type":"integer","format":"int64"},"execPrice":{"type":"number"},"execQuantity":{"type":"number"},"expireTime":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"margin":{"type":"number","format":"double"},"orderId":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"number"},"rejectReason":{"type":"string"},"side":{"type":"string"},"status":{"type":"string"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"symbolAndReturn":{"type":"string"},"takeProfit":{"type":"number"},"timeInForceType":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string"}},"title":"GetOrderDtoResponseReq"},"GetOrderDtoResponseRes":{"type":"object","properties":{"accountId":{"type":"integer","format":"int64"},"execPrice":{"type":"number"},"execQuantity":{"type":"number"},"expireTime":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"margin":{"type":"number","format":"double"},"orderId":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"number"},"rejectReason":{"type":"string"},"side":{"type":"string"},"status":{"type":"string"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"timeInForceType":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string"}},"title":"GetOrderDtoResponseRes"},"GetOrderRequest":{"type":"object","required":["apiKey","orderId","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"orderId":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"GetOrderRequest"},"InternalQuote":{"type":"object","properties":{"bid":{"type":"number","format":"double"},"bidQty":{"type":"number","format":"double"},"ofr":{"type":"number","format":"double"},"ofrQty":{"type":"number","format":"double"},"symbolName":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"InternalQuote"},"KLinesRequest":{"type":"object","required":["interval","symbol"],"properties":{"endTime":{"type":"integer","format":"int64"},"interval":{"type":"string"},"limit":{"type":"integer","format":"int32"},"priceType":{"type":"string"},"startTime":{"type":"integer","format":"int64"},"symbol":{"type":"string"},"type":{"type":"string"}},"title":"KLinesRequest"},"KLinesResponse":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"array","items":{"type":"object"}}}},"title":"KLinesResponse"},"LeverageSettingsRequest":{"type":"object","required":["apiKey","signature","symbol","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"LeverageSettingsRequest"},"LeverageSettingsResponse":{"type":"object","properties":{"value":{"type":"integer","format":"int32"},"values":{"type":"array","items":{"type":"integer","format":"int32"}}},"title":"LeverageSettingsResponse"},"MarketDepthData":{"type":"object","properties":{"bid":{"type":"object","additionalProperties":{"type":"number"}},"ofr":{"type":"object","additionalProperties":{"type":"number"}},"ts":{"type":"integer","format":"int64"}},"title":"MarketDepthData"},"MarketDepthEvent":{"type":"object","properties":{"data":{"$ref":"#/definitions/MarketDepthData"},"symbol":{"type":"string"}},"title":"MarketDepthEvent"},"MyTradesResponse":{"type":"object","properties":{"buyer":{"type":"boolean"},"commission":{"type":"string"},"commissionAsset":{"type":"string"},"id":{"type":"string"},"isBuyer":{"type":"boolean"},"isMaker":{"type":"boolean"},"maker":{"type":"boolean"},"orderId":{"type":"string"},"price":{"type":"string"},"qty":{"type":"string"},"quoteQty":{"type":"string"},"symbol":{"type":"string"},"time":{"type":"integer","format":"int64"}},"title":"MyTradesResponse"},"NewOrderResponseRESULT":{"type":"object","properties":{"executedQty":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"margin":{"type":"number"},"orderId":{"type":"string"},"origQty":{"type":"string"},"price":{"type":"string"},"profitDistance":{"type":"number"},"rejectMessage":{"type":"string"},"side":{"type":"string","enum":["BUY","SELL"]},"status":{"type":"string","enum":["CANCELED","EXPIRED","FILLED","NEW","PARTIALLY_FILLED","PENDING_CANCEL","REJECTED"]},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"timeInForce":{"type":"string","enum":["FOK","GTC","IOC"]},"trailingStopLoss":{"type":"boolean"},"transactTime":{"type":"integer","format":"int64"},"type":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]}},"title":"NewOrderResponseRESULT"},"OHLCBar":{"type":"object","properties":{"c":{"type":"number","format":"double"},"h":{"type":"number","format":"double"},"interval":{"type":"string"},"l":{"type":"number","format":"double"},"o":{"type":"number","format":"double"},"symbol":{"type":"string"},"t":{"type":"integer","format":"int64"},"type":{"type":"string"}},"title":"OHLCBar"},"OHLCSubscribeRequest":{"type":"object","properties":{"intervals":{"type":"array","description":"Identifies intervals for subscription. Available: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w. Default: 1m.","items":{"type":"string"}},"symbols":{"type":"array","description":"Identifies symbols for subscription.","items":{"type":"string"}},"type":{"type":"string","description":"Type of candlestick. Available: classic, heikin-ashi."}},"title":"OHLCSubscribeRequest","description":"Class representing an OHLC market data subscription."},"OpenOrdersReponse":{"type":"object","properties":{"openOrders":{"type":"array","items":{"$ref":"#/definitions/QueryOrderResponse"}}},"title":"OpenOrdersReponse"},"OptionalAuthRequest":{"type":"object","properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"OptionalAuthRequest"},"OvernightRate":{"type":"object","properties":{"longRate":{"type":"number","format":"double"},"shortRate":{"type":"number","format":"double"}},"title":"OvernightRate"},"PingRequest":{"type":"object","title":"PingRequest"},"PingResponse":{"type":"object","title":"PingResponse"},"PositionDto":{"type":"object","required":["accountId","closePrice","closeQuantity","createdTimestamp","currency","id","instrumentId","margin","openPrice","openQuantity","openTimestamp","orderId","state"],"properties":{"accountId":{"type":"string"},"closePrice":{"type":"number"},"closeQuantity":{"type":"number"},"closeTimestamp":{"type":"integer","format":"int64"},"cost":{"type":"number"},"createdTimestamp":{"type":"integer","format":"int64"},"currency":{"type":"string"},"currentTrailingPrice":{"type":"number"},"currentTrailingPriceUpdatedTimestamp":{"type":"integer","format":"int64"},"dividend":{"type":"number"},"fee":{"type":"number"},"guaranteedStopLoss":{"type":"boolean"},"id":{"type":"string","format":"uuid"},"instrumentId":{"type":"integer","format":"int64"},"margin":{"type":"number"},"openPrice":{"type":"number"},"openQuantity":{"type":"number"},"openTimestamp":{"type":"integer","format":"int64"},"orderId":{"type":"string","format":"uuid"},"rpl":{"type":"number"},"rplConverted":{"type":"number"},"state":{"type":"string","enum":["ACTIVE","INACTIVE","INVALID"]},"stopLoss":{"type":"number"},"swap":{"type":"number"},"swapConverted":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"trailingQuotedPrice":{"type":"number"},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string","enum":["HEDGE","NET"]},"upl":{"type":"number"},"uplConverted":{"type":"number"}},"title":"PositionDto"},"PositionExecutionReportDto":{"type":"object","required":["accountCurrency","accountId","createdTimestamp","currency","execId","execTimestamp","instrumentId","positionId","source","status"],"properties":{"accountCurrency":{"type":"string"},"accountId":{"type":"integer","format":"int64"},"createdTimestamp":{"type":"integer","format":"int64"},"currency":{"type":"string"},"execId":{"type":"string"},"execTimestamp":{"type":"integer","format":"int64"},"executionType":{"type":"string","enum":["GTC","IOC"]},"fee":{"type":"number"},"feeDetails":{"type":"object","additionalProperties":{"type":"number"}},"fxRate":{"type":"number"},"gSL":{"type":"boolean"},"instrumentId":{"type":"integer","format":"int64"},"positionId":{"type":"string"},"price":{"type":"number"},"quantity":{"type":"number"},"rejectReason":{"type":"string","enum":["ACCOUNT_NOT_FOUND","CLOSED_MARKET","CLOSE_ONLY","ENGINE_BUSY","HEDGING_MODE_GSL","INSTRUMENT_NOT_AVAILABLE","INSTRUMENT_NOT_FOUND","INVALID_ORDER","INVALID_ORDER_QTY","INVALID_PRICE","LONG_ONLY","OFF_MARKET","ORDER_NOT_FOUND","ORIGINAL_GSL_UPDATE","POSITION_NOT_FOUND","RC_INSTRUMENT_CLIENT_MOP","RC_INSTRUMENT_GLOBAL_MOP","RC_NOT_ENOUGH_MARGIN","RC_NOT_FOUND","RC_NO_RATES","RC_SETTLEMENT","RC_UNKNOWN","REQUIRED_GSL","RISK_CHECK","THROTTLING","UNKNOWN"]},"rpl":{"type":"number"},"rplConverted":{"type":"number"},"source":{"type":"string","enum":["CLOSE_OUT","DEALER","SL","SYSTEM","TP","USER"]},"status":{"type":"string","enum":["CLOSED","DIVIDEND","MODIFIED","MODIFY_REJECT","OPENED","SWAP"]},"stopLoss":{"type":"number"},"swap":{"type":"number"},"swapConverted":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"trailingStopLoss":{"type":"boolean"}},"title":"PositionExecutionReportDto"},"PositionHistoryRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"from":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"to":{"type":"integer","format":"int64"}},"title":"PositionHistoryRequest"},"QueryOrderResponse":{"type":"object","properties":{"accountId":{"type":"string"},"executedQty":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"icebergQty":{"type":"string"},"leverage":{"type":"boolean"},"margin":{"type":"number"},"orderId":{"type":"string"},"origQty":{"type":"string"},"price":{"type":"string"},"side":{"type":"string","enum":["BUY","SELL"]},"status":{"type":"string","enum":["CANCELED","EXPIRED","FILLED","NEW","PARTIALLY_FILLED","PENDING_CANCEL","REJECTED"]},"stopLoss":{"type":"number"},"symbol":{"type":"string"},"takeProfit":{"type":"number"},"time":{"type":"integer","format":"int64"},"timeInForce":{"type":"string","enum":["FOK","GTC","IOC"]},"trailingStopLoss":{"type":"boolean"},"type":{"type":"string","enum":["LIMIT","MARKET","STOP","TRAILING_STOP"]},"updateTime":{"type":"integer","format":"int64"},"working":{"type":"boolean"}},"title":"QueryOrderResponse"},"RateLimits":{"type":"object","properties":{"interval":{"type":"string"},"intervalNum":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"rateLimitType":{"type":"string"}},"title":"RateLimits"},"RequestDto":{"type":"object","required":["accountId","createdTimestamp","id","rqType","state"],"properties":{"accountId":{"type":"string"},"createdTimestamp":{"type":"integer","format":"int64"},"id":{"type":"integer","format":"int64"},"orderId":{"type":"string"},"positionId":{"type":"string"},"rejectReason":{"type":"string","enum":["ACCOUNT_NOT_FOUND","CLOSED_MARKET","CLOSE_ONLY","ENGINE_BUSY","HEDGING_MODE_GSL","INSTRUMENT_NOT_AVAILABLE","INSTRUMENT_NOT_FOUND","INVALID_ORDER","INVALID_ORDER_QTY","INVALID_PRICE","LONG_ONLY","OFF_MARKET","ORDER_NOT_FOUND","ORIGINAL_GSL_UPDATE","POSITION_NOT_FOUND","RC_INSTRUMENT_CLIENT_MOP","RC_INSTRUMENT_GLOBAL_MOP","RC_NOT_ENOUGH_MARGIN","RC_NOT_FOUND","RC_NO_RATES","RC_SETTLEMENT","RC_UNKNOWN","REQUIRED_GSL","RISK_CHECK","THROTTLING","UNKNOWN"]},"rqType":{"type":"string","enum":["ORDER_CANCEL","ORDER_MODIFY","ORDER_NEW","POSITION_MODIFY"]},"state":{"type":"string","enum":["CANCELLED","PENDING","PROCESSED"]}},"title":"RequestDto"},"ServerTime":{"type":"object","properties":{"serverTime":{"type":"integer","format":"int64"}},"title":"ServerTime"},"SignedBySymbolRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"symbol":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"SignedBySymbolRequest"},"SignedRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"timestamp":{"type":"integer","format":"int64"}},"title":"SignedRequest"},"SubscribeRequest":{"type":"object","properties":{"symbols":{"type":"array","description":"Identifies symbols for subscription.","items":{"type":"string"}}},"title":"SubscribeRequest","description":"Class representing an subscription."},"SubscribeResponse":{"type":"object","properties":{"errorCode":{"type":"string"},"subscriptions":{"type":"object","additionalProperties":{"type":"string"}}},"title":"SubscribeResponse"},"SymbolFilter":{"type":"object","properties":{"filterType":{"type":"string"}},"title":"SymbolFilter"},"SymbolRequest":{"type":"object","properties":{"symbol":{"type":"string"}},"title":"SymbolRequest"},"Ticker24HResponse":{"type":"object","properties":{"tickers":{"type":"array","items":{"$ref":"#/definitions/Ticker24hr"}}},"title":"Ticker24HResponse"},"Ticker24hr":{"type":"object","properties":{"askPrice":{"type":"string"},"bidPrice":{"type":"string"},"closeTime":{"type":"integer","format":"int64"},"highPrice":{"type":"string"},"lastPrice":{"type":"string"},"lastQty":{"type":"string"},"lowPrice":{"type":"string"},"openPrice":{"type":"string"},"openTime":{"type":"integer","format":"int64"},"prevClosePrice":{"type":"string"},"priceChange":{"type":"string"},"priceChangePercent":{"type":"string"},"quoteVolume":{"type":"string"},"symbol":{"type":"string"},"volume":{"type":"string"},"weightedAvgPrice":{"type":"string"}},"title":"Ticker24hr"},"TradeEventReq":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"orderId":{"type":"string"},"price":{"type":"number","format":"double"},"size":{"type":"number","format":"double"},"symbol":{"type":"string"},"ts":{"type":"integer","format":"int64"}},"title":"TradeEventReq"},"TradeEventRes":{"type":"object","properties":{"buyer":{"type":"boolean"},"id":{"type":"integer","format":"int32"},"orderId":{"type":"string"},"price":{"type":"number","format":"double"},"size":{"type":"number","format":"double"},"symbol":{"type":"string"},"ts":{"type":"integer","format":"int64"}},"title":"TradeEventRes"},"TradingFeesResponse":{"type":"object","properties":{"fee":{"type":"number","format":"double"},"name":{"type":"string"},"overnightFeeTimestamp":{"type":"integer","format":"int64"},"overnightRates":{"$ref":"#/definitions/OvernightRate"},"symbol":{"type":"string"}},"title":"TradingFeesResponse"},"TradingFeesResponseWS":{"type":"object","properties":{"fees":{"type":"array","items":{"$ref":"#/definitions/TradingFeesResponse"}}},"title":"TradingFeesResponseWS"},"TradingLimitsResponse":{"type":"object","properties":{"lastPrice":{"type":"number"},"maxVolume":{"type":"number","format":"double"},"minStep":{"type":"number","format":"double"},"minVolume":{"type":"number","format":"double"},"name":{"type":"string"},"symbol":{"type":"string"},"tickSize":{"type":"number","format":"double"}},"title":"TradingLimitsResponse"},"TradingLimitsResponseWS":{"type":"object","properties":{"limits":{"type":"array","items":{"$ref":"#/definitions/TradingLimitsResponse"}}},"title":"TradingLimitsResponseWS"},"TradingOrderUpdateResponse":{"type":"object","required":["requestId","state"],"properties":{"requestId":{"type":"integer","format":"int64"},"state":{"type":"string","enum":["CANCELLED","PENDING","PROCESSED"]}},"title":"TradingOrderUpdateResponse"},"TradingPositionCloseAllResponse":{"type":"object","properties":{"request":{"type":"array","items":{"$ref":"#/definitions/RequestDto"}}},"title":"TradingPositionCloseAllResponse"},"TradingPositionHistoryResponse":{"type":"object","properties":{"history":{"type":"array","items":{"$ref":"#/definitions/PositionExecutionReportDto"}}},"title":"TradingPositionHistoryResponse"},"TradingPositionListResponse":{"type":"object","properties":{"positions":{"type":"array","items":{"$ref":"#/definitions/PositionDto"}}},"title":"TradingPositionListResponse"},"TradingPositionUpdateResponse":{"type":"object","required":["requestId","state"],"properties":{"requestId":{"type":"integer","format":"int64"},"state":{"type":"string","enum":["CANCELLED","PENDING","PROCESSED"]}},"title":"TradingPositionUpdateResponse"},"TransactionDTOResponse":{"type":"object","properties":{"amount":{"type":"number"},"balance":{"type":"number"},"blockchainTransactionHash":{"type":"string"},"commission":{"type":"number"},"currency":{"type":"string"},"id":{"type":"integer","format":"int64"},"paymentMethod":{"type":"string"},"status":{"type":"string"},"timestamp":{"type":"integer","format":"int64"},"type":{"type":"string"}},"title":"TransactionDTOResponse"},"TransactionsRequest":{"type":"object","required":["apiKey","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"endTime":{"type":"integer","format":"int64"},"limit":{"type":"integer","format":"int32"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"startTime":{"type":"integer","format":"int64"},"timestamp":{"type":"integer","format":"int64"}},"title":"TransactionsRequest"},"TransactionsResponse":{"type":"object","properties":{"transactions":{"type":"array","items":{"$ref":"#/definitions/TransactionDTOResponse"}}},"title":"TransactionsResponse"},"UpdateTradingOrderRequest":{"type":"object","required":["apiKey","orderId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"expireTimestamp":{"type":"integer","format":"int64"},"guaranteedStopLoss":{"type":"boolean"},"newPrice":{"type":"number"},"orderId":{"type":"string"},"profitDistance":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"takeProfit":{"type":"number"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"}},"title":"UpdateTradingOrderRequest"},"UpdateTradingPositionRequest":{"type":"object","required":["apiKey","positionId","signature","timestamp"],"properties":{"apiKey":{"type":"string"},"guaranteedStopLoss":{"type":"boolean"},"positionId":{"type":"string"},"profitDistance":{"type":"number"},"recvWindow":{"type":"integer","format":"int64","maximum":60000,"exclusiveMaximum":false},"signature":{"type":"string"},"stopDistance":{"type":"number"},"stopLoss":{"type":"number"},"takeProfit":{"type":"number"},"timestamp":{"type":"integer","format":"int64"},"trailingStopLoss":{"type":"boolean"}},"title":"UpdateTradingPositionRequest"}}} +docs/market_intelligence/builds/build-015-3-common-models-engine-registration.md:116:enabled_by_default +docs/market_intelligence/builds/build-006-1-common-models-engine-metadata.md:119: enabled_by_default: bool = True +docs/market_intelligence/builds/build-006-1-common-models-engine-metadata.md:174:### enabled_by_default +((.venv) ) segeba@mbpbsg dzentra_bot % \ No newline at end of file diff --git a/docs/reference_model/knowledge_architecture_charter_v1.1.md b/docs/reference_model/knowledge_architecture_charter_v1.1.md new file mode 100644 index 0000000..f641fcc --- /dev/null +++ b/docs/reference_model/knowledge_architecture_charter_v1.1.md @@ -0,0 +1,395 @@ +# Dzentra Knowledge Architecture Charter + +**Версия:** 1.1\ +**Статус:** Release\ +**Проект:** Dzentra\ +**Подсистема:** Market Intelligence + +------------------------------------------------------------------------ + +# Цель + +Настоящий документ определяет фундаментальные принципы разработки +Knowledge Architecture проекта Dzentra. + +Knowledge Architecture определяет правила построения моделей знаний, +архитектурных стандартов и инженерной документации проекта Dzentra. + +Все последующие архитектурные решения, документы, спецификации, Engine и +программный код должны соответствовать настоящему Charter. + +При возникновении противоречий настоящий документ имеет наивысший +приоритет среди документов Knowledge Architecture. + +------------------------------------------------------------------------ + +# 1. Главная цель Dzentra + +Главная задача Dzentra --- не предсказывать рынок. + +Главная задача Dzentra --- максимально достоверно моделировать текущее +состояние наблюдаемого рынка. + +Торговое решение является следствием качества этой модели, а не целью +самой модели. + +------------------------------------------------------------------------ + +# 2. Главная философия + +Dzentra не является системой, принимающей решения на основании отдельных +индикаторов. + +Dzentra является системой построения модели рынка. + +Переход осуществляется от подхода: + +``` text +Индикатор + ↓ +Сигнал + ↓ +Сделка +``` + +к подходу: + +``` text +Наблюдение + ↓ +Свойства + ↓ +Состояния + ↓ +Контекст + ↓ +Модель рынка + ↓ +Decision + ↓ +Execution +``` + +------------------------------------------------------------------------ + +# 3. Архитектура платформы + +Утверждённая архитектура верхнего уровня: + +``` text +Market Data + │ + ▼ +Market Intelligence + │ + ▼ +Decision + │ + ▼ +Execution + │ + ▼ +Exchange +``` + +Данная архитектура не пересматривается без выпуска новой версии +архитектурного стандарта. + +------------------------------------------------------------------------ + +# 4. Разделение ответственности + +- **Market Intelligence** отвечает исключительно за построение модели + наблюдаемого рынка. +- **Decision** отвечает исключительно за принятие торгового решения. +- **Execution** отвечает исключительно за исполнение принятого + решения. + +Никакой слой не должен выполнять функции другого слоя. + +------------------------------------------------------------------------ + +# 5. Архитектура знаний + +Разработка начинается с предметной области. + +Обязательная последовательность: + +``` text +Предметная область + ↓ +Reference Model + ↓ +Knowledge References + ↓ +Engine Specifications + ↓ +Runtime Contracts + ↓ +Build Tasks + ↓ +Code + ↓ +Tests +``` + +Ни один этап не должен пропускаться. + +------------------------------------------------------------------------ + +# 6. Документация определяет код + +Документация является единственным источником истины. + +Код является реализацией документации. + +Если код противоречит документации, код приводится в соответствие с +документацией. + +Документация не изменяется ради существующей реализации. + +------------------------------------------------------------------------ + +# 7. Принципы построения документации + +Документы Knowledge Architecture являются инженерными стандартами. + +Документация должна быть: + +- нормативной; +- однозначной; +- самодостаточной; +- внутренне непротиворечивой; +- независимой от реализации. + +Каждое утверждение включается только в том случае, если оно необходимо +для дальнейшего проектирования системы. + +## Принцип читаемости + +Документы разрабатываются по принципам инженерных стандартов, но пишутся +простым профессиональным языком. + +Текст должен быть: + +- точным; +- логически последовательным; +- понятным специалисту предметной области; +- свободным от излишнего канцелярита. + +Строгость не должна ухудшать читаемость. + +------------------------------------------------------------------------ + +# 8. Reference Model + +Reference Model описывает исключительно предметную область. + +Reference Model: + +- описывает знания о рынке; +- не описывает программную реализацию; +- не описывает алгоритмы; +- не описывает конкретные индикаторы; +- не определяет распределение ответственности между Engine; +- не определяет способы вычислений. + +Reference Model отвечает на вопрос: + +> Что существует в предметной области и какие знания могут быть +> получены? + +------------------------------------------------------------------------ + +# 9. Принцип проектирования + +Сначала определяется: + +1. что существует; +2. какие знания можно получить; +3. как знания связаны; +4. какой Engine отвечает за конкретный домен; +5. как реализовать систему. + +Никогда наоборот. + +------------------------------------------------------------------------ + +# 10. Engine + +Каждый Engine отвечает только за один домен знаний. + +Engine никогда не принимает торговых решений. + +Engine возвращает исключительно знания. + +------------------------------------------------------------------------ + +# 11. Decision + +Decision объединяет результаты Engine и формирует торговое решение. + +------------------------------------------------------------------------ + +# 12. Execution + +Execution не анализирует рынок. + +Execution выполняет принятое решение. + +------------------------------------------------------------------------ + +# 13. Терминология + +Каждый термин имеет одно официальное определение. + +Использование нескольких терминов для обозначения одного понятия не +допускается. + +------------------------------------------------------------------------ + +# 14. Принцип последовательности + +Любое понятие может использовать только термины, определённые ранее. + +Использование неопределённых понятий запрещается. + +------------------------------------------------------------------------ + +# 15. Иерархия знаний + +``` text +Рынок + ↓ +Наблюдение + ↓ +Свойство + ↓ +Состояние + ↓ +Контекст + ↓ +Оценка + ↓ +Решение +``` + +Переход между уровнями является односторонним. + +------------------------------------------------------------------------ + +# 16. Принцип объективности + +Reference Model описывает рынок независимо от наблюдателя. + +Market Intelligence строит внутреннюю модель наблюдаемого рынка. + +------------------------------------------------------------------------ + +# 17. Независимость предметной области + +Определения предметной области не должны зависеть от: + +- формата данных; +- биржи; +- таймфрейма; +- языка программирования; +- способов реализации. + +------------------------------------------------------------------------ + +# 18. Наблюдаемость + +Любое знание должно быть основано: + +- на наблюдаемых свойствах рынка; +- либо на логически выводимых следствиях. + +Предположения о намерениях участников рынка не входят в модель знаний. + +------------------------------------------------------------------------ + +# 19. Академическая строгость + +Необходимо различать: + +- наблюдения; +- свойства; +- состояния; +- контекст; +- вероятностные оценки; +- торговые решения. + +Эти уровни не смешиваются. + +------------------------------------------------------------------------ + +# 20. Независимость от школ анализа + +Dzentra может использовать идеи различных профессиональных подходов при +условии их рациональной и наблюдаемой обоснованности. + +Ни одна школа анализа не является официальной моделью Dzentra. + +------------------------------------------------------------------------ + +# 21. Жизненный цикл документации + +Каждый документ проходит стадии: + +``` text +Draft + ↓ +Review + ↓ +Release + ↓ +Evolution +``` + +------------------------------------------------------------------------ + +# 22. Методология разработки документов + +Сначала проектируется структура документа. + +После утверждения структуры главы разрабатываются последовательно и +проходят архитектурную проверку до утверждения. + +------------------------------------------------------------------------ + +# 23. Главный принцип проекта + +> Документация Dzentra не описывает код. Документация Dzentra определяет +> код. + +------------------------------------------------------------------------ + +# 24. Методологический приоритет + +Приоритеты Knowledge Architecture: + +1. Архитектурная целостность. +2. Корректность модели предметной области. +3. Масштабируемость. +4. Простота реализации. + +------------------------------------------------------------------------ + +# 25. Цель Knowledge Architecture + +Создать формальную, внутренне непротиворечивую и масштабируемую модель +знаний о рынке, которая станет фундаментом всех будущих компонентов +Dzentra независимо от языка программирования, реализации и алгоритмов. + +------------------------------------------------------------------------ + +# 26. Эволюция Charter + +Настоящий Charter является конституцией Knowledge Architecture. + +В него включаются только универсальные принципы, сохраняющие +актуальность независимо от этапа разработки и конкретной реализации. + +Изменение Charter допускается только после архитектурного обсуждения и +выпуска новой версии. diff --git a/docs/stages/stage-08_2-architecture_build_log.md b/docs/stages/stage-08_2-architecture_build_log.md new file mode 100644 index 0000000..0f584e6 --- /dev/null +++ b/docs/stages/stage-08_2-architecture_build_log.md @@ -0,0 +1,527 @@ +принимаю твое предложение +Переделай уже имеющийся stage-08_2-architecture_build_log.md и отформатируй его в markdown, и далее дополнения присылай так же в формате Markdown + +Stage-08.2 — Architecture Build Log + +Документ фиксирует процесс построения архитектуры Market Intelligence. + +В отличие от Git, данный журнал хранит не историю изменения строк кода, а историю архитектурных решений. + +Каждый файл проходит одинаковый жизненный цикл: + +Design + ↓ +Implementation + ↓ +Compile Check + ↓ +Architecture Review + ↓ +Accepted + +Файл считается завершённым только после успешного прохождения всех этапов. + +⸻ + +Build №001 + +Файл + +app/src/trading/market_intelligence/common/enums.py + +Назначение + +Создание единого набора перечислений (Enum), используемых всеми аналитическими движками платформы. + +Данный файл является фундаментом всей подсистемы Market Intelligence и определяет общий язык взаимодействия между Engine. + +Реализовано + +Добавлены перечисления: + +* MarketDirection +* MarketBias +* MarketPhase +* MarketRegime +* MarketQuality +* EngineStatus +* ConfidenceLevel +* SignalFreshness +* RiskLevel +* TimeframeRole +* CheckStatus +* ProcessingStage + +Архитектурные решения + +Приняты следующие решения: + +* перечисления не содержат торговой логики; +* перечисления не принимают торговых решений; +* используются только как описание состояния рынка; +* комментарии ориентированы на разработчика, а не на трейдера; +* проверки движков описываются этапами обработки (ProcessingStage), а не именами файлов; +* добавлен ConfidenceLevel как человекочитаемая интерпретация числовой уверенности; +* добавлен статус SKIPPED для корректного отображения намеренно пропущенных этапов проверки. + +Compile Check + +PASSED + +Architecture Review + +PASSED + +Обязательные замечания + +Нет. + +Рекомендации + +В дальнейшем допускается расширение ProcessingStage, если архитектура платформы потребует новых этапов обработки. До появления реальной необходимости перечисление не расширяется. + +Статус + +ACCEPTED + +⸻ + +Build №002 + +Файл + +app/src/trading/market_intelligence/common/types.py + +Назначение + +Создание единого набора базовых типовых алиасов, используемых всеми аналитическими движками. + +Файл определяет общий типовой контракт Market Intelligence. + +Реализовано + +Добавлены типы: + +* SymbolName +* TimeframeName +* EngineName +* EngineVersion +* ReasonCode +* ReasonText +* ScoreValue +* ConfidenceValue +* ProbabilityValue +* WeightValue +* AgeSeconds +* DurationMs +* MetricsDict +* PayloadDict +* ContextDict +* MarketData +* DependencyResults +* DiagnosticMessages +* DiagnosticValue + +Повторно используются существующие типы проекта: + +* JsonDict +* JsonList + +из src.core.types. + +Архитектурные решения + +Приняты следующие решения: + +* используется единый источник истины (core.types); +* отсутствует дублирование базовых типов проекта; +* отсутствуют зависимости от Runtime, Telegram, Execution, Journal и Exchange; +* файл не содержит торговой логики; +* комментарии объясняют назначение типов, а не синтаксис Python. + +Compile Check + +PASSED + +Architecture Review + +PASSED + +Обязательные замечания + +Нет. + +Рекомендации + +Тип DependencyResults временно использует Any. После появления общего EngineResult в common/models.py рекомендуется заменить значение словаря на специализированный тип результата движка. + +В перспективе допускается переход от универсальных словарей (PayloadDict, MetricsDict, ContextDict) к специализированным TypedDict, если это потребуется для усиления типизации и улучшения поддержки IDE. + +Статус + +ACCEPTED + +⸻ + +Общий прогресс Stage-08.2 + +Common + +Файл Статус +enums.py ✅ Accepted +types.py ✅ Accepted +constants.py ✅ Accepted +reasons.py ⏳ Planned +scores.py ⏳ Planned +models.py ⏳ Planned +validation.py ⏳ Planned +checks.py ⏳ Planned +payloads.py ⏳ Planned +snapshots.py ⏳ Planned +events.py ⏳ Planned +timeframes.py ⏳ Planned + +⸻ + +Архитектурные принципы Stage-08 + +На текущем этапе подтверждены следующие принципы разработки: + +* архитектура проектируется раньше реализации; +* каждый файл проходит обязательную компиляцию; +* каждый файл проходит обязательный Architecture Review; +* обязательные и рекомендательные замечания фиксируются отдельно; +* новый код не должен содержать преждевременных сущностей “на будущее”; +* комментарии должны быть понятны разработчику без знаний трейдинга; +* все изменения должны соответствовать Engine Runtime Contract; +* развитие платформы ведётся небольшими логически завершёнными шагами с обязательной проверкой качества каждого шага. + +⸻ + +Эволюция процесса разработки + +По мере развития Stage-08 процесс разработки был дополнен обязательными архитектурными проверками. + +Начиная с Build №004 каждый новый файл проходит полный цикл проверки качества. + +Полный цикл разработки + +Architecture Design + ↓ +Implementation + ↓ +Compile Check + ↓ +Architecture Review + ↓ +Domain Review + ↓ +Accepted + +⸻ + +Compile Check + +Проверяет техническую корректность файла. + +Цель проверки: + +* успешная компиляция; +* отсутствие синтаксических ошибок; +* корректность импортов; +* возможность безопасного включения файла в проект. + +Без успешного Compile Check дальнейшие проверки не выполняются. + +⸻ + +Architecture Review + +Проверяет соответствие архитектуре Dzentra. + +Во время проверки анализируется: + +* соблюдение зон ответственности; +* отсутствие нарушения слоёв архитектуры; +* отсутствие циклических зависимостей; +* возможность масштабирования; +* соответствие Engine Runtime Contract; +* соответствие принятому стилю проекта. + +Architecture Review оценивает качество архитектуры независимо от предметной области. + +⸻ + +Domain Review + +Проверяет соответствие предметной области Market Intelligence. + +Во время проверки анализируется: + +* правильность используемой терминологии; +* соответствие названий реальному поведению рынка; +* отсутствие смешивания анализа рынка и торговых решений; +* отсутствие логики открытия, закрытия или сопровождения сделок внутри аналитических компонентов; +* понятность комментариев разработчику без специальных знаний трейдинга; +* корректность описания рыночных состояний и процессов. + +Domain Review гарантирует, что Market Intelligence остаётся системой анализа поведения рынка, а не системой принятия торговых решений. + +⸻ + +Правила разработки Stage-08 + +При реализации Stage-08 приняты следующие обязательные правила. + +1. Архитектура проектируется раньше кода + +Каждый новый компонент сначала проектируется, после чего начинается его реализация. + +⸻ + +2. Не использовать предположения о существующем коде + +Если для реализации нового файла требуется существующая часть проекта, соответствующий файл предварительно запрашивается и используется как источник истины. + +Запрещается: + +* дублировать существующие сущности; +* самостоятельно создавать альтернативные реализации уже существующих моделей; +* делать предположения о текущем состоянии проекта. + +⸻ + +3. Один источник истины + +Общие сущности повторно используются из существующих модулей проекта. + +Новые реализации создаются только при отсутствии соответствующей функциональности. + +⸻ + +4. Каждый файл должен быть логически завершён + +Файл считается завершённым только после успешного прохождения полного цикла проверки качества. + +Частично реализованные решения не считаются завершёнными независимо от объёма написанного кода. + +⸻ + +5. Комментарии ориентированы на разработчика + +Комментарии должны объяснять назначение компонента простым техническим языком. + +Предпочтительно объяснять: + +* зачем существует объект; +* какую задачу он решает; +* какие ограничения существуют. + +Следует избегать объяснения синтаксиса Python. + +⸻ + +6. Аналитика не принимает торговых решений + +Компоненты Market Intelligence описывают состояние рынка. + +Они не должны: + +* открывать сделки; +* закрывать сделки; +* изменять позиции; +* рассчитывать объёмы ордеров; +* выполнять действия биржи. + +Принятие торговых решений выполняется только верхними уровнями архитектуры платформы. + +⸻ + +История развития процесса + +Build Изменение процесса +Build №001 Введён обязательный Compile Check +Build №001 Введён обязательный Architecture Review +Build №003 Введён Architecture Build Log +Build №004 Введён обязательный Domain Review +Build №004 Запрещено предполагать существующий код — все необходимые файлы предварительно запрашиваются + +⸻ + +Build №003 — Post Review Notes + +После завершения Build №003 были приняты дополнительные архитектурные решения. + +Решение №001 + +Архитектурные константы должны содержать только ограничения платформы. + +В common/constants.py запрещается размещать: + +* параметры технических индикаторов; +* параметры торговых стратегий; +* настройки открытия и закрытия сделок; +* параметры биржи; +* параметры управления позицией. + +Подобные константы должны размещаться только внутри соответствующих Engine. + +⸻ + +Решение №002 + +Константы должны быть сгруппированы по смысловым разделам. + +При дальнейшем развитии файла рекомендуется придерживаться следующего порядка: + +Score +Confidence +Probability +Runtime +Diagnostics +Architecture +Timeframes +Safety + +Это обеспечивает единый стиль оформления и упрощает сопровождение файла по мере роста платформы. + +⸻ + +Решение №003 + +Временные интервалы являются частью конфигурации платформы, а не жёстким ограничением архитектуры. + +DEFAULT_TIMEFRAMES описывает базовую конфигурацию первого этапа разработки Market Intelligence. + +В дальнейшем архитектура должна позволять использовать дополнительные интервалы времени без изменения логики Engine. + +⸻ + +Решение №004 + +Возраст результата анализа и возраст торгового сигнала являются разными понятиями. + +Используются две независимые архитектурные константы: + +* DEFAULT_STALE_AFTER_SECONDS — определяет момент, после которого аналитический результат считается устаревшим. +* DEFAULT_SIGNAL_TTL_SECONDS — определяет момент, после которого влияние аналитического сигнала начинает постепенно уменьшаться. + +Совпадение их значений допускается, однако их назначение принципиально различается. + +⸻ + +Решение №005 + +Константы не должны зависеть от конкретного Engine. + +Все значения, размещаемые в common/constants.py, должны быть одинаково применимы для любого аналитического движка платформы. + +При появлении константы, относящейся только к одному Engine, она переносится в каталог соответствующего Engine. + +⸻ + +Итог Build №003 + +Build №003 полностью соответствует принятой архитектуре Stage-08 и остаётся базовым источником архитектурных ограничений Market Intelligence. + +⸻ + +Build №004 + +Файл + +app/src/trading/market_intelligence/common/reasons.py + +Назначение + +Создание единого реестра машинных кодов причин (ReasonCode), используемых всеми аналитическими движками Market Intelligence. + +Файл определяет стандартный словарь причин, который применяется при диагностике, построении результатов, публикации событий и журналировании. + +Реализовано + +Добавлен единый класс ReasonCode, включающий причины для следующих областей: + +* Common +* Data +* Engine Runtime +* Validation +* Market State +* Structure +* Trend +* Momentum +* Volatility +* Wave +* Cycle +* Liquidity +* Regime +* Confidence +* Signal Aging +* Timeframe + +Архитектурные решения + +Приняты следующие решения: + +* движки публикуют только стандартизированные коды причин; +* движки не формируют человекочитаемый текст; +* причины полностью отделены от пользовательского интерфейса; +* причины не содержат торговых действий; +* единый словарь причин используется всеми Engine платформы; +* причины описывают только состояние рынка и состояние работы движка. + +Compile Check + +PASSED + +Architecture Review + +PASSED + +Domain Review + +PASSED + +Обязательные замечания + +Нет. + +Рекомендации + +После завершения проектирования моделей рекомендуется реализовать отдельный слой формирования человекочитаемых объяснений (common/reason_texts.py или аналогичный модуль), который будет преобразовывать ReasonCode в диагностические сообщения для журнала, интерфейса и отчётов. + +Статус + +ACCEPTED + +⸻ + +Общий прогресс Stage-08.2 + +Common + +Файл Статус +enums.py ✅ Accepted +types.py ✅ Accepted +constants.py ✅ Accepted +reasons.py ✅ Accepted +scores.py ⏳ Planned +models.py ⏳ Planned +payloads.py ⏳ Planned +snapshots.py ⏳ Planned +events.py ⏳ Planned +timeframes.py ⏳ Planned + +⸻ + +История развития процесса + +Начиная с Build №004 каждый логически завершённый файл сопровождается обязательным обновлением Architecture Build Log. + +Это гарантирует синхронное развитие: + +* архитектуры; +* исходного кода; +* инженерной документации. + +Журнал является частью процесса разработки и обновляется одновременно с завершением каждого Build. \ No newline at end of file diff --git a/scripts/create_market_common.sh b/scripts/create_market_common.sh new file mode 100755 index 0000000..5bda317 --- /dev/null +++ b/scripts/create_market_common.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE="app/src/trading/market_intelligence" +COMMON="$BASE/common" + +echo "Creating directories..." + +mkdir -p "$COMMON" + +echo "Creating __init__.py files..." + +cat > "$BASE/__init__.py" <<'EOF' +# app/src/trading/market_intelligence/__init__.py +EOF + +cat > "$COMMON/__init__.py" <<'EOF' +# app/src/trading/market_intelligence/common/__init__.py +EOF + +echo "Creating common files..." + +FILES=( + constants.py + enums.py + events.py + models.py + payloads.py + reasons.py + scores.py + snapshots.py + timeframes.py + types.py +) + +for FILE in "${FILES[@]}"; do +cat > "$COMMON/$FILE" <" + echo "Example: $0 trend" + exit 1 +fi + +ENGINE_NAME="$1" +BASE="app/src/trading/market_intelligence" +ENGINE_DIR="$BASE/$ENGINE_NAME" + +mkdir -p "$ENGINE_DIR" + +FILES=( + __init__.py + constants.py + models.py + engine.py + payloads.py + calculators.py + evaluators.py +) + +for FILE in "${FILES[@]}"; do +cat > "$ENGINE_DIR/$FILE" <