cfquant 0.2.67__tar.gz → 0.2.70__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.67 → cfquant-0.2.70}/PKG-INFO +1 -1
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/__init__.py +2 -1
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/client.py +2 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/monitor_notifications.py +3 -30
- cfquant-0.2.70/cfquant/notify.py +52 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_bridge.py +105 -30
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_python_runtime.py +39 -10
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_deploy.py +11 -3
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_python_runtime_browser.py +82 -2
- cfquant-0.2.70/cfquant/tests/test_binding_webhook_browser.py +106 -0
- cfquant-0.2.70/cfquant/tests/test_history_download_memory.py +68 -0
- cfquant-0.2.70/cfquant/tests/test_notify.py +214 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_auto_login_integration.py +32 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_python_runtime.py +180 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_deploy.py +19 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trading_monitor.py +57 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trading_monitor_browser.py +73 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/trading_monitor.py +16 -4
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tx_trade_bridge.py +109 -30
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/version.py +2 -2
- cfquant-0.2.70/cfquant/webhook.py +151 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xtdata.py +2 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/PKG-INFO +1 -1
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/SOURCES.txt +6 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant_web_server.py +262 -46
- {cfquant-0.2.67 → cfquant-0.2.70}/pyproject.toml +1 -1
- {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT_LITE.py +108 -31
- {cfquant-0.2.67 → cfquant-0.2.70}/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 +108 -31
- {cfquant-0.2.67 → cfquant-0.2.70}/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 +108 -31
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/api-tester.js +1 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/app.js +212 -53
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/index.html +44 -5
- cfquant-0.2.70/web_dashboard/notify-api-data.js +38 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/python-api.js +4 -4
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/styles.css +8 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/trading-monitor.js +106 -16
- {cfquant-0.2.67 → cfquant-0.2.70}/LICENSE +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/__init__.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/Config.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/LTtx_server.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/__init__.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/LTtx/tx/tx.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/MANIFEST.in +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/README.md +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/_editable_install.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/account_routing.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/algo.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/algo_broker.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/batch_orders.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/build_info.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/callback_dispatcher.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/cftrader.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/channels.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/cli.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/conditional_orders.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/config.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/diagnostic_context.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/diagnostics.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/flow_control.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/hub.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/level2.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/log_management.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/logging_i18n.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/management.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/monitor_performance.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/normal_bridge.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/order_identity.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/order_meta.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_bridge.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_client.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_hub.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/pipe_transport.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/planning_broker.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/position_sizing.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/protocol.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_binding_defaults.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_binding_settings.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_component_agent.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_component_login.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_credentials.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_dependencies.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_inner.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_inner_catalog.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_inner_runtime.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_mac_agent.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_native_files.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_native_worker.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_pipe_runtime.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_agent.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_package.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_strategy_runtime.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_update.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_agent.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_discovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_runtime.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_settings.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/qmt_vm_worker.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/quote_history.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/quote_leases.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/quote_service.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/remote_adapter.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/remote_management.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/remote_supervisor.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/rpc_timing.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/runtime_report.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/shared_quotes.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/smart_orders.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/stock_connect.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_bridge.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_client.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_manager.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/subscription_store.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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" +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/cfquant/tests/29_Level2/350/241/214/346/203/205/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/cfquant/tests/34_/345/205/250/346/216/250/345/206/205/351/203/250/345/273/266/350/277/237/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/cfquant/tests/6_/345/233/236/350/260/203/346/265/213/350/257/225.py" +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/cfquant/tests/_helpers.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/qmt_vm_native_fixture.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/readme.md +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_account_query_timing.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_account_subscription_probe.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_activity_log_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_advanced_parity.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_algo.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_algo_broker.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_api_tester_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_api_tester_web.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_auto_login_app.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_delete_confirmation.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_live_refresh_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_runtime_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_binding_vm_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_callback_recovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_callback_requests.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_cftrader.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_cftrader_tutorial.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_cftrader_web.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_credit_compat.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_dataframe_encoding.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_diagnostic_context.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_json_serialization.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_latency_flow_control.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_account_routing.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_deployment_paths.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_parity.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_lite_qmt_dispatch.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_local_trading_calendar.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_log_management.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_management.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_management_batch.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_market_data_fallback.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_market_timing.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_monitor_notifications.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_monitor_performance.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_navigation_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_order_identity.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_order_time_callback.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_pipe_shutdown.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_project_updater.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_python_api_reference.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_auto_login_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_binding_defaults.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_binding_settings.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_component_login.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_dependencies.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_fileless.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner_reference.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_inner_web.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_files.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_reuse.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_native_worker.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_package_import.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_capabilities.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_recovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_strategy_web.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_update.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_discovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_native.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_runtime.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_qmt_vm_settings.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_quote_latency.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_quote_latency_encoding.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_quote_service.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_remote_management.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_remote_management_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_remote_version_check.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_setup_qmt_prompt.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_shared_quotes.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_shared_quotes_generated.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_smart_orders.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_smart_orders_api.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_smart_orders_docs.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_split_order_identity.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_startup_recovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_stock_connect.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_stock_connect_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_strategy_recovery_api.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_subscription_congestion.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_subscription_persistence.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_table_conversion.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trade_strategy_callback.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trader_subscription_recovery.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_trading_dates.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_tutorial_reader.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_tx_auto_import.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_tx_shutdown.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_version_check.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tests/test_version_check_browser.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/THIRD_PARTY.md +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/COPYING +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/LICENSE +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/NEWS +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/README +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/THANKS.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx-doc.html +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx-doc.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx.1 +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tools/upx/upx.exe +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/trading_calendar.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/tx.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xtconstant.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xttrader.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant/xttype.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/dependency_links.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/entry_points.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/requires.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant.egg-info/top_level.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/cfquant_pipe_hub.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/CFQUANT_TRADE_LOWLAT.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/qmt_scripts/__init__.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/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" +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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" +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/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.67 → cfquant-0.2.70}/requirements.txt +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/setup.cfg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/__init__.py +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/activity-log.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/amber-pulse.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/copper-grid.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/market-blue.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/rose-circuit.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/signal-green.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/slate-wave.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/teal-orbit.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/avatars/violet-node.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/cfquant-architecture.html +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/cftrader-api-data.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/docs.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/favicon.svg +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-10.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-11.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-12.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-13.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-14.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-15.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-16.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-17.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-18.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-19.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-20.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-21.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-4.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-5.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-6.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-7.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-8.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/image/qmt_deploy/image-9.png +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/navigation.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/python-api-data.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/qmt-inner-api-data.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/remote-management.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/smart-order-api-data.js +0 -0
- {cfquant-0.2.67 → cfquant-0.2.70}/web_dashboard/vendor/mermaid.min.js +0 -0
|
@@ -12,7 +12,7 @@ cfquant: 面向 QMT 的 xtquant 兼容层。
|
|
|
12
12
|
import sys as _sys
|
|
13
13
|
|
|
14
14
|
from . import xtconstant, xtdata, xttrader, xttype
|
|
15
|
-
from . import cftrader
|
|
15
|
+
from . import cftrader, notify
|
|
16
16
|
from .qmt_inner import QmtInnerApi
|
|
17
17
|
from .client import CfquantError, CfquantTimeout, configure, get_client
|
|
18
18
|
from .version import __version__
|
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"xtdata",
|
|
29
29
|
"xttrader",
|
|
30
30
|
"cftrader",
|
|
31
|
+
"notify",
|
|
31
32
|
"QmtInnerApi",
|
|
32
33
|
"xttype",
|
|
33
34
|
"configure",
|
|
@@ -6,11 +6,9 @@ never perform SMTP or HTTP work on a QMT callback/status thread.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import copy
|
|
9
|
-
import base64
|
|
10
9
|
from datetime import datetime, timedelta, timezone
|
|
11
10
|
from email.message import EmailMessage
|
|
12
11
|
import hashlib
|
|
13
|
-
import hmac
|
|
14
12
|
import json
|
|
15
13
|
import os
|
|
16
14
|
from pathlib import Path
|
|
@@ -20,9 +18,7 @@ import ssl
|
|
|
20
18
|
import tempfile
|
|
21
19
|
import threading
|
|
22
20
|
import time
|
|
23
|
-
import
|
|
24
|
-
import urllib.parse
|
|
25
|
-
import urllib.request
|
|
21
|
+
from .webhook import send_webhook
|
|
26
22
|
|
|
27
23
|
|
|
28
24
|
SHANGHAI = timezone(timedelta(hours=8))
|
|
@@ -566,28 +562,5 @@ class MonitorNotificationService:
|
|
|
566
562
|
server.send_message(msg)
|
|
567
563
|
|
|
568
564
|
def _send_webhook(self, config, event):
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
raise ValueError("webhook url is required")
|
|
572
|
-
kind = str(config.get("type") or "generic").lower()
|
|
573
|
-
if kind == "dingtalk" and config.get("secret"):
|
|
574
|
-
timestamp = str(int(time.time() * 1000))
|
|
575
|
-
secret = str(config.get("secret"))
|
|
576
|
-
raw = "%s\n%s" % (timestamp, secret)
|
|
577
|
-
sign = base64.b64encode(hmac.new(secret.encode("utf-8"), raw.encode("utf-8"), hashlib.sha256).digest()).decode("ascii")
|
|
578
|
-
separator = "&" if "?" in url else "?"
|
|
579
|
-
url = "%s%stimestamp=%s&sign=%s" % (url, separator, urllib.parse.quote(timestamp), urllib.parse.quote(sign))
|
|
580
|
-
text = self._format_text(event)
|
|
581
|
-
if kind == "feishu":
|
|
582
|
-
body = {"msg_type": "text", "content": {"text": text}}
|
|
583
|
-
elif kind in ("dingtalk", "wecom"):
|
|
584
|
-
body = {"msgtype": "text", "text": {"content": text}}
|
|
585
|
-
else:
|
|
586
|
-
body = {"event_type": event.get("event_type"), "title": event.get("title"),
|
|
587
|
-
"message": event.get("message"), "payload": event.get("payload"),
|
|
588
|
-
"occurred_at": event.get("occurred_at")}
|
|
589
|
-
request = urllib.request.Request(url, data=json.dumps(body, ensure_ascii=False, default=str).encode("utf-8"),
|
|
590
|
-
headers={"Content-Type": "application/json; charset=utf-8", "User-Agent": "cfquant-monitor"}, method="POST")
|
|
591
|
-
with urllib.request.urlopen(request, timeout=8) as response:
|
|
592
|
-
if int(getattr(response, "status", 200)) >= 400:
|
|
593
|
-
raise urllib.error.HTTPError(url, response.status, "webhook returned %s" % response.status, response.headers, None)
|
|
565
|
+
payload = {key: event.get(key) for key in ("event_type", "title", "message", "payload", "occurred_at")}
|
|
566
|
+
return send_webhook(config, self._format_text(event), payload=payload)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""User-authored messages: explicit Webhook first, otherwise account binding."""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from .webhook import WebhookError, normalize_webhook, send_webhook, validate_message
|
|
7
|
+
|
|
8
|
+
_client = None
|
|
9
|
+
_client_lock = threading.Lock()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def close():
|
|
13
|
+
"""Release the notification RPC connection; the next call reconnects."""
|
|
14
|
+
global _client
|
|
15
|
+
with _client_lock:
|
|
16
|
+
client, _client = _client, None
|
|
17
|
+
if client is not None:
|
|
18
|
+
client.close()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
atexit.register(close)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def send(message, account_id=None, *, title="", webhook_url=None, provider=None,
|
|
25
|
+
secret=None, account_type=None, account_key=None, bridge_id=None, timeout=8):
|
|
26
|
+
"""Send text synchronously; returns sent/provider/source, or raises on failure.
|
|
27
|
+
|
|
28
|
+
webhook_url and provider must be supplied together and override the binding
|
|
29
|
+
completely (including its secret). Account-based sends require cfquant Web
|
|
30
|
+
and LTtx, but do not require QMT to be online. No automatic retry is performed.
|
|
31
|
+
"""
|
|
32
|
+
timeout = validate_message(message, title, timeout)
|
|
33
|
+
if webhook_url is not None or provider is not None or secret is not None:
|
|
34
|
+
if not webhook_url or not provider:
|
|
35
|
+
raise ValueError("调用时覆盖推送配置必须同时提供 webhook_url 和 provider")
|
|
36
|
+
config = normalize_webhook({"enabled": True, "url": webhook_url, "provider": provider, "secret": secret})
|
|
37
|
+
return dict(send_webhook(config, message, title=title, timeout=timeout), source="explicit")
|
|
38
|
+
if not str(account_id or "").strip() and not str(account_key or "").strip():
|
|
39
|
+
raise ValueError("请指定资金账号 account_id 或 account_key,或提供 webhook_url 和 provider")
|
|
40
|
+
params = {"message": message, "title": title, "timeout": timeout}
|
|
41
|
+
params.update({key: value for key, value in dict(account_id=account_id, account_type=account_type,
|
|
42
|
+
account_key=account_key, bridge_id=bridge_id).items() if value is not None})
|
|
43
|
+
global _client
|
|
44
|
+
with _client_lock:
|
|
45
|
+
if _client is None:
|
|
46
|
+
from .client import create_rpc_client
|
|
47
|
+
_client = create_rpc_client(transport="web_lttx")
|
|
48
|
+
client = _client
|
|
49
|
+
return client.request("cfquant.notify.send", params, timeout=timeout + 5)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["send", "close", "WebhookError"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .diagnostic_context import versioned_message
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
import os
|
|
4
|
+
import gc
|
|
4
5
|
import queue
|
|
5
6
|
import sys
|
|
6
7
|
import threading
|
|
@@ -516,47 +517,91 @@ class CfquantQmtBridge(object):
|
|
|
516
517
|
})
|
|
517
518
|
raise NotImplementedError("当前QMT环境未提供download_history_data2")
|
|
518
519
|
|
|
519
|
-
|
|
520
|
+
stock_list = params.get("stock_list", params.get("code_list", []))
|
|
521
|
+
if isinstance(stock_list, str):
|
|
522
|
+
stock_list = [item.strip() for item in stock_list.replace(",", ",").split(",") if item.strip()]
|
|
523
|
+
else:
|
|
524
|
+
stock_list = list(stock_list or [])
|
|
525
|
+
batches = self._history_download_batches(stock_list, params)
|
|
526
|
+
total = len(stock_list)
|
|
527
|
+
completed = 0
|
|
528
|
+
|
|
529
|
+
def callback(data, batch_offset=0, batch_index=0, batch_total=1):
|
|
520
530
|
if callback_event and client_id:
|
|
531
|
+
payload = dict(data) if isinstance(data, dict) else {"message": str(data)}
|
|
532
|
+
batch_finished = payload.get("finished")
|
|
533
|
+
try:
|
|
534
|
+
batch_finished = max(0, min(int(batch_finished), len(batches[batch_index])))
|
|
535
|
+
except (TypeError, ValueError, IndexError):
|
|
536
|
+
batch_finished = 0
|
|
537
|
+
payload.update({
|
|
538
|
+
"finished": min(total, batch_offset + batch_finished),
|
|
539
|
+
"total": total,
|
|
540
|
+
"batch_index": batch_index + 1,
|
|
541
|
+
"batch_total": batch_total,
|
|
542
|
+
"batch_finished": batch_finished,
|
|
543
|
+
"batch_size": len(batches[batch_index]) if batch_index < len(batches) else 0,
|
|
544
|
+
})
|
|
521
545
|
self._send_event(
|
|
522
546
|
client_id,
|
|
523
547
|
callback_event,
|
|
524
|
-
|
|
548
|
+
payload,
|
|
525
549
|
meta=self._download_event_meta(params, "history", "progress"),
|
|
526
550
|
)
|
|
527
551
|
|
|
528
|
-
callback_func = callback if callback_event else None
|
|
529
|
-
variants = []
|
|
530
|
-
incrementally = params.get("incrementally")
|
|
531
|
-
if incrementally is not None:
|
|
532
|
-
variants.append((
|
|
533
|
-
(
|
|
534
|
-
params.get("stock_list", params.get("code_list", [])),
|
|
535
|
-
params.get("period", "1d"),
|
|
536
|
-
params.get("start_time", ""),
|
|
537
|
-
params.get("end_time", ""),
|
|
538
|
-
callback_func,
|
|
539
|
-
incrementally,
|
|
540
|
-
),
|
|
541
|
-
{},
|
|
542
|
-
))
|
|
543
|
-
variants.append((
|
|
544
|
-
(
|
|
545
|
-
params.get("stock_list", params.get("code_list", [])),
|
|
546
|
-
params.get("period", "1d"),
|
|
547
|
-
params.get("start_time", ""),
|
|
548
|
-
params.get("end_time", ""),
|
|
549
|
-
callback_func,
|
|
550
|
-
),
|
|
551
|
-
{},
|
|
552
|
-
))
|
|
553
552
|
if emit_lifecycle:
|
|
554
553
|
self._send_download_event(client_id, params, "history", "submitted", {
|
|
555
554
|
"stage": "submitted",
|
|
556
555
|
"message": "history download request submitted",
|
|
556
|
+
"total": total,
|
|
557
|
+
"batch_total": len(batches),
|
|
558
|
+
"batch_size": self._history_download_batch_size(params),
|
|
557
559
|
})
|
|
558
560
|
try:
|
|
559
|
-
|
|
561
|
+
incrementally = params.get("incrementally")
|
|
562
|
+
batch_total = len(batches)
|
|
563
|
+
for batch_index, batch in enumerate(batches):
|
|
564
|
+
batch_offset = completed
|
|
565
|
+
callback_func = (
|
|
566
|
+
(lambda data, offset=batch_offset, index=batch_index:
|
|
567
|
+
callback(data, offset, index, batch_total))
|
|
568
|
+
if callback_event else None
|
|
569
|
+
)
|
|
570
|
+
variants = []
|
|
571
|
+
if incrementally is not None:
|
|
572
|
+
variants.append((
|
|
573
|
+
(
|
|
574
|
+
batch,
|
|
575
|
+
params.get("period", "1d"),
|
|
576
|
+
params.get("start_time", ""),
|
|
577
|
+
params.get("end_time", ""),
|
|
578
|
+
callback_func,
|
|
579
|
+
incrementally,
|
|
580
|
+
),
|
|
581
|
+
{},
|
|
582
|
+
))
|
|
583
|
+
variants.append((
|
|
584
|
+
(
|
|
585
|
+
batch,
|
|
586
|
+
params.get("period", "1d"),
|
|
587
|
+
params.get("start_time", ""),
|
|
588
|
+
params.get("end_time", ""),
|
|
589
|
+
callback_func,
|
|
590
|
+
),
|
|
591
|
+
{},
|
|
592
|
+
))
|
|
593
|
+
self._call_variants(func, variants)
|
|
594
|
+
completed += len(batch)
|
|
595
|
+
if emit_lifecycle:
|
|
596
|
+
self._send_download_event(client_id, params, "history", "batch_done", {
|
|
597
|
+
"stage": "batch_done",
|
|
598
|
+
"finished": completed,
|
|
599
|
+
"total": total,
|
|
600
|
+
"batch_index": batch_index + 1,
|
|
601
|
+
"batch_total": batch_total,
|
|
602
|
+
"batch_size": len(batch),
|
|
603
|
+
})
|
|
604
|
+
gc.collect()
|
|
560
605
|
except Exception as e:
|
|
561
606
|
if emit_lifecycle:
|
|
562
607
|
self._send_download_event(client_id, params, "history", "error", {
|
|
@@ -567,9 +612,39 @@ class CfquantQmtBridge(object):
|
|
|
567
612
|
if emit_lifecycle:
|
|
568
613
|
self._send_download_event(client_id, params, "history", "request_done", {
|
|
569
614
|
"stage": "request_done",
|
|
570
|
-
"result":
|
|
615
|
+
"result": None,
|
|
616
|
+
"finished": completed,
|
|
617
|
+
"total": total,
|
|
571
618
|
})
|
|
572
|
-
return
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
@staticmethod
|
|
622
|
+
def _history_download_batch_size(params):
|
|
623
|
+
params = params or {}
|
|
624
|
+
value = params.get("download_batch_size", params.get("batch_size"))
|
|
625
|
+
if value is None:
|
|
626
|
+
value = os.environ.get("CFQUANT_HISTORY_DOWNLOAD_BATCH_SIZE")
|
|
627
|
+
if value is None or str(value).strip() == "":
|
|
628
|
+
period = str(params.get("period") or "").lower()
|
|
629
|
+
if period == "tick":
|
|
630
|
+
return 20
|
|
631
|
+
if period == "1m":
|
|
632
|
+
return 50
|
|
633
|
+
if period.endswith("m") or period.endswith("h"):
|
|
634
|
+
return 100
|
|
635
|
+
return 200
|
|
636
|
+
try:
|
|
637
|
+
value = int(value)
|
|
638
|
+
except (TypeError, ValueError):
|
|
639
|
+
return 50
|
|
640
|
+
return max(1, value)
|
|
641
|
+
|
|
642
|
+
@classmethod
|
|
643
|
+
def _history_download_batches(cls, stock_list, params):
|
|
644
|
+
if not stock_list:
|
|
645
|
+
return [stock_list]
|
|
646
|
+
size = cls._history_download_batch_size(params)
|
|
647
|
+
return [stock_list[index:index + size] for index in range(0, len(stock_list), size)]
|
|
573
648
|
|
|
574
649
|
def _get_instrument_detail(self, params):
|
|
575
650
|
func = getattr(self.context, "get_instrument_detail")
|
|
@@ -593,7 +593,8 @@ class QmtPythonRuntime:
|
|
|
593
593
|
time.sleep(0.25)
|
|
594
594
|
raise QmtPythonRuntimeError("QMT did not start after runtime switch")
|
|
595
595
|
|
|
596
|
-
def validate_selection(self, runtime, *, account_id=None
|
|
596
|
+
def validate_selection(self, runtime, *, account_id=None,
|
|
597
|
+
allow_missing_account_config=False):
|
|
597
598
|
runtime = _canonical_runtime(runtime)
|
|
598
599
|
effective_account = account_id if account_id is not None else self.account_id
|
|
599
600
|
binding = self.binding_status(account_id=effective_account)
|
|
@@ -603,6 +604,8 @@ class QmtPythonRuntime:
|
|
|
603
604
|
% (binding.get("qmt_version") or "unknown"))
|
|
604
605
|
if effective_account:
|
|
605
606
|
if not binding["config_exists"]:
|
|
607
|
+
if allow_missing_account_config and binding["state"] == "account_config_missing":
|
|
608
|
+
return binding
|
|
606
609
|
raise QmtPythonRuntimeError("QMT account Config.xml not found: %s; log in to QMT first" % effective_account)
|
|
607
610
|
text = Path(binding["config"]).read_text(encoding="utf-8")
|
|
608
611
|
if not re.search(r'<python\b[^>]*\blibs_path="[^"]*"', text):
|
|
@@ -611,9 +614,19 @@ class QmtPythonRuntime:
|
|
|
611
614
|
|
|
612
615
|
def download_and_switch(self, runtime="py312", *, restart=True, account_id=None,
|
|
613
616
|
progress_callback=None, cancel_event=None,
|
|
614
|
-
restart_callback=None, reuse_installed=False
|
|
617
|
+
restart_callback=None, reuse_installed=False,
|
|
618
|
+
allow_missing_account_config=False):
|
|
615
619
|
runtime = _canonical_runtime(runtime)
|
|
616
|
-
binding = self.validate_selection(
|
|
620
|
+
binding = self.validate_selection(
|
|
621
|
+
runtime,
|
|
622
|
+
account_id=account_id,
|
|
623
|
+
allow_missing_account_config=allow_missing_account_config,
|
|
624
|
+
)
|
|
625
|
+
deferred_account_config = bool(
|
|
626
|
+
allow_missing_account_config
|
|
627
|
+
and (account_id or self.account_id)
|
|
628
|
+
and binding.get("state") == "account_config_missing"
|
|
629
|
+
)
|
|
617
630
|
reuse = bool(reuse_installed and binding["runtimes"][runtime]["installed"])
|
|
618
631
|
package, archive = None, None
|
|
619
632
|
if reuse:
|
|
@@ -624,17 +637,30 @@ class QmtPythonRuntime:
|
|
|
624
637
|
cancel_event=cancel_event)
|
|
625
638
|
if cancel_event and cancel_event.is_set():
|
|
626
639
|
raise QmtPythonRuntimeError("QMT Python runtime operation cancelled")
|
|
627
|
-
_callback(progress_callback, QmtPythonProgress(
|
|
628
|
-
|
|
640
|
+
_callback(progress_callback, QmtPythonProgress(
|
|
641
|
+
runtime, "activating",
|
|
642
|
+
message=("正在停止 QMT 并安装运行库" if deferred_account_config
|
|
643
|
+
else "正在停止 QMT 并配置执行器"),
|
|
644
|
+
))
|
|
645
|
+
was_running = bool(self._qmt_processes())
|
|
646
|
+
if restart and was_running:
|
|
629
647
|
self._stop_for_switch()
|
|
630
|
-
elif not restart and
|
|
648
|
+
elif not restart and was_running:
|
|
631
649
|
raise QmtPythonRuntimeError(
|
|
632
|
-
"QMT is running; set restart=True before activating a Python runtime")
|
|
650
|
+
"QMT is running; set restart=True before installing or activating a Python runtime")
|
|
633
651
|
try:
|
|
634
652
|
installed = self.inspect(runtime, refresh_remote=False) if reuse else self.install(runtime, package=package, progress_callback=progress_callback)
|
|
635
|
-
switched =
|
|
653
|
+
switched = None if deferred_account_config else (
|
|
654
|
+
self.switch_account(runtime, account_id=account_id)
|
|
655
|
+
if (account_id or self.account_id) else None
|
|
656
|
+
)
|
|
636
657
|
finally:
|
|
637
|
-
|
|
658
|
+
# A managed Web binding can supply a bootstrap callback that starts
|
|
659
|
+
# QMT, waits for the first login to create Config.xml, and then
|
|
660
|
+
# performs the final runtime switch/restart. Keep the standalone
|
|
661
|
+
# API's old safe behavior (leave a brand-new, unconfigured QMT
|
|
662
|
+
# stopped) when no callback was supplied.
|
|
663
|
+
if restart and (not deferred_account_config or was_running or restart_callback):
|
|
638
664
|
_callback(progress_callback, QmtPythonProgress(runtime, "restarting", message="正在重新启动 QMT"))
|
|
639
665
|
if restart_callback:
|
|
640
666
|
restarted = restart_callback()
|
|
@@ -646,7 +672,10 @@ class QmtPythonRuntime:
|
|
|
646
672
|
restarted = None
|
|
647
673
|
return {"runtime": runtime, "package": package.as_dict() if package else None,
|
|
648
674
|
"archive": str(archive) if archive else None, "reused_installed": reuse,
|
|
649
|
-
"installed": installed, "switched": switched, "restarted": restarted
|
|
675
|
+
"installed": installed, "switched": switched, "restarted": restarted,
|
|
676
|
+
"account_config_required": deferred_account_config,
|
|
677
|
+
"message": ("运行库已安装;请先在 QMT 登录此账号,再重新检测并应用执行器。"
|
|
678
|
+
if deferred_account_config else "")}
|
|
650
679
|
|
|
651
680
|
def _stop_for_switch(self):
|
|
652
681
|
for process in self._qmt_processes():
|
|
@@ -82,7 +82,7 @@ STATES = {
|
|
|
82
82
|
"waiting_start": "QMT 已启动,等待托管策略启动及通道连接",
|
|
83
83
|
"waiting_manual_start": "QMT 已启动,自启未开启,请在模型交易中运行托管策略",
|
|
84
84
|
"running": "托管策略已运行",
|
|
85
|
-
"waiting_account": "
|
|
85
|
+
"waiting_account": "等待 QMT 登录生成账号绑定键;首次登录后请正常退出 QMT,再重新启动以完成策略部署。仍无法识别时再补充模型账号 Key",
|
|
86
86
|
"configured": "模型已配置,等待下次启动 QMT 生效",
|
|
87
87
|
"disabled": "自动运行已关闭,托管策略已禁用",
|
|
88
88
|
"error": "策略部署失败",
|
|
@@ -915,15 +915,16 @@ class QmtStrategyManager:
|
|
|
915
915
|
self._save()
|
|
916
916
|
|
|
917
917
|
def prepare_for_start(self, directory):
|
|
918
|
-
"""Finish
|
|
918
|
+
"""Finish offline deployment, or allow login to supply a missing key."""
|
|
919
919
|
root = qmt_root(directory)
|
|
920
|
+
waiting_account = []
|
|
920
921
|
with self.lock:
|
|
921
922
|
if self.load_error:
|
|
922
923
|
raise ValueError(self.load_error)
|
|
923
924
|
jobs = [job for job in self.jobs.values()
|
|
924
925
|
if os.path.normcase(str(qmt_root(job["root"]))) == os.path.normcase(str(root))]
|
|
925
926
|
if not jobs:
|
|
926
|
-
return
|
|
927
|
+
return {"waiting_account": waiting_account}
|
|
927
928
|
if self.process_checker(root):
|
|
928
929
|
raise RuntimeError("QMT 尚未完全退出,无法完成启动前策略部署")
|
|
929
930
|
try:
|
|
@@ -957,6 +958,12 @@ class QmtStrategyManager:
|
|
|
957
958
|
job["deploy_retry_after"] = time.time() + min(10.0, 2.0 * count)
|
|
958
959
|
self._state(job, "error", str(error))
|
|
959
960
|
raise
|
|
961
|
+
if job.get("state") == "waiting_account":
|
|
962
|
+
# QMT creates its account authorization on first login.
|
|
963
|
+
# Do not fabricate a model key or make login depend on
|
|
964
|
+
# the model that can only be prepared after that login.
|
|
965
|
+
waiting_account.append(job["account_id"])
|
|
966
|
+
continue
|
|
960
967
|
if (job.get("state") not in ("configured", "waiting_import", "disabled")
|
|
961
968
|
or (job.get("enabled") and any(
|
|
962
969
|
not role.get("model_prepared") or role.get("reimport_required")
|
|
@@ -965,6 +972,7 @@ class QmtStrategyManager:
|
|
|
965
972
|
(job.get("message") or job.get("state", "")))
|
|
966
973
|
finally:
|
|
967
974
|
self._save()
|
|
975
|
+
return {"waiting_account": waiting_account}
|
|
968
976
|
|
|
969
977
|
def _advance(self, job):
|
|
970
978
|
root = qmt_root(job["root"])
|
|
@@ -81,6 +81,15 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
|
|
|
81
81
|
expect(button).to_be_disabled()
|
|
82
82
|
assert not page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.classList.contains('is-done')")
|
|
83
83
|
expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("等待切换")
|
|
84
|
+
for phase in ("extract", "activating", "restarting"):
|
|
85
|
+
page.evaluate("""phase => bindingPythonRuntimeProgressEvent({
|
|
86
|
+
meta: {job_id: state.bindingPythonRuntimeJobId, stage: phase},
|
|
87
|
+
data: {phase, percent: 0, eta: null}
|
|
88
|
+
})""", phase)
|
|
89
|
+
expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("0%")
|
|
90
|
+
expect(page.locator("#bindingPythonRuntimeProgressMeta")).not_to_contain_text("剩余约 0")
|
|
91
|
+
assert page.locator("#bindingPythonRuntimeProgressBar").evaluate("node => node.getBoundingClientRect().width > 0")
|
|
92
|
+
assert page.evaluate("!!state.bindingPythonRuntimeJobId")
|
|
84
93
|
status["binding"].update(selected_runtime="py312", configured_runtime="py312", state="ready")
|
|
85
94
|
status["binding"]["runtimes"]["py312"]["installed"] = True
|
|
86
95
|
page.evaluate("""bindingPythonRuntimeProgressEvent({
|
|
@@ -95,6 +104,45 @@ def test_selected_executor_is_applied_through_existing_job_until_restart_finishe
|
|
|
95
104
|
assert not errors
|
|
96
105
|
|
|
97
106
|
|
|
107
|
+
def test_lost_job_record_releases_busy_state_without_claiming_success(page, frontend_url):
|
|
108
|
+
errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
|
|
109
|
+
page.route("**/api/qmt/python-runtime/download", lambda route: route.fulfill(json={"ok": True, "data": {"state": "queued"}}))
|
|
110
|
+
page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(status=404, json={"ok": False, "error": "job not found"}))
|
|
111
|
+
page.locator("#bindingPythonRuntimeDownloadBtn").click()
|
|
112
|
+
page.wait_for_function("!state.bindingPythonRuntimeJobId && state.bindingPythonRuntimeProgress?.meta?.stage === 'error'")
|
|
113
|
+
expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("记录已失效")
|
|
114
|
+
expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
|
|
115
|
+
assert not errors
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_save_rechecks_completed_runtime_job_even_without_progress_notification(page, frontend_url):
|
|
119
|
+
errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
|
|
120
|
+
page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
|
|
121
|
+
"ok": True, "data": {"state": "done", "result": {"account_config_required": True}}}))
|
|
122
|
+
saved = []
|
|
123
|
+
def save(route):
|
|
124
|
+
body = route.request.post_data_json
|
|
125
|
+
saved.append(body)
|
|
126
|
+
row = dict(body, account_key="TEST_ONLY|STOCK|test", bridge_id="test")
|
|
127
|
+
route.fulfill(json={"ok": True, "data": {
|
|
128
|
+
"account": row, "account_configs": {row["account_key"]: row}, "account_pairs": {},
|
|
129
|
+
"bridges": {"test": {"name": "Test"}}, "qmt_strategy_deploy": {"enabled": False},
|
|
130
|
+
"qmt_auto_login": {"enabled": False}}})
|
|
131
|
+
page.route("**/api/account-config", save)
|
|
132
|
+
page.locator("#bindingQmtAutoLogin").uncheck()
|
|
133
|
+
page.evaluate("""() => {
|
|
134
|
+
fillQmtStrategySettings('binding', {enabled: false});
|
|
135
|
+
state.bindingPythonRuntimeJobId = 'missed-done';
|
|
136
|
+
state.bindingPythonRuntimeJobContext = bindingPythonRuntimeContextKey();
|
|
137
|
+
state.bindingPythonRuntimeJobRuntime = 'py36';
|
|
138
|
+
}""")
|
|
139
|
+
page.locator('#bindingForm [type="submit"]').click()
|
|
140
|
+
expect(page.locator("#bindingDialogOverlay")).not_to_be_visible()
|
|
141
|
+
assert len(saved) == 1
|
|
142
|
+
assert not page.evaluate("state.bindingPythonRuntimeJobId")
|
|
143
|
+
assert not errors
|
|
144
|
+
|
|
145
|
+
|
|
98
146
|
def test_directory_change_invalidates_options_and_discards_late_response(page, frontend_url):
|
|
99
147
|
errors = open_runtime(page, frontend_url, runtime_status())
|
|
100
148
|
pending = []
|
|
@@ -119,6 +167,38 @@ def test_missing_account_config_shows_capabilities_but_requires_login(page, fron
|
|
|
119
167
|
errors = open_runtime(page, frontend_url, runtime_status(config_exists=False))
|
|
120
168
|
expect(page.locator("#bindingPythonRuntimeSelect option")).to_have_count(2)
|
|
121
169
|
page.locator("#bindingPythonRuntimeSelect").select_option("py312")
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
submitted = []
|
|
171
|
+
page.route("**/api/qmt/python-runtime/download", lambda route: (
|
|
172
|
+
submitted.append(route.request.post_data_json),
|
|
173
|
+
route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
|
|
174
|
+
expect(page.locator("#bindingPythonRuntimeDownloadBtn")).to_be_enabled()
|
|
175
|
+
expect(page.locator("#bindingPythonRuntimeChoiceHint")).to_contain_text("先下载")
|
|
176
|
+
page.locator("#bindingPythonRuntimeDownloadBtn").click()
|
|
177
|
+
page.wait_for_function("state.bindingPythonRuntimeJobId")
|
|
178
|
+
assert submitted[0]["allow_missing_account_config"] is True
|
|
179
|
+
assert not errors
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.parametrize("socket_throws", [False, True], ids=["socket-disconnected", "socket-unavailable"])
|
|
183
|
+
def test_poll_completion_before_first_login_unlocks_binding(page, frontend_url, socket_throws):
|
|
184
|
+
status = runtime_status(config_exists=False)
|
|
185
|
+
errors = open_runtime(page, frontend_url, status)
|
|
186
|
+
submitted = []
|
|
187
|
+
page.route("**/api/qmt/python-runtime/download", lambda route: (
|
|
188
|
+
submitted.append(route.request.post_data_json),
|
|
189
|
+
route.fulfill(json={"ok": True, "data": {"state": "queued"}})))
|
|
190
|
+
# The last progress event is not a terminal event. HTTP state/result must
|
|
191
|
+
# recover completion even if the WebSocket notification never arrived.
|
|
192
|
+
page.route("**/api/qmt/python-runtime/jobs?*", lambda route: route.fulfill(json={
|
|
193
|
+
"ok": True, "data": {"state": "done", "progress": {"phase": "installed", "percent": 100},
|
|
194
|
+
"result": {"account_config_required": True, "restarted": None}}}))
|
|
195
|
+
if socket_throws:
|
|
196
|
+
page.evaluate("() => { window.WebSocket = function() { throw new Error('WebSocket unavailable'); }; }")
|
|
197
|
+
page.locator("#bindingPythonRuntimeDownloadBtn").click()
|
|
198
|
+
page.wait_for_function("state.bindingPythonRuntimeJobId === '' && state.bindingPythonRuntimeProgress?.meta?.stage === 'done'")
|
|
199
|
+
assert len(submitted) == 1
|
|
200
|
+
assert page.evaluate("state.bindingPythonRuntimeDeferred")
|
|
201
|
+
assert not page.evaluate("state.bindingPythonRuntimeChoiceDirty")
|
|
202
|
+
expect(page.locator("#bindingPythonRuntimeProgressMeta")).to_contain_text("100%")
|
|
203
|
+
expect(page.locator("#bindingPythonRuntimeProgressText")).to_contain_text("登录")
|
|
124
204
|
assert not errors
|