Files
qihuo/app.py
T
dekun e5a586f903 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>
2026-07-01 14:42:16 +08:00

869 lines
32 KiB
Python

# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
import os
import sys
_ROOT = os.path.dirname(os.path.abspath(__file__))
_legacy = os.path.join(_ROOT, "_legacy")
if _legacy not in sys.path:
sys.path.insert(0, _legacy)
from modules.core.paths import ROOT, UPLOADS_DIR, DB_PATH, ensure_runtime_dirs, resolve_env_file
from locale_fix import ensure_process_locale
ensure_process_locale()
ensure_runtime_dirs()
import time
import threading
import requests
from datetime import date, datetime, timedelta
from typing import Optional
from functools import wraps
from zoneinfo import ZoneInfo
from werkzeug.utils import secure_filename
from dotenv import load_dotenv
from flask import (
Flask, render_template, request, redirect, url_for,
flash, session, jsonify, Response, stream_with_context,
)
from werkzeug.security import check_password_hash, generate_password_hash
from functools import wraps
from symbols import (
search_symbols,
ths_to_codes,
list_main_contracts_grouped,
list_recommended_symbols_grouped,
refresh_main_index,
)
from contract_specs import calc_position_metrics
from fee_specs import (
calc_fee_breakdown,
calc_round_trip_fee,
list_fee_rates_for_ui,
count_fee_rates_by_source,
purge_non_ctp_fee_rates,
)
from nav_settings import NAV_TOGGLES, get_nav_items, nav_enabled, save_nav_items
from stats_engine import (
STATS_VIEWS,
build_all_stats,
get_calendar_day,
get_calendar_month,
load_stats_cache,
refresh_stats_cache,
)
from kline_store import ensure_kline_tables
from kline_stream import kline_hub, sse_format
from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
from db_conn import OperationalError, connect_db, database_label, is_benign_migration_error, is_db_contention_error, is_schema_migration_error, rollback_if_postgres
from admin_settings import save_admin_credentials
from db_backup import (
backup_dir,
backup_in_progress,
default_restore_dir,
get_backup_last_at,
list_backups,
resolve_backup_file,
schedule_backup,
start_backup_worker,
)
from strategy.strategy_db import init_strategy_tables
load_dotenv(resolve_env_file())
load_dotenv(os.path.join(ROOT, ".env")) # 兼容旧路径
app = Flask(
__name__,
template_folder=os.path.join(ROOT, "modules", "web", "templates"),
static_folder=os.path.join(ROOT, "modules", "web", "static"),
)
app.secret_key = os.getenv("SECRET_KEY", "futures_monitor_default_secret")
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "6600"))
DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
UPLOAD_DIR = str(UPLOADS_DIR)
TZ = ZoneInfo("Asia/Shanghai")
OPEN_TYPES = ["突破开仓", "回调开仓", "追涨杀跌", "计划内开仓", "震荡摸顶底", "其他"]
EXIT_TRIGGERS = ["止盈", "止损", "手工平仓", "移动止损", "时间离场", "其他"]
BEHAVIOR_TAGS = ["怕踏空", "报复开仓", "盈利飘了", "拿不住单", "扛单", "重仓违规"]
KLINE_PERIODS = ["1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d"]
KLINE_CUTOFFS = ["平仓时间", "开仓时间", "当前时间"]
def today_str() -> str:
return datetime.now(TZ).date().isoformat()
def calc_holding_duration(open_time: str, close_time: str) -> str:
try:
o = datetime.fromisoformat(open_time.strip().replace(" ", "T")[:19])
c = datetime.fromisoformat(close_time.strip().replace(" ", "T")[:19])
delta = c - o
if delta.total_seconds() < 0:
return ""
secs = int(delta.total_seconds())
h, rem = divmod(secs, 3600)
m, _ = divmod(rem, 60)
if h:
return f"{h}小时{m}分钟"
return f"{m}分钟"
except Exception:
return ""
def holding_to_minutes(open_time: str, close_time: str) -> int:
try:
o = datetime.fromisoformat(open_time.strip().replace(" ", "T"))
c = datetime.fromisoformat(close_time.strip().replace(" ", "T"))
secs = int((c - o).total_seconds())
return max(0, secs // 60)
except Exception:
return 0
def classify_close_result(direction: str, close: float, sl: float, tp: float) -> str:
"""根据平仓价与止损/止盈距离判断结果。"""
if close is None:
return "手动平仓"
tol = max(abs(close) * 0.002, 1.0)
if abs(close - tp) <= tol:
return "止盈"
if abs(close - sl) <= tol:
return "止损"
return "手动平仓"
def calc_rr_ratio(direction: str, entry: float, stop: float, target: float) -> Optional[float]:
"""盈亏比 = 盈利空间 / 风险空间。"""
if entry is None or stop is None or target is None:
return None
if direction == "long":
risk = entry - stop
if risk <= 0:
return None
return round((target - entry) / risk, 2)
if direction == "short":
risk = stop - entry
if risk <= 0:
return None
return round((entry - target) / risk, 2)
return None
def calc_theoretical_pnl(direction: str, entry: float, target: float, lots: float) -> Optional[float]:
if entry is None or target is None or lots is None:
return None
if direction == "long":
return round((target - entry) * lots, 2)
if direction == "short":
return round((entry - target) * lots, 2)
return None
def parse_review_date_filter(preset: str, start: str, end: str) -> tuple[str, str]:
today = datetime.now(TZ).date()
if preset == "today":
s = today.isoformat()
return s, s
if preset == "week":
monday = today - timedelta(days=today.weekday())
return monday.isoformat(), today.isoformat()
if preset == "month":
return today.replace(day=1).isoformat(), today.isoformat()
return start.strip(), end.strip()
def expire_old_plans():
"""当日结束后计划自动失效,保留历史。"""
today = today_str()
conn = get_db()
conn.execute(
"UPDATE order_plans SET status='expired' WHERE plan_date < ? AND status IN ('planned', 'active')",
(today,),
)
conn.execute(
"UPDATE order_plans SET plan_date=date(created_at) WHERE plan_date IS NULL OR plan_date=''"
)
conn.commit()
conn.close()
def get_db():
return connect_db()
def get_setting(key: str, default: str = "") -> str:
conn = get_db()
row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
conn.close()
return row["value"] if row else default
def set_setting(key: str, value: str):
conn = get_db()
conn.execute(
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=?",
(key, value, value),
)
conn.commit()
conn.close()
def require_nav(key: str):
"""导航项关闭时拒绝访问对应页面。"""
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
if not nav_enabled(get_setting, key):
flash("该页面已在系统设置中关闭")
return redirect(url_for("positions"))
return f(*args, **kwargs)
return wrapped
return decorator
def _static_asset_v() -> str:
base = os.path.dirname(os.path.abspath(__file__))
rels = (
"static/js/trade.js",
"static/js/dashboard.js",
"static/js/orientation.js",
"static/css/records.css",
"static/js/records.js",
"static/js/settings.js",
"static/css/mobile.css",
"static/css/responsive.css",
"static/css/trade.css",
"static/css/dashboard.css",
"static/css/doc.css",
"static/css/base.css",
)
mtimes = []
for rel in rels:
path = os.path.join(base, rel.replace("/", os.sep))
if os.path.isfile(path):
mtimes.append(os.path.getmtime(path))
return str(int(max(mtimes))) if mtimes else "0"
def _ua_is_phone(ua: str) -> bool:
ua_l = (ua or "").lower()
if "ipad" in ua_l:
return False
if "android" in ua_l and "mobile" not in ua_l:
return False
if any(x in ua_l for x in ("iphone", "ipod", "windows phone", "iemobile")):
return True
if "android" in ua_l and "mobile" in ua_l:
return True
if "mobile" in ua_l or "harmonyos" in ua_l or "openharmony" in ua_l:
return True
return False
@app.context_processor
def inject_globals():
return {"nav_items": get_nav_items(get_setting), "asset_v": _static_asset_v()}
def _trading_mode() -> str:
return (get_setting("trading_mode", "simulation") or "simulation").strip()
def touch_stats_cache():
try:
conn = get_db()
capital = float(get_setting("live_capital", "0") or 0)
refresh_stats_cache(conn, capital)
conn.close()
except Exception as exc:
app.logger.warning("stats cache refresh failed: %s", exc)
def get_stats_data() -> dict:
conn = get_db()
try:
capital = float(get_setting("live_capital", "0") or 0)
data = load_stats_cache(conn)
if data:
return data
try:
return refresh_stats_cache(conn, capital)
except OperationalError as exc:
if not is_db_contention_error(exc):
raise
app.logger.warning("stats cache refresh contention, compute without save: %s", exc)
return build_all_stats(conn, capital)
finally:
conn.close()
def init_db():
import strategy.strategy_db as strategy_db
import risk.account_risk_lib as account_risk_lib
strategy_db._TABLES_READY = False
account_risk_lib._SCHEMA_READY = False
conn = get_db()
c = conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)")
c.execute('''CREATE TABLE IF NOT EXISTS order_plans
(id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT, symbol_name TEXT, direction TEXT,
zone_upper REAL, zone_lower REAL,
stop_loss REAL, take_profit REAL,
status TEXT DEFAULT 'planned',
triggered_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS key_monitors
(id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT, symbol_name TEXT, monitor_type TEXT, direction TEXT,
upper REAL, lower REAL,
upper_triggered INTEGER DEFAULT 0,
lower_triggered INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS trade_records
(id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT, symbol_name TEXT, monitor_type TEXT, direction TEXT,
trigger_price REAL, stop_loss REAL, take_profit REAL,
result TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
conn.commit()
migrations = [
"ALTER TABLE key_monitors ADD COLUMN symbol_name TEXT",
"ALTER TABLE key_monitors ADD COLUMN upper_triggered INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN lower_triggered INTEGER DEFAULT 0",
"ALTER TABLE trade_records ADD COLUMN symbol_name TEXT",
"ALTER TABLE order_plans ADD COLUMN sina_code TEXT",
"ALTER TABLE order_plans ADD COLUMN market_code TEXT",
"ALTER TABLE key_monitors ADD COLUMN market_code TEXT",
"ALTER TABLE key_monitors ADD COLUMN sina_code TEXT",
"ALTER TABLE trade_records ADD COLUMN market_code TEXT",
"ALTER TABLE order_plans ADD COLUMN plan_date TEXT",
"ALTER TABLE order_plans ADD COLUMN decision_reason TEXT",
"ALTER TABLE key_monitors ADD COLUMN status TEXT DEFAULT 'active'",
"ALTER TABLE key_monitors ADD COLUMN archived_at TEXT",
"ALTER TABLE key_monitors ADD COLUMN trade_mode TEXT DEFAULT '顺势'",
"ALTER TABLE key_monitors ADD COLUMN risk_reward REAL DEFAULT 2",
"ALTER TABLE key_monitors ADD COLUMN trailing_be INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN last_trigger_bar TEXT",
"ALTER TABLE key_monitors ADD COLUMN alert_push_count INTEGER DEFAULT 0",
"ALTER TABLE key_monitors ADD COLUMN alert_last_push_at TEXT",
"ALTER TABLE key_monitors ADD COLUMN alert_break_side TEXT",
"ALTER TABLE key_monitors ADD COLUMN breakout_bar_time TEXT",
"ALTER TABLE key_monitors ADD COLUMN alert_close_price REAL",
"ALTER TABLE key_monitors ADD COLUMN bar_period TEXT DEFAULT '5m'",
"ALTER TABLE review_records ADD COLUMN direction TEXT",
"ALTER TABLE review_records ADD COLUMN entry_price REAL",
"ALTER TABLE review_records ADD COLUMN stop_loss REAL",
"ALTER TABLE review_records ADD COLUMN take_profit REAL",
"ALTER TABLE review_records ADD COLUMN close_price REAL",
"ALTER TABLE review_records ADD COLUMN lots REAL",
"ALTER TABLE review_records ADD COLUMN holding_duration TEXT",
"ALTER TABLE review_records ADD COLUMN initial_pnl REAL",
"ALTER TABLE review_records ADD COLUMN actual_pnl REAL",
"ALTER TABLE review_records ADD COLUMN is_emotion INTEGER DEFAULT 0",
"ALTER TABLE review_records ADD COLUMN symbol_name TEXT",
"ALTER TABLE review_records ADD COLUMN market_code TEXT",
"ALTER TABLE review_records ADD COLUMN sina_code TEXT",
"ALTER TABLE trade_logs ADD COLUMN fee REAL",
"ALTER TABLE trade_logs ADD COLUMN pnl_net REAL",
"ALTER TABLE trade_logs ADD COLUMN margin_pct REAL",
"ALTER TABLE trade_logs ADD COLUMN equity_after REAL",
"ALTER TABLE review_records ADD COLUMN fee REAL",
"ALTER TABLE review_records ADD COLUMN pnl_net REAL",
]
for sql in migrations:
try:
c.execute(sql)
conn.commit()
except Exception as exc:
if not is_schema_migration_error(exc):
raise
rollback_if_postgres(conn)
c.execute('''CREATE TABLE IF NOT EXISTS review_records
(id INTEGER PRIMARY KEY AUTOINCREMENT,
open_time TEXT, close_time TEXT,
symbol TEXT, timeframe TEXT,
pnl REAL,
open_type TEXT, expected_rr REAL, actual_rr REAL,
exit_trigger TEXT, exit_supplement TEXT,
watch_after_breakeven TEXT, new_position_while_occupied TEXT,
screenshot TEXT,
auto_kline INTEGER DEFAULT 0,
kline_period1 TEXT, kline_period2 TEXT,
kline_count INTEGER, kline_cutoff TEXT,
behavior_tags TEXT, notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS position_monitors
(id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT, symbol_name TEXT, market_code TEXT, sina_code TEXT,
direction TEXT, lots REAL, entry_price REAL,
stop_loss REAL, take_profit REAL, open_time TEXT,
status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS trade_logs
(id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT, symbol_name TEXT, market_code TEXT, sina_code TEXT,
monitor_type TEXT, direction TEXT,
entry_price REAL, stop_loss REAL, take_profit REAL, close_price REAL,
lots REAL, margin REAL, holding_minutes INTEGER,
open_time TEXT, close_time TEXT,
pnl REAL, result TEXT,
verified INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute('''CREATE TABLE IF NOT EXISTS fee_rates
(product TEXT PRIMARY KEY,
exchange TEXT,
mult INTEGER,
open_fixed REAL DEFAULT 0,
open_ratio REAL DEFAULT 0,
close_yesterday_fixed REAL DEFAULT 0,
close_yesterday_ratio REAL DEFAULT 0,
close_today_fixed REAL DEFAULT 0,
close_today_ratio REAL DEFAULT 0,
updated_at TEXT)''')
c.execute('''CREATE TABLE IF NOT EXISTS stats_cache
(key TEXT PRIMARY KEY,
data_json TEXT NOT NULL,
updated_at TEXT NOT NULL)''')
conn.commit()
for sql in (
"ALTER TABLE fee_rates ADD COLUMN source TEXT DEFAULT 'local'",
):
try:
c.execute(sql)
conn.commit()
except Exception as exc:
if not is_schema_migration_error(exc):
raise
rollback_if_postgres(conn)
ensure_kline_tables(conn)
init_strategy_tables(conn)
from risk.account_risk_lib import ensure_account_risk_schema
from recommend_store import ensure_recommend_tables
ensure_account_risk_schema(conn)
ensure_recommend_tables(conn)
from ai_messages import ensure_ai_messages_table
ensure_ai_messages_table(conn)
conn.commit()
conn.close()
sync_admin_from_env()
if not get_setting("wechat_webhook") and os.getenv("WECHAT_WEBHOOK"):
set_setting("wechat_webhook", os.getenv("WECHAT_WEBHOOK"))
if not get_setting("ths_refresh_token") and os.getenv("THS_REFRESH_TOKEN"):
set_setting("ths_refresh_token", os.getenv("THS_REFRESH_TOKEN"))
from ctp_settings import seed_ctp_settings_from_env
seed_ctp_settings_from_env(set_setting)
os.makedirs(UPLOAD_DIR, exist_ok=True)
expire_old_plans()
if not get_setting("fee_multiplier"):
set_setting("fee_multiplier", "2")
if not get_setting("trading_mode"):
set_setting("trading_mode", "simulation")
if not get_setting("position_sizing_mode"):
set_setting("position_sizing_mode", "fixed")
if not get_setting("fixed_lots"):
set_setting("fixed_lots", "1")
if not get_setting("fixed_amount"):
set_setting("fixed_amount", "5000")
if not get_setting("risk_percent"):
set_setting("risk_percent", "1")
if not get_setting("max_margin_pct"):
set_setting("max_margin_pct", "30")
if not get_setting("roll_max_margin_pct"):
set_setting("roll_max_margin_pct", "50")
if not get_setting("trailing_be_tick_buffer"):
set_setting("trailing_be_tick_buffer", "2")
if not get_setting("pending_order_timeout_min"):
set_setting("pending_order_timeout_min", "5")
if not get_setting("ai_enabled"):
set_setting("ai_enabled", "0")
if not get_setting("ai_provider"):
set_setting("ai_provider", "ollama")
if not get_setting("ai_ollama_base_url"):
set_setting("ai_ollama_base_url", "http://127.0.0.1:11434")
if not get_setting("ai_ollama_model"):
set_setting("ai_ollama_model", "qwen2.5:7b")
if not get_setting("ai_openai_base_url"):
set_setting("ai_openai_base_url", "https://api.openai.com/v1")
if not get_setting("ai_openai_model"):
set_setting("ai_openai_model", "gpt-4o-mini")
if not get_setting("ai_daily_report_enabled"):
set_setting("ai_daily_report_enabled", "1")
if not get_setting("ai_daily_report_hour"):
set_setting("ai_daily_report_hour", "15")
if not get_setting("ai_daily_report_minute"):
set_setting("ai_daily_report_minute", "5")
if not get_setting("backup_auto_enabled"):
set_setting("backup_auto_enabled", "1")
if not get_setting("backup_auto_hour"):
set_setting("backup_auto_hour", "3")
if not get_setting("backup_keep_count"):
set_setting("backup_keep_count", "30")
if not get_setting("fee_source_mode"):
set_setting("fee_source_mode", "ctp")
set_setting("fee_source_mode", "ctp")
try:
purge_non_ctp_fee_rates()
except Exception:
pass
def sync_admin_from_env():
"""
从 .env 同步管理员账号。
- 首次建库:自动写入 ADMIN_USERNAME / ADMIN_PASSWORD
- 已建库后改 .env:需设 ADMIN_SYNC_FROM_ENV=true 并重启服务
"""
sync = os.getenv("ADMIN_SYNC_FROM_ENV", "false").lower() in ("1", "true", "yes")
env_username = os.getenv("ADMIN_USERNAME", "").strip()
env_password = os.getenv("ADMIN_PASSWORD", "").strip()
placeholder_passwords = {"", "change-me-on-first-login", "admin123"}
if not get_setting("admin_username"):
username = env_username or "admin"
password = env_password if env_password not in placeholder_passwords else "admin123"
set_setting("admin_username", username)
set_setting("admin_password_hash", generate_password_hash(password))
return
if not sync:
return
if env_username:
set_setting("admin_username", env_username)
if env_password and env_password not in placeholder_passwords:
set_setting("admin_password_hash", generate_password_hash(env_password))
if os.getenv("QIHUO_SKIP_INIT_DB") != "1":
init_db()
app.logger.info("数据库: %s", database_label())
def sync_ths_token():
set_ths_refresh_token(get_setting("ths_refresh_token"))
if os.getenv("QIHUO_INIT_ONLY") != "1":
sync_ths_token()
def build_market_quote_payload(
symbol: str,
market_code: str = "",
sina_code: str = "",
*,
prefer_sina: bool = False,
) -> dict:
if not market_code or not sina_code:
codes = ths_to_codes(symbol)
if codes:
market_code = codes.get("market_code", "") or market_code
sina_code = codes.get("sina_code", "") or sina_code
quote_source = "sina"
price = None
prev_close = None
if not prefer_sina:
try:
from vnpy_bridge import ctp_status, ctp_get_tick_detail
from trading_context import get_trading_mode
mode = get_trading_mode(get_setting)
if ctp_status(mode).get("connected"):
detail = ctp_get_tick_detail(mode, symbol)
if detail.get("price"):
price = detail["price"]
quote_source = "ctp"
if detail.get("pre_close") is not None:
prev_close = detail["pre_close"]
except Exception:
pass
if price is None:
price = fetch_price(symbol, market_code, sina_code)
name = symbol
codes = ths_to_codes(symbol)
if codes:
name = codes.get("name", symbol)
if prev_close is None and sina_code:
from market import fetch_raw_for_volume
raw = fetch_raw_for_volume(sina_code)
if raw and raw.get("prev_close") is not None:
prev_close = raw["prev_close"]
return {
"symbol": symbol,
"name": name,
"price": price,
"prev_close": prev_close,
"quote_source": quote_source,
}
# —————————————— 推送 ——————————————
def send_wechat_msg(content: str):
webhook = get_setting("wechat_webhook")
if not webhook:
return
full = f"【国内期货】\n{content}"
data = {"msgtype": "text", "text": {"content": full}}
try:
requests.post(webhook, json=data, timeout=10)
except Exception:
pass
# —————————————— 行情 ——————————————
def resolve_market_codes(ths_code: str, market_code: str = "", sina_code: str = "") -> tuple[str, str]:
"""返回 (market_code, sina_code) 用于行情拉取。"""
if market_code:
return market_code, sina_code
if sina_code and "." in sina_code:
return sina_code, ""
codes = ths_to_codes(ths_code)
if codes:
return codes["market_code"], codes["sina_code"]
if ths_code.startswith("nf_") or ths_code.startswith("CFF_RE_"):
return ths_code, ths_code
return "", sina_code or ""
def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Optional[float]:
sym = (ths_code or "").strip()
if sym:
try:
from vnpy_bridge import ctp_status, ctp_get_tick_price
from trading_context import get_trading_mode
mode = get_trading_mode(get_setting)
if ctp_status(mode).get("connected"):
p = ctp_get_tick_price(mode, sym)
if p and p > 0:
return p
except Exception:
pass
mc, sc = resolve_market_codes(sym, market_code, sina_code)
if not mc and not sc:
return None
return market_get_price(mc, sc)
# —————————————— 监控逻辑 ——————————————
def check_order_plans():
expire_old_plans()
today = today_str()
conn = get_db()
rows = conn.execute(
"SELECT * FROM order_plans WHERE plan_date=? AND status IN ('planned', 'active')",
(today,),
).fetchall()
for r in rows:
sym = r["symbol"]
sina = r["sina_code"] if "sina_code" in r.keys() else ""
market = r["market_code"] if "market_code" in r.keys() else ""
p = fetch_price(sym, market, sina)
if not p:
continue
direction = r["direction"]
zone_upper = r["zone_upper"]
zone_lower = r["zone_lower"]
stop_loss = r["stop_loss"]
take_profit = r["take_profit"]
status = r["status"]
pid = r["id"]
name = r["symbol_name"] or sym
reason = r["decision_reason"] if "decision_reason" in r.keys() and r["decision_reason"] else ""
# 计划状态:价格进入决策区间则激活并通知
if status == "planned":
in_zone = zone_lower <= p <= zone_upper
if in_zone:
msg = (
f"【开单计划触发】{name} ({sym})\n"
f"方向:{'做多' if direction == 'long' else '做空'}\n"
f"决策区间:{zone_lower} ~ {zone_upper}\n"
f"决策理由:{reason}\n"
f"当前价:{p}\n"
f"止损:{stop_loss} 止盈:{take_profit}"
)
send_wechat_msg(msg)
conn.execute(
"UPDATE order_plans SET status='active', triggered_at=? WHERE id=?",
(datetime.now().isoformat(), pid),
)
status = "active"
# 激活状态:监控止盈止损
if status == "active":
res = None
if direction == "long":
if p >= take_profit:
res = "止盈"
elif p <= stop_loss:
res = "止损"
elif direction == "short":
if p <= take_profit:
res = "止盈"
elif p >= stop_loss:
res = "止损"
if res:
msg = (
f"[{'做多' if direction == 'long' else '做空'}] {name}{res}\n"
f"决策区间:{zone_lower} ~ {zone_upper}\n"
f"止损:{stop_loss} 止盈:{take_profit}\n"
f"当前价:{p}"
)
send_wechat_msg(msg)
conn.execute(
"""INSERT INTO trade_records
(symbol, symbol_name, monitor_type, direction,
trigger_price, stop_loss, take_profit, result)
VALUES (?,?,?,?,?,?,?,?)""",
(sym, name, "开单计划", direction, p, stop_loss, take_profit, res),
)
conn.execute(
"UPDATE order_plans SET status='closed' WHERE id=?", (pid,)
)
conn.commit()
conn.close()
def check_key_monitors():
from db_conn import DB_PATH
from key_monitor_lib import run_key_monitor_check
from trading_context import get_trading_mode
conn = get_db()
try:
execute_fn = getattr(app, "_execute_key_breakout", None)
run_key_monitor_check(
conn,
db_path=DB_PATH,
get_trading_mode_fn=lambda: get_trading_mode(get_setting),
send_wechat=send_wechat_msg,
execute_breakout_fn=execute_fn,
)
conn.commit()
finally:
conn.close()
def background_task():
while True:
try:
expire_old_plans()
check_key_monitors()
fn_roll = getattr(app, "_check_roll_monitors", None)
if fn_roll:
fn_roll()
check_order_plans()
fn = getattr(app, "_check_trend_plans", None)
if fn:
fn(app)
except Exception:
pass
time.sleep(3)
def start_background_threads():
from trading_context import get_trading_mode
threading.Thread(target=background_task, daemon=True).start()
threading.Thread(
target=lambda: kline_hub.worker_loop(
DB_PATH,
lambda sym, mc, sc: build_market_quote_payload(
sym, mc, sc, prefer_sina=True,
),
get_mode_fn=lambda: get_trading_mode(get_setting),
),
daemon=True,
).start()
threading.Thread(target=refresh_main_index, daemon=True).start()
start_backup_worker(get_setting_fn=get_setting, set_setting_fn=set_setting)
# —————————————— 登录 ——————————————
def login_required(f):
@wraps(f)
def wrap(*args, **kwargs):
if not session.get("logged_in"):
return redirect(url_for("login"))
return f(*args, **kwargs)
return wrap
from modules.core import AppDeps, register_all_modules, start_module_workers
if os.getenv("QIHUO_INIT_ONLY") != "1":
_deps = AppDeps(
app=app,
get_db=get_db,
get_setting=get_setting,
set_setting=set_setting,
login_required=login_required,
require_nav=require_nav,
fetch_price=fetch_price,
send_wechat_msg=send_wechat_msg,
touch_stats_cache=touch_stats_cache,
get_stats_data=get_stats_data,
build_market_quote_payload=build_market_quote_payload,
today_str=today_str,
expire_old_plans=expire_old_plans,
check_order_plans=check_order_plans,
check_key_monitors=check_key_monitors,
background_task=background_task,
start_background_threads=start_background_threads,
tz=TZ,
db_path=DB_PATH,
upload_dir=UPLOAD_DIR,
open_types=OPEN_TYPES,
exit_triggers=EXIT_TRIGGERS,
behavior_tags=BEHAVIOR_TAGS,
kline_periods=KLINE_PERIODS,
kline_cutoffs=KLINE_CUTOFFS,
calc_holding_duration=calc_holding_duration,
holding_to_minutes=holding_to_minutes,
classify_close_result=classify_close_result,
calc_rr_ratio=calc_rr_ratio,
calc_theoretical_pnl=calc_theoretical_pnl,
parse_review_date_filter=parse_review_date_filter,
trading_mode=_trading_mode,
static_asset_v=_static_asset_v,
ua_is_phone=_ua_is_phone,
)
register_all_modules(_deps)
start_module_workers(_deps)
# —————————————— 启动 ——————————————
if __name__ == "__main__":
app.run(host=HOST, port=PORT, debug=DEBUG, threaded=True)