部署改回/opt;接入同花顺iFinD HTTP行情,新浪作回退
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,3 +12,9 @@ ADMIN_PASSWORD=change-me-on-first-login
|
|||||||
|
|
||||||
# 企业微信 Webhook(也可在系统设置页面修改)
|
# 企业微信 Webhook(也可在系统设置页面修改)
|
||||||
WECHAT_WEBHOOK=
|
WECHAT_WEBHOOK=
|
||||||
|
|
||||||
|
# 行情数据源: auto(优先同花顺,失败回退新浪)| ths | sina
|
||||||
|
QUOTE_SOURCE=auto
|
||||||
|
|
||||||
|
# 同花顺 iFinD HTTP refresh_token(也可在系统设置页面修改)
|
||||||
|
THS_REFRESH_TOKEN=
|
||||||
|
|||||||
@@ -22,23 +22,24 @@
|
|||||||
| 郑商所 | `SR609`、`MA606` | 大写品种 + 3 位年月 |
|
| 郑商所 | `SR609`、`MA606` | 大写品种 + 3 位年月 |
|
||||||
| 中金所 | `IF2606`、`IH2606` | 大写品种 + 4 位年月 |
|
| 中金所 | `IF2606`、`IH2606` | 大写品种 + 4 位年月 |
|
||||||
|
|
||||||
界面展示同花顺代码;行情在后台通过新浪 API 拉取(内部自动转换,无需手动填写新浪代码)。
|
界面展示同花顺代码;行情优先走**同花顺 iFinD HTTP API**,未配置或失败时自动回退新浪。
|
||||||
|
|
||||||
## 快速部署(Ubuntu root + /root/qihuo)
|
## 快速部署(Ubuntu root + /opt/qihuo)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 以 root 登录后,在项目目录执行
|
# root 登录后执行
|
||||||
|
cd /opt/qihuo # 或先 git clone 再 bash deploy.sh
|
||||||
bash deploy.sh
|
bash deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
默认安装路径:`/root/qihuo`,服务端口:`6600`。
|
默认安装路径:`/opt/qihuo`,服务端口:`6600`。
|
||||||
|
|
||||||
部署完成后访问:`http://服务器IP:6600`
|
部署完成后访问:`http://服务器IP:6600`
|
||||||
|
|
||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
- Ubuntu 20.04+(推荐)
|
- Ubuntu 20.04+(推荐)
|
||||||
- **root 用户**运行(部署目录 `/root/qihuo`)
|
- **root 用户**运行(部署目录 `/opt/qihuo`)
|
||||||
- Python 3.10+
|
- Python 3.10+
|
||||||
- Node.js + PM2(进程守护)
|
- Node.js + PM2(进程守护)
|
||||||
- 网络可访问 `hq.sinajs.cn`(行情)及企业微信 API
|
- 网络可访问 `hq.sinajs.cn`(行情)及企业微信 API
|
||||||
@@ -53,11 +54,11 @@ apt install -y python3 python3-venv python3-pip git nodejs npm
|
|||||||
npm install -g pm2
|
npm install -g pm2
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 克隆到 /root/qihuo
|
### 2. 克隆到 /opt/qihuo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.bz121.com/dekun/qihuo.git /root/qihuo
|
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
|
||||||
cd /root/qihuo
|
cd /opt/qihuo
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 虚拟环境与依赖
|
### 3. 虚拟环境与依赖
|
||||||
@@ -82,8 +83,12 @@ SECRET_KEY=随机长字符串
|
|||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=首次登录密码
|
ADMIN_PASSWORD=首次登录密码
|
||||||
WECHAT_WEBHOOK=企业微信机器人地址(可选,也可在页面配置)
|
WECHAT_WEBHOOK=企业微信机器人地址(可选,也可在页面配置)
|
||||||
|
QUOTE_SOURCE=auto
|
||||||
|
THS_REFRESH_TOKEN=同花顺 refresh_token(可选,也可在页面配置)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 同花顺行情需在 [同花顺数据接口](https://quantapi.10jqka.com.cn/) 申请 iFinD HTTP 权限,在「超级命令 → 工具」获取 `refresh_token`。未配置时自动使用新浪行情。
|
||||||
|
|
||||||
> 管理员密码首次从 `.env` 写入数据库并哈希存储,之后请在「系统设置」中修改。
|
> 管理员密码首次从 `.env` 写入数据库并哈希存储,之后请在「系统设置」中修改。
|
||||||
|
|
||||||
### 5. PM2 启动
|
### 5. PM2 启动
|
||||||
@@ -118,7 +123,8 @@ python app.py
|
|||||||
```
|
```
|
||||||
qihuo/
|
qihuo/
|
||||||
├── app.py # 主程序
|
├── app.py # 主程序
|
||||||
├── symbols.py # 期货品种映射
|
├── market.py # 同花顺/新浪行情拉取
|
||||||
|
├── symbols.py # 期货品种与同花顺代码映射
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── .env.example
|
├── .env.example
|
||||||
├── deploy.sh # Ubuntu 一键部署
|
├── deploy.sh # Ubuntu 一键部署
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from symbols import search_symbols, get_price, ths_to_sina_code
|
from symbols import search_symbols, ths_to_codes
|
||||||
|
from market import get_price as market_get_price, set_ths_refresh_token
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -83,8 +84,9 @@ def init_db():
|
|||||||
"ALTER TABLE key_monitors ADD COLUMN lower_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 trade_records ADD COLUMN symbol_name TEXT",
|
||||||
"ALTER TABLE order_plans ADD COLUMN sina_code TEXT",
|
"ALTER TABLE order_plans ADD COLUMN sina_code TEXT",
|
||||||
"ALTER TABLE key_monitors ADD COLUMN sina_code TEXT",
|
"ALTER TABLE order_plans ADD COLUMN market_code TEXT",
|
||||||
"ALTER TABLE trade_records ADD COLUMN sina_code TEXT",
|
"ALTER TABLE key_monitors ADD COLUMN market_code TEXT",
|
||||||
|
"ALTER TABLE trade_records ADD COLUMN market_code TEXT",
|
||||||
]
|
]
|
||||||
for sql in migrations:
|
for sql in migrations:
|
||||||
try:
|
try:
|
||||||
@@ -103,9 +105,19 @@ def init_db():
|
|||||||
if not get_setting("wechat_webhook") and os.getenv("WECHAT_WEBHOOK"):
|
if not get_setting("wechat_webhook") and os.getenv("WECHAT_WEBHOOK"):
|
||||||
set_setting("wechat_webhook", 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"))
|
||||||
|
|
||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
|
def sync_ths_token():
|
||||||
|
set_ths_refresh_token(get_setting("ths_refresh_token"))
|
||||||
|
|
||||||
|
|
||||||
|
sync_ths_token()
|
||||||
|
|
||||||
# —————————————— 推送 ——————————————
|
# —————————————— 推送 ——————————————
|
||||||
|
|
||||||
def send_wechat_msg(content: str):
|
def send_wechat_msg(content: str):
|
||||||
@@ -121,20 +133,25 @@ def send_wechat_msg(content: str):
|
|||||||
|
|
||||||
# —————————————— 行情 ——————————————
|
# —————————————— 行情 ——————————————
|
||||||
|
|
||||||
def resolve_sina_code(ths_code: str, sina_code: str = "") -> Optional[str]:
|
def resolve_market_codes(ths_code: str, market_code: str = "", sina_code: str = "") -> tuple[str, str]:
|
||||||
"""同花顺代码 -> 新浪行情代码;兼容旧数据中的新浪格式。"""
|
"""返回 (market_code, sina_code) 用于行情拉取。"""
|
||||||
if sina_code:
|
if market_code:
|
||||||
return sina_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_"):
|
if ths_code.startswith("nf_") or ths_code.startswith("CFF_RE_"):
|
||||||
return ths_code
|
return ths_code, ths_code
|
||||||
return ths_to_sina_code(ths_code)
|
return "", sina_code or ""
|
||||||
|
|
||||||
|
|
||||||
def fetch_price(ths_code: str, sina_code: str = "") -> Optional[float]:
|
def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Optional[float]:
|
||||||
code = resolve_sina_code(ths_code, sina_code)
|
mc, sc = resolve_market_codes(ths_code, market_code, sina_code)
|
||||||
if not code:
|
if not mc and not sc:
|
||||||
return None
|
return None
|
||||||
return get_price(code)
|
return market_get_price(mc, sc)
|
||||||
|
|
||||||
# —————————————— 监控逻辑 ——————————————
|
# —————————————— 监控逻辑 ——————————————
|
||||||
|
|
||||||
@@ -147,7 +164,8 @@ def check_order_plans():
|
|||||||
for r in rows:
|
for r in rows:
|
||||||
sym = r["symbol"]
|
sym = r["symbol"]
|
||||||
sina = r["sina_code"] if "sina_code" in r.keys() else ""
|
sina = r["sina_code"] if "sina_code" in r.keys() else ""
|
||||||
p = fetch_price(sym, sina)
|
market = r["market_code"] if "market_code" in r.keys() else ""
|
||||||
|
p = fetch_price(sym, market, sina)
|
||||||
if not p:
|
if not p:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -229,8 +247,9 @@ def check_key_monitors():
|
|||||||
name = r["symbol_name"] or sym
|
name = r["symbol_name"] or sym
|
||||||
pid = r["id"]
|
pid = r["id"]
|
||||||
sina = r["sina_code"] if "sina_code" in r.keys() else ""
|
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, sina)
|
p = fetch_price(sym, market, sina)
|
||||||
if not p:
|
if not p:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -337,20 +356,22 @@ def add_plan():
|
|||||||
direction = d.get("direction")
|
direction = d.get("direction")
|
||||||
symbol = d.get("symbol", "").strip()
|
symbol = d.get("symbol", "").strip()
|
||||||
symbol_name = d.get("symbol_name", "").strip()
|
symbol_name = d.get("symbol_name", "").strip()
|
||||||
|
market_code = d.get("market_code", "").strip()
|
||||||
sina_code = d.get("sina_code", "").strip()
|
sina_code = d.get("sina_code", "").strip()
|
||||||
if not direction:
|
if not direction:
|
||||||
flash("请选择多空方向")
|
flash("请选择多空方向")
|
||||||
return redirect(url_for("plans"))
|
return redirect(url_for("plans"))
|
||||||
if not symbol or not sina_code:
|
if not symbol or not market_code:
|
||||||
flash("请从下拉列表选择品种(同花顺合约代码)")
|
flash("请从下拉列表选择品种(同花顺合约代码)")
|
||||||
return redirect(url_for("plans"))
|
return redirect(url_for("plans"))
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO order_plans
|
"""INSERT INTO order_plans
|
||||||
(symbol, symbol_name, sina_code, direction, zone_upper, zone_lower, stop_loss, take_profit)
|
(symbol, symbol_name, market_code, sina_code, direction,
|
||||||
|
zone_upper, zone_lower, stop_loss, take_profit)
|
||||||
VALUES (?,?,?,?,?,?,?,?)""",
|
VALUES (?,?,?,?,?,?,?,?)""",
|
||||||
(
|
(
|
||||||
symbol, symbol_name, sina_code, direction,
|
symbol, symbol_name, market_code, sina_code, direction,
|
||||||
float(d["zone_upper"]), float(d["zone_lower"]),
|
float(d["zone_upper"]), float(d["zone_lower"]),
|
||||||
float(d["stop_loss"]), float(d["take_profit"]),
|
float(d["stop_loss"]), float(d["take_profit"]),
|
||||||
),
|
),
|
||||||
@@ -388,19 +409,20 @@ def add_key():
|
|||||||
direction = d.get("direction")
|
direction = d.get("direction")
|
||||||
symbol = d.get("symbol", "").strip()
|
symbol = d.get("symbol", "").strip()
|
||||||
symbol_name = d.get("symbol_name", "").strip()
|
symbol_name = d.get("symbol_name", "").strip()
|
||||||
|
market_code = d.get("market_code", "").strip()
|
||||||
sina_code = d.get("sina_code", "").strip()
|
sina_code = d.get("sina_code", "").strip()
|
||||||
if not direction:
|
if not direction:
|
||||||
flash("请选择多空方向")
|
flash("请选择多空方向")
|
||||||
return redirect(url_for("keys"))
|
return redirect(url_for("keys"))
|
||||||
if not symbol or not sina_code:
|
if not symbol or not market_code:
|
||||||
flash("请从下拉列表选择品种(同花顺合约代码)")
|
flash("请从下拉列表选择品种(同花顺合约代码)")
|
||||||
return redirect(url_for("keys"))
|
return redirect(url_for("keys"))
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO key_monitors
|
"""INSERT INTO key_monitors
|
||||||
(symbol, symbol_name, sina_code, monitor_type, direction, upper, lower)
|
(symbol, symbol_name, market_code, sina_code, monitor_type, direction, upper, lower)
|
||||||
VALUES (?,?,?,?,?,?,?)""",
|
VALUES (?,?,?,?,?,?,?,?)""",
|
||||||
(symbol, symbol_name, sina_code, d["type"], direction, float(d["upper"]), float(d["lower"])),
|
(symbol, symbol_name, market_code, sina_code, d["type"], direction, float(d["upper"]), float(d["lower"])),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -499,6 +521,11 @@ def settings():
|
|||||||
webhook = request.form.get("wechat_webhook", "").strip()
|
webhook = request.form.get("wechat_webhook", "").strip()
|
||||||
set_setting("wechat_webhook", webhook)
|
set_setting("wechat_webhook", webhook)
|
||||||
flash("企业微信配置已保存")
|
flash("企业微信配置已保存")
|
||||||
|
elif action == "ths":
|
||||||
|
token = request.form.get("ths_refresh_token", "").strip()
|
||||||
|
set_setting("ths_refresh_token", token)
|
||||||
|
sync_ths_token()
|
||||||
|
flash("同花顺行情配置已保存")
|
||||||
elif action == "password":
|
elif action == "password":
|
||||||
old_p = request.form.get("old_password", "")
|
old_p = request.form.get("old_password", "")
|
||||||
new_p = request.form.get("new_password", "")
|
new_p = request.form.get("new_password", "")
|
||||||
@@ -516,8 +543,16 @@ def settings():
|
|||||||
return redirect(url_for("settings"))
|
return redirect(url_for("settings"))
|
||||||
|
|
||||||
webhook = get_setting("wechat_webhook")
|
webhook = get_setting("wechat_webhook")
|
||||||
|
ths_token = get_setting("ths_refresh_token")
|
||||||
username = get_setting("admin_username")
|
username = get_setting("admin_username")
|
||||||
return render_template("settings.html", webhook=webhook, username=username)
|
quote_source = os.getenv("QUOTE_SOURCE", "auto")
|
||||||
|
return render_template(
|
||||||
|
"settings.html",
|
||||||
|
webhook=webhook,
|
||||||
|
ths_token=ths_token,
|
||||||
|
username=username,
|
||||||
|
quote_source=quote_source,
|
||||||
|
)
|
||||||
|
|
||||||
# —————————————— 启动 ——————————————
|
# —————————————— 启动 ——————————————
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 国内期货监控系统 - Ubuntu 一键部署(root 用户,/root/qihuo)
|
# 国内期货监控系统 - Ubuntu 一键部署
|
||||||
# 端口: 6600 进程守护: PM2
|
# root 用户 | 目录 /opt/qihuo | 端口 6600 | PM2
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
APP_DIR="/root/qihuo"
|
APP_DIR="/opt/qihuo"
|
||||||
REPO_URL="https://git.bz121.com/dekun/qihuo.git"
|
REPO_URL="https://git.bz121.com/dekun/qihuo.git"
|
||||||
SERVICE_NAME="qihuo"
|
SERVICE_NAME="qihuo"
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ pip install --upgrade pip -q
|
|||||||
pip install -r "$APP_DIR/requirements.txt" -q
|
pip install -r "$APP_DIR/requirements.txt" -q
|
||||||
|
|
||||||
if [ ! -f "$APP_DIR/.env" ]; then
|
if [ ! -f "$APP_DIR/.env" ]; then
|
||||||
echo "==> 生成 .env(请编辑 ADMIN_PASSWORD 后重启)..."
|
echo "==> 生成 .env(请编辑 ADMIN_PASSWORD、THS_REFRESH_TOKEN)..."
|
||||||
cp "$APP_DIR/.env.example" "$APP_DIR/.env"
|
cp "$APP_DIR/.env.example" "$APP_DIR/.env"
|
||||||
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}/" "$APP_DIR/.env"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name: "qihuo",
|
name: "qihuo",
|
||||||
script: "app.py",
|
script: "app.py",
|
||||||
cwd: "/root/qihuo",
|
cwd: "/opt/qihuo",
|
||||||
interpreter: "/root/qihuo/venv/bin/python",
|
interpreter: "/opt/qihuo/venv/bin/python",
|
||||||
instances: 1,
|
instances: 1,
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
watch: false,
|
watch: false,
|
||||||
@@ -12,8 +12,8 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
},
|
},
|
||||||
error_file: "/root/qihuo/logs/pm2-error.log",
|
error_file: "/opt/qihuo/logs/pm2-error.log",
|
||||||
out_file: "/root/qihuo/logs/pm2-out.log",
|
out_file: "/opt/qihuo/logs/pm2-out.log",
|
||||||
time: true,
|
time: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""
|
||||||
|
行情拉取:优先同花顺 iFinD HTTP API,失败或未配置时回退新浪。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
THS_TOKEN_URL = "https://quantapi.51ifind.com/api/v1/get_access_token"
|
||||||
|
THS_QUOTE_URL = "https://quantapi.51ifind.com/api/v1/real_time_quotation"
|
||||||
|
|
||||||
|
# iFinD HTTP 期货交易所后缀
|
||||||
|
THS_EX_SUFFIX = {
|
||||||
|
"SHFE": "SHFE",
|
||||||
|
"DCE": "DCE",
|
||||||
|
"CZCE": "CZCE",
|
||||||
|
"CFFEX": "CFFEX",
|
||||||
|
"INE": "INE",
|
||||||
|
}
|
||||||
|
|
||||||
|
_token_cache: dict = {"token": "", "expires": 0.0, "refresh": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def _quote_source() -> str:
|
||||||
|
return os.getenv("QUOTE_SOURCE", "auto").strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
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}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("sina fetch failed %s: %s", sina_code, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_sina_price(sina_code: str) -> Optional[float]:
|
||||||
|
raw = _fetch_sina_raw(sina_code)
|
||||||
|
return raw["price"] if raw else None
|
||||||
|
|
||||||
|
|
||||||
|
_runtime_refresh_token: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def set_ths_refresh_token(token: str):
|
||||||
|
global _runtime_refresh_token
|
||||||
|
_runtime_refresh_token = (token or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_refresh_token() -> str:
|
||||||
|
if _runtime_refresh_token:
|
||||||
|
return _runtime_refresh_token
|
||||||
|
return os.getenv("THS_REFRESH_TOKEN", "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ths_access_token(refresh_token: str) -> Optional[str]:
|
||||||
|
if not refresh_token:
|
||||||
|
return None
|
||||||
|
now = time.time()
|
||||||
|
if (
|
||||||
|
_token_cache["token"]
|
||||||
|
and _token_cache["refresh"] == refresh_token
|
||||||
|
and now < _token_cache["expires"]
|
||||||
|
):
|
||||||
|
return _token_cache["token"]
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
THS_TOKEN_URL,
|
||||||
|
headers={"Content-Type": "application/json", "refresh_token": refresh_token},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("errorcode") != 0:
|
||||||
|
logger.warning("THS token error: %s", data.get("errmsg"))
|
||||||
|
return None
|
||||||
|
access = data["data"]["access_token"]
|
||||||
|
_token_cache.update({
|
||||||
|
"token": access,
|
||||||
|
"refresh": refresh_token,
|
||||||
|
"expires": now + 3600 * 6,
|
||||||
|
})
|
||||||
|
return access
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("THS token request failed: %s", exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_ths_quote(data: dict) -> Optional[float]:
|
||||||
|
"""从同花顺实时行情响应解析最新价。"""
|
||||||
|
try:
|
||||||
|
tables = data.get("tables") or []
|
||||||
|
for table in tables:
|
||||||
|
t = table.get("table") or {}
|
||||||
|
for key in ("latest", "new", "close", "trade", "last"):
|
||||||
|
val = t.get(key)
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
return float(val[0])
|
||||||
|
if isinstance(val, (int, float, str)) and str(val):
|
||||||
|
return float(val)
|
||||||
|
# 部分响应嵌套在 data 字段
|
||||||
|
if "data" in data and isinstance(data["data"], dict):
|
||||||
|
return _parse_ths_quote(data["data"])
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("parse ths quote failed: %s", exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ths_price(ths_full_code: str, refresh_token: str = "") -> Optional[float]:
|
||||||
|
"""ths_full_code 如 ag2608.SHFE、IF2606.CFFEX"""
|
||||||
|
token = refresh_token or _get_refresh_token()
|
||||||
|
access = _get_ths_access_token(token)
|
||||||
|
if not access:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
THS_QUOTE_URL,
|
||||||
|
headers={"Content-Type": "application/json", "access_token": access},
|
||||||
|
json={"codes": ths_full_code, "indicators": "latest"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("errorcode") != 0:
|
||||||
|
logger.warning("THS quote error %s: %s", ths_full_code, data.get("errmsg"))
|
||||||
|
return None
|
||||||
|
return _parse_ths_quote(data)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("THS quote failed %s: %s", ths_full_code, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_price(market_code: str, sina_fallback: str = "") -> Optional[float]:
|
||||||
|
"""
|
||||||
|
统一取价入口。
|
||||||
|
market_code: 同花顺完整代码 ag2608.SHFE(优先)
|
||||||
|
sina_fallback: 新浪代码 nf_AG2608(回退)
|
||||||
|
"""
|
||||||
|
source = _quote_source()
|
||||||
|
|
||||||
|
if source in ("ths", "auto") and market_code and "." in market_code:
|
||||||
|
price = get_ths_price(market_code)
|
||||||
|
if price is not None:
|
||||||
|
return price
|
||||||
|
|
||||||
|
if source == "ths":
|
||||||
|
return None
|
||||||
|
|
||||||
|
if sina_fallback:
|
||||||
|
return get_sina_price(sina_fallback)
|
||||||
|
|
||||||
|
# market_code 本身就是新浪格式
|
||||||
|
if market_code.startswith("nf_") or market_code.startswith("CFF_RE_"):
|
||||||
|
return get_sina_price(market_code)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_raw_for_volume(sina_code: str) -> Optional[dict]:
|
||||||
|
"""主力合约扫描用(成交量),走新浪。"""
|
||||||
|
return _fetch_sina_raw(sina_code)
|
||||||
+13
-7
@@ -3,6 +3,7 @@
|
|||||||
const input = wrapper.querySelector('.symbol-input');
|
const input = wrapper.querySelector('.symbol-input');
|
||||||
const hiddenThs = wrapper.querySelector('input[name="symbol"]');
|
const hiddenThs = wrapper.querySelector('input[name="symbol"]');
|
||||||
const hiddenName = wrapper.querySelector('input[name="symbol_name"]');
|
const hiddenName = wrapper.querySelector('input[name="symbol_name"]');
|
||||||
|
const hiddenMarket = wrapper.querySelector('input[name="market_code"]');
|
||||||
const hiddenSina = wrapper.querySelector('input[name="sina_code"]');
|
const hiddenSina = wrapper.querySelector('input[name="sina_code"]');
|
||||||
const dropdown = wrapper.querySelector('.symbol-dropdown');
|
const dropdown = wrapper.querySelector('.symbol-dropdown');
|
||||||
const selectedEl = wrapper.querySelector('.symbol-selected');
|
const selectedEl = wrapper.querySelector('.symbol-selected');
|
||||||
@@ -16,21 +17,25 @@
|
|||||||
input.value = item.name;
|
input.value = item.name;
|
||||||
hiddenThs.value = item.ths_code;
|
hiddenThs.value = item.ths_code;
|
||||||
hiddenName.value = item.name;
|
hiddenName.value = item.name;
|
||||||
if (hiddenSina) hiddenSina.value = item.sina_code;
|
if (hiddenMarket) hiddenMarket.value = item.market_code || '';
|
||||||
selectedEl.textContent = '同花顺: ' + item.ths_code + ' | 主力 ' + (item.contract || item.ths_code);
|
if (hiddenSina) hiddenSina.value = item.sina_code || '';
|
||||||
|
selectedEl.textContent = '同花顺: ' + item.ths_code +
|
||||||
|
(item.market_code ? ' (' + item.market_code + ')' : '');
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItems(items) {
|
function renderItems(items) {
|
||||||
dropdown.innerHTML = '';
|
dropdown.innerHTML = '';
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
dropdown.innerHTML = '<div class="symbol-option">无匹配品种,可输入同花顺合约如 ag2606</div>';
|
dropdown.innerHTML = '<div class="symbol-option">无匹配,可输入同花顺代码如 ag2608</div>';
|
||||||
} else {
|
} else {
|
||||||
items.forEach(function (item) {
|
items.forEach(function (item) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'symbol-option';
|
div.className = 'symbol-option';
|
||||||
div.innerHTML = item.display +
|
div.innerHTML = item.display +
|
||||||
'<div class="sub">同花顺 ' + item.ths_code + ' · ' + item.exchange + '</div>';
|
'<div class="sub">同花顺 ' + item.ths_code +
|
||||||
|
(item.market_code ? ' · ' + item.market_code : '') +
|
||||||
|
' · ' + item.exchange + '</div>';
|
||||||
div.addEventListener('mousedown', function (e) {
|
div.addEventListener('mousedown', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selectItem(item);
|
selectItem(item);
|
||||||
@@ -44,6 +49,7 @@
|
|||||||
input.addEventListener('input', function () {
|
input.addEventListener('input', function () {
|
||||||
hiddenThs.value = '';
|
hiddenThs.value = '';
|
||||||
hiddenName.value = '';
|
hiddenName.value = '';
|
||||||
|
if (hiddenMarket) hiddenMarket.value = '';
|
||||||
if (hiddenSina) hiddenSina.value = '';
|
if (hiddenSina) hiddenSina.value = '';
|
||||||
selectedEl.textContent = '';
|
selectedEl.textContent = '';
|
||||||
const q = input.value.trim();
|
const q = input.value.trim();
|
||||||
@@ -81,15 +87,15 @@
|
|||||||
if (!form.querySelector('.symbol-wrap')) return;
|
if (!form.querySelector('.symbol-wrap')) return;
|
||||||
form.addEventListener('submit', function (e) {
|
form.addEventListener('submit', function (e) {
|
||||||
const ths = form.querySelector('input[name="symbol"]');
|
const ths = form.querySelector('input[name="symbol"]');
|
||||||
const sina = form.querySelector('input[name="sina_code"]');
|
const market = form.querySelector('input[name="market_code"]');
|
||||||
if (ths && !ths.value.trim()) {
|
if (ths && !ths.value.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('请从下拉列表选择品种');
|
alert('请从下拉列表选择品种');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sina && !sina.value.trim()) {
|
if (market && !market.value.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
alert('请从下拉列表选择品种(需含同花顺合约代码)');
|
alert('请从下拉列表选择品种(需含同花顺行情代码)');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+93
-100
@@ -1,15 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
期货品种与同花顺代码映射。
|
期货品种与同花顺代码映射。
|
||||||
界面展示同花顺格式(如 ag2606、SR609、IF2606),行情通过新浪 API(内部 sina_code)获取。
|
展示同花顺合约代码(ag2608);行情优先走同花顺 iFinD HTTP,回退新浪。
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
from market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX
|
||||||
|
|
||||||
# 品种字母:ths=同花顺展示用,sina=新浪 nf_ 前缀后字母(通常大写)
|
|
||||||
PRODUCTS = [
|
PRODUCTS = [
|
||||||
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
|
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
|
||||||
{"name": "黄金", "ths": "au", "sina": "AU", "exchange": "上期所", "ex": "SHFE"},
|
{"name": "黄金", "ths": "au", "sina": "AU", "exchange": "上期所", "ex": "SHFE"},
|
||||||
@@ -64,35 +63,11 @@ PRODUCTS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
_MAIN_CACHE: dict[str, tuple[float, dict]] = {}
|
_MAIN_CACHE: dict[str, tuple[float, dict]] = {}
|
||||||
_CACHE_TTL = 300 # 5 分钟
|
_CACHE_TTL = 300
|
||||||
|
|
||||||
|
|
||||||
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:
|
def build_ths_code(product: dict, year: int, month: int) -> str:
|
||||||
"""同花顺合约代码。"""
|
"""同花顺软件内显示的合约代码。"""
|
||||||
ex = product["ex"]
|
ex = product["ex"]
|
||||||
letters = product["ths"]
|
letters = product["ths"]
|
||||||
if ex == "CZCE":
|
if ex == "CZCE":
|
||||||
@@ -100,67 +75,108 @@ def build_ths_code(product: dict, year: int, month: int) -> str:
|
|||||||
return f"{letters}{year % 100:02d}{month:02d}"
|
return f"{letters}{year % 100:02d}{month:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_ths_full_code(product: dict, year: int, month: int) -> str:
|
||||||
|
"""同花顺 iFinD HTTP API 代码,如 ag2608.SHFE"""
|
||||||
|
ths = build_ths_code(product, year, month)
|
||||||
|
suffix = THS_EX_SUFFIX.get(product["ex"], product["ex"])
|
||||||
|
return f"{ths}.{suffix}"
|
||||||
|
|
||||||
|
|
||||||
def build_sina_code(product: dict, year: int, month: int) -> str:
|
def build_sina_code(product: dict, year: int, month: int) -> str:
|
||||||
"""新浪行情代码(用于拉价)。"""
|
|
||||||
ex = product["ex"]
|
|
||||||
letters = product["sina"]
|
letters = product["sina"]
|
||||||
suffix = f"{year % 100:02d}{month:02d}"
|
suffix = f"{year % 100:02d}{month:02d}"
|
||||||
if ex == "CFFEX":
|
if product["ex"] == "CFFEX":
|
||||||
return f"CFF_RE_{letters}{suffix}"
|
return f"CFF_RE_{letters}{suffix}"
|
||||||
return f"nf_{letters}{suffix}"
|
return f"nf_{letters}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
def build_sina_main_code(product: dict) -> str:
|
def build_sina_main_code(product: dict) -> str:
|
||||||
"""新浪主力连续代码。"""
|
|
||||||
ex = product["ex"]
|
|
||||||
letters = product["sina"]
|
letters = product["sina"]
|
||||||
if ex == "CFFEX":
|
if product["ex"] == "CFFEX":
|
||||||
return f"CFF_RE_{letters}0"
|
return f"CFF_RE_{letters}0"
|
||||||
return f"nf_{letters}0"
|
return f"nf_{letters}0"
|
||||||
|
|
||||||
|
|
||||||
def ths_to_sina_code(ths_code: str) -> Optional[str]:
|
def _find_product_by_letters(letters: str) -> Optional[dict]:
|
||||||
"""将同花顺代码转为新浪代码(用户直接输入合约时使用)。"""
|
letters_up = letters.upper()
|
||||||
|
for p in PRODUCTS:
|
||||||
|
if p["ths"].upper() == letters_up or p["sina"] == letters_up:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ths_to_codes(ths_code: str) -> Optional[dict]:
|
||||||
|
"""同花顺合约代码 -> ths_full + sina 回退代码。"""
|
||||||
code = ths_code.strip()
|
code = ths_code.strip()
|
||||||
if not code:
|
if not code:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# CFFEX / 四位月份: IF2606, ag2606
|
m4 = re.match(r"^([A-Za-z]+)(\d{4})$", code)
|
||||||
m = re.match(r"^([A-Za-z]+)(\d{4})$", code)
|
if m4:
|
||||||
if m:
|
letters, digits = m4.group(1), m4.group(2)
|
||||||
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])
|
year = 2000 + int(digits[:2])
|
||||||
month = int(digits[2:])
|
month = int(digits[2:])
|
||||||
if 1 <= month <= 12:
|
if not 1 <= month <= 12:
|
||||||
return build_sina_code(p, year, month)
|
return None
|
||||||
|
product = _find_product_by_letters(letters)
|
||||||
|
if product:
|
||||||
|
return {
|
||||||
|
"ths_code": build_ths_code(product, year, month),
|
||||||
|
"market_code": build_ths_full_code(product, year, month),
|
||||||
|
"sina_code": build_sina_code(product, year, month),
|
||||||
|
}
|
||||||
|
letters_up = letters.upper()
|
||||||
if letters_up in ("IF", "IH", "IC", "IM", "T", "TF", "TS"):
|
if letters_up in ("IF", "IH", "IC", "IM", "T", "TF", "TS"):
|
||||||
return f"CFF_RE_{letters_up}{digits}"
|
ths = f"{letters_up}{digits}"
|
||||||
|
return {
|
||||||
|
"ths_code": ths,
|
||||||
|
"market_code": f"{ths}.CFFEX",
|
||||||
|
"sina_code": f"CFF_RE_{letters_up}{digits}",
|
||||||
|
}
|
||||||
|
|
||||||
# CZCE 3-digit: SR609
|
|
||||||
m3 = re.match(r"^([A-Za-z]+)(\d{3})$", code)
|
m3 = re.match(r"^([A-Za-z]+)(\d{3})$", code)
|
||||||
if m3:
|
if m3:
|
||||||
letters, digits = m3.group(1), m3.group(2)
|
letters, digits = m3.group(1), m3.group(2)
|
||||||
letters_up = letters.upper()
|
|
||||||
y_digit = int(digits[0])
|
y_digit = int(digits[0])
|
||||||
month = int(digits[1:])
|
month = int(digits[1:])
|
||||||
if 1 <= month <= 12:
|
if not 1 <= month <= 12:
|
||||||
|
return None
|
||||||
year = date.today().year
|
year = date.today().year
|
||||||
decade = year // 10 * 10
|
decade = year // 10 * 10
|
||||||
candidate = decade + y_digit
|
candidate = decade + y_digit
|
||||||
if candidate < year - 1:
|
if candidate < year - 1:
|
||||||
candidate += 10
|
candidate += 10
|
||||||
for p in PRODUCTS:
|
product = _find_product_by_letters(letters)
|
||||||
if p["ths"].upper() == letters_up or p["sina"] == letters_up:
|
if product:
|
||||||
return build_sina_code(p, candidate, month)
|
return {
|
||||||
|
"ths_code": build_ths_code(product, candidate, month),
|
||||||
|
"market_code": build_ths_full_code(product, candidate, month),
|
||||||
|
"sina_code": build_sina_code(product, candidate, month),
|
||||||
|
}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ths_to_sina_code(ths_code: str) -> Optional[str]:
|
||||||
|
codes = ths_to_codes(ths_code)
|
||||||
|
return codes["sina_code"] if codes else None
|
||||||
|
|
||||||
|
|
||||||
|
def _make_symbol_item(product: dict, year: int, month: int, volume: float) -> dict:
|
||||||
|
ths = build_ths_code(product, year, month)
|
||||||
|
return {
|
||||||
|
"name": product["name"],
|
||||||
|
"ths_code": ths,
|
||||||
|
"market_code": build_ths_full_code(product, year, month),
|
||||||
|
"sina_code": build_sina_code(product, year, month),
|
||||||
|
"exchange": product["exchange"],
|
||||||
|
"contract": f"主力 {ths}",
|
||||||
|
"display": f"{product['name']} 主力 {ths}",
|
||||||
|
"volume": volume,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def resolve_main_contract(product: dict) -> Optional[dict]:
|
def resolve_main_contract(product: dict) -> Optional[dict]:
|
||||||
"""按成交量选取当前主力月份合约。"""
|
|
||||||
cache_key = product["sina"]
|
cache_key = product["sina"]
|
||||||
now = time.time()
|
now = time.time()
|
||||||
cached = _MAIN_CACHE.get(cache_key)
|
cached = _MAIN_CACHE.get(cache_key)
|
||||||
@@ -177,30 +193,27 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
|
|||||||
cm -= 12
|
cm -= 12
|
||||||
cy += 1
|
cy += 1
|
||||||
sina = build_sina_code(product, cy, cm)
|
sina = build_sina_code(product, cy, cm)
|
||||||
raw = _fetch_sina_raw(sina)
|
raw = fetch_raw_for_volume(sina)
|
||||||
if raw and raw["volume"] > 0:
|
if raw and raw["volume"] > 0:
|
||||||
ths = build_ths_code(product, cy, cm)
|
item = _make_symbol_item(product, cy, cm, raw["volume"])
|
||||||
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"]:
|
if best is None or raw["volume"] > best["volume"]:
|
||||||
best = item
|
best = item
|
||||||
|
|
||||||
if best is None:
|
if best is None:
|
||||||
sina_main = build_sina_main_code(product)
|
sina_main = build_sina_main_code(product)
|
||||||
raw = _fetch_sina_raw(sina_main)
|
raw = fetch_raw_for_volume(sina_main)
|
||||||
if raw:
|
if raw:
|
||||||
ths_letters = product["ths"]
|
ths_letters = product["ths"]
|
||||||
ths_main = f"{ths_letters}888" if product["ex"] != "CFFEX" else f"{ths_letters.upper()}888"
|
ths_main = (
|
||||||
|
f"{ths_letters}888"
|
||||||
|
if product["ex"] != "CFFEX"
|
||||||
|
else f"{ths_letters.upper()}888"
|
||||||
|
)
|
||||||
|
suffix = THS_EX_SUFFIX.get(product["ex"], product["ex"])
|
||||||
best = {
|
best = {
|
||||||
"name": product["name"],
|
"name": product["name"],
|
||||||
"ths_code": ths_main,
|
"ths_code": ths_main,
|
||||||
|
"market_code": f"{ths_main}.{suffix}",
|
||||||
"sina_code": sina_main,
|
"sina_code": sina_main,
|
||||||
"exchange": product["exchange"],
|
"exchange": product["exchange"],
|
||||||
"contract": f"主力连续 {ths_main}",
|
"contract": f"主力连续 {ths_main}",
|
||||||
@@ -227,44 +240,24 @@ def search_symbols(query: str) -> list:
|
|||||||
if main:
|
if main:
|
||||||
results.append(main)
|
results.append(main)
|
||||||
|
|
||||||
# 用户直接输入同花顺合约代码
|
|
||||||
if not results and len(q) >= 3:
|
if not results and len(q) >= 3:
|
||||||
sina = ths_to_sina_code(query.strip())
|
codes = ths_to_codes(query.strip())
|
||||||
if sina:
|
if codes:
|
||||||
raw = _fetch_sina_raw(sina)
|
raw = fetch_raw_for_volume(codes["sina_code"])
|
||||||
if raw:
|
name = raw["name"] if raw else query.strip()
|
||||||
results.append({
|
results.append({
|
||||||
"name": raw["name"],
|
"name": name,
|
||||||
"ths_code": query.strip(),
|
"ths_code": codes["ths_code"],
|
||||||
"sina_code": sina,
|
"market_code": codes["market_code"],
|
||||||
|
"sina_code": codes["sina_code"],
|
||||||
"exchange": "",
|
"exchange": "",
|
||||||
"contract": query.strip(),
|
"contract": codes["ths_code"],
|
||||||
"display": f"{raw['name']} ({query.strip()})",
|
"display": f"{name} ({codes['ths_code']})",
|
||||||
"volume": raw.get("volume", 0),
|
"volume": raw.get("volume", 0) if raw else 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
return results[:12]
|
return results[:12]
|
||||||
|
|
||||||
|
|
||||||
def get_price(sina_code: str) -> Optional[float]:
|
def get_price(market_code: str, sina_code: str = "") -> Optional[float]:
|
||||||
raw = _fetch_sina_raw(sina_code)
|
return market_get_price(market_code, sina_code)
|
||||||
return raw["price"] if raw else None
|
|
||||||
|
|
||||||
|
|
||||||
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": raw["name"],
|
|
||||||
"ths_code": ths_code,
|
|
||||||
"sina_code": sina,
|
|
||||||
"exchange": "",
|
|
||||||
"contract": ths_code,
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,8 @@
|
|||||||
<input type="text" class="symbol-input" placeholder="输入中文名或同花顺代码" autocomplete="off" required>
|
<input type="text" class="symbol-input" placeholder="输入中文名或同花顺代码" autocomplete="off" required>
|
||||||
<input type="hidden" name="symbol" required>
|
<input type="hidden" name="symbol" required>
|
||||||
<input type="hidden" name="symbol_name">
|
<input type="hidden" name="symbol_name">
|
||||||
<input type="hidden" name="sina_code" required>
|
<input type="hidden" name="market_code" required>
|
||||||
|
<input type="hidden" name="sina_code">
|
||||||
<div class="symbol-dropdown"></div>
|
<div class="symbol-dropdown"></div>
|
||||||
<div class="symbol-selected"></div>
|
<div class="symbol-selected"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
<input type="text" class="symbol-input" placeholder="输入中文名或同花顺代码" autocomplete="off" required>
|
<input type="text" class="symbol-input" placeholder="输入中文名或同花顺代码" autocomplete="off" required>
|
||||||
<input type="hidden" name="symbol" required>
|
<input type="hidden" name="symbol" required>
|
||||||
<input type="hidden" name="symbol_name">
|
<input type="hidden" name="symbol_name">
|
||||||
<input type="hidden" name="sina_code" required>
|
<input type="hidden" name="market_code" required>
|
||||||
|
<input type="hidden" name="sina_code">
|
||||||
<div class="symbol-dropdown"></div>
|
<div class="symbol-dropdown"></div>
|
||||||
<div class="symbol-selected"></div>
|
<div class="symbol-selected"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,21 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-title">系统设置</h1>
|
<h1 class="page-title">系统设置</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>同花顺行情(iFinD HTTP)</h2>
|
||||||
|
<form action="{{ url_for('settings') }}" method="post" class="form-row">
|
||||||
|
<input type="hidden" name="action" value="ths">
|
||||||
|
<input name="ths_refresh_token" type="text" placeholder="refresh_token" value="{{ ths_token }}" style="flex:1;min-width:300px">
|
||||||
|
<button type="submit" class="btn-primary">保存</button>
|
||||||
|
</form>
|
||||||
|
<p style="font-size:.8rem;color:#888;margin-top:.75rem">
|
||||||
|
当前行情源:<strong>{{ quote_source }}</strong>(auto=优先同花顺,失败回退新浪)。
|
||||||
|
在 iFinD 接口包「超级命令 → 工具」查询 refresh_token,
|
||||||
|
或前往 <a href="https://quantapi.10jqka.com.cn/" target="_blank" style="color:#4cc2ff">同花顺数据接口</a> 申请试用。
|
||||||
|
未配置 token 时自动使用新浪行情。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>企业微信推送</h2>
|
<h2>企业微信推送</h2>
|
||||||
<form action="{{ url_for('settings') }}" method="post" class="form-row">
|
<form action="{{ url_for('settings') }}" method="post" class="form-row">
|
||||||
|
|||||||
Reference in New Issue
Block a user