diff --git a/LICENSE.zh-CN.txt b/LICENSE.zh-CN.txt index afafe44..8192528 100644 --- a/LICENSE.zh-CN.txt +++ b/LICENSE.zh-CN.txt @@ -1,4 +1,4 @@ -国内期货交易监控复盘系统 — 软件使用许可与版权声明 +国内期货 · 交易复盘系统 — 软件使用许可与版权声明 著作权人:马建军 Copyright (c) 2025-2026 马建军. All rights reserved. diff --git a/README.md b/README.md index f4855ad..bb126bf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 国内期货交易监控复盘系统 +# 国内期货 · 交易复盘系统 基于 Flask 的国内期货 **CTP 下单 + 监控 + 复盘 + 统计** Web 应用。模拟盘连接 SimNow,实盘连接期货公司 CTP;支持关键位/计划提醒、交易记录同步、资金曲线、可开仓品种(仓位纪律)与企业微信推送。 diff --git a/deploy.sh b/deploy.sh index 37b98e2..db6153b 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# 国内期货监控系统 - Ubuntu 一键部署 / 更新 +# 国内期货 · 交易复盘系统 - Ubuntu 一键部署 / 更新 # root 用户 | 目录 /opt/qihuo | 端口 6600 | PM2 # # 已内置修复(避免重复踩坑): diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 5db9d52..5864813 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,6 +1,6 @@ # 部署文档 -国内期货交易监控复盘系统 — Ubuntu 服务器部署、更新与运维说明。 +国内期货 · 交易复盘系统 — Ubuntu 服务器部署、更新与运维说明。 --- diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 431a856..dfddff3 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,6 +1,6 @@ # 功能说明文档 -国内期货交易监控复盘系统(Flask + SQLite + vnpy_ctp + PM2)。 +国内期货 · 交易复盘系统(Flask + SQLite + vnpy_ctp + PM2)。 --- diff --git a/docs/软件购买与使用协议.md b/docs/软件购买与使用协议.md index a2971df..62ba8d5 100644 --- a/docs/软件购买与使用协议.md +++ b/docs/软件购买与使用协议.md @@ -26,7 +26,7 @@ ## 第一条 软件与交付内容 -1.1 甲方向乙方提供的软件名称为 **「国内期货交易监控复盘系统」**(以下简称「本软件」),包括甲方交付时约定版本的源代码、部署说明及必要配置指导。 +1.1 甲方向乙方提供的软件名称为 **「国内期货 · 交易复盘系统」**(以下简称「本软件」),包括甲方交付时约定版本的源代码、部署说明及必要配置指导。 1.2 **交付方式**(勾选适用项): diff --git a/nav_settings.py b/nav_settings.py index 1a7efe2..1b98b9f 100644 --- a/nav_settings.py +++ b/nav_settings.py @@ -19,6 +19,7 @@ NAV_TOGGLES: dict[str, str] = { } DEFAULT_NAV: dict[str, bool] = {k: True for k in NAV_TOGGLES} +DEFAULT_NAV["contract"] = False def get_nav_items(get_setting: Callable[[str, str], str]) -> dict[str, bool]: diff --git a/product_recommend.py b/product_recommend.py index c7be387..398ccf5 100644 --- a/product_recommend.py +++ b/product_recommend.py @@ -66,6 +66,8 @@ def assess_product_for_capital( "name": name, "exchange": exchange, "category": category, + "mult": mult, + "tick_size": tick, "status": "no_price", "status_label": "暂无行情", "min_capital_one_lot": None, @@ -153,11 +155,14 @@ def list_product_recommendations( return row except Exception as exc: logger.warning("recommend product failed %s: %s", ths, exc) + spec = get_contract_spec(ths + "8888") return { "ths": ths, "name": product.get("name") or ths, "exchange": product.get("exchange") or "", "category": product.get("category") or product_category(ths), + "mult": spec["mult"], + "tick_size": float(spec.get("tick_size") or 1.0), "status": "no_price", "status_label": "计算失败", "main_code": "", diff --git a/recommend_store.py b/recommend_store.py index 3438e8b..14dde65 100644 --- a/recommend_store.py +++ b/recommend_store.py @@ -12,6 +12,7 @@ import math from datetime import datetime from typing import Callable, Optional +from contract_specs import get_contract_spec from fee_specs import ensure_fee_rates_schema from product_recommend import _attach_turnover, list_product_recommendations from recommend_trend import sort_recommend_by_trend @@ -71,6 +72,12 @@ def rows_missing_turnover(rows: list[dict]) -> bool: return any("turnover" not in r for r in rows) +def rows_missing_contract_spec(rows: list[dict]) -> bool: + if not rows: + return False + return any("mult" not in r or "tick_size" not in r for r in rows) + + def recommend_cache_needs_refresh( cached: dict, *, @@ -90,6 +97,8 @@ def recommend_cache_needs_refresh( return True if rows_missing_turnover(rows): return True + if rows_missing_contract_spec(rows): + return True if float(capital or 0) > 0 and not rows: return True return False @@ -107,23 +116,43 @@ def enrich_recommend_rows( pct = max(1.0, min(100.0, float(max_margin_pct or 30.0))) budget = cap * pct / 100.0 if cap > 0 else 0.0 ctp_connected = False + ctp_lookup_spec = None + ctp_estimate_margin_one_lot_fn = None try: - from vnpy_bridge import ctp_estimate_margin_one_lot, ctp_status + from vnpy_bridge import ctp_estimate_margin_one_lot, ctp_lookup_contract_spec, ctp_status ctp_connected = bool(ctp_status(trading_mode).get("connected")) + ctp_lookup_spec = ctp_lookup_contract_spec + ctp_estimate_margin_one_lot_fn = ctp_estimate_margin_one_lot except Exception: pass enriched: list[dict] = [] for raw in rows: row = dict(raw) + ths = (row.get("ths") or "").strip() + main_code = (row.get("main_code") or "").strip() + spec_code = main_code or (ths + "8888" if ths else "") + if spec_code: + spec = get_contract_spec(spec_code) + if row.get("mult") in (None, ""): + row["mult"] = spec["mult"] + if row.get("tick_size") in (None, ""): + row["tick_size"] = float(spec.get("tick_size") or 1.0) + if ctp_connected and main_code and ctp_lookup_spec: + ctp_spec = ctp_lookup_spec(trading_mode, main_code) + if ctp_spec: + if ctp_spec.get("mult"): + row["mult"] = ctp_spec["mult"] + if ctp_spec.get("tick_size"): + row["tick_size"] = ctp_spec["tick_size"] + row["spec_source"] = "ctp" margin_one = 0.0 try: margin_one = float(row.get("margin_one_lot") or 0) except (TypeError, ValueError): margin_one = 0.0 price = float(row.get("price") or 0) - main_code = (row.get("main_code") or "").strip() - if ctp_connected and main_code and price > 0: - ctp_margin = ctp_estimate_margin_one_lot(trading_mode, main_code, price) + if ctp_connected and main_code and price > 0 and ctp_estimate_margin_one_lot_fn: + ctp_margin = ctp_estimate_margin_one_lot_fn(trading_mode, main_code, price) if ctp_margin and ctp_margin > 0: margin_one = ctp_margin row["margin_one_lot"] = ctp_margin diff --git a/static/css/responsive.css b/static/css/responsive.css index 59475b3..b004e15 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -245,8 +245,8 @@ body { } .site-title-sub { - font-size: .58rem; - letter-spacing: .14em; + font-size: .68rem; + letter-spacing: .04em; } .site-nav { diff --git a/static/css/tech.css b/static/css/tech.css index f0333f1..dc3eb8c 100644 --- a/static/css/tech.css +++ b/static/css/tech.css @@ -73,10 +73,11 @@ filter:drop-shadow(0 0 24px var(--title-glow)); } .site-title-sub{ - display:block;font-size:.72rem;font-weight:500; - letter-spacing:.22em;text-transform:uppercase; - color:var(--text-muted);margin-top:.35rem; + display:block;font-size:.78rem;font-weight:400; + letter-spacing:.06em;text-transform:none; + color:var(--text-muted);margin-top:.4rem; -webkit-text-fill-color:var(--text-muted); + filter:none; } .site-nav a{ diff --git a/static/css/trade.css b/static/css/trade.css index da67f57..e1c1d07 100644 --- a/static/css/trade.css +++ b/static/css/trade.css @@ -63,6 +63,8 @@ } .rec-sort-dir-btn:hover{border-color:var(--accent);color:var(--accent)} .gap-badge{font-size:.72rem} +.rec-market-link{color:inherit;text-decoration:none;display:inline-flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .35rem} +.rec-market-link:hover strong,.rec-market-link:hover .text-accent{color:var(--accent);text-decoration:underline} .rec-change-up{color:var(--profit)} .rec-change-down{color:var(--loss)} #recommend .trade-table-wrap{max-height:min(70vh,520px)} diff --git a/static/js/trade.js b/static/js/trade.js index 25383f0..049e379 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -40,7 +40,8 @@ var recIndustryFilter = ''; var REC_SORT_CACHE = 'qihuo_rec_sort_v2'; var REC_INDUSTRY_CACHE = 'qihuo_rec_industry_v1'; - var REC_COLSPAN = 16; + var REC_COLSPAN = 18; + var marketNavEnabled = !!window.MARKET_NAV_ENABLED; var productCategories = window.PRODUCT_CATEGORIES || []; var POS_CACHE_KEY = 'qihuo_trading_live_v3'; @@ -1274,6 +1275,35 @@ return '' + label + ''; } + function fmtRecNum(v) { + if (v == null || v === '') return '—'; + var n = Number(v); + if (!isFinite(n)) return '—'; + return String(n); + } + + function recSpecSuffix(r) { + return r.spec_source === 'ctp' ? ' (柜台)' : ''; + } + + function recSymbolCellHtml(r) { + var code = r.main_code || r.ths || ''; + var nameCls = r.trend_transition ? ' class="trend-name"' : ''; + var name = r.name || ''; + if (marketNavEnabled && r.main_code) { + var href = '/market?symbol=' + encodeURIComponent(r.main_code); + return ( + '