cfquant 0.2.49__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.49 → cfquant-0.2.50}/PKG-INFO +1 -1
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/normal_bridge.py +1 -0
- cfquant-0.2.50/cfquant/order_identity.py +173 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/order_meta.py +2 -1
- {cfquant-0.2.49 → 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.50/cfquant/tests/test_lite_parity.py +140 -0
- cfquant-0.2.50/cfquant/tests/test_trade_strategy_callback.py +205 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tx_trade_bridge.py +39 -11
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/version.py +2 -2
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant.egg-info/SOURCES.txt +3 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant_web_server.py +14 -5
- {cfquant-0.2.49 → cfquant-0.2.50}/pyproject.toml +1 -1
- {cfquant-0.2.49 → cfquant-0.2.50}/qmt_scripts/CFQUANT_LITE.py +916 -18
- {cfquant-0.2.49 → cfquant-0.2.50}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +9 -2
- {cfquant-0.2.49 → 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.49 → 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.49 → 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.49 → 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.49 → cfquant-0.2.50}/web_dashboard/app.js +1 -1
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/index.html +2 -2
- cfquant-0.2.49/cfquant/order_identity.py +0 -55
- {cfquant-0.2.49 → cfquant-0.2.50}/LICENSE +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/LTtx/__init__.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/MANIFEST.in +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/README.md +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/__init__.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/build_info.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/channels.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/cli.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/client.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/config.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/hub.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/level2.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/log_management.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/management.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/protocol.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/qmt_bridge.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → 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.49 → cfquant-0.2.50}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.49 → 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.49 → 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.49 → cfquant-0.2.50}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_qmt_strategy_deploy.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/tx.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/xtdata.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant/xttype.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.49 → 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.49 → 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.49 → 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.49 → cfquant-0.2.50}/requirements.txt +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/setup.cfg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/api-tester.js +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/python-api.js +0 -0
- {cfquant-0.2.49 → cfquant-0.2.50}/web_dashboard/styles.css +0 -0
- {cfquant-0.2.49 → 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"
|
|
@@ -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
|
|
@@ -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)
|