From bd7f0da1ca88d2a020d3cec56e0b1b1ca73af060 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 15 Jun 2026 11:10:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E8=8A=B1=E9=A1=BA=E5=90=88=E7=BA=A6?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=98=A0=E5=B0=84=E4=B8=8E/root=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- README.md | 56 ++++---- app.py | 59 ++++---- deploy.sh | 54 ++++--- ecosystem.config.cjs | 8 +- static/js/symbol.js | 32 +++-- symbols.py | 329 +++++++++++++++++++++++++++++++++---------- templates/keys.html | 5 +- templates/plans.html | 5 +- 8 files changed, 370 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index b5910dc..72932c7 100644 --- a/README.md +++ b/README.md @@ -12,53 +12,55 @@ | **统计分析** | 总交易、胜率,按品种/类型/方向统计 | | **系统设置** | 修改密码、配置企业微信 Webhook | -## 品种输入 +## 品种与合约代码(同花顺格式) -在各表单中输入中文品种名(如「白银」「螺纹钢」),自动联想新浪行情代码及主力合约标识。 +输入中文品种名(如「白银」「螺纹钢」)或同花顺合约代码(如 `ag2606`、`SR609`、`IF2606`),系统自动匹配**当前主力月份合约**。 + +| 交易所 | 同花顺示例 | 说明 | +|--------|-----------|------| +| 上期所 / 大商所 / 上期能源 | `ag2606`、`rb2605`、`m2609` | 小写品种 + 4 位年月 | +| 郑商所 | `SR609`、`MA606` | 大写品种 + 3 位年月 | +| 中金所 | `IF2606`、`IH2606` | 大写品种 + 4 位年月 | + +界面展示同花顺代码;行情在后台通过新浪 API 拉取(内部自动转换,无需手动填写新浪代码)。 + +## 快速部署(Ubuntu root + /root/qihuo) + +```bash +# 以 root 登录后,在项目目录执行 +bash deploy.sh +``` + +默认安装路径:`/root/qihuo`,服务端口:`6600`。 + +部署完成后访问:`http://服务器IP:6600` ## 环境要求 - Ubuntu 20.04+(推荐) +- **root 用户**运行(部署目录 `/root/qihuo`) - Python 3.10+ -- Node.js(仅 PM2 进程守护需要) +- Node.js + PM2(进程守护) - 网络可访问 `hq.sinajs.cn`(行情)及企业微信 API -## 快速部署(Ubuntu + /opt) - -```bash -# 克隆仓库后,在项目目录执行一键部署 -sudo bash deploy.sh -``` - -默认安装路径:`/opt/qihuo`,服务端口:`6600`。 - -部署完成后访问:`http://服务器IP:6600` - ## 手动部署 ### 1. 安装系统依赖 ```bash -sudo apt update -sudo apt install -y python3 python3-venv python3-pip git -# PM2(进程守护) -curl -fsSL https://get.pnpm.io/install.sh | sh - # 或直接用 npm -sudo npm install -g pm2 +apt update +apt install -y python3 python3-venv python3-pip git nodejs npm +npm install -g pm2 ``` -### 2. 克隆到 /opt +### 2. 克隆到 /root/qihuo ```bash -sudo mkdir -p /opt -sudo git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo -sudo chown -R $USER:$USER /opt/qihuo -cd /opt/qihuo +git clone https://git.bz121.com/dekun/qihuo.git /root/qihuo +cd /root/qihuo ``` ### 3. 虚拟环境与依赖 - -```bash -python3 -m venv venv source venv/bin/activate pip install -r requirements.txt ``` diff --git a/app.py b/app.py index 0d6db03..79d4156 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,7 @@ from flask import ( ) from werkzeug.security import check_password_hash, generate_password_hash -from symbols import search_symbols, get_by_code +from symbols import search_symbols, get_price, ths_to_sina_code load_dotenv() @@ -82,6 +82,9 @@ def init_db(): "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 key_monitors ADD COLUMN sina_code TEXT", + "ALTER TABLE trade_records ADD COLUMN sina_code TEXT", ] for sql in migrations: try: @@ -118,20 +121,20 @@ def send_wechat_msg(content: str): # —————————————— 行情 —————————————— -def get_price(symbol: str) -> Optional[float]: - try: - url = f"https://hq.sinajs.cn/list={symbol}" - headers = {"Referer": "https://finance.sina.com.cn"} - resp = requests.get(url, headers=headers, timeout=5) - text = resp.text - if "=" not in text: - return None - data = text.split("=")[1].strip().strip('"').split(",") - if len(data) < 9: - return None - return float(data[8]) - except Exception: +def resolve_sina_code(ths_code: str, sina_code: str = "") -> Optional[str]: + """同花顺代码 -> 新浪行情代码;兼容旧数据中的新浪格式。""" + if sina_code: + return sina_code + if ths_code.startswith("nf_") or ths_code.startswith("CFF_RE_"): + return ths_code + return ths_to_sina_code(ths_code) + + +def fetch_price(ths_code: str, sina_code: str = "") -> Optional[float]: + code = resolve_sina_code(ths_code, sina_code) + if not code: return None + return get_price(code) # —————————————— 监控逻辑 —————————————— @@ -143,7 +146,8 @@ def check_order_plans(): for r in rows: sym = r["symbol"] - p = get_price(sym) + sina = r["sina_code"] if "sina_code" in r.keys() else "" + p = fetch_price(sym, sina) if not p: continue @@ -224,8 +228,9 @@ def check_key_monitors(): low_trig = r["lower_triggered"] name = r["symbol_name"] or sym pid = r["id"] + sina = r["sina_code"] if "sina_code" in r.keys() else "" - p = get_price(sym) + p = fetch_price(sym, sina) if not p: continue @@ -332,19 +337,20 @@ def add_plan(): direction = d.get("direction") symbol = d.get("symbol", "").strip() symbol_name = d.get("symbol_name", "").strip() + sina_code = d.get("sina_code", "").strip() if not direction: flash("请选择多空方向") return redirect(url_for("plans")) - if not symbol: - flash("请选择有效品种") + if not symbol or not sina_code: + flash("请从下拉列表选择品种(同花顺合约代码)") return redirect(url_for("plans")) conn = get_db() conn.execute( """INSERT INTO order_plans - (symbol, symbol_name, direction, zone_upper, zone_lower, stop_loss, take_profit) - VALUES (?,?,?,?,?,?,?)""", + (symbol, symbol_name, sina_code, direction, zone_upper, zone_lower, stop_loss, take_profit) + VALUES (?,?,?,?,?,?,?,?)""", ( - symbol, symbol_name, direction, + symbol, symbol_name, sina_code, direction, float(d["zone_upper"]), float(d["zone_lower"]), float(d["stop_loss"]), float(d["take_profit"]), ), @@ -382,18 +388,19 @@ def add_key(): direction = d.get("direction") symbol = d.get("symbol", "").strip() symbol_name = d.get("symbol_name", "").strip() + sina_code = d.get("sina_code", "").strip() if not direction: flash("请选择多空方向") return redirect(url_for("keys")) - if not symbol: - flash("请选择有效品种") + if not symbol or not sina_code: + flash("请从下拉列表选择品种(同花顺合约代码)") return redirect(url_for("keys")) conn = get_db() conn.execute( """INSERT INTO key_monitors - (symbol, symbol_name, monitor_type, direction, upper, lower) - VALUES (?,?,?,?,?,?)""", - (symbol, symbol_name, d["type"], direction, float(d["upper"]), float(d["lower"])), + (symbol, symbol_name, sina_code, monitor_type, direction, upper, lower) + VALUES (?,?,?,?,?,?,?)""", + (symbol, symbol_name, sina_code, d["type"], direction, float(d["upper"]), float(d["lower"])), ) conn.commit() conn.close() diff --git a/deploy.sh b/deploy.sh index 687db1b..4d39c31 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,40 +1,42 @@ #!/usr/bin/env bash -# 国内期货监控系统 - Ubuntu 一键部署脚本 -# 安装路径: /opt/qihuo 端口: 6600 进程守护: PM2 +# 国内期货监控系统 - Ubuntu 一键部署(root 用户,/root/qihuo) +# 端口: 6600 进程守护: PM2 set -euo pipefail -APP_DIR="/opt/qihuo" +APP_DIR="/root/qihuo" REPO_URL="https://git.bz121.com/dekun/qihuo.git" SERVICE_NAME="qihuo" -echo "==> 检查系统依赖..." +if [ "$(id -u)" -ne 0 ]; then + echo "请使用 root 用户运行: sudo bash deploy.sh" + exit 1 +fi -install_pkg() { +echo "==> 检查系统依赖..." +apt-get update -qq + +need_install() { if ! command -v "$1" &>/dev/null; then - echo "安装 $1..." - sudo apt-get install -y "$2" + apt-get install -y "$2" fi } -sudo apt-get update -qq -install_pkg python3 python3 -install_pkg python3-venv python3-venv -install_pkg git git +need_install python3 python3 +need_install python3-venv python3-venv +need_install git git if ! command -v pm2 &>/dev/null; then echo "==> 安装 PM2..." - if command -v npm &>/dev/null; then - sudo npm install -g pm2 - else - install_pkg nodejs nodejs - install_pkg npm npm - sudo npm install -g pm2 + if ! command -v npm &>/dev/null; then + need_install nodejs nodejs + need_install npm npm fi + npm install -g pm2 fi echo "==> 准备应用目录 ${APP_DIR}..." -sudo mkdir -p "$(dirname "$APP_DIR")" +mkdir -p "$(dirname "$APP_DIR")" if [ -d "$APP_DIR/.git" ]; then echo "==> 更新已有仓库..." @@ -45,16 +47,10 @@ else echo "目录 ${APP_DIR} 已存在且非 git 仓库,请手动处理后重试" exit 1 fi - echo "==> 克隆仓库..." - sudo git clone "$REPO_URL" "$APP_DIR" + git clone "$REPO_URL" "$APP_DIR" cd "$APP_DIR" fi -# 确保当前用户可写(若以 root 克隆则 chown) -if [ "$(id -u)" -ne 0 ]; then - sudo chown -R "$(whoami):$(whoami)" "$APP_DIR" -fi - echo "==> 创建 Python 虚拟环境..." python3 -m venv "$APP_DIR/venv" source "$APP_DIR/venv/bin/activate" @@ -62,17 +58,16 @@ pip install --upgrade pip -q pip install -r "$APP_DIR/requirements.txt" -q if [ ! -f "$APP_DIR/.env" ]; then - echo "==> 生成 .env(请稍后编辑 SECRET_KEY 和密码)..." + echo "==> 生成 .env(请编辑 ADMIN_PASSWORD 后重启)..." cp "$APP_DIR/.env.example" "$APP_DIR/.env" - # 生成随机 SECRET_KEY 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" - echo "已创建 $APP_DIR/.env ,请编辑 ADMIN_PASSWORD 后重启服务" fi +mkdir -p "$APP_DIR/logs" + echo "==> PM2 启动/重启服务..." cd "$APP_DIR" -mkdir -p "$APP_DIR/logs" pm2 delete "$SERVICE_NAME" 2>/dev/null || true pm2 start ecosystem.config.cjs pm2 save @@ -81,6 +76,7 @@ echo "" echo "==========================================" echo " 部署完成" echo " 目录: ${APP_DIR}" +echo " 用户: root" echo " 端口: 6600" echo " 访问: http://<服务器IP>:6600" echo " 日志: pm2 logs ${SERVICE_NAME}" diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 8f26aeb..dcaa241 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -3,8 +3,8 @@ module.exports = { { name: "qihuo", script: "app.py", - cwd: "/opt/qihuo", - interpreter: "/opt/qihuo/venv/bin/python", + cwd: "/root/qihuo", + interpreter: "/root/qihuo/venv/bin/python", instances: 1, autorestart: true, watch: false, @@ -12,8 +12,8 @@ module.exports = { env: { NODE_ENV: "production", }, - error_file: "/opt/qihuo/logs/pm2-error.log", - out_file: "/opt/qihuo/logs/pm2-out.log", + error_file: "/root/qihuo/logs/pm2-error.log", + out_file: "/root/qihuo/logs/pm2-out.log", time: true, }, ], diff --git a/static/js/symbol.js b/static/js/symbol.js index 0178b0e..89c0309 100644 --- a/static/js/symbol.js +++ b/static/js/symbol.js @@ -1,8 +1,9 @@ (function () { function initSymbolInput(wrapper) { const input = wrapper.querySelector('.symbol-input'); - const hiddenCode = wrapper.querySelector('input[name="symbol"]'); + const hiddenThs = wrapper.querySelector('input[name="symbol"]'); const hiddenName = wrapper.querySelector('input[name="symbol_name"]'); + const hiddenSina = wrapper.querySelector('input[name="sina_code"]'); const dropdown = wrapper.querySelector('.symbol-dropdown'); const selectedEl = wrapper.querySelector('.symbol-selected'); let timer = null; @@ -13,21 +14,23 @@ function selectItem(item) { input.value = item.name; - hiddenCode.value = item.code; + hiddenThs.value = item.ths_code; hiddenName.value = item.name; - selectedEl.textContent = item.display + ' | ' + item.code; + if (hiddenSina) hiddenSina.value = item.sina_code; + selectedEl.textContent = '同花顺: ' + item.ths_code + ' | 主力 ' + (item.contract || item.ths_code); hideDropdown(); } function renderItems(items) { dropdown.innerHTML = ''; if (!items.length) { - dropdown.innerHTML = '
无匹配品种
'; + dropdown.innerHTML = '
无匹配品种,可输入同花顺合约如 ag2606
'; } else { items.forEach(function (item) { const div = document.createElement('div'); div.className = 'symbol-option'; - div.innerHTML = item.display + '
' + item.code + ' · ' + item.exchange + '
'; + div.innerHTML = item.display + + '
同花顺 ' + item.ths_code + ' · ' + item.exchange + '
'; div.addEventListener('mousedown', function (e) { e.preventDefault(); selectItem(item); @@ -39,8 +42,9 @@ } input.addEventListener('input', function () { - hiddenCode.value = ''; + hiddenThs.value = ''; hiddenName.value = ''; + if (hiddenSina) hiddenSina.value = ''; selectedEl.textContent = ''; const q = input.value.trim(); if (!q) { @@ -53,7 +57,7 @@ .then(function (r) { return r.json(); }) .then(renderItems) .catch(function () { hideDropdown(); }); - }, 200); + }, 300); }); input.addEventListener('blur', function () { @@ -62,7 +66,7 @@ input.addEventListener('focus', function () { const q = input.value.trim(); - if (q && !hiddenCode.value) { + if (q && !hiddenThs.value) { fetch('/api/symbols/search?q=' + encodeURIComponent(q)) .then(function (r) { return r.json(); }) .then(renderItems); @@ -76,10 +80,16 @@ document.querySelectorAll('form').forEach(function (form) { if (!form.querySelector('.symbol-wrap')) return; form.addEventListener('submit', function (e) { - const hidden = form.querySelector('input[name="symbol"]'); - if (hidden && !hidden.value.trim()) { + const ths = form.querySelector('input[name="symbol"]'); + const sina = form.querySelector('input[name="sina_code"]'); + if (ths && !ths.value.trim()) { e.preventDefault(); - alert('请从下拉列表中选择品种'); + alert('请从下拉列表选择品种'); + return; + } + if (sina && !sina.value.trim()) { + e.preventDefault(); + alert('请从下拉列表选择品种(需含同花顺合约代码)'); } }); }); diff --git a/symbols.py b/symbols.py index 553f05e..e8e937b 100644 --- a/symbols.py +++ b/symbols.py @@ -1,95 +1,270 @@ -# 国内期货品种映射:中文名 -> 新浪行情代码(0 表示主力连续) -SYMBOLS = [ - {"name": "白银", "code": "nf_AG0", "exchange": "上期所"}, - {"name": "黄金", "code": "nf_AU0", "exchange": "上期所"}, - {"name": "铜", "code": "nf_CU0", "exchange": "上期所"}, - {"name": "铝", "code": "nf_AL0", "exchange": "上期所"}, - {"name": "锌", "code": "nf_ZN0", "exchange": "上期所"}, - {"name": "铅", "code": "nf_PB0", "exchange": "上期所"}, - {"name": "镍", "code": "nf_NI0", "exchange": "上期所"}, - {"name": "锡", "code": "nf_SN0", "exchange": "上期所"}, - {"name": "螺纹钢", "code": "nf_RB0", "exchange": "上期所"}, - {"name": "热卷", "code": "nf_HC0", "exchange": "上期所"}, - {"name": "不锈钢", "code": "nf_SS0", "exchange": "上期所"}, - {"name": "原油", "code": "nf_SC0", "exchange": "上期所"}, - {"name": "燃油", "code": "nf_FU0", "exchange": "上期所"}, - {"name": "沥青", "code": "nf_BU0", "exchange": "上期所"}, - {"name": "橡胶", "code": "nf_RU0", "exchange": "上期所"}, - {"name": "纸浆", "code": "nf_SP0", "exchange": "上期所"}, - {"name": "铁矿石", "code": "nf_I0", "exchange": "大商所"}, - {"name": "焦炭", "code": "nf_J0", "exchange": "大商所"}, - {"name": "焦煤", "code": "nf_JM0", "exchange": "大商所"}, - {"name": "豆粕", "code": "nf_M0", "exchange": "大商所"}, - {"name": "豆油", "code": "nf_Y0", "exchange": "大商所"}, - {"name": "棕榈油", "code": "nf_P0", "exchange": "大商所"}, - {"name": "玉米", "code": "nf_C0", "exchange": "大商所"}, - {"name": "淀粉", "code": "nf_CS0", "exchange": "大商所"}, - {"name": "鸡蛋", "code": "nf_JD0", "exchange": "大商所"}, - {"name": "生猪", "code": "nf_LH0", "exchange": "大商所"}, - {"name": "聚乙烯", "code": "nf_L0", "exchange": "大商所"}, - {"name": "聚丙烯", "code": "nf_PP0", "exchange": "大商所"}, - {"name": "PVC", "code": "nf_V0", "exchange": "大商所"}, - {"name": "乙二醇", "code": "nf_EG0", "exchange": "大商所"}, - {"name": "苯乙烯", "code": "nf_EB0", "exchange": "大商所"}, - {"name": "液化气", "code": "nf_PG0", "exchange": "大商所"}, - {"name": "菜粕", "code": "nf_RM0", "exchange": "郑商所"}, - {"name": "菜油", "code": "nf_OI0", "exchange": "郑商所"}, - {"name": "白糖", "code": "nf_SR0", "exchange": "郑商所"}, - {"name": "棉花", "code": "nf_CF0", "exchange": "郑商所"}, - {"name": "甲醇", "code": "nf_MA0", "exchange": "郑商所"}, - {"name": "PTA", "code": "nf_TA0", "exchange": "郑商所"}, - {"name": "玻璃", "code": "nf_FG0", "exchange": "郑商所"}, - {"name": "纯碱", "code": "nf_SA0", "exchange": "郑商所"}, - {"name": "尿素", "code": "nf_UR0", "exchange": "郑商所"}, - {"name": "硅铁", "code": "nf_SF0", "exchange": "郑商所"}, - {"name": "锰硅", "code": "nf_SM0", "exchange": "郑商所"}, - {"name": "苹果", "code": "nf_AP0", "exchange": "郑商所"}, - {"name": "红枣", "code": "nf_CJ0", "exchange": "郑商所"}, - {"name": "花生", "code": "nf_PK0", "exchange": "郑商所"}, - {"name": "沪深300", "code": "CFF_RE_IF0", "exchange": "中金所"}, - {"name": "上证50", "code": "CFF_RE_IH0", "exchange": "中金所"}, - {"name": "中证500", "code": "CFF_RE_IC0", "exchange": "中金所"}, - {"name": "中证1000", "code": "CFF_RE_IM0", "exchange": "中金所"}, +""" +期货品种与同花顺代码映射。 +界面展示同花顺格式(如 ag2606、SR609、IF2606),行情通过新浪 API(内部 sina_code)获取。 +""" +import re +import time +from datetime import date +from typing import Optional + +import requests + +# 品种字母:ths=同花顺展示用,sina=新浪 nf_ 前缀后字母(通常大写) +PRODUCTS = [ + {"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"}, + {"name": "黄金", "ths": "au", "sina": "AU", "exchange": "上期所", "ex": "SHFE"}, + {"name": "铜", "ths": "cu", "sina": "CU", "exchange": "上期所", "ex": "SHFE"}, + {"name": "铝", "ths": "al", "sina": "AL", "exchange": "上期所", "ex": "SHFE"}, + {"name": "锌", "ths": "zn", "sina": "ZN", "exchange": "上期所", "ex": "SHFE"}, + {"name": "铅", "ths": "pb", "sina": "PB", "exchange": "上期所", "ex": "SHFE"}, + {"name": "镍", "ths": "ni", "sina": "NI", "exchange": "上期所", "ex": "SHFE"}, + {"name": "锡", "ths": "sn", "sina": "SN", "exchange": "上期所", "ex": "SHFE"}, + {"name": "螺纹钢", "ths": "rb", "sina": "RB", "exchange": "上期所", "ex": "SHFE"}, + {"name": "热卷", "ths": "hc", "sina": "HC", "exchange": "上期所", "ex": "SHFE"}, + {"name": "不锈钢", "ths": "ss", "sina": "SS", "exchange": "上期所", "ex": "SHFE"}, + {"name": "原油", "ths": "sc", "sina": "SC", "exchange": "上期能源", "ex": "INE"}, + {"name": "燃油", "ths": "fu", "sina": "FU", "exchange": "上期所", "ex": "SHFE"}, + {"name": "沥青", "ths": "bu", "sina": "BU", "exchange": "上期所", "ex": "SHFE"}, + {"name": "橡胶", "ths": "ru", "sina": "RU", "exchange": "上期所", "ex": "SHFE"}, + {"name": "纸浆", "ths": "sp", "sina": "SP", "exchange": "上期所", "ex": "SHFE"}, + {"name": "铁矿石", "ths": "i", "sina": "I", "exchange": "大商所", "ex": "DCE"}, + {"name": "焦炭", "ths": "j", "sina": "J", "exchange": "大商所", "ex": "DCE"}, + {"name": "焦煤", "ths": "jm", "sina": "JM", "exchange": "大商所", "ex": "DCE"}, + {"name": "豆粕", "ths": "m", "sina": "M", "exchange": "大商所", "ex": "DCE"}, + {"name": "豆油", "ths": "y", "sina": "Y", "exchange": "大商所", "ex": "DCE"}, + {"name": "棕榈油", "ths": "p", "sina": "P", "exchange": "大商所", "ex": "DCE"}, + {"name": "玉米", "ths": "c", "sina": "C", "exchange": "大商所", "ex": "DCE"}, + {"name": "淀粉", "ths": "cs", "sina": "CS", "exchange": "大商所", "ex": "DCE"}, + {"name": "鸡蛋", "ths": "jd", "sina": "JD", "exchange": "大商所", "ex": "DCE"}, + {"name": "生猪", "ths": "lh", "sina": "LH", "exchange": "大商所", "ex": "DCE"}, + {"name": "聚乙烯", "ths": "l", "sina": "L", "exchange": "大商所", "ex": "DCE"}, + {"name": "聚丙烯", "ths": "pp", "sina": "PP", "exchange": "大商所", "ex": "DCE"}, + {"name": "PVC", "ths": "v", "sina": "V", "exchange": "大商所", "ex": "DCE"}, + {"name": "乙二醇", "ths": "eg", "sina": "EG", "exchange": "大商所", "ex": "DCE"}, + {"name": "苯乙烯", "ths": "eb", "sina": "EB", "exchange": "大商所", "ex": "DCE"}, + {"name": "液化气", "ths": "pg", "sina": "PG", "exchange": "大商所", "ex": "DCE"}, + {"name": "菜粕", "ths": "RM", "sina": "RM", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "菜油", "ths": "OI", "sina": "OI", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "白糖", "ths": "SR", "sina": "SR", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "棉花", "ths": "CF", "sina": "CF", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "甲醇", "ths": "MA", "sina": "MA", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "PTA", "ths": "TA", "sina": "TA", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "玻璃", "ths": "FG", "sina": "FG", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "纯碱", "ths": "SA", "sina": "SA", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "尿素", "ths": "UR", "sina": "UR", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "硅铁", "ths": "SF", "sina": "SF", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "锰硅", "ths": "SM", "sina": "SM", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "苹果", "ths": "AP", "sina": "AP", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "红枣", "ths": "CJ", "sina": "CJ", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "花生", "ths": "PK", "sina": "PK", "exchange": "郑商所", "ex": "CZCE"}, + {"name": "沪深300", "ths": "IF", "sina": "IF", "exchange": "中金所", "ex": "CFFEX"}, + {"name": "上证50", "ths": "IH", "sina": "IH", "exchange": "中金所", "ex": "CFFEX"}, + {"name": "中证500", "ths": "IC", "sina": "IC", "exchange": "中金所", "ex": "CFFEX"}, + {"name": "中证1000", "ths": "IM", "sina": "IM", "exchange": "中金所", "ex": "CFFEX"}, ] +_MAIN_CACHE: dict[str, tuple[float, dict]] = {} +_CACHE_TTL = 300 # 5 分钟 -def _contract_label(code: str) -> str: - """从新浪代码提取品种字母部分,用于展示主力合约标识。""" - raw = code.replace("nf_", "").replace("CFF_RE_", "") - letters = "".join(c for c in raw if c.isalpha()) - return f"{letters}主力" + +def _sina_headers() -> dict: + return {"Referer": "https://finance.sina.com.cn"} + + +def _fetch_sina_raw(sina_code: str) -> Optional[dict]: + try: + url = f"https://hq.sinajs.cn/list={sina_code}" + resp = requests.get(url, headers=_sina_headers(), timeout=5) + resp.encoding = "gbk" + if '"' not in resp.text: + return None + body = resp.text.split('"')[1] + if not body: + return None + parts = body.split(",") + if len(parts) < 9: + return None + price = float(parts[8]) + volume = float(parts[14]) if len(parts) > 14 and parts[14] else 0 + return {"name": parts[0], "price": price, "volume": volume, "parts": parts} + except Exception: + return None + + +def build_ths_code(product: dict, year: int, month: int) -> str: + """同花顺合约代码。""" + ex = product["ex"] + letters = product["ths"] + if ex == "CZCE": + return f"{letters}{year % 10}{month:02d}" + return f"{letters}{year % 100:02d}{month:02d}" + + +def build_sina_code(product: dict, year: int, month: int) -> str: + """新浪行情代码(用于拉价)。""" + ex = product["ex"] + letters = product["sina"] + suffix = f"{year % 100:02d}{month:02d}" + if ex == "CFFEX": + return f"CFF_RE_{letters}{suffix}" + return f"nf_{letters}{suffix}" + + +def build_sina_main_code(product: dict) -> str: + """新浪主力连续代码。""" + ex = product["ex"] + letters = product["sina"] + if ex == "CFFEX": + return f"CFF_RE_{letters}0" + return f"nf_{letters}0" + + +def ths_to_sina_code(ths_code: str) -> Optional[str]: + """将同花顺代码转为新浪代码(用户直接输入合约时使用)。""" + code = ths_code.strip() + if not code: + return None + + # CFFEX / 四位月份: IF2606, ag2606 + m = re.match(r"^([A-Za-z]+)(\d{4})$", code) + if m: + letters, digits = m.group(1), m.group(2) + letters_up = letters.upper() + for p in PRODUCTS: + if p["ths"].upper() == letters_up or p["sina"] == letters_up: + year = 2000 + int(digits[:2]) + month = int(digits[2:]) + if 1 <= month <= 12: + return build_sina_code(p, year, month) + if letters_up in ("IF", "IH", "IC", "IM", "T", "TF", "TS"): + return f"CFF_RE_{letters_up}{digits}" + + # CZCE 3-digit: SR609 + m3 = re.match(r"^([A-Za-z]+)(\d{3})$", code) + if m3: + letters, digits = m3.group(1), m3.group(2) + letters_up = letters.upper() + y_digit = int(digits[0]) + month = int(digits[1:]) + if 1 <= month <= 12: + year = date.today().year + decade = year // 10 * 10 + candidate = decade + y_digit + if candidate < year - 1: + candidate += 10 + for p in PRODUCTS: + if p["ths"].upper() == letters_up or p["sina"] == letters_up: + return build_sina_code(p, candidate, month) + + return None + + +def resolve_main_contract(product: dict) -> Optional[dict]: + """按成交量选取当前主力月份合约。""" + cache_key = product["sina"] + now = time.time() + cached = _MAIN_CACHE.get(cache_key) + if cached and now - cached[0] < _CACHE_TTL: + return cached[1] + + today = date.today() + y, m = today.year, today.month + best = None + + for i in range(14): + cy, cm = y, m + i + while cm > 12: + cm -= 12 + cy += 1 + sina = build_sina_code(product, cy, cm) + raw = _fetch_sina_raw(sina) + if raw and raw["volume"] > 0: + ths = build_ths_code(product, cy, cm) + item = { + "name": product["name"], + "ths_code": ths, + "sina_code": sina, + "exchange": product["exchange"], + "contract": f"主力 {ths}", + "display": f"{product['name']} 主力 {ths}", + "volume": raw["volume"], + } + if best is None or raw["volume"] > best["volume"]: + best = item + + if best is None: + sina_main = build_sina_main_code(product) + raw = _fetch_sina_raw(sina_main) + if raw: + ths_letters = product["ths"] + ths_main = f"{ths_letters}888" if product["ex"] != "CFFEX" else f"{ths_letters.upper()}888" + best = { + "name": product["name"], + "ths_code": ths_main, + "sina_code": sina_main, + "exchange": product["exchange"], + "contract": f"主力连续 {ths_main}", + "display": f"{product['name']} 主力连续 {ths_main}", + "volume": raw.get("volume", 0), + } + + if best: + _MAIN_CACHE[cache_key] = (now, best) + return best def search_symbols(query: str) -> list: q = query.strip().lower() if not q: return [] + results = [] - for s in SYMBOLS: - name = s["name"] - code = s["code"] - contract = _contract_label(code) - if q in name.lower() or q in code.lower() or q in contract.lower(): - results.append({ - "name": name, - "code": code, - "exchange": s["exchange"], - "contract": contract, - "display": f"{name} ({contract})", - }) + for p in PRODUCTS: + name = p["name"] + if q not in name.lower() and q not in p["ths"].lower() and q not in p["sina"].lower(): + continue + main = resolve_main_contract(p) + if main: + results.append(main) + + # 用户直接输入同花顺合约代码 + if not results and len(q) >= 3: + sina = ths_to_sina_code(query.strip()) + if sina: + raw = _fetch_sina_raw(sina) + if raw: + results.append({ + "name": raw["name"], + "ths_code": query.strip(), + "sina_code": sina, + "exchange": "", + "contract": query.strip(), + "display": f"{raw['name']} ({query.strip()})", + "volume": raw.get("volume", 0), + }) + return results[:12] -from typing import Optional +def get_price(sina_code: str) -> Optional[float]: + raw = _fetch_sina_raw(sina_code) + return raw["price"] if raw else None -def get_by_code(code: str) -> Optional[dict]: - for s in SYMBOLS: - if s["code"] == code: +def get_by_ths_code(ths_code: str) -> Optional[dict]: + for p in PRODUCTS: + main = resolve_main_contract(p) + if main and main["ths_code"].lower() == ths_code.lower(): + return main + sina = ths_to_sina_code(ths_code) + if sina: + raw = _fetch_sina_raw(sina) + if raw: return { - "name": s["name"], - "code": s["code"], - "exchange": s["exchange"], - "contract": _contract_label(s["code"]), + "name": raw["name"], + "ths_code": ths_code, + "sina_code": sina, + "exchange": "", + "contract": ths_code, } return None diff --git a/templates/keys.html b/templates/keys.html index 3b81e15..993592f 100644 --- a/templates/keys.html +++ b/templates/keys.html @@ -7,9 +7,10 @@

新增监控

- + +
@@ -40,7 +41,7 @@ {{ '做多' if k.direction == 'long' else '做空' }}
上: {{ k.upper }} | 下: {{ k.lower }}
-
{{ k.symbol }}
+
同花顺: {{ k.symbol }}
删除 {% else %} diff --git a/templates/plans.html b/templates/plans.html index 5a68f13..20fdb50 100644 --- a/templates/plans.html +++ b/templates/plans.html @@ -7,9 +7,10 @@

新增计划

- + +
@@ -42,7 +43,7 @@
区间: {{ p.zone_lower }} ~ {{ p.zone_upper }}
止损: {{ p.stop_loss }} | 止盈: {{ p.take_profit }}
-
{{ p.symbol }}
+
同花顺: {{ p.symbol }}
删除 {% else %}