cfquant 0.2.48__tar.gz → 0.2.50__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cfquant-0.2.48 → cfquant-0.2.50}/PKG-INFO +1 -1
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/normal_bridge.py +1 -0
- cfquant-0.2.50/cfquant/order_identity.py +173 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/order_meta.py +2 -1
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/qmt_strategy_deploy.py +45 -27
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/20_/350/201/224/350/260/203/351/227/256/351/242/230/344/277/256/345/244/215/346/265/213/350/257/225.py +1 -1
- cfquant-0.2.50/cfquant/tests/test_advanced_parity.py +153 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_lite_deployment_paths.py +29 -0
- cfquant-0.2.50/cfquant/tests/test_lite_parity.py +140 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_qmt_strategy_deploy.py +62 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_qmt_strategy_web.py +2 -2
- cfquant-0.2.50/cfquant/tests/test_trade_strategy_callback.py +205 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tx_trade_bridge.py +39 -11
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/version.py +2 -2
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant.egg-info/SOURCES.txt +3 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant_web_server.py +14 -5
- {cfquant-0.2.48 → cfquant-0.2.50}/pyproject.toml +1 -1
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts/CFQUANT_LITE.py +916 -18
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +9 -2
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SH.py +916 -18
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_LITE_SZ.py +916 -18
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SH.py +9 -2
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_TRADE_LOWLAT_SZ.py +9 -2
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/app.js +1 -1
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/index.html +2 -2
- cfquant-0.2.48/cfquant/order_identity.py +0 -55
- {cfquant-0.2.48 → cfquant-0.2.50}/LICENSE +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/LTtx/__init__.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/MANIFEST.in +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/README.md +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/__init__.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/build_info.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/channels.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/cli.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/client.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/config.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/hub.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/level2.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/log_management.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/management.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/protocol.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/10_/345/221/275/344/273/244/350/241/214/346/216/245/345/217/243/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/11_/346/231/256/351/200/232/346/241/245/346/216/245/346/264/276/345/217/221/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/12_/350/256/242/345/215/225/345/244/207/346/263/250/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/13_/347/256/241/351/201/223/344/270/255/345/277/203/350/277/236/346/216/245/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/14_/350/277/220/350/241/214/346/227/266/346/212/245/345/221/212/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/15_/347/275/221/351/241/265/347/212/266/346/200/201/347/274/223/345/255/230/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/16_/345/220/257/345/212/250/345/256/211/350/243/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/17_/345/244/247QMT/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/18_/351/253/230/347/272/247/346/250/241/345/274/217/345/256/236/346/234/272/350/201/224/350/260/203.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/19_/345/256/236/346/234/272/350/201/224/350/260/203/345/210/244/345/256/232/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/1_/350/241/214/346/203/205/346/216/245/346/224/266/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/21_/345/215/225/350/202/241/350/241/214/346/203/205/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/22_/350/277/236/346/216/245/350/257/212/346/226/255/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/23_/347/244/272/344/276/213/351/205/215/347/275/256/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/24_/350/264/246/346/210/267/347/274/223/345/255/230/346/214/211/351/234/200/345/210/267/346/226/260/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/25_/350/264/246/346/210/267/347/274/223/345/255/230/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/26_/345/205/250/346/216/250/350/256/242/351/230/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/27_/345/205/250/346/216/250/350/256/242/351/230/205/351/241/265/351/235/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/28_Level2/346/216/245/345/217/243/351/200/202/351/205/215/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/2_/346/225/260/346/215/256/350/216/267/345/217/226/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/30_cftrader/346/211/271/351/207/217/344/270/213/345/215/225/346/200/247/350/203/275/345/237/272/345/207/206.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/31_cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/32_/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/33_/345/220/214/346/255/245/344/270/213/345/215/225/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/3_/346/225/260/346/215/256/344/270/213/350/275/275/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/4_/344/272/244/346/230/223/345/247/224/346/211/230/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/5_/347/234/237/345/256/236/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/7_/345/220/214/346/255/245/345/274/202/346/255/245/344/270/213/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/8_/346/210/220/344/272/244/350/256/242/345/215/225/345/222/214/345/247/224/346/211/230/350/256/242/345/215/225/346/237/245/350/257/242/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/9_/345/217/257/346/222/244/350/256/242/345/215/225/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260911_024617.md" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/log/cftrader/346/250/241/346/213/237/350/264/246/345/217/267/346/211/271/351/207/217/344/270/213/345/215/225/346/265/213/350/257/225_20260921_175732.md" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/tx.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant/xttype.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SH.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/qmt_scripts//345/220/214/350/264/246/345/217/267/347/213/254/347/253/213/345/270/202/345/234/272/readme.md" +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/requirements.txt +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/setup.cfg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.48 → cfquant-0.2.50}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -893,6 +893,7 @@ class NormalQmtBridge(TxTradeBridge):
|
|
|
893
893
|
self._resolve_pending_sync_order_callback(data)
|
|
894
894
|
relay_sync_order_callback(self.context, data)
|
|
895
895
|
self._handle_async_order_callback(data)
|
|
896
|
+
self._remember_order_callback_fields(data)
|
|
896
897
|
payload = {
|
|
897
898
|
"type": "event",
|
|
898
899
|
"event": event_name,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Per-submission QMT remarks, compatible with embedded Python 3.6.
|
|
2
|
+
|
|
3
|
+
The public remark is user data, never a unique order key. Keep a UUID in the
|
|
4
|
+
native remark so independent QMT processes and restarts can recover both.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from collections import OrderedDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_ORDER_REMARK_SUFFIX = re.compile(r"__cfq_([0-9a-f]{32})$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def original_order_remark(value):
|
|
18
|
+
text = str(value or "")
|
|
19
|
+
match = _ORDER_REMARK_SUFFIX.search(text)
|
|
20
|
+
return text[:match.start()] if match else text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_unique_order_remark(value):
|
|
24
|
+
return bool(_ORDER_REMARK_SUFFIX.search(str(value or "")))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def prepare_order_remark(params, fallback=""):
|
|
28
|
+
original = next((params[name] for name in ("order_remark", "remark", "strategy_name")
|
|
29
|
+
if params.get(name) not in (None, "")), "")
|
|
30
|
+
original = str(original or "")
|
|
31
|
+
internal = params.get("cfquant_order_remark")
|
|
32
|
+
if not (is_unique_order_remark(internal) and
|
|
33
|
+
original_order_remark(internal) in (original, original or str(fallback or ""))):
|
|
34
|
+
original = original or str(fallback or "")
|
|
35
|
+
internal = original + "__cfq_" + uuid.uuid4().hex
|
|
36
|
+
params["cfquant_order_remark"] = internal
|
|
37
|
+
return internal
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def order_remark_key(data):
|
|
41
|
+
getter = data.get if isinstance(data, dict) else lambda name: getattr(data, name, None)
|
|
42
|
+
values = [getter(name) for name in (
|
|
43
|
+
"cfquant_order_remark", "user_order_id", "client_order_id",
|
|
44
|
+
"order_remark", "m_strRemark", "m_strOrderRemark", "remark",
|
|
45
|
+
)]
|
|
46
|
+
for value in values:
|
|
47
|
+
if is_unique_order_remark(value):
|
|
48
|
+
return str(value)
|
|
49
|
+
return next((str(value) for value in values if value not in (None, "")), "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def restore_order_remark(data):
|
|
53
|
+
if isinstance(data, dict):
|
|
54
|
+
internal = order_remark_key(data)
|
|
55
|
+
if is_unique_order_remark(internal):
|
|
56
|
+
data["cfquant_order_remark"] = internal
|
|
57
|
+
data["order_remark"] = original_order_remark(internal)
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def order_remark_matches_reference(expected, actual):
|
|
62
|
+
"""Compare remarks only AFTER an order reference has matched.
|
|
63
|
+
|
|
64
|
+
Some DEAL reports echo the public remark without our UUID. A different
|
|
65
|
+
UUID still identifies a different submission, even with the same text.
|
|
66
|
+
"""
|
|
67
|
+
return (not actual or expected == actual or
|
|
68
|
+
(is_unique_order_remark(expected) and
|
|
69
|
+
not is_unique_order_remark(actual) and
|
|
70
|
+
original_order_remark(expected) == actual))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class OrderCallbackCache(object):
|
|
74
|
+
"""Bounded, daily order metadata indexed by separate local/broker IDs."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, max_orders=4096):
|
|
77
|
+
self.max_orders = max_orders
|
|
78
|
+
self.lock = threading.RLock()
|
|
79
|
+
self.day = ""
|
|
80
|
+
self.records = OrderedDict()
|
|
81
|
+
self.by_ref = {}
|
|
82
|
+
|
|
83
|
+
def _identity(self, data, account_type):
|
|
84
|
+
day = time.strftime("%Y%m%d")
|
|
85
|
+
if day != self.day:
|
|
86
|
+
self.records.clear()
|
|
87
|
+
self.by_ref.clear()
|
|
88
|
+
self.day = day
|
|
89
|
+
# Historical query rows must not seed today's broker-ID mapping.
|
|
90
|
+
for name in ("order_date", "trade_date", "m_strInsertDate", "m_strTradeDate", "m_strTradingDay"):
|
|
91
|
+
value = str(data.get(name) or "").replace("-", "").replace("/", "")
|
|
92
|
+
if len(value) == 8 and value.isdigit() and value != day:
|
|
93
|
+
return None, set()
|
|
94
|
+
account = str(data.get("account_id") or data.get("m_strAccountID") or "").strip()
|
|
95
|
+
stock = str(data.get("stock_code") or data.get("m_strInstrumentID") or "").strip().upper()
|
|
96
|
+
market = str(data.get("market") or data.get("m_strExchangeID") or "").strip().upper()
|
|
97
|
+
if "." in stock:
|
|
98
|
+
stock, market = stock.rsplit(".", 1)
|
|
99
|
+
if not account or not stock:
|
|
100
|
+
return None, set()
|
|
101
|
+
scope = (account_type, account, stock, market)
|
|
102
|
+
refs = set()
|
|
103
|
+
for name in ("order_id", "m_nRef", "m_nOrderID", "m_strOrderRef", "m_strOrderID"):
|
|
104
|
+
value = str(data.get(name) or "").strip()
|
|
105
|
+
if value.isdigit() and int(value) > 0:
|
|
106
|
+
refs.add(("local", str(int(value))))
|
|
107
|
+
for name in ("order_sysid", "m_strOrderSysID"):
|
|
108
|
+
value = str(data.get(name) or "").strip()
|
|
109
|
+
if value and value not in ("0", "-1"):
|
|
110
|
+
refs.add(("broker", value))
|
|
111
|
+
return scope, refs
|
|
112
|
+
|
|
113
|
+
def _matches(self, scope, refs, data):
|
|
114
|
+
groups = [self.by_ref[(scope, ref)] for ref in refs if (scope, ref) in self.by_ref]
|
|
115
|
+
candidates = set.intersection(*groups) if groups else set()
|
|
116
|
+
matches = []
|
|
117
|
+
for key in candidates:
|
|
118
|
+
record = self.records[key]
|
|
119
|
+
expected, actual = order_remark_key(record["data"]), order_remark_key(data)
|
|
120
|
+
if expected and not order_remark_matches_reference(expected, actual):
|
|
121
|
+
continue
|
|
122
|
+
local = {ref for ref in refs if ref[0] == "local"}
|
|
123
|
+
known = {ref for ref in record["refs"] if ref[0] == "local"}
|
|
124
|
+
if local and known and not local.intersection(known):
|
|
125
|
+
continue
|
|
126
|
+
matches.append(key)
|
|
127
|
+
return matches
|
|
128
|
+
|
|
129
|
+
def remember(self, data, account_type):
|
|
130
|
+
if not (data.get("strategy_name") or data.get("m_strStrategyName") or order_remark_key(data)):
|
|
131
|
+
return
|
|
132
|
+
with self.lock:
|
|
133
|
+
scope, refs = self._identity(data, account_type)
|
|
134
|
+
if scope is None or not refs:
|
|
135
|
+
return
|
|
136
|
+
matches = self._matches(scope, refs, data)
|
|
137
|
+
remark = order_remark_key(data)
|
|
138
|
+
local = sorted(ref for ref in refs if ref[0] == "local")
|
|
139
|
+
identity = ("remark", remark) if is_unique_order_remark(remark) else (local or sorted(refs))[0]
|
|
140
|
+
key = (scope, identity)
|
|
141
|
+
if len(matches) == 1:
|
|
142
|
+
key = matches[0]
|
|
143
|
+
record = self.records.get(key, {"data": {}, "refs": set()})
|
|
144
|
+
record["data"].update({name: value for name, value in data.items() if value not in (None, "")})
|
|
145
|
+
record["refs"].update(refs)
|
|
146
|
+
self.records[key] = record
|
|
147
|
+
self.records.move_to_end(key)
|
|
148
|
+
for ref in refs:
|
|
149
|
+
self.by_ref.setdefault((scope, ref), set()).add(key)
|
|
150
|
+
while len(self.records) > self.max_orders:
|
|
151
|
+
old_key, old = self.records.popitem(last=False)
|
|
152
|
+
for ref in old["refs"]:
|
|
153
|
+
index = (old_key[0], ref)
|
|
154
|
+
self.by_ref[index].discard(old_key)
|
|
155
|
+
if not self.by_ref[index]:
|
|
156
|
+
del self.by_ref[index]
|
|
157
|
+
|
|
158
|
+
def enrich(self, data, account_type):
|
|
159
|
+
with self.lock:
|
|
160
|
+
scope, refs = self._identity(data, account_type)
|
|
161
|
+
matches = self._matches(scope, refs, data) if scope is not None else []
|
|
162
|
+
if len(matches) != 1:
|
|
163
|
+
return data
|
|
164
|
+
saved = self.records[matches[0]]["data"]
|
|
165
|
+
strategy = saved.get("strategy_name") or saved.get("m_strStrategyName")
|
|
166
|
+
remark = order_remark_key(saved)
|
|
167
|
+
for name, value in (("strategy_name", strategy), ("m_strStrategyName", strategy),
|
|
168
|
+
("order_remark", remark), ("m_strRemark", remark), ("m_strOrderRemark", remark)):
|
|
169
|
+
if value and not data.get(name):
|
|
170
|
+
data[name] = value
|
|
171
|
+
if is_unique_order_remark(remark):
|
|
172
|
+
data["cfquant_order_remark"] = remark
|
|
173
|
+
return restore_order_remark(data)
|
|
@@ -13,6 +13,7 @@ import re
|
|
|
13
13
|
import threading
|
|
14
14
|
from .stock_connect import connect_account_type
|
|
15
15
|
from .order_identity import order_remark_key, restore_order_remark, is_unique_order_remark
|
|
16
|
+
from .order_identity import order_remark_matches_reference
|
|
16
17
|
import time
|
|
17
18
|
|
|
18
19
|
|
|
@@ -609,7 +610,7 @@ class OrderMetaCache(object):
|
|
|
609
610
|
for order_ref in order_refs:
|
|
610
611
|
record = self.by_ref.get(self._ctx(callback_record) + (order_ref,))
|
|
611
612
|
if record and user_order_id and is_unique_order_remark(user_order_id_from_data(record)):
|
|
612
|
-
if
|
|
613
|
+
if not order_remark_matches_reference(user_order_id_from_data(record), user_order_id):
|
|
613
614
|
record = None
|
|
614
615
|
if record:
|
|
615
616
|
confidence = "order_ref"
|
|
@@ -343,13 +343,23 @@ def managed_source(source_path, identity_path, identity, descriptor):
|
|
|
343
343
|
for start, end, text in sorted(replacements, reverse=True):
|
|
344
344
|
lines[start:end] = [text]
|
|
345
345
|
lines[0] = "# coding: utf-8\n"
|
|
346
|
-
source = "".join(lines)
|
|
347
|
-
ast.parse(source, feature_version=(3, 6))
|
|
348
|
-
helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
346
|
+
source = "".join(lines)
|
|
347
|
+
ast.parse(source, feature_version=(3, 6))
|
|
348
|
+
helper = Path(__file__).with_name("qmt_strategy_runtime.py").read_text(encoding="utf-8")
|
|
349
|
+
source_lines = source.splitlines(keepends=True)
|
|
350
|
+
# Adjacent literals compile to the same source string, while keeping every
|
|
351
|
+
# entry line visible in QMT's editor. A single ascii(source) hid thousands
|
|
352
|
+
# of Lite lines behind the same short lease wrapper as the other modes.
|
|
353
|
+
embedded = "\n".join(" " + ascii(line) for line in source_lines)
|
|
354
|
+
header = ("# coding: utf-8\n# cfquant managed strategy\n"
|
|
355
|
+
+ "# Mode: " + ascii(descriptor["mode"]) + "\n"
|
|
356
|
+
+ "# Entry: " + ascii(Path(source_path).name) + "\n"
|
|
357
|
+
+ "# Embedded entry: %d lines, preserved line by line below.\n" % len(source_lines)
|
|
358
|
+
+ "# The lease wrapper is shared by all modes.\n\n")
|
|
359
|
+
generated = (header + helper + "\n_cq_descriptor = " + ascii(descriptor)
|
|
360
|
+
+ "\n_cq_guard = _CqStrategyLease(_cq_descriptor).acquire()\n"
|
|
361
|
+
+ "_cq_guard.namespace = globals()\ntry:\n"
|
|
362
|
+
+ " exec(compile((\n%s\n ), %s, 'exec'), globals())\n" % (embedded, ascii(str(source_path)))
|
|
353
363
|
+ " for _cq_name in ('_cf_bridge', '_normal_bridge', '_trade_bridge'):\n"
|
|
354
364
|
+ " if globals().get(_cq_name) is not None:\n"
|
|
355
365
|
+ " globals()[_cq_name].account_type = %s\n" % ascii(identity["account_type"])
|
|
@@ -408,13 +418,17 @@ class QmtStrategyManager:
|
|
|
408
418
|
job.pop("deploy_retry_count", None)
|
|
409
419
|
job.pop("deploy_retry_after", None)
|
|
410
420
|
|
|
411
|
-
def _clear_generation_transition(self, job):
|
|
412
|
-
changed = False
|
|
413
|
-
for key in ("control_generation", "accepted_generations"):
|
|
414
|
-
if key in job:
|
|
415
|
-
job.pop(key, None)
|
|
416
|
-
changed = True
|
|
417
|
-
|
|
421
|
+
def _clear_generation_transition(self, job):
|
|
422
|
+
changed = False
|
|
423
|
+
for key in ("control_generation", "accepted_generations"):
|
|
424
|
+
if key in job:
|
|
425
|
+
job.pop(key, None)
|
|
426
|
+
changed = True
|
|
427
|
+
for role in job.get("roles", []):
|
|
428
|
+
if "reimport_from_generation" in role:
|
|
429
|
+
role.pop("reimport_from_generation", None)
|
|
430
|
+
changed = True
|
|
431
|
+
return changed
|
|
418
432
|
|
|
419
433
|
def _prepare_generation_transition(self, job, previous=None):
|
|
420
434
|
"""Keep old same-name containers valid until the replacement starts."""
|
|
@@ -467,19 +481,23 @@ class QmtStrategyManager:
|
|
|
467
481
|
)
|
|
468
482
|
job["control_generation"] = legacy
|
|
469
483
|
job["accepted_generations"] = accepted
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
for role in job.get("roles", []):
|
|
485
|
+
if (_runtime_generation(role) == legacy
|
|
486
|
+
and role.get("reimport_from_generation") != legacy):
|
|
487
|
+
# The old report survives QMT shutdown and import. Handle it
|
|
488
|
+
# once per replacement job, including across Web restarts;
|
|
489
|
+
# resetting every poll destroys the prepared/imported model.
|
|
490
|
+
updates = {
|
|
491
|
+
"reimport_from_generation": legacy,
|
|
492
|
+
"reimport_required": True,
|
|
493
|
+
"imported": False,
|
|
494
|
+
"queued": False,
|
|
495
|
+
"model_prepared": False,
|
|
496
|
+
}
|
|
497
|
+
for key, value in updates.items():
|
|
498
|
+
if role.get(key) != value:
|
|
499
|
+
role[key] = value
|
|
500
|
+
changed = True
|
|
483
501
|
if changed and job.get("state") == "error":
|
|
484
502
|
self._state(job, "waiting_exit")
|
|
485
503
|
return changed
|
|
@@ -1091,7 +1091,7 @@ def test_local_query_does_not_swallow_internal_type_error():
|
|
|
1091
1091
|
("xtdata.get_full_tick", {}, "trade"),
|
|
1092
1092
|
("xtdata.get_instrument_detail", {}, "trade"),
|
|
1093
1093
|
("xtdata.get_cb_info", {}, "trade"),
|
|
1094
|
-
("xtdata.get_local_data", {}, "
|
|
1094
|
+
("xtdata.get_local_data", {}, "normal"),
|
|
1095
1095
|
("xtdata.subscribe_quote", {}, "normal"),
|
|
1096
1096
|
("xtdata.subscribe_whole_quote", {}, "normal"),
|
|
1097
1097
|
("xtdata.get_market_data_ex", {"callback_event": "probe"}, "normal"),
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Offline regressions for split-QMT account setup and downloaded data."""
|
|
2
|
+
import ast
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
import tokenize
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
import cfquant_web_server as web
|
|
12
|
+
from cfquant.normal_bridge import NormalQmtBridge
|
|
13
|
+
from cfquant.pipe_bridge import PipeNormalQmtBridge, PipeTradeBridge
|
|
14
|
+
from cfquant.tx_trade_bridge import TxTradeBridge
|
|
15
|
+
from cfquant.tests.test_lite_parity import LITE_PATHS, _lite_bridge_class
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
19
|
+
TRADE_ENTRIES = sorted((ROOT / "qmt_scripts").rglob("CFQUANT_TRADE_LOWLAT*.py"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize("path", TRADE_ENTRIES, ids=lambda path: path.stem)
|
|
23
|
+
@pytest.mark.parametrize("source", ["runtime", "environment"])
|
|
24
|
+
def test_advanced_trade_entry_binds_credit_account_before_init(monkeypatch, path, source):
|
|
25
|
+
for name in ("CFQUANT_ACCOUNT_ID", "CFQUANT_ACCOUNT_TYPE"):
|
|
26
|
+
monkeypatch.delenv(name, raising=False)
|
|
27
|
+
identity = {"account_id": "C123", "account_type": "CREDIT"}
|
|
28
|
+
if source == "environment":
|
|
29
|
+
for name, value in identity.items():
|
|
30
|
+
monkeypatch.setenv("CFQUANT_" + name.upper(), value)
|
|
31
|
+
with tokenize.open(str(path)) as stream:
|
|
32
|
+
tree = ast.parse(stream.read())
|
|
33
|
+
bridge = SimpleNamespace(account_type="", run_forever=lambda **kwargs: None)
|
|
34
|
+
bound = []
|
|
35
|
+
bridge.set_context = lambda context: bound.append((bridge.account_id, bridge.account_type))
|
|
36
|
+
|
|
37
|
+
def start(context, **kwargs):
|
|
38
|
+
bridge.account_id = kwargs["account_id"]
|
|
39
|
+
return bridge
|
|
40
|
+
|
|
41
|
+
namespace = dict(os=os, BRIDGE_ID="test", RUNTIME_CONFIG={}, RUNTIME_CHANNELS={},
|
|
42
|
+
QMT_MARKET="", USER_BRIDGE_ID="default", BRIDGE_CHANNELS={"trade": "trade"},
|
|
43
|
+
_load_runtime_config=lambda: identity if source == "runtime" else {},
|
|
44
|
+
_env_allows_runtime_override=lambda *args: True,
|
|
45
|
+
_print_log=lambda *args: None, _ENTRY_VERSION="test",
|
|
46
|
+
start_tx_trade_bridge=start)
|
|
47
|
+
# Execute account setup and bridge construction from each shipped entry,
|
|
48
|
+
# omitting imports and transport startup so this cannot contact a live QMT.
|
|
49
|
+
nodes = []
|
|
50
|
+
for node in tree.body:
|
|
51
|
+
if isinstance(node, ast.Assign) and any(
|
|
52
|
+
isinstance(target, ast.Name) and target.id in ("DEFAULT_ACCOUNT_ID", "DEFAULT_ACCOUNT_TYPE")
|
|
53
|
+
for target in node.targets
|
|
54
|
+
):
|
|
55
|
+
nodes.append(node)
|
|
56
|
+
elif isinstance(node, ast.FunctionDef) and node.name in ("_apply_runtime_config", "_config_bool", "init"):
|
|
57
|
+
nodes.append(node)
|
|
58
|
+
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(path), "exec"), namespace)
|
|
59
|
+
namespace["_apply_runtime_config"]()
|
|
60
|
+
nodes = [node for node in tree.body if (
|
|
61
|
+
isinstance(node, ast.Assign) and isinstance(node.value, ast.Call)
|
|
62
|
+
and isinstance(node.value.func, ast.Name) and node.value.func.id == "start_tx_trade_bridge"
|
|
63
|
+
) or (isinstance(node, ast.If) and "DEFAULT_ACCOUNT_TYPE" in ast.unparse(node.test))]
|
|
64
|
+
exec(compile(ast.Module(body=nodes, type_ignores=[]), str(path), "exec"), namespace)
|
|
65
|
+
namespace["init"](object())
|
|
66
|
+
assert bound == [("C123", "CREDIT")]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize("mode", ["lttx", "ctypes"])
|
|
70
|
+
@pytest.mark.parametrize("requested_channel", [None, "trade"])
|
|
71
|
+
@pytest.mark.parametrize("read_action", [
|
|
72
|
+
"get_local_data", "get_market_data", "get_market_data_ex", "get_financial_data",
|
|
73
|
+
"get_financial_data_ori", "get_financial_data_raw",
|
|
74
|
+
])
|
|
75
|
+
def test_download_then_read_uses_same_qmt_storage(monkeypatch, mode, requested_channel, read_action):
|
|
76
|
+
stores = {"normal": {}, "trade": {}}
|
|
77
|
+
if mode == "ctypes":
|
|
78
|
+
stores["trade"] = stores["normal"]
|
|
79
|
+
frames = {"000001.SZ": pd.DataFrame({"close": [11.6]})}
|
|
80
|
+
|
|
81
|
+
def functions(store):
|
|
82
|
+
return {
|
|
83
|
+
"download_history_data": lambda *args: store.update(history=frames),
|
|
84
|
+
"download_financial_data": lambda *args: store.update(financial=frames),
|
|
85
|
+
"get_market_data_ex": lambda *args: store.get("history", {}),
|
|
86
|
+
"get_financial_data": lambda *args: store.get("financial", {}),
|
|
87
|
+
"get_raw_financial_data": lambda *args: store.get("financial", {}),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
normal_cls, trade_cls = (NormalQmtBridge, TxTradeBridge) if mode == "lttx" else (PipeNormalQmtBridge, PipeTradeBridge)
|
|
91
|
+
bridges = {channel: cls(object(), globals_dict=functions(stores[channel]), show=False)
|
|
92
|
+
for channel, cls in (("normal", normal_cls), ("trade", trade_cls))}
|
|
93
|
+
monkeypatch.setattr(web, "WEB_CONFIG", None)
|
|
94
|
+
monkeypatch.setattr(web, "resolve_bridge_id", lambda **kwargs: "test")
|
|
95
|
+
monkeypatch.setattr(web, "resolve_account_mode", lambda *args, **kwargs: mode)
|
|
96
|
+
monkeypatch.setattr(web, "resolve_market_route_for_request", lambda **kwargs: ("test", None))
|
|
97
|
+
calls = []
|
|
98
|
+
|
|
99
|
+
def request(bridge_id, channel, action, params, **kwargs):
|
|
100
|
+
calls.append((action, channel))
|
|
101
|
+
return bridges[channel]._dispatch(action, params, {"client_id": "offline"})
|
|
102
|
+
|
|
103
|
+
monkeypatch.setattr(web.CLIENTS, "request", request)
|
|
104
|
+
params = {"stock_code": "000001.SZ", "stock_list": ["000001.SZ"],
|
|
105
|
+
"field_list": ["close"], "period": "1d"}
|
|
106
|
+
download = "download_financial_data" if "financial" in read_action else "download_history_data"
|
|
107
|
+
web.account_request("C123", "test", requested_channel, "xtdata." + download, params)
|
|
108
|
+
result = web.account_request("C123", "test", requested_channel, "xtdata." + read_action, params)["result"]
|
|
109
|
+
assert result, calls
|
|
110
|
+
assert calls == [("xtdata." + download, "normal"), ("xtdata." + read_action, "normal")]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@pytest.mark.parametrize("action", ["xtdata.get_market_data", "xtdata.get_market_data_ex"])
|
|
114
|
+
@pytest.mark.parametrize("period", ["l2quote", "l2order", "l2transaction"])
|
|
115
|
+
def test_level2_market_reads_keep_trade_channel(action, period):
|
|
116
|
+
assert web.route_channel_for_account("C123", mode="lttx", action=action,
|
|
117
|
+
params={"period": period}) == "trade"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
BRIDGES = [TxTradeBridge, NormalQmtBridge, PipeTradeBridge, PipeNormalQmtBridge]
|
|
121
|
+
BRIDGES += [_lite_bridge_class(path) for path in LITE_PATHS]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize("bridge_cls", BRIDGES)
|
|
125
|
+
@pytest.mark.parametrize("raw", [
|
|
126
|
+
{"success": "false", "msg": "rejected", "apply_id": "-1"},
|
|
127
|
+
{"success": True, "msg": "accepted", "apply_id": []},
|
|
128
|
+
{"success": True, "msg": "accepted", "apply_id": True},
|
|
129
|
+
{"success": True, "apply_id": "A1"},
|
|
130
|
+
])
|
|
131
|
+
def test_all_modes_reject_malformed_smt_response(bridge_cls, raw):
|
|
132
|
+
bridge = bridge_cls(object(), show=False, globals_dict={"smt_appointment_cancel": lambda *args: raw})
|
|
133
|
+
bridge._send_trader_event = lambda *args: pytest.fail("malformed response delivered")
|
|
134
|
+
with pytest.raises(RuntimeError, match="business response"):
|
|
135
|
+
bridge._dispatch("xttrader.smt_appointment_cancel", {
|
|
136
|
+
"account": {"account_id": "C123", "account_type": "CREDIT"}, "seq": 1, "args": ["A1"],
|
|
137
|
+
}, {"client_id": "offline"})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize("bridge_cls", BRIDGES)
|
|
141
|
+
def test_all_modes_accept_native_smt_fields(bridge_cls):
|
|
142
|
+
raw = type("BrokerResponse", (), dict(__slots__=(), m_bSuccess=True,
|
|
143
|
+
m_strMsg="accepted", m_strApplyID="000123", m_nSeq=987))()
|
|
144
|
+
bridge = bridge_cls(object(), show=False, globals_dict={"smt_appointment_cancel": lambda *args: raw})
|
|
145
|
+
events = []
|
|
146
|
+
bridge._send_trader_event = lambda client, event, data: events.append(data)
|
|
147
|
+
result = bridge._dispatch("xttrader.smt_appointment_cancel", {
|
|
148
|
+
"account": {"account_id": "C123", "account_type": "CREDIT"}, "seq": 1, "args": ["A1"],
|
|
149
|
+
}, {"client_id": "offline"})
|
|
150
|
+
assert result == {"seq": 1, "accepted": True}
|
|
151
|
+
assert len(events) == 1
|
|
152
|
+
assert events[0]["success"] is True and events[0]["apply_id"] == "000123"
|
|
153
|
+
assert events[0]["seq"] == 1
|
|
@@ -98,6 +98,35 @@ def test_edit_without_mode_preserves_lite_binding_and_payload(web_deployment):
|
|
|
98
98
|
assert_lite_package(manager, root)
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def test_edit_advanced_binding_to_lite_retires_both_advanced_models(web_deployment):
|
|
102
|
+
web, manager, row, infos, root = web_deployment
|
|
103
|
+
trade_root = root.with_name("QMT_trade")
|
|
104
|
+
shutil.copytree(root, trade_root)
|
|
105
|
+
row.update(mode="lttx", qmt_trade_dir=str(trade_root / "bin.x64"))
|
|
106
|
+
row["qmt_strategy"]["autorun"] = True
|
|
107
|
+
trade_info = dict(infos[0], qmt_role="trade", core_dir=str(trade_root / "bin.x64"),
|
|
108
|
+
path=str(trade_root / "bin.x64" / "cfquant_bridge_config.json"))
|
|
109
|
+
manager.configure(row, infos + [trade_info])
|
|
110
|
+
advanced_name = "CFQ_%s_LTTX" % row["account_id"]
|
|
111
|
+
assert models(root)[advanced_name].getAttribute("startupAutorun") == "1"
|
|
112
|
+
assert models(trade_root)[advanced_name].getAttribute("startupAutorun") == "1"
|
|
113
|
+
|
|
114
|
+
body = {key: row[key] for key in ("account_id", "account_type", "bridge_id", "qmt_dir")}
|
|
115
|
+
result = web.save_account_runtime_config(dict(body, mode="lite", qmt_trade_dir=""))
|
|
116
|
+
|
|
117
|
+
assert result["account"]["mode"] == "lite"
|
|
118
|
+
assert result["account"]["qmt_trade_dir"] == ""
|
|
119
|
+
assert not result["qmt_strategy_deploy"].get("error")
|
|
120
|
+
job = assert_lite_package(manager, root)
|
|
121
|
+
assert job["roles"][0]["name"] == "CFQ_%s_LITE" % row["account_id"]
|
|
122
|
+
assert models(root)[job["roles"][0]["name"]].getAttribute("startupAutorun") == "1"
|
|
123
|
+
assert models(root)[advanced_name].getAttribute("startupAutorun") == "0"
|
|
124
|
+
assert models(trade_root)[advanced_name].getAttribute("startupAutorun") == "0"
|
|
125
|
+
trade_job = next(job for job in manager.jobs.values() if job["root"] == str(trade_root))
|
|
126
|
+
assert trade_job["enabled"] is False
|
|
127
|
+
assert trade_job["state"] == "disabled"
|
|
128
|
+
|
|
129
|
+
|
|
101
130
|
@pytest.mark.parametrize("single_account", [False, True])
|
|
102
131
|
def test_shared_qmt_directory_does_not_drop_lite_market_entries(single_account, web_deployment):
|
|
103
132
|
web, _, row, _, root = web_deployment
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Focused runtime checks for the standalone Lite compatibility adapters."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from cfquant import level2
|
|
10
|
+
from cfquant.tx_trade_bridge import TxTradeBridge
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
14
|
+
LITE_PATHS = sorted((ROOT / "qmt_scripts").rglob("CFQUANT_LITE*.py"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _lite_bridge_class(path):
|
|
18
|
+
source = path.read_bytes().decode("gbk")
|
|
19
|
+
tree = ast.parse(source)
|
|
20
|
+
class_node = next(
|
|
21
|
+
node for node in tree.body
|
|
22
|
+
if isinstance(node, ast.ClassDef) and node.name == "TxTradeBridge"
|
|
23
|
+
)
|
|
24
|
+
methods = {
|
|
25
|
+
"_dispatch_xtdata_compat", "_get_market_data", "_get_market_data_ex",
|
|
26
|
+
"_get_local_data", "_call_local_data", "_dispatch_xttrader_compat",
|
|
27
|
+
"_query_account_id", "_credit_account_id", "_call_trade_detail_data",
|
|
28
|
+
"_credit_row_data", "_credit_field_value", "_format_credit_rows",
|
|
29
|
+
"_query_credit_detail", "_query_qmt_objects", "_query_stk_compacts",
|
|
30
|
+
"_smt_request", "_smt_response_data", "_format_transfer_result", "_format_compat_query_objects",
|
|
31
|
+
"_format_compat_query_object", "_known_query_fields", "_plain_object",
|
|
32
|
+
"_first_present",
|
|
33
|
+
}
|
|
34
|
+
class_node.bases = [ast.Name(id="BaseTradeBridge", ctx=ast.Load())]
|
|
35
|
+
class_node.body = [
|
|
36
|
+
node for node in class_node.body
|
|
37
|
+
if isinstance(node, ast.FunctionDef) and node.name in methods
|
|
38
|
+
]
|
|
39
|
+
constants = {
|
|
40
|
+
"SMT_ASYNC_ARGUMENT_COUNTS", "XTTRADER_TRANSFER_RESULT_METHODS",
|
|
41
|
+
"LITE_OBJECT_QUERY_METHODS", "_LITE_CREDIT_FIELD_ALIASES",
|
|
42
|
+
"L2_GET_PERIODS", "L2_PERIODS", "KLINE_PERIODS",
|
|
43
|
+
"XTDATA_COMPAT_CANDIDATES", "XTDATA_MAINCHAIN_UNSUPPORTED",
|
|
44
|
+
}
|
|
45
|
+
body = [
|
|
46
|
+
node for node in tree.body
|
|
47
|
+
if isinstance(node, ast.Assign)
|
|
48
|
+
and any(isinstance(target, ast.Name) and target.id in constants for target in node.targets)
|
|
49
|
+
]
|
|
50
|
+
body.append(class_node)
|
|
51
|
+
module = ast.Module(body=body, type_ignores=[])
|
|
52
|
+
namespace = dict(vars(__import__("cfquant.tx_trade_bridge", fromlist=["*"])))
|
|
53
|
+
namespace.update(
|
|
54
|
+
BaseTradeBridge=TxTradeBridge,
|
|
55
|
+
market_data_result_needs_fallback=level2.market_data_result_needs_fallback,
|
|
56
|
+
)
|
|
57
|
+
exec(compile(ast.fix_missing_locations(module), str(path), "exec"), namespace)
|
|
58
|
+
return namespace["TxTradeBridge"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _FakeQmt:
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.calls = []
|
|
64
|
+
|
|
65
|
+
def get_market_data(self, *args):
|
|
66
|
+
self.calls.append(("legacy", args))
|
|
67
|
+
return pd.DataFrame()
|
|
68
|
+
|
|
69
|
+
def get_market_data_ex(self, *args):
|
|
70
|
+
self.calls.append(("modern", args))
|
|
71
|
+
return {"000001.SZ": pd.DataFrame({"close": [11.6]})}
|
|
72
|
+
|
|
73
|
+
def get_trade_detail_data(self, account_id, account_type, detail_type, strategy_name):
|
|
74
|
+
self.calls.append(("detail", account_id, account_type, detail_type, strategy_name))
|
|
75
|
+
if detail_type == "account":
|
|
76
|
+
return [SimpleNamespace(m_strAccountID=account_id, m_dBalance=100.0, m_dFinUsedQuota=8.0)]
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
def get_unclosed_compacts(self, account_id, account_type):
|
|
80
|
+
self.calls.append(("compact", account_id, account_type))
|
|
81
|
+
return [SimpleNamespace(m_strAccountID=account_id, m_strInstrumentID="000001", m_eCompactType=1)]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _bridge(path):
|
|
85
|
+
bridge = _lite_bridge_class(path)(_FakeQmt(), account_id="CREDIT-1", show=False)
|
|
86
|
+
bridge._log = lambda *args, **kwargs: None
|
|
87
|
+
bridge.tx = SimpleNamespace(push=lambda *args, **kwargs: None)
|
|
88
|
+
return bridge
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_all_lite_entries_contain_shared_data_and_credit_adapters():
|
|
92
|
+
expected = {
|
|
93
|
+
"_get_market_data", "_get_local_data", "_get_instrument_detail",
|
|
94
|
+
"_get_cb_info", "_get_divid_factors", "_get_stock_list_in_sector",
|
|
95
|
+
"_query_credit_detail", "_query_stk_compacts", "_call_trade_detail_data",
|
|
96
|
+
}
|
|
97
|
+
for path in LITE_PATHS:
|
|
98
|
+
tree = ast.parse(path.read_bytes().decode("gbk"))
|
|
99
|
+
bridge = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TxTradeBridge")
|
|
100
|
+
names = {node.name for node in bridge.body if isinstance(node, ast.FunctionDef)}
|
|
101
|
+
assert expected <= names, path
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_lite_credit_detail_supports_qmt_four_argument_signature():
|
|
105
|
+
bridge = _bridge(LITE_PATHS[0])
|
|
106
|
+
result = bridge._dispatch(
|
|
107
|
+
"xttrader.query_credit_detail",
|
|
108
|
+
{"account": {"account_id": "CREDIT-1", "account_type": "CREDIT"}},
|
|
109
|
+
{"client_id": "test"},
|
|
110
|
+
)
|
|
111
|
+
assert result[0]["account_id"] == "CREDIT-1"
|
|
112
|
+
assert result[0]["account_type"] == 3
|
|
113
|
+
assert result[0]["cfquant_qmt_fields"]["m_dFinUsedQuota"] == 8.0
|
|
114
|
+
assert result[0]["cfquant_source"] == "get_trade_detail_data"
|
|
115
|
+
assert ("detail", "CREDIT-1", "credit", "account", "") in bridge.context.calls
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_lite_credit_compacts_and_market_fallback_match_common_contract():
|
|
119
|
+
bridge = _bridge(LITE_PATHS[0])
|
|
120
|
+
compact = bridge._dispatch(
|
|
121
|
+
"xttrader.query_stk_compacts",
|
|
122
|
+
{"account_id": "CREDIT-1", "account_type": "CREDIT"},
|
|
123
|
+
{"client_id": "test"},
|
|
124
|
+
)
|
|
125
|
+
assert compact[0]["account_id"] == "CREDIT-1"
|
|
126
|
+
assert compact[0]["instrument_id"] == "000001"
|
|
127
|
+
|
|
128
|
+
result = bridge._dispatch(
|
|
129
|
+
"xtdata.get_market_data",
|
|
130
|
+
{"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d"},
|
|
131
|
+
{"client_id": "test"},
|
|
132
|
+
)
|
|
133
|
+
assert "close" in result
|
|
134
|
+
local = bridge._dispatch(
|
|
135
|
+
"xtdata.get_local_data",
|
|
136
|
+
{"field_list": ["close"], "stock_list": ["000001.SZ"], "period": "1d"},
|
|
137
|
+
{"client_id": "test"},
|
|
138
|
+
)
|
|
139
|
+
assert "000001.SZ" in local
|
|
140
|
+
assert any(call[0] == "modern" and len(call[1]) == 9 for call in bridge.context.calls)
|