# 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)