Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-61
@@ -1,61 +1,2 @@
|
|||||||
# 服务配置
|
# 环境变量模板已迁移至 config/.env.example
|
||||||
HOST=0.0.0.0
|
# 使用: cp config/.env.example config/.env
|
||||||
PORT=6600
|
|
||||||
DEBUG=false
|
|
||||||
|
|
||||||
SECRET_KEY=change-this-to-a-random-secret-key
|
|
||||||
|
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=change-me-on-first-login
|
|
||||||
ADMIN_SYNC_FROM_ENV=false
|
|
||||||
|
|
||||||
WECHAT_WEBHOOK=
|
|
||||||
|
|
||||||
QUOTE_SOURCE=sina
|
|
||||||
THS_REFRESH_TOKEN=
|
|
||||||
|
|
||||||
# 交易模式:simulation=SimNow,live=期货公司(系统设置页可改)
|
|
||||||
TRADING_MODE=simulation
|
|
||||||
POSITION_SIZING_MODE=risk
|
|
||||||
RISK_PERCENT=1
|
|
||||||
|
|
||||||
# CTP 断线后后台自动重连(true/false)
|
|
||||||
CTP_AUTO_RECONNECT=true
|
|
||||||
|
|
||||||
# —— SimNow 模拟盘(也可在「系统设置 → CTP 连接」配置,优先于本文件)——
|
|
||||||
SIMNOW_USER=
|
|
||||||
SIMNOW_PASSWORD=
|
|
||||||
SIMNOW_BROKER_ID=9999
|
|
||||||
# 7×24 / 日盘前置(deploy.sh 会自动 nc 探测并写入可用线路)
|
|
||||||
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
|
|
||||||
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
|
|
||||||
SIMNOW_APP_ID=simnow_client_test
|
|
||||||
SIMNOW_AUTH_CODE=0000000000000000
|
|
||||||
# SimNow 看穿式前置固定用「实盘」;仅穿透式测评才用「测试」
|
|
||||||
SIMNOW_ENV=实盘
|
|
||||||
|
|
||||||
# —— 期货公司实盘(后期接入)——
|
|
||||||
CTP_LIVE_USER=
|
|
||||||
CTP_LIVE_PASSWORD=
|
|
||||||
CTP_LIVE_BROKER_ID=
|
|
||||||
CTP_LIVE_TD_ADDRESS=
|
|
||||||
CTP_LIVE_MD_ADDRESS=
|
|
||||||
CTP_LIVE_APP_ID=
|
|
||||||
CTP_LIVE_AUTH_CODE=
|
|
||||||
CTP_LIVE_PRODUCT_INFO=
|
|
||||||
|
|
||||||
# 账户冷静期
|
|
||||||
RISK_CONTROL_ENABLED=true
|
|
||||||
RISK_COOLING_HOURS_MANUAL=4
|
|
||||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
|
||||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
|
||||||
MAX_ACTIVE_POSITIONS=1
|
|
||||||
RISK_DAILY_POSITION_LIMIT=5
|
|
||||||
RISK_DAILY_TRADING_RISK_PCT=2
|
|
||||||
TRADING_DAY_RESET_HOUR=8
|
|
||||||
|
|
||||||
# —— 数据库(生产推荐 PostgreSQL,见 docs/POSTGRES.md)——
|
|
||||||
# 未配置 DATABASE_URL 时使用本地 SQLite futures.db
|
|
||||||
# DATABASE_URL=postgresql://qihuo:your_password@127.0.0.1:5432/qihuo
|
|
||||||
# PG_POOL_MIN=2
|
|
||||||
# PG_POOL_MAX=20
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
|
config/.env
|
||||||
*.db
|
*.db
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.settings.admin_settings
|
||||||
|
from modules.settings.admin_settings import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.notify.ai_client
|
||||||
|
from modules.notify.ai_client import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.notify.ai_messages
|
||||||
|
from modules.notify.ai_messages import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.notify.ai_worker
|
||||||
|
from modules.notify.ai_worker import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.contract_profile
|
||||||
|
from modules.core.contract_profile import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.contract_specs
|
||||||
|
from modules.core.contract_specs import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_entry_price
|
||||||
|
from modules.ctp.ctp_entry_price import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_fee_sync
|
||||||
|
from modules.ctp.ctp_fee_sync import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_fee_worker
|
||||||
|
from modules.ctp.ctp_fee_worker import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_ipc_client
|
||||||
|
from modules.ctp.ctp_ipc_client import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_kline
|
||||||
|
from modules.ctp.ctp_kline import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_premarket_connect
|
||||||
|
from modules.ctp.ctp_premarket_connect import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_reconnect
|
||||||
|
from modules.ctp.ctp_reconnect import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_settings
|
||||||
|
from modules.ctp.ctp_settings import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_symbol
|
||||||
|
from modules.ctp.ctp_symbol import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_trade_sync
|
||||||
|
from modules.ctp.ctp_trade_sync import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_trading_state
|
||||||
|
from modules.ctp.ctp_trading_state import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.ctp_worker
|
||||||
|
from modules.ctp.ctp_worker import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.stats.dashboard_lib
|
||||||
|
from modules.stats.dashboard_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.backup.db_backup
|
||||||
|
from modules.backup.db_backup import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.db_conn
|
||||||
|
from modules.core.db_conn import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.doc_render
|
||||||
|
from modules.core.doc_render import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.env_file
|
||||||
|
from modules.core.env_file import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.fees.fee_specs
|
||||||
|
from modules.fees.fee_specs import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.fees.fee_sync
|
||||||
|
from modules.fees.fee_sync import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.keys.key_monitor_lib
|
||||||
|
from modules.keys.key_monitor_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.market.kline_chart
|
||||||
|
from modules.market.kline_chart import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.market.kline_store
|
||||||
|
from modules.market.kline_store import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.market.kline_stream
|
||||||
|
from modules.market.kline_stream import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.locale_fix
|
||||||
|
from modules.core.locale_fix import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.market.market
|
||||||
|
from modules.market.market import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.market.market_sessions
|
||||||
|
from modules.market.market_sessions import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.settings.nav_settings
|
||||||
|
from modules.settings.nav_settings import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.order_pending
|
||||||
|
from modules.trading.order_pending import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.pending_order_worker
|
||||||
|
from modules.trading.pending_order_worker import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.position_sizing
|
||||||
|
from modules.trading.position_sizing import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.position_stream
|
||||||
|
from modules.trading.position_stream import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.product_recommend
|
||||||
|
from modules.trading.product_recommend import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.recommend_store
|
||||||
|
from modules.trading.recommend_store import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.recommend_stream
|
||||||
|
from modules.trading.recommend_stream import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.recommend_trend
|
||||||
|
from modules.trading.recommend_trend import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.risk.account_risk_lib
|
||||||
|
from modules.risk.account_risk_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.sl_tp_guard
|
||||||
|
from modules.trading.sl_tp_guard import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.stats.stats_engine
|
||||||
|
from modules.stats.stats_engine import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.strategy.fib_lib
|
||||||
|
from modules.strategy.fib_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.strategy.strategy_db
|
||||||
|
from modules.strategy.strategy_db import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.strategy.strategy_roll_lib
|
||||||
|
from modules.strategy.strategy_roll_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.strategy.strategy_roll_monitor_lib
|
||||||
|
from modules.strategy.strategy_roll_monitor_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.strategy.strategy_snapshot_lib
|
||||||
|
from modules.strategy.strategy_snapshot_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.strategy.strategy_trend_lib
|
||||||
|
from modules.strategy.strategy_trend_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.symbols
|
||||||
|
from modules.core.symbols import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.trade_log_lib
|
||||||
|
from modules.trading.trade_log_lib import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.trading.trade_notify
|
||||||
|
from modules.trading.trade_notify import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.core.trading_context
|
||||||
|
from modules.core.trading_context import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.ctp.vnpy_bridge
|
||||||
|
from modules.ctp.vnpy_bridge import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Compatibility shim — use modules.notify.wechat_notify
|
||||||
|
from modules.notify.wechat_notify import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 服务配置
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=6600
|
||||||
|
DEBUG=false
|
||||||
|
|
||||||
|
SECRET_KEY=change-this-to-a-random-secret-key
|
||||||
|
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=change-me-on-first-login
|
||||||
|
ADMIN_SYNC_FROM_ENV=false
|
||||||
|
|
||||||
|
WECHAT_WEBHOOK=
|
||||||
|
|
||||||
|
QUOTE_SOURCE=sina
|
||||||
|
THS_REFRESH_TOKEN=
|
||||||
|
|
||||||
|
# 交易模式:simulation=SimNow,live=期货公司(系统设置页可改)
|
||||||
|
TRADING_MODE=simulation
|
||||||
|
POSITION_SIZING_MODE=risk
|
||||||
|
RISK_PERCENT=1
|
||||||
|
|
||||||
|
# CTP 断线后后台自动重连(true/false)
|
||||||
|
CTP_AUTO_RECONNECT=true
|
||||||
|
|
||||||
|
# —— SimNow 模拟盘(也可在「系统设置 → CTP 连接」配置,优先于本文件)——
|
||||||
|
SIMNOW_USER=
|
||||||
|
SIMNOW_PASSWORD=
|
||||||
|
SIMNOW_BROKER_ID=9999
|
||||||
|
# 7×24 / 日盘前置(deploy.sh 会自动 nc 探测并写入可用线路)
|
||||||
|
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
|
||||||
|
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
|
||||||
|
SIMNOW_APP_ID=simnow_client_test
|
||||||
|
SIMNOW_AUTH_CODE=0000000000000000
|
||||||
|
# SimNow 看穿式前置固定用「实盘」;仅穿透式测评才用「测试」
|
||||||
|
SIMNOW_ENV=实盘
|
||||||
|
|
||||||
|
# —— 期货公司实盘(后期接入)——
|
||||||
|
CTP_LIVE_USER=
|
||||||
|
CTP_LIVE_PASSWORD=
|
||||||
|
CTP_LIVE_BROKER_ID=
|
||||||
|
CTP_LIVE_TD_ADDRESS=
|
||||||
|
CTP_LIVE_MD_ADDRESS=
|
||||||
|
CTP_LIVE_APP_ID=
|
||||||
|
CTP_LIVE_AUTH_CODE=
|
||||||
|
CTP_LIVE_PRODUCT_INFO=
|
||||||
|
|
||||||
|
# 账户冷静期
|
||||||
|
RISK_CONTROL_ENABLED=true
|
||||||
|
RISK_COOLING_HOURS_MANUAL=4
|
||||||
|
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||||
|
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||||
|
MAX_ACTIVE_POSITIONS=1
|
||||||
|
RISK_DAILY_POSITION_LIMIT=5
|
||||||
|
RISK_DAILY_TRADING_RISK_PCT=2
|
||||||
|
TRADING_DAY_RESET_HOUR=8
|
||||||
|
|
||||||
|
# —— 数据库(生产推荐 PostgreSQL,见 docs/POSTGRES.md)——
|
||||||
|
# 未配置 DATABASE_URL 时使用本地 SQLite futures.db
|
||||||
|
# DATABASE_URL=postgresql://qihuo:your_password@127.0.0.1:5432/qihuo
|
||||||
|
# PG_POOL_MIN=2
|
||||||
|
# PG_POOL_MAX=20
|
||||||
@@ -161,25 +161,27 @@ pip install --upgrade pip -q
|
|||||||
pip install -r "$APP_DIR/requirements.txt"
|
pip install -r "$APP_DIR/requirements.txt"
|
||||||
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
|
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
|
||||||
|
|
||||||
echo "==> 配置 .env..."
|
echo "==> 配置 config/.env..."
|
||||||
if [ ! -f "$APP_DIR/.env" ]; then
|
ENV_FILE="$APP_DIR/config/.env"
|
||||||
cp "$APP_DIR/.env.example" "$APP_DIR/.env"
|
mkdir -p "$APP_DIR/config"
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
cp "$APP_DIR/config/.env.example" "$ENV_FILE"
|
||||||
RAND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
RAND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||||
sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$APP_DIR/.env"
|
sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$ENV_FILE"
|
||||||
echo " 已生成 .env,请编辑 SIMNOW_USER / ADMIN_PASSWORD"
|
echo " 已生成 config/.env,请编辑 SIMNOW_USER / ADMIN_PASSWORD"
|
||||||
fi
|
fi
|
||||||
ensure_env_key "$APP_DIR/.env" "SIMNOW_ENV" "实盘"
|
ensure_env_key "$ENV_FILE" "SIMNOW_ENV" "实盘"
|
||||||
ensure_env_key "$APP_DIR/.env" "CTP_AUTO_RECONNECT" "true"
|
ensure_env_key "$ENV_FILE" "CTP_AUTO_RECONNECT" "true"
|
||||||
ensure_env_key "$APP_DIR/.env" "SIMNOW_BROKER_ID" "9999"
|
ensure_env_key "$ENV_FILE" "SIMNOW_BROKER_ID" "9999"
|
||||||
ensure_env_key "$APP_DIR/.env" "SIMNOW_APP_ID" "simnow_client_test"
|
ensure_env_key "$ENV_FILE" "SIMNOW_APP_ID" "simnow_client_test"
|
||||||
ensure_env_key "$APP_DIR/.env" "SIMNOW_AUTH_CODE" "0000000000000000"
|
ensure_env_key "$ENV_FILE" "SIMNOW_AUTH_CODE" "0000000000000000"
|
||||||
update_simnow_front_in_env "$APP_DIR/.env" || true
|
update_simnow_front_in_env "$ENV_FILE" || true
|
||||||
|
|
||||||
mkdir -p "$APP_DIR/logs"
|
mkdir -p "$APP_DIR/logs"
|
||||||
|
|
||||||
echo "==> 验证 CTP 环境..."
|
echo "==> 验证 CTP 环境..."
|
||||||
if grep -q "^SIMNOW_USER=.\+" "$APP_DIR/.env" 2>/dev/null && \
|
if grep -q "^SIMNOW_USER=.\+" "$ENV_FILE" 2>/dev/null && \
|
||||||
grep -q "^SIMNOW_PASSWORD=.\+" "$APP_DIR/.env" 2>/dev/null; then
|
grep -q "^SIMNOW_PASSWORD=.\+" "$ENV_FILE" 2>/dev/null; then
|
||||||
set +e
|
set +e
|
||||||
python "$APP_DIR/scripts/test_simnow.py"
|
python "$APP_DIR/scripts/test_simnow.py"
|
||||||
CTP_TEST=$?
|
CTP_TEST=$?
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 主目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
qihuo/ # 主文件夹(仓库根)
|
||||||
|
├── app.py # 主程序入口(Flask 启动)
|
||||||
|
├── requirements.txt
|
||||||
|
├── deploy.sh # 一键部署脚本
|
||||||
|
├── ecosystem.config.cjs # PM2 启动配置
|
||||||
|
├── config/
|
||||||
|
│ ├── .env.example # 环境变量模板
|
||||||
|
│ └── .env # 运行时配置(git 忽略)
|
||||||
|
├── modules/ # 业务模块(每个模块 register(deps))
|
||||||
|
│ ├── core/ # DB、路径、公共工具
|
||||||
|
│ ├── web/ # 页面路由 + static/ + templates/
|
||||||
|
│ ├── trading/ # 下单监控、持仓、推荐
|
||||||
|
│ ├── ctp/ # vn.py / CTP 连接与报单
|
||||||
|
│ ├── risk/ # 账户风控
|
||||||
|
│ ├── strategy/ # 趋势、滚仓策略
|
||||||
|
│ ├── keys/ # 关键位
|
||||||
|
│ ├── plans/ # 开单计划
|
||||||
|
│ ├── market/ # 行情、K 线
|
||||||
|
│ ├── records/ # 交易记录、复盘
|
||||||
|
│ ├── stats/ # 统计、看板
|
||||||
|
│ ├── settings/ # 系统设置
|
||||||
|
│ ├── notify/ # 微信、AI 消息
|
||||||
|
│ ├── fees/ # 手续费
|
||||||
|
│ └── backup/ # 备份
|
||||||
|
├── _legacy/ # 旧 import 兼容 shim(PM2 PYTHONPATH)
|
||||||
|
├── data/ # 静态数据(如 fee_rates.json)
|
||||||
|
├── docs/ # 文档
|
||||||
|
├── scripts/ # 运维/诊断脚本(非运行时)
|
||||||
|
├── futures.db # SQLite(未配 PG 时)
|
||||||
|
├── uploads/
|
||||||
|
└── logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
根目录 `_legacy/` 为旧 `import db_conn` 等路径的兼容层;新代码请 `from modules.xxx import ...`。
|
||||||
|
|
||||||
|
## 进程模型
|
||||||
|
|
||||||
|
- **单进程**:PM2 仅 `qihuo`(`app.py` + CTP 同进程)
|
||||||
|
- 详见 [DEPLOY.md](./DEPLOY.md)
|
||||||
|
|
||||||
|
## 模块契约
|
||||||
|
|
||||||
|
每个 `modules/<name>/` 提供 `register(deps: AppDeps)`;主程序 `app.py` 只做串联,不写业务。
|
||||||
|
|
||||||
|
## 发布
|
||||||
|
|
||||||
|
见 [DEPLOY.md](./DEPLOY.md):**本地修改 → git push → 服务器 git pull**,禁止 SCP。
|
||||||
+15
-16
@@ -43,7 +43,7 @@ pm2 save
|
|||||||
|
|
||||||
以下文件 **不** 随 `git pull` 更新,卸载/重装时须 **单独备份与恢复**:
|
以下文件 **不** 随 `git pull` 更新,卸载/重装时须 **单独备份与恢复**:
|
||||||
|
|
||||||
- `/opt/qihuo/.env`
|
- `/opt/qihuo/config/.env`(兼容旧版 `/opt/qihuo/.env`)
|
||||||
- `/opt/qihuo/futures.db`(SQLite)或 PostgreSQL 数据
|
- `/opt/qihuo/futures.db`(SQLite)或 PostgreSQL 数据
|
||||||
- `/opt/qihuo/uploads/`
|
- `/opt/qihuo/uploads/`
|
||||||
- `/opt/qihuo/backups/`(若有)
|
- `/opt/qihuo/backups/`(若有)
|
||||||
@@ -58,18 +58,17 @@ pm2 save
|
|||||||
| 运行用户 | `root`(与 `deploy.sh` / PM2 配置一致) |
|
| 运行用户 | `root`(与 `deploy.sh` / PM2 配置一致) |
|
||||||
| Web 端口 | `6600`(对外) |
|
| Web 端口 | `6600`(对外) |
|
||||||
| CTP Worker 端口 | `6601`(仅 `127.0.0.1`,Web 进程 IPC 调用,勿对外开放) |
|
| CTP Worker 端口 | `6601`(仅 `127.0.0.1`,Web 进程 IPC 调用,勿对外开放) |
|
||||||
| 进程管理 | PM2:`qihuo`(Flask Web)+ `qihuo-ctp`(CTP / vn.py 独立进程) |
|
| 进程管理 | PM2:**仅** `qihuo`(Flask + CTP 单进程) |
|
||||||
| 数据库 | **生产推荐 PostgreSQL**(见 [POSTGRES.md](./POSTGRES.md));未配置 `DATABASE_URL` 时使用 SQLite `futures.db` |
|
| 数据库 | **生产推荐 PostgreSQL**(见 [POSTGRES.md](./POSTGRES.md));未配置 `DATABASE_URL` 时使用 SQLite `futures.db` |
|
||||||
| 仓库 | https://git.bz121.com/dekun/qihuo.git |
|
| 仓库 | https://git.bz121.com/dekun/qihuo.git |
|
||||||
|
|
||||||
### 进程架构(2026-03 起)
|
### 进程架构(2026-07 起:单进程)
|
||||||
|
|
||||||
| PM2 应用 | 角色 | 说明 |
|
| PM2 应用 | 说明 |
|
||||||
|----------|------|------|
|
|----------|------|
|
||||||
| `qihuo` | Web(`QIHUO_CTP_ROLE=client`) | Flask、页面、API、数据库;通过 HTTP 调用本机 Worker |
|
| `qihuo` | Flask Web + **vn.py / CTP 同进程**(`vnpy_bridge.CtpBridge`) |
|
||||||
| `qihuo-ctp` | Worker(`QIHUO_CTP_ROLE=worker`) | **唯一** 加载 vn.py / vnpy_ctp;CTP 连接、报单、持仓回调、止盈止损 tick、滚仓监控 |
|
|
||||||
|
|
||||||
Web 进程崩溃或重启 **不会** 直接带走 CTP 原生连接;Worker 重启后 Web 会自动通过 IPC 恢复读写。两个进程的 Token 须一致(见 `ecosystem.config.cjs` 中 `QIHUO_CTP_WORKER_TOKEN`)。
|
详见 [ARCHITECTURE.md](./ARCHITECTURE.md)。旧版 `qihuo-ctp` 独立 Worker **已废弃**,`ecosystem.config.cjs` 不再启动该进程。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ bash deploy.sh
|
|||||||
6. 首次生成 `.env`,并补全 `SIMNOW_ENV=实盘`、`CTP_AUTO_RECONNECT=true` 等缺项
|
6. 首次生成 `.env`,并补全 `SIMNOW_ENV=实盘`、`CTP_AUTO_RECONNECT=true` 等缺项
|
||||||
7. **自动探测 SimNow 前置**(`nc` 测端口),写入可用的 `SIMNOW_TD/MD_ADDRESS`(优先 `182.254.243.31`,其次 `180.168.146.187`)
|
7. **自动探测 SimNow 前置**(`nc` 测端口),写入可用的 `SIMNOW_TD/MD_ADDRESS`(优先 `182.254.243.31`,其次 `180.168.146.187`)
|
||||||
8. 若已配置 SimNow 账号,运行 `scripts/test_simnow.py` 验证连接
|
8. 若已配置 SimNow 账号,运行 `scripts/test_simnow.py` 验证连接
|
||||||
9. `pm2 restart ecosystem.config.cjs --update-env` 或首次 `pm2 start ecosystem.config.cjs`,并 `pm2 save`(同时启动 **`qihuo`** 与 **`qihuo-ctp`**)
|
9. `pm2 restart ecosystem.config.cjs --update-env` 或首次 `pm2 start ecosystem.config.cjs`,并 `pm2 save`(仅 **`qihuo`** 一个进程)
|
||||||
|
|
||||||
部署完成后访问:`http://<服务器IP>:6600`
|
部署完成后访问:`http://<服务器IP>:6600`
|
||||||
|
|
||||||
@@ -141,7 +140,7 @@ MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 在服务器上
|
# 在服务器上
|
||||||
cp /opt/qihuo/.env /root/qihuo.env.bak
|
cp /opt/qihuo/config/.env /root/qihuo.env.bak 2>/dev/null || cp /opt/qihuo/.env /root/qihuo.env.bak 2>/dev/null || true
|
||||||
# SQLite
|
# SQLite
|
||||||
cp /opt/qihuo/futures.db /root/futures.db.bak 2>/dev/null || true
|
cp /opt/qihuo/futures.db /root/futures.db.bak 2>/dev/null || true
|
||||||
# PostgreSQL 见 POSTGRES.md 备份命令
|
# PostgreSQL 见 POSTGRES.md 备份命令
|
||||||
@@ -151,8 +150,8 @@ tar czf /root/qihuo_uploads.bak.tar.gz -C /opt/qihuo uploads 2>/dev/null || true
|
|||||||
### 2. 卸载 PM2 与代码目录
|
### 2. 卸载 PM2 与代码目录
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pm2 stop qihuo qihuo-ctp 2>/dev/null || true
|
pm2 stop qihuo 2>/dev/null || true
|
||||||
pm2 delete qihuo qihuo-ctp 2>/dev/null || true
|
pm2 delete qihuo 2>/dev/null || true
|
||||||
pm2 save
|
pm2 save
|
||||||
rm -rf /opt/qihuo
|
rm -rf /opt/qihuo
|
||||||
```
|
```
|
||||||
@@ -174,7 +173,7 @@ bash deploy.sh
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/qihuo && git log -1 --oneline # 须与远端 main 最新提交一致
|
cd /opt/qihuo && git log -1 --oneline # 须与远端 main 最新提交一致
|
||||||
pm2 status # qihuo、qihuo-ctp 均为 online
|
pm2 status # qihuo 为 online
|
||||||
```
|
```
|
||||||
|
|
||||||
浏览器访问 `http://<服务器IP>:6600` 登录验证。
|
浏览器访问 `http://<服务器IP>:6600` 登录验证。
|
||||||
@@ -225,8 +224,8 @@ python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
|
|||||||
若提示找不到模块,查看本文「CTP / vnpy 故障排查」一节。
|
若提示找不到模块,查看本文「CTP / vnpy 故障排查」一节。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp config/.env.example config/.env
|
||||||
nano .env
|
nano config/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
@@ -318,7 +317,7 @@ pm2 restart ecosystem.config.cjs --update-env
|
|||||||
pm2 save
|
pm2 save
|
||||||
```
|
```
|
||||||
|
|
||||||
> 须 **同时重启** `qihuo` 与 `qihuo-ctp`。仅 `pm2 restart qihuo` 会导致 Web 与 Worker 代码/协议不一致。
|
> 更新后执行 `pm2 restart ecosystem.config.cjs --update-env` 即可(仅 `qihuo`)。
|
||||||
|
|
||||||
若服务器曾用 SCP 覆盖文件导致 `git pull` 冲突,用 `git reset --hard origin/main` 与远端对齐。
|
若服务器曾用 SCP 覆盖文件导致 `git pull` 冲突,用 `git reset --hard origin/main` 与远端对齐。
|
||||||
|
|
||||||
|
|||||||
+1
-27
@@ -21,12 +21,10 @@ module.exports = {
|
|||||||
max_memory_restart: "8192M",
|
max_memory_restart: "8192M",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
|
PYTHONPATH: path.join(ROOT, "_legacy"),
|
||||||
LANG: "zh_CN.UTF-8",
|
LANG: "zh_CN.UTF-8",
|
||||||
LC_ALL: "zh_CN.UTF-8",
|
LC_ALL: "zh_CN.UTF-8",
|
||||||
LC_CTYPE: "zh_CN.UTF-8",
|
LC_CTYPE: "zh_CN.UTF-8",
|
||||||
QIHUO_CTP_ROLE: "client",
|
|
||||||
QIHUO_CTP_WORKER_URL: "http://127.0.0.1:6601",
|
|
||||||
QIHUO_CTP_WORKER_TOKEN: "qihuo-local-ctp",
|
|
||||||
QIHUO_STARTUP_WORKERS: "8",
|
QIHUO_STARTUP_WORKERS: "8",
|
||||||
QIHUO_MEMORY_MB: "8192",
|
QIHUO_MEMORY_MB: "8192",
|
||||||
},
|
},
|
||||||
@@ -34,29 +32,5 @@ module.exports = {
|
|||||||
out_file: path.join(ROOT, "logs", "pm2-out.log"),
|
out_file: path.join(ROOT, "logs", "pm2-out.log"),
|
||||||
time: true,
|
time: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "qihuo-ctp",
|
|
||||||
script: "ctp_worker.py",
|
|
||||||
cwd: ROOT,
|
|
||||||
interpreter,
|
|
||||||
instances: 1,
|
|
||||||
autorestart: true,
|
|
||||||
watch: false,
|
|
||||||
max_memory_restart: "8192M",
|
|
||||||
env: {
|
|
||||||
NODE_ENV: "production",
|
|
||||||
LANG: "zh_CN.UTF-8",
|
|
||||||
LC_ALL: "zh_CN.UTF-8",
|
|
||||||
LC_CTYPE: "zh_CN.UTF-8",
|
|
||||||
QIHUO_CTP_ROLE: "worker",
|
|
||||||
QIHUO_CTP_WORKER_HOST: "127.0.0.1",
|
|
||||||
QIHUO_CTP_WORKER_PORT: "6601",
|
|
||||||
QIHUO_CTP_WORKER_TOKEN: "qihuo-local-ctp",
|
|
||||||
QIHUO_MEMORY_MB: "8192",
|
|
||||||
},
|
|
||||||
error_file: path.join(ROOT, "logs", "pm2-ctp-error.log"),
|
|
||||||
out_file: path.join(ROOT, "logs", "pm2-ctp-out.log"),
|
|
||||||
time: true,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-4684
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
"""Qihuo feature modules package."""
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
from modules.backup.routes import register
|
||||||
|
|
||||||
|
__all__ = ["register"]
|
||||||
@@ -22,7 +22,7 @@ from pathlib import Path
|
|||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from db_conn import DB_PATH, db_backend
|
from modules.core.db_conn import DB_PATH, db_backend
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -107,7 +107,8 @@ cp -a uploads/. /opt/qihuo/uploads/
|
|||||||
|
|
||||||
|
|
||||||
def _app_root() -> Path:
|
def _app_root() -> Path:
|
||||||
return Path(os.path.dirname(os.path.abspath(__file__)))
|
from modules.core.paths import ROOT
|
||||||
|
return ROOT
|
||||||
|
|
||||||
|
|
||||||
def default_backup_dir() -> str:
|
def default_backup_dir() -> str:
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
"""HTTP routes for backup module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Response,
|
||||||
|
flash,
|
||||||
|
jsonify,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
send_file,
|
||||||
|
session,
|
||||||
|
stream_with_context,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register(deps) -> None:
|
||||||
|
app = deps.app
|
||||||
|
login_required = deps.login_required
|
||||||
|
require_nav = deps.require_nav
|
||||||
|
get_db = deps.get_db
|
||||||
|
get_setting = deps.get_setting
|
||||||
|
set_setting = deps.set_setting
|
||||||
|
fetch_price = deps.fetch_price
|
||||||
|
send_wechat_msg = deps.send_wechat_msg
|
||||||
|
touch_stats_cache = deps.touch_stats_cache
|
||||||
|
get_stats_data = deps.get_stats_data
|
||||||
|
build_market_quote_payload = deps.build_market_quote_payload
|
||||||
|
today_str = deps.today_str
|
||||||
|
expire_old_plans = deps.expire_old_plans
|
||||||
|
TZ = deps.tz
|
||||||
|
DB_PATH = deps.db_path
|
||||||
|
UPLOAD_DIR = deps.upload_dir
|
||||||
|
OPEN_TYPES = deps.open_types
|
||||||
|
EXIT_TRIGGERS = deps.exit_triggers
|
||||||
|
BEHAVIOR_TAGS = deps.behavior_tags
|
||||||
|
KLINE_PERIODS = deps.kline_periods
|
||||||
|
KLINE_CUTOFFS = deps.kline_cutoffs
|
||||||
|
calc_holding_duration = deps.calc_holding_duration
|
||||||
|
holding_to_minutes = deps.holding_to_minutes
|
||||||
|
classify_close_result = deps.classify_close_result
|
||||||
|
calc_rr_ratio = deps.calc_rr_ratio
|
||||||
|
calc_theoretical_pnl = deps.calc_theoretical_pnl
|
||||||
|
parse_review_date_filter = deps.parse_review_date_filter
|
||||||
|
_trading_mode = deps.trading_mode
|
||||||
|
_ua_is_phone = deps.ua_is_phone
|
||||||
|
_static_asset_v = deps.static_asset_v
|
||||||
|
|
||||||
|
from modules.backup.db_backup import list_backups, resolve_backup_file
|
||||||
|
|
||||||
|
@app.route("/api/backup/list")
|
||||||
|
@login_required
|
||||||
|
def api_backup_list():
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"dir": str(backup_dir()),
|
||||||
|
"last_at": get_backup_last_at(get_setting),
|
||||||
|
"running": backup_in_progress(),
|
||||||
|
"items": list_backups(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/backup/download/<filename>")
|
||||||
|
@login_required
|
||||||
|
def api_backup_download(filename):
|
||||||
|
from flask import send_file
|
||||||
|
|
||||||
|
try:
|
||||||
|
path = resolve_backup_file(filename)
|
||||||
|
except (ValueError, FileNotFoundError) as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 404
|
||||||
|
return send_file(path, as_attachment=True, download_name=path.name)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
"""Core bootstrap and shared types."""
|
||||||
|
|
||||||
|
from modules.core.bootstrap import register_all_modules, start_module_workers
|
||||||
|
from modules.core.deps import AppDeps
|
||||||
|
|
||||||
|
__all__ = ["AppDeps", "register_all_modules", "start_module_workers"]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
"""Application module registry and startup wiring."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.core.deps import AppDeps
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Registration order: core services first, trading last among features.
|
||||||
|
_MODULE_NAMES = (
|
||||||
|
"modules.web",
|
||||||
|
"modules.market",
|
||||||
|
"modules.keys",
|
||||||
|
"modules.plans",
|
||||||
|
"modules.notify",
|
||||||
|
"modules.records",
|
||||||
|
"modules.stats",
|
||||||
|
"modules.fees",
|
||||||
|
"modules.backup",
|
||||||
|
"modules.settings",
|
||||||
|
"modules.risk",
|
||||||
|
"modules.strategy",
|
||||||
|
"modules.ctp",
|
||||||
|
"modules.trading",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_all_modules(deps: "AppDeps") -> None:
|
||||||
|
for name in _MODULE_NAMES:
|
||||||
|
mod = importlib.import_module(name)
|
||||||
|
register = getattr(mod, "register", None)
|
||||||
|
if not callable(register):
|
||||||
|
logger.warning("module %s has no register()", name)
|
||||||
|
continue
|
||||||
|
register(deps)
|
||||||
|
logger.debug("registered %s", name)
|
||||||
|
|
||||||
|
|
||||||
|
def start_module_workers(deps: "AppDeps") -> None:
|
||||||
|
"""Background threads owned by feature modules."""
|
||||||
|
from modules.ctp.vnpy_bridge import try_init_vnpy
|
||||||
|
|
||||||
|
try_init_vnpy({})
|
||||||
|
for name in ("modules.market",):
|
||||||
|
mod = importlib.import_module(name)
|
||||||
|
start = getattr(mod, "start_workers", None)
|
||||||
|
if callable(start):
|
||||||
|
start(deps)
|
||||||
@@ -10,8 +10,8 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from contract_specs import get_contract_spec
|
from modules.core.contract_specs import get_contract_spec
|
||||||
from symbols import ths_to_codes, search_symbols
|
from modules.core.symbols import ths_to_codes, search_symbols
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ def margin_one_lot(
|
|||||||
est = round(float(price) * spec["mult"] * spec["margin_rate"], 2)
|
est = round(float(price) * spec["mult"] * spec["margin_rate"], 2)
|
||||||
if trading_mode:
|
if trading_mode:
|
||||||
try:
|
try:
|
||||||
from vnpy_bridge import ctp_estimate_margin_one_lot, ctp_lookup_contract_spec, ctp_status
|
from modules.ctp.vnpy_bridge import ctp_estimate_margin_one_lot, ctp_lookup_contract_spec, ctp_status
|
||||||
|
|
||||||
if ctp_status(trading_mode).get("connected"):
|
if ctp_status(trading_mode).get("connected"):
|
||||||
ctp_margin = ctp_estimate_margin_one_lot(
|
ctp_margin = ctp_estimate_margin_one_lot(
|
||||||
@@ -13,7 +13,9 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Any, Iterable, Optional, Sequence
|
from typing import Any, Iterable, Optional, Sequence
|
||||||
|
|
||||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
|
from modules.core.paths import DB_PATH as _ROOT_DB_PATH
|
||||||
|
|
||||||
|
DB_PATH = _ROOT_DB_PATH
|
||||||
|
|
||||||
_backend_lock = threading.Lock()
|
_backend_lock = threading.Lock()
|
||||||
_backend: Optional[str] = None
|
_backend: Optional[str] = None
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
"""Shared dependencies passed into each feature module at register() time."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AppDeps:
|
||||||
|
app: Any
|
||||||
|
get_db: Callable
|
||||||
|
get_setting: Callable
|
||||||
|
set_setting: Callable
|
||||||
|
login_required: Callable
|
||||||
|
require_nav: Callable
|
||||||
|
fetch_price: Callable
|
||||||
|
send_wechat_msg: Callable
|
||||||
|
touch_stats_cache: Callable
|
||||||
|
get_stats_data: Callable
|
||||||
|
build_market_quote_payload: Callable
|
||||||
|
today_str: Callable
|
||||||
|
expire_old_plans: Callable
|
||||||
|
check_order_plans: Callable
|
||||||
|
check_key_monitors: Callable
|
||||||
|
background_task: Callable
|
||||||
|
start_background_threads: Callable
|
||||||
|
tz: Any
|
||||||
|
db_path: str
|
||||||
|
upload_dir: str
|
||||||
|
open_types: list
|
||||||
|
exit_triggers: list
|
||||||
|
behavior_tags: list
|
||||||
|
kline_periods: list
|
||||||
|
kline_cutoffs: list
|
||||||
|
calc_holding_duration: Callable
|
||||||
|
holding_to_minutes: Callable
|
||||||
|
classify_close_result: Callable
|
||||||
|
calc_rr_ratio: Callable
|
||||||
|
calc_theoretical_pnl: Callable
|
||||||
|
parse_review_date_filter: Callable
|
||||||
|
trading_mode: Callable
|
||||||
|
static_asset_v: Callable
|
||||||
|
ua_is_phone: Callable
|
||||||
@@ -9,12 +9,26 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
from modules.core.paths import ENV_FILE, LEGACY_ENV_FILE
|
||||||
|
|
||||||
|
|
||||||
|
def _default_env_path() -> str:
|
||||||
|
if ENV_FILE.is_file():
|
||||||
|
return str(ENV_FILE)
|
||||||
|
if LEGACY_ENV_FILE.is_file():
|
||||||
|
return str(LEGACY_ENV_FILE)
|
||||||
|
return str(ENV_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
ENV_PATH = _default_env_path()
|
||||||
_KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=")
|
_KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=")
|
||||||
|
|
||||||
|
|
||||||
def env_file_path(path: str | None = None) -> str:
|
def env_file_path(path: str | None = None) -> str:
|
||||||
return path or ENV_PATH
|
if path:
|
||||||
|
return path
|
||||||
|
from modules.core.paths import resolve_env_file
|
||||||
|
return resolve_env_file()
|
||||||
|
|
||||||
|
|
||||||
def _quote_env_value(value: str) -> str:
|
def _quote_env_value(value: str) -> str:
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
"""Repository layout paths — single source for config, data, uploads."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# .../qihuo/modules/core/paths.py -> repo root
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
CONFIG_DIR = ROOT / "config"
|
||||||
|
ENV_FILE = CONFIG_DIR / ".env"
|
||||||
|
LEGACY_ENV_FILE = ROOT / ".env"
|
||||||
|
|
||||||
|
DATA_DIR = ROOT / "data"
|
||||||
|
UPLOADS_DIR = ROOT / "uploads"
|
||||||
|
LOGS_DIR = ROOT / "logs"
|
||||||
|
|
||||||
|
DB_PATH = str(ROOT / "futures.db")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_runtime_dirs() -> None:
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_env_file() -> str:
|
||||||
|
"""Prefer config/.env, fall back to legacy root .env."""
|
||||||
|
if ENV_FILE.is_file():
|
||||||
|
return str(ENV_FILE)
|
||||||
|
if LEGACY_ENV_FILE.is_file():
|
||||||
|
return str(LEGACY_ENV_FILE)
|
||||||
|
return str(ENV_FILE)
|
||||||
@@ -15,7 +15,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX
|
from modules.market.market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX
|
||||||
|
|
||||||
PRODUCTS = [
|
PRODUCTS = [
|
||||||
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
|
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
|
||||||
@@ -106,7 +106,7 @@ def product_has_night_session(ths_or_product) -> bool:
|
|||||||
|
|
||||||
def filter_for_trading_session(rows: list[dict]) -> list[dict]:
|
def filter_for_trading_session(rows: list[dict]) -> list[dict]:
|
||||||
"""夜盘时段隐藏无夜盘品种。"""
|
"""夜盘时段隐藏无夜盘品种。"""
|
||||||
from market_sessions import is_night_trading_session
|
from modules.market.market_sessions import is_night_trading_session
|
||||||
|
|
||||||
if not is_night_trading_session():
|
if not is_night_trading_session():
|
||||||
return rows
|
return rows
|
||||||
@@ -467,8 +467,8 @@ def search_symbols(query: str, *, capital: float | None = None, ctp_connected: b
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
q_lower = q.lower()
|
q_lower = q.lower()
|
||||||
from market_sessions import is_night_trading_session
|
from modules.market.market_sessions import is_night_trading_session
|
||||||
from product_recommend import filter_products_for_capital, should_apply_small_account_scope
|
from modules.trading.product_recommend import filter_products_for_capital, should_apply_small_account_scope
|
||||||
|
|
||||||
night_only = is_night_trading_session()
|
night_only = is_night_trading_session()
|
||||||
product_pool = PRODUCTS
|
product_pool = PRODUCTS
|
||||||
@@ -503,7 +503,7 @@ def search_symbols(query: str, *, capital: float | None = None, ctp_connected: b
|
|||||||
if capital is not None and should_apply_small_account_scope(
|
if capital is not None and should_apply_small_account_scope(
|
||||||
capital, ctp_connected=ctp_connected,
|
capital, ctp_connected=ctp_connected,
|
||||||
):
|
):
|
||||||
from product_recommend import product_in_small_account_whitelist
|
from modules.trading.product_recommend import product_in_small_account_whitelist
|
||||||
if not product or not product_in_small_account_whitelist(product):
|
if not product or not product_in_small_account_whitelist(product):
|
||||||
return results
|
return results
|
||||||
raw = fetch_raw_for_volume(codes["sina_code"])
|
raw = fetch_raw_for_volume(codes["sina_code"])
|
||||||
@@ -631,7 +631,7 @@ def list_recommended_symbols_grouped(recommend_rows: list[dict]) -> list[dict]:
|
|||||||
if not product:
|
if not product:
|
||||||
continue
|
continue
|
||||||
if not product_has_night_session(product):
|
if not product_has_night_session(product):
|
||||||
from market_sessions import is_night_trading_session
|
from modules.market.market_sessions import is_night_trading_session
|
||||||
if is_night_trading_session():
|
if is_night_trading_session():
|
||||||
continue
|
continue
|
||||||
seen.add(ths_key)
|
seen.add(ths_key)
|
||||||
@@ -18,7 +18,7 @@ def get_trading_mode(get_setting: Callable[[str, str], str]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_sizing_mode(get_setting: Callable[[str, str], str]) -> str:
|
def get_sizing_mode(get_setting: Callable[[str, str], str]) -> str:
|
||||||
from position_sizing import normalize_sizing_mode
|
from modules.trading.position_sizing import normalize_sizing_mode
|
||||||
return normalize_sizing_mode(get_setting("position_sizing_mode", "fixed"))
|
return normalize_sizing_mode(get_setting("position_sizing_mode", "fixed"))
|
||||||
|
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from position_stream import position_hub
|
from modules.trading.position_stream import position_hub
|
||||||
|
|
||||||
snap = position_hub.get_snapshot() or {}
|
snap = position_hub.get_snapshot() or {}
|
||||||
cap = float(snap.get("capital") or 0)
|
cap = float(snap.get("capital") or 0)
|
||||||
@@ -93,7 +93,7 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
from db_conn import connect_db
|
from modules.core.db_conn import connect_db
|
||||||
|
|
||||||
conn = connect_db()
|
conn = connect_db()
|
||||||
try:
|
try:
|
||||||
@@ -121,7 +121,7 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
|
|||||||
def _ctp_status_from_snapshot(mode: str) -> Optional[dict]:
|
def _ctp_status_from_snapshot(mode: str) -> Optional[dict]:
|
||||||
"""读持仓快照中的 CTP 状态,避免页面渲染同步 IPC。"""
|
"""读持仓快照中的 CTP 状态,避免页面渲染同步 IPC。"""
|
||||||
try:
|
try:
|
||||||
from position_stream import position_hub
|
from modules.trading.position_stream import position_hub
|
||||||
|
|
||||||
snap = position_hub.get_snapshot() or {}
|
snap = position_hub.get_snapshot() or {}
|
||||||
st = snap.get("ctp_status")
|
st = snap.get("ctp_status")
|
||||||
@@ -142,7 +142,7 @@ def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
|||||||
if balance > 0:
|
if balance > 0:
|
||||||
return balance
|
return balance
|
||||||
try:
|
try:
|
||||||
from vnpy_bridge import ctp_status, get_ctp_balance
|
from modules.ctp.vnpy_bridge import ctp_status, get_ctp_balance
|
||||||
|
|
||||||
st = ctp_status(mode)
|
st = ctp_status(mode)
|
||||||
if st.get("connected"):
|
if st.get("connected"):
|
||||||
@@ -159,7 +159,7 @@ def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
|||||||
|
|
||||||
def get_recommend_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
def get_recommend_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
||||||
"""可开仓品种表用权益:已连接 CTP 用柜台权益,未连接固定 10 万。"""
|
"""可开仓品种表用权益:已连接 CTP 用柜台权益,未连接固定 10 万。"""
|
||||||
from product_recommend import DISCONNECTED_RECOMMEND_CAPITAL
|
from modules.trading.product_recommend import DISCONNECTED_RECOMMEND_CAPITAL
|
||||||
|
|
||||||
if is_ctp_connected(get_setting):
|
if is_ctp_connected(get_setting):
|
||||||
return get_account_capital(conn, get_setting)
|
return get_account_capital(conn, get_setting)
|
||||||
@@ -173,7 +173,7 @@ def is_ctp_connected(get_setting: Callable[[str, str], str]) -> bool:
|
|||||||
if st is not None:
|
if st is not None:
|
||||||
return bool(st.get("connected"))
|
return bool(st.get("connected"))
|
||||||
try:
|
try:
|
||||||
from vnpy_bridge import ctp_status
|
from modules.ctp.vnpy_bridge import ctp_status
|
||||||
|
|
||||||
return bool(ctp_status(mode).get("connected"))
|
return bool(ctp_status(mode).get("connected"))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
"""CTP / vn.py integration — single-process mode."""
|
||||||
|
|
||||||
|
|
||||||
|
def register(deps) -> None:
|
||||||
|
del deps
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["register"]
|
||||||
@@ -6,9 +6,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from contract_specs import get_contract_spec
|
from modules.core.contract_specs import get_contract_spec
|
||||||
from ctp_symbol import ths_to_vnpy_symbol
|
from modules.ctp.ctp_symbol import ths_to_vnpy_symbol
|
||||||
from symbols import ths_to_codes
|
from modules.core.symbols import ths_to_codes
|
||||||
|
|
||||||
|
|
||||||
def symbols_match(ctp_sym: str, ths: str) -> bool:
|
def symbols_match(ctp_sym: str, ths: str) -> bool:
|
||||||
@@ -11,9 +11,9 @@ import re
|
|||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from contract_specs import get_contract_spec
|
from modules.core.contract_specs import get_contract_spec
|
||||||
from fee_specs import upsert_fee_rate
|
from modules.fees.fee_specs import upsert_fee_rate
|
||||||
from vnpy_bridge import get_bridge
|
from modules.ctp.vnpy_bridge import get_bridge
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ def _collect_main_ths_codes() -> list[str]:
|
|||||||
"""从主力列表收集同花顺合约代码(供 CTP 手续费查询)。"""
|
"""从主力列表收集同花顺合约代码(供 CTP 手续费查询)。"""
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from symbols import PRODUCTS, build_ths_code, list_main_contracts_grouped
|
from modules.core.symbols import PRODUCTS, build_ths_code, list_main_contracts_grouped
|
||||||
|
|
||||||
symbols: list[str] = []
|
symbols: list[str] = []
|
||||||
for group in list_main_contracts_grouped():
|
for group in list_main_contracts_grouped():
|
||||||
@@ -58,7 +58,7 @@ def try_daily_ctp_fee_sync(
|
|||||||
return 0, "今日已从 CTP 同步过"
|
return 0, "今日已从 CTP 同步过"
|
||||||
|
|
||||||
t0 = time.monotonic()
|
t0 = time.monotonic()
|
||||||
from ctp_fee_sync import sync_fees_from_ctp
|
from modules.ctp.ctp_fee_sync import sync_fees_from_ctp
|
||||||
|
|
||||||
count, msg = sync_fees_from_ctp(mode)
|
count, msg = sync_fees_from_ctp(mode)
|
||||||
elapsed = time.monotonic() - t0
|
elapsed = time.monotonic() - t0
|
||||||
@@ -113,7 +113,7 @@ def start_ctp_fee_worker(
|
|||||||
time.sleep(20)
|
time.sleep(20)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
from vnpy_bridge import ctp_status
|
from modules.ctp.vnpy_bridge import ctp_status
|
||||||
|
|
||||||
mode = get_mode_fn()
|
mode = get_mode_fn()
|
||||||
st = ctp_status(mode)
|
st = ctp_status(mode)
|
||||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from kline_chart import (
|
from modules.market.kline_chart import (
|
||||||
PERIOD_MINUTES,
|
PERIOD_MINUTES,
|
||||||
_aggregate_bars,
|
_aggregate_bars,
|
||||||
_bar_datetime,
|
_bar_datetime,
|
||||||
@@ -76,7 +76,7 @@ def compose_period_bars(bars_1m: list, period: str) -> list:
|
|||||||
def fetch_ctp_klines(symbol: str, period: str, mode: str) -> Optional[list]:
|
def fetch_ctp_klines(symbol: str, period: str, mode: str) -> Optional[list]:
|
||||||
"""CTP 已连接时由 tick 聚合 K 线;失败返回 None。"""
|
"""CTP 已连接时由 tick 聚合 K 线;失败返回 None。"""
|
||||||
try:
|
try:
|
||||||
from vnpy_bridge import ctp_status, get_bridge
|
from modules.ctp.vnpy_bridge import ctp_status, get_bridge
|
||||||
|
|
||||||
if not ctp_status(mode).get("connected"):
|
if not ctp_status(mode).get("connected"):
|
||||||
return None
|
return None
|
||||||
@@ -12,13 +12,13 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from market_sessions import (
|
from modules.market.market_sessions import (
|
||||||
in_premarket_connect_window,
|
in_premarket_connect_window,
|
||||||
in_postmarket_grace_window,
|
in_postmarket_grace_window,
|
||||||
is_trading_session,
|
is_trading_session,
|
||||||
should_keep_ctp_connected,
|
should_keep_ctp_connected,
|
||||||
)
|
)
|
||||||
from vnpy_bridge import ctp_start_connect, ctp_status
|
from modules.ctp.vnpy_bridge import ctp_start_connect, ctp_status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from ctp_premarket_connect import premarket_minutes_before, should_auto_connect_now
|
from modules.ctp.ctp_premarket_connect import premarket_minutes_before, should_auto_connect_now
|
||||||
from market_sessions import in_premarket_connect_window, is_trading_session
|
from modules.market.market_sessions import in_premarket_connect_window, is_trading_session
|
||||||
from vnpy_bridge import ctp_try_auto_reconnect
|
from modules.ctp.vnpy_bridge import ctp_try_auto_reconnect
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ CTP_DISABLED_HINT = "CTP 自动连接已关闭(非交易时段不重连;开
|
|||||||
def is_ctp_auto_connect_enabled(get_setting=None) -> bool:
|
def is_ctp_auto_connect_enabled(get_setting=None) -> bool:
|
||||||
"""系统设置:是否允许手动连接及非交易时段自动重连(盘前/交易时段计划连接不受此限制)。"""
|
"""系统设置:是否允许手动连接及非交易时段自动重连(盘前/交易时段计划连接不受此限制)。"""
|
||||||
if get_setting is None:
|
if get_setting is None:
|
||||||
from fee_specs import get_setting as _gs
|
from modules.fees.fee_specs import get_setting as _gs
|
||||||
|
|
||||||
get_setting = _gs
|
get_setting = _gs
|
||||||
val = (get_setting(CTP_AUTO_CONNECT_KEY, "1") or "1").strip().lower()
|
val = (get_setting(CTP_AUTO_CONNECT_KEY, "1") or "1").strip().lower()
|
||||||
@@ -60,7 +60,7 @@ def save_ctp_auto_connect(form: Any, set_setting: Callable[[str, str], None]) ->
|
|||||||
|
|
||||||
|
|
||||||
def _get_db_setting(key: str, default: str = "") -> str:
|
def _get_db_setting(key: str, default: str = "") -> str:
|
||||||
from fee_specs import get_setting
|
from modules.fees.fee_specs import get_setting
|
||||||
|
|
||||||
return (get_setting(key, default) or default).strip()
|
return (get_setting(key, default) or default).strip()
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from symbols import ths_to_codes
|
from modules.core.symbols import ths_to_codes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from vnpy.trader.constant import Exchange
|
from vnpy.trader.constant import Exchange
|
||||||
@@ -12,17 +12,17 @@ from datetime import datetime
|
|||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from contract_specs import calc_position_metrics
|
from modules.core.contract_specs import calc_position_metrics
|
||||||
from ctp_symbol import ths_to_vnpy_symbol
|
from modules.ctp.ctp_symbol import ths_to_vnpy_symbol
|
||||||
from fee_specs import calc_round_trip_fee
|
from modules.fees.fee_specs import calc_round_trip_fee
|
||||||
from symbols import ths_to_codes
|
from modules.core.symbols import ths_to_codes
|
||||||
from trade_log_lib import (
|
from modules.trading.trade_log_lib import (
|
||||||
calc_equity_after,
|
calc_equity_after,
|
||||||
purge_duplicate_local_trade_logs,
|
purge_duplicate_local_trade_logs,
|
||||||
ensure_trade_log_columns,
|
ensure_trade_log_columns,
|
||||||
refresh_trade_log_equity_chain,
|
refresh_trade_log_equity_chain,
|
||||||
)
|
)
|
||||||
from vnpy_bridge import ctp_list_trades, ctp_status
|
from modules.ctp.vnpy_bridge import ctp_list_trades, ctp_status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
TZ = ZoneInfo("Asia/Shanghai")
|
TZ = ZoneInfo("Asia/Shanghai")
|
||||||
@@ -291,11 +291,11 @@ def sync_trade_logs_from_ctp(
|
|||||||
)
|
)
|
||||||
stats["synced"] += 1
|
stats["synced"] += 1
|
||||||
try:
|
try:
|
||||||
from trade_notify import notify_trade_log_close
|
from modules.trading.trade_notify import notify_trade_log_close
|
||||||
from trading_context import trading_mode_label
|
from modules.core.trading_context import trading_mode_label
|
||||||
from app import get_setting, send_wechat_msg
|
from app import get_setting, send_wechat_msg
|
||||||
from ai_worker import schedule_ai_event_analysis
|
from modules.notify.ai_worker import schedule_ai_event_analysis
|
||||||
from db_conn import DB_PATH
|
from modules.core.db_conn import DB_PATH
|
||||||
|
|
||||||
notify_trade_log_close(
|
notify_trade_log_close(
|
||||||
send_wechat=send_wechat_msg,
|
send_wechat=send_wechat_msg,
|
||||||
@@ -323,7 +323,7 @@ def sync_trade_logs_from_ctp(
|
|||||||
|
|
||||||
if stats["synced"] or stats["updated"]:
|
if stats["synced"] or stats["updated"]:
|
||||||
try:
|
try:
|
||||||
from stats_engine import refresh_stats_cache
|
from modules.stats.stats_engine import refresh_stats_cache
|
||||||
refresh_stats_cache(conn, capital)
|
refresh_stats_cache(conn, capital)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("stats refresh after ctp trade sync: %s", exc)
|
logger.debug("stats refresh after ctp trade sync: %s", exc)
|
||||||
@@ -44,7 +44,7 @@ def reconcile_position_avg(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""手数变化时采用柜台回报均价;手数不变时保持已锁定柜台价。"""
|
"""手数变化时采用柜台回报均价;手数不变时保持已锁定柜台价。"""
|
||||||
del tick, trades
|
del tick, trades
|
||||||
from ctp_entry_price import round_to_tick
|
from modules.ctp.ctp_entry_price import round_to_tick
|
||||||
|
|
||||||
row = dict(new)
|
row = dict(new)
|
||||||
lots = int(row.get("lots") or 0)
|
lots = int(row.get("lots") or 0)
|
||||||
@@ -19,15 +19,15 @@ os.environ.setdefault("QIHUO_CTP_ROLE", "worker")
|
|||||||
|
|
||||||
from flask import Flask, jsonify, request
|
from flask import Flask, jsonify, request
|
||||||
|
|
||||||
from ctp_ipc_client import worker_token
|
from modules.ctp.ctp_ipc_client import worker_token
|
||||||
from db_conn import DB_PATH, commit_retry, connect_db
|
from modules.core.db_conn import DB_PATH, commit_retry, connect_db
|
||||||
from fee_specs import get_setting, set_setting
|
from modules.fees.fee_specs import get_setting, set_setting
|
||||||
from locale_fix import ensure_process_locale
|
from modules.core.locale_fix import ensure_process_locale
|
||||||
from market_sessions import is_trading_session
|
from modules.market.market_sessions import is_trading_session
|
||||||
from sl_tp_guard import check_sl_tp_on_tick, ensure_monitor_order_columns, start_sl_tp_guard_worker
|
from modules.trading.sl_tp_guard import check_sl_tp_on_tick, ensure_monitor_order_columns, start_sl_tp_guard_worker
|
||||||
from strategy.strategy_db import init_strategy_tables
|
from strategy.strategy_db import init_strategy_tables
|
||||||
from trading_context import get_account_capital, get_trading_mode, get_trailing_be_tick_buffer
|
from modules.core.trading_context import get_account_capital, get_trading_mode, get_trailing_be_tick_buffer
|
||||||
from vnpy_bridge import (
|
from modules.ctp.vnpy_bridge import (
|
||||||
_ctp_td_lock,
|
_ctp_td_lock,
|
||||||
ctp_cancel_order,
|
ctp_cancel_order,
|
||||||
ctp_disconnect,
|
ctp_disconnect,
|
||||||
@@ -103,7 +103,7 @@ def _mode_from_request() -> str:
|
|||||||
|
|
||||||
def _fast_status(mode: str) -> dict[str, Any]:
|
def _fast_status(mode: str) -> dict[str, Any]:
|
||||||
"""Return worker/native bridge state without slow network probing."""
|
"""Return worker/native bridge state without slow network probing."""
|
||||||
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
|
from modules.ctp.ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = dict(get_bridge().status(mode) or {})
|
st = dict(get_bridge().status(mode) or {})
|
||||||
@@ -255,11 +255,11 @@ def _start_background_workers() -> None:
|
|||||||
set_tick_sl_tp_callback(_on_tick_sl_tp)
|
set_tick_sl_tp_callback(_on_tick_sl_tp)
|
||||||
set_ctp_connected_callback(_on_ctp_connected)
|
set_ctp_connected_callback(_on_ctp_connected)
|
||||||
|
|
||||||
from ctp_fee_worker import start_ctp_fee_worker
|
from modules.ctp.ctp_fee_worker import start_ctp_fee_worker
|
||||||
from ctp_premarket_connect import start_ctp_premarket_connect_worker
|
from modules.ctp.ctp_premarket_connect import start_ctp_premarket_connect_worker
|
||||||
from ctp_reconnect import start_ctp_reconnect_worker
|
from modules.ctp.ctp_reconnect import start_ctp_reconnect_worker
|
||||||
from order_pending import reconcile_pending_orders
|
from modules.trading.order_pending import reconcile_pending_orders
|
||||||
from pending_order_worker import start_pending_order_worker
|
from modules.trading.pending_order_worker import start_pending_order_worker
|
||||||
|
|
||||||
def _mode() -> str:
|
def _mode() -> str:
|
||||||
return get_trading_mode(get_setting)
|
return get_trading_mode(get_setting)
|
||||||
@@ -15,14 +15,14 @@ from collections import deque
|
|||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
import ctp_ipc_client
|
import ctp_ipc_client
|
||||||
from locale_fix import ensure_process_locale
|
from modules.core.locale_fix import ensure_process_locale
|
||||||
|
|
||||||
if ctp_ipc_client.is_worker_role():
|
if ctp_ipc_client.is_worker_role():
|
||||||
ensure_process_locale()
|
ensure_process_locale()
|
||||||
|
|
||||||
from ctp_settings import live_setting_dict, simnow_setting_dict
|
from modules.ctp.ctp_settings import live_setting_dict, simnow_setting_dict
|
||||||
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
|
from modules.ctp.ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
|
||||||
from contract_specs import get_contract_spec
|
from modules.core.contract_specs import get_contract_spec
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -37,11 +37,15 @@ CTP_LAST_ERROR_KEY = "ctp_last_error"
|
|||||||
|
|
||||||
|
|
||||||
def _use_ctp_worker_client() -> bool:
|
def _use_ctp_worker_client() -> bool:
|
||||||
|
"""默认单进程直连 CTP;仅当显式设置 QIHUO_CTP_WORKER=1 时使用独立 Worker IPC。"""
|
||||||
|
flag = (os.getenv("QIHUO_CTP_WORKER", "") or "").strip().lower()
|
||||||
|
if flag not in ("1", "true", "yes"):
|
||||||
|
return False
|
||||||
return not ctp_ipc_client.is_worker_role()
|
return not ctp_ipc_client.is_worker_role()
|
||||||
|
|
||||||
|
|
||||||
def _persist_login_cooldown(seconds: float) -> None:
|
def _persist_login_cooldown(seconds: float) -> None:
|
||||||
from fee_specs import get_setting, set_setting
|
from modules.fees.fee_specs import get_setting, set_setting
|
||||||
|
|
||||||
new_until = time.time() + max(0.0, seconds)
|
new_until = time.time() + max(0.0, seconds)
|
||||||
try:
|
try:
|
||||||
@@ -53,7 +57,7 @@ def _persist_login_cooldown(seconds: float) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _persisted_login_cooldown_remaining() -> int:
|
def _persisted_login_cooldown_remaining() -> int:
|
||||||
from fee_specs import get_setting
|
from modules.fees.fee_specs import get_setting
|
||||||
|
|
||||||
try:
|
try:
|
||||||
until = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
|
until = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
|
||||||
@@ -63,19 +67,19 @@ def _persisted_login_cooldown_remaining() -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _clear_persisted_login_cooldown() -> None:
|
def _clear_persisted_login_cooldown() -> None:
|
||||||
from fee_specs import set_setting
|
from modules.fees.fee_specs import set_setting
|
||||||
|
|
||||||
set_setting(CTP_COOLDOWN_UNTIL_KEY, "0")
|
set_setting(CTP_COOLDOWN_UNTIL_KEY, "0")
|
||||||
|
|
||||||
|
|
||||||
def _persist_last_error(msg: str) -> None:
|
def _persist_last_error(msg: str) -> None:
|
||||||
from fee_specs import set_setting
|
from modules.fees.fee_specs import set_setting
|
||||||
|
|
||||||
set_setting(CTP_LAST_ERROR_KEY, (msg or "").strip())
|
set_setting(CTP_LAST_ERROR_KEY, (msg or "").strip())
|
||||||
|
|
||||||
|
|
||||||
def _load_persisted_last_error() -> str:
|
def _load_persisted_last_error() -> str:
|
||||||
from fee_specs import get_setting
|
from modules.fees.fee_specs import get_setting
|
||||||
|
|
||||||
return (get_setting(CTP_LAST_ERROR_KEY, "") or "").strip()
|
return (get_setting(CTP_LAST_ERROR_KEY, "") or "").strip()
|
||||||
|
|
||||||
@@ -382,7 +386,7 @@ class CtpBridge:
|
|||||||
|
|
||||||
def _on_position(event) -> None:
|
def _on_position(event) -> None:
|
||||||
try:
|
try:
|
||||||
from ctp_trading_state import trading_state
|
from modules.ctp.ctp_trading_state import trading_state
|
||||||
|
|
||||||
pos = event.data
|
pos = event.data
|
||||||
row = self._position_row_from_vnpy(pos)
|
row = self._position_row_from_vnpy(pos)
|
||||||
@@ -401,7 +405,7 @@ class CtpBridge:
|
|||||||
if vol <= 0:
|
if vol <= 0:
|
||||||
exchange = getattr(pos, "exchange", None)
|
exchange = getattr(pos, "exchange", None)
|
||||||
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
||||||
from ctp_trading_state import position_key
|
from modules.ctp.ctp_trading_state import position_key
|
||||||
|
|
||||||
trading_state.remove_position(
|
trading_state.remove_position(
|
||||||
position_key(ex_name, sym, d), notify=False,
|
position_key(ex_name, sym, d), notify=False,
|
||||||
@@ -433,7 +437,7 @@ class CtpBridge:
|
|||||||
|
|
||||||
def _on_order(event) -> None:
|
def _on_order(event) -> None:
|
||||||
try:
|
try:
|
||||||
from ctp_trading_state import trading_state
|
from modules.ctp.ctp_trading_state import trading_state
|
||||||
|
|
||||||
order = event.data
|
order = event.data
|
||||||
row = self._order_row_from_vnpy(order)
|
row = self._order_row_from_vnpy(order)
|
||||||
@@ -540,7 +544,7 @@ class CtpBridge:
|
|||||||
"td_volume": td,
|
"td_volume": td,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
from ctp_entry_price import round_to_tick
|
from modules.ctp.ctp_entry_price import round_to_tick
|
||||||
|
|
||||||
ths = CtpBridge._vnpy_sym_to_ths(sym, ex_name) or sym
|
ths = CtpBridge._vnpy_sym_to_ths(sym, ex_name) or sym
|
||||||
if price > 0:
|
if price > 0:
|
||||||
@@ -555,7 +559,7 @@ class CtpBridge:
|
|||||||
def calibrate_trading_state(self) -> None:
|
def calibrate_trading_state(self) -> None:
|
||||||
"""全量校准内存簿(读 vnpy 缓存,不 query 柜台)。"""
|
"""全量校准内存簿(读 vnpy 缓存,不 query 柜台)。"""
|
||||||
try:
|
try:
|
||||||
from ctp_trading_state import trading_state
|
from modules.ctp.ctp_trading_state import trading_state
|
||||||
|
|
||||||
with _ctp_td_lock:
|
with _ctp_td_lock:
|
||||||
orders = self.list_active_orders()
|
orders = self.list_active_orders()
|
||||||
@@ -650,7 +654,7 @@ class CtpBridge:
|
|||||||
self._last_position_query_ts = 0.0
|
self._last_position_query_ts = 0.0
|
||||||
self._last_instruments_ready_ts = 0.0
|
self._last_instruments_ready_ts = 0.0
|
||||||
try:
|
try:
|
||||||
from ctp_trading_state import trading_state
|
from modules.ctp.ctp_trading_state import trading_state
|
||||||
|
|
||||||
trading_state.clear()
|
trading_state.clear()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -736,7 +740,7 @@ class CtpBridge:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def connect(self, mode: str, *, force: bool = False, scheduled: bool = False) -> None:
|
def connect(self, mode: str, *, force: bool = False, scheduled: bool = False) -> None:
|
||||||
from ctp_settings import CTP_DISABLED_HINT
|
from modules.ctp.ctp_settings import CTP_DISABLED_HINT
|
||||||
|
|
||||||
if not _ctp_connect_permitted(scheduled=scheduled):
|
if not _ctp_connect_permitted(scheduled=scheduled):
|
||||||
self._last_error = CTP_DISABLED_HINT
|
self._last_error = CTP_DISABLED_HINT
|
||||||
@@ -852,7 +856,7 @@ class CtpBridge:
|
|||||||
self, mode: str, *, force: bool = False, scheduled: bool = False,
|
self, mode: str, *, force: bool = False, scheduled: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""后台连接,不阻塞 HTTP 请求。"""
|
"""后台连接,不阻塞 HTTP 请求。"""
|
||||||
from ctp_settings import CTP_DISABLED_HINT
|
from modules.ctp.ctp_settings import CTP_DISABLED_HINT
|
||||||
|
|
||||||
if not _ctp_connect_permitted(scheduled=scheduled):
|
if not _ctp_connect_permitted(scheduled=scheduled):
|
||||||
self._last_error = CTP_DISABLED_HINT
|
self._last_error = CTP_DISABLED_HINT
|
||||||
@@ -1033,7 +1037,7 @@ class CtpBridge:
|
|||||||
|
|
||||||
def reconnect_after_settings_saved(self, mode: str) -> dict[str, Any]:
|
def reconnect_after_settings_saved(self, mode: str) -> dict[str, Any]:
|
||||||
"""保存前置/账号后关闭旧连接,并用数据库中的新配置重连。"""
|
"""保存前置/账号后关闭旧连接,并用数据库中的新配置重连。"""
|
||||||
from ctp_settings import is_ctp_auto_connect_enabled
|
from modules.ctp.ctp_settings import is_ctp_auto_connect_enabled
|
||||||
|
|
||||||
self._close_gateway()
|
self._close_gateway()
|
||||||
self._last_error = ""
|
self._last_error = ""
|
||||||
@@ -1048,14 +1052,14 @@ class CtpBridge:
|
|||||||
def _run() -> None:
|
def _run() -> None:
|
||||||
time.sleep(45)
|
time.sleep(45)
|
||||||
try:
|
try:
|
||||||
from ctp_fee_worker import try_daily_ctp_fee_sync
|
from modules.ctp.ctp_fee_worker import try_daily_ctp_fee_sync
|
||||||
|
|
||||||
def _gs(key: str, default: str = "") -> str:
|
def _gs(key: str, default: str = "") -> str:
|
||||||
from fee_specs import get_setting
|
from modules.fees.fee_specs import get_setting
|
||||||
return get_setting(key, default)
|
return get_setting(key, default)
|
||||||
|
|
||||||
def _ss(key: str, val: str) -> None:
|
def _ss(key: str, val: str) -> None:
|
||||||
from fee_specs import set_setting
|
from modules.fees.fee_specs import set_setting
|
||||||
set_setting(key, val)
|
set_setting(key, val)
|
||||||
|
|
||||||
try_daily_ctp_fee_sync(
|
try_daily_ctp_fee_sync(
|
||||||
@@ -1472,7 +1476,7 @@ class CtpBridge:
|
|||||||
price = self._price_from_tick(tick)
|
price = self._price_from_tick(tick)
|
||||||
if price and price > 0:
|
if price and price > 0:
|
||||||
try:
|
try:
|
||||||
from ctp_trading_state import trading_state
|
from modules.ctp.ctp_trading_state import trading_state
|
||||||
|
|
||||||
trading_state.set_tick_price(ex_s, sym, price)
|
trading_state.set_tick_price(ex_s, sym, price)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -2359,13 +2363,13 @@ def vnpy_available() -> bool:
|
|||||||
|
|
||||||
def _ctp_connect_permitted(*, scheduled: bool = False) -> bool:
|
def _ctp_connect_permitted(*, scheduled: bool = False) -> bool:
|
||||||
"""scheduled=True:盘前/交易时段计划连接,不受「自动连接」开关限制。"""
|
"""scheduled=True:盘前/交易时段计划连接,不受「自动连接」开关限制。"""
|
||||||
from ctp_settings import is_ctp_auto_connect_enabled
|
from modules.ctp.ctp_settings import is_ctp_auto_connect_enabled
|
||||||
|
|
||||||
if is_ctp_auto_connect_enabled():
|
if is_ctp_auto_connect_enabled():
|
||||||
return True
|
return True
|
||||||
if not scheduled:
|
if not scheduled:
|
||||||
return False
|
return False
|
||||||
from ctp_premarket_connect import should_auto_connect_now
|
from modules.ctp.ctp_premarket_connect import should_auto_connect_now
|
||||||
|
|
||||||
return should_auto_connect_now()
|
return should_auto_connect_now()
|
||||||
|
|
||||||
@@ -2375,7 +2379,7 @@ def ctp_disconnect(*, set_disabled_hint: bool = False) -> None:
|
|||||||
if _use_ctp_worker_client():
|
if _use_ctp_worker_client():
|
||||||
ctp_ipc_client.disconnect(set_disabled_hint=set_disabled_hint)
|
ctp_ipc_client.disconnect(set_disabled_hint=set_disabled_hint)
|
||||||
return
|
return
|
||||||
from ctp_settings import CTP_DISABLED_HINT
|
from modules.ctp.ctp_settings import CTP_DISABLED_HINT
|
||||||
|
|
||||||
b = get_bridge()
|
b = get_bridge()
|
||||||
b._close_gateway()
|
b._close_gateway()
|
||||||
@@ -2450,7 +2454,7 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def ctp_status(mode: str) -> dict[str, Any]:
|
def ctp_status(mode: str) -> dict[str, Any]:
|
||||||
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
|
from modules.ctp.ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
|
||||||
|
|
||||||
if _use_ctp_worker_client():
|
if _use_ctp_worker_client():
|
||||||
st = ctp_ipc_client.status(mode)
|
st = ctp_ipc_client.status(mode)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||||
|
|
||||||
|
from modules.fees.routes import register
|
||||||
|
|
||||||
|
__all__ = ["register"]
|
||||||
@@ -10,9 +10,9 @@ import re
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from contract_specs import get_contract_spec
|
from modules.core.contract_specs import get_contract_spec
|
||||||
|
|
||||||
from db_conn import connect_db, is_benign_migration_error
|
from modules.core.db_conn import connect_db, is_benign_migration_error
|
||||||
|
|
||||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
|
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
|
||||||
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
||||||
@@ -145,7 +145,7 @@ def get_fee_spec(ths_code: str, *, trading_mode: str = "simulation") -> dict:
|
|||||||
if row:
|
if row:
|
||||||
return _row_to_spec(row, mult)
|
return _row_to_spec(row, mult)
|
||||||
try:
|
try:
|
||||||
from ctp_fee_sync import sync_fee_for_symbol
|
from modules.ctp.ctp_fee_sync import sync_fee_for_symbol
|
||||||
fields = sync_fee_for_symbol(trading_mode, ths_code)
|
fields = sync_fee_for_symbol(trading_mode, ths_code)
|
||||||
if fields:
|
if fields:
|
||||||
return {"product": product, **fields}
|
return {"product": product, **fields}
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from contract_specs import get_contract_spec
|
from modules.core.contract_specs import get_contract_spec
|
||||||
from fee_specs import get_fee_multiplier, upsert_fee_rate
|
from modules.fees.fee_specs import get_fee_multiplier, upsert_fee_rate
|
||||||
|
|
||||||
|
|
||||||
def _to_float(val: Any) -> float:
|
def _to_float(val: Any) -> float:
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user