新增品种简介查询页,支持东方财富/新浪合约规格展示
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
| **交易记录与复盘** | 平仓记录核对、填入复盘、K 线自动生成 |
|
| **交易记录与复盘** | 平仓记录核对、填入复盘、K 线自动生成 |
|
||||||
| **统计分析** | 胜率、手续费与净盈亏汇总 |
|
| **统计分析** | 胜率、手续费与净盈亏汇总 |
|
||||||
| **手续费配置** | 本地费率表(默认标准×2),可选 AKShare 同步 |
|
| **手续费配置** | 本地费率表(默认标准×2),可选 AKShare 同步 |
|
||||||
|
| **品种简介** | 合约规格查询(东方财富 / 新浪) |
|
||||||
| **系统设置** | 实盘资金、企业微信、改密码、深色/浅色主题 |
|
| **系统设置** | 实盘资金、企业微信、改密码、深色/浅色主题 |
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from fee_specs import (
|
|||||||
upsert_fee_rate,
|
upsert_fee_rate,
|
||||||
)
|
)
|
||||||
from fee_sync import sync_fees_from_akshare
|
from fee_sync import sync_fees_from_akshare
|
||||||
|
from contract_profile import get_contract_profile
|
||||||
from kline_chart import generate_review_kline_chart
|
from kline_chart import generate_review_kline_chart
|
||||||
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
|
from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label
|
||||||
|
|
||||||
@@ -1291,6 +1292,43 @@ def stats():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/contract")
|
||||||
|
@login_required
|
||||||
|
def contract_profile_page():
|
||||||
|
symbol = request.args.get("symbol", "").strip()
|
||||||
|
profile = None
|
||||||
|
error = None
|
||||||
|
if symbol:
|
||||||
|
try:
|
||||||
|
profile = get_contract_profile(symbol)
|
||||||
|
if not profile:
|
||||||
|
error = "未查询到该合约简介,请检查合约代码"
|
||||||
|
except Exception as exc:
|
||||||
|
app.logger.warning("contract profile failed: %s", exc)
|
||||||
|
error = f"查询失败:{exc}"
|
||||||
|
return render_template(
|
||||||
|
"contract.html",
|
||||||
|
symbol=symbol,
|
||||||
|
profile=profile,
|
||||||
|
error=error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/contract_profile")
|
||||||
|
@login_required
|
||||||
|
def api_contract_profile():
|
||||||
|
symbol = request.args.get("symbol", "").strip()
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({"error": "请提供合约代码"}), 400
|
||||||
|
try:
|
||||||
|
profile = get_contract_profile(symbol)
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 500
|
||||||
|
if not profile:
|
||||||
|
return jsonify({"error": "未查询到合约简介"}), 404
|
||||||
|
return jsonify(profile)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/fees", methods=["GET", "POST"])
|
@app.route("/fees", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def fees():
|
def fees():
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
"""期货合约简介:东方财富 / 新浪 / AKShare。"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from contract_specs import get_contract_spec
|
||||||
|
from symbols import ths_to_codes, search_symbols
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EM_LABEL_MAP = {
|
||||||
|
"vname": "交易品种",
|
||||||
|
"vcode": "交易代码",
|
||||||
|
"jydw": "交易单位",
|
||||||
|
"bjdw": "报价单位",
|
||||||
|
"market": "交易所",
|
||||||
|
"zxbddw": "最小变动价位",
|
||||||
|
"zdtbfd": "涨跌停幅度",
|
||||||
|
"hyjgyf": "合约月份",
|
||||||
|
"jysj": "交易时间",
|
||||||
|
"zhjyr": "最后交易日",
|
||||||
|
"zhjgr": "交割日期",
|
||||||
|
"jgpj": "交割品级",
|
||||||
|
"zcjybzj": "最低交易保证金",
|
||||||
|
"jgfs": "交割方式",
|
||||||
|
"jgdd": "交割地点",
|
||||||
|
"ssrq": "上市日期",
|
||||||
|
}
|
||||||
|
|
||||||
|
DISPLAY_ORDER = [
|
||||||
|
"交易品种",
|
||||||
|
"交易代码",
|
||||||
|
"交易单位",
|
||||||
|
"报价单位",
|
||||||
|
"最小变动价位",
|
||||||
|
"最低交易保证金",
|
||||||
|
"涨跌停幅度",
|
||||||
|
"合约月份",
|
||||||
|
"交易时间",
|
||||||
|
"最后交易日",
|
||||||
|
"交割日期",
|
||||||
|
"交割方式",
|
||||||
|
"交割地点",
|
||||||
|
"交割品级",
|
||||||
|
"上市日期",
|
||||||
|
"交易所",
|
||||||
|
]
|
||||||
|
|
||||||
|
SKIP_ITEMS = {"", "-", "None", "nan", "null"}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_ths_code(raw: str) -> Optional[str]:
|
||||||
|
code = (raw or "").strip()
|
||||||
|
if not code:
|
||||||
|
return None
|
||||||
|
# 已是完整合约
|
||||||
|
if re.match(r"^[A-Za-z]+\d{3,4}$", code):
|
||||||
|
return code
|
||||||
|
# 仅品种字母时尝试匹配主力
|
||||||
|
results = search_symbols(code)
|
||||||
|
if results:
|
||||||
|
return results[0].get("ths_code") or code
|
||||||
|
codes = ths_to_codes(code)
|
||||||
|
if codes:
|
||||||
|
return codes["ths_code"]
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def _to_sina_quote_symbol(ths_code: str) -> str:
|
||||||
|
m = re.match(r"^([A-Za-z]+)(\d+)$", ths_code.strip())
|
||||||
|
if not m:
|
||||||
|
return ths_code.upper()
|
||||||
|
return m.group(1).upper() + m.group(2)
|
||||||
|
|
||||||
|
|
||||||
|
def _to_em_page_symbol(ths_code: str) -> str:
|
||||||
|
return ths_code.strip().lower() + "F"
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_value(val: Any) -> str:
|
||||||
|
if val is None:
|
||||||
|
return ""
|
||||||
|
s = str(val).strip()
|
||||||
|
if s in SKIP_ITEMS:
|
||||||
|
return ""
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _rows_from_dict(data: dict[str, str]) -> list[dict]:
|
||||||
|
rows: list[dict] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for label in DISPLAY_ORDER:
|
||||||
|
val = _clean_value(data.get(label))
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
hint = _clean_value(data.get(f"{label}_hint"))
|
||||||
|
rows.append({"label": label, "value": val, "hint": hint})
|
||||||
|
seen.add(label)
|
||||||
|
for label, val in data.items():
|
||||||
|
if label.endswith("_hint") or label in seen:
|
||||||
|
continue
|
||||||
|
val = _clean_value(val)
|
||||||
|
if val:
|
||||||
|
rows.append({"label": label, "value": val, "hint": ""})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _add_computed_hints(ths_code: str, data: dict[str, str]) -> None:
|
||||||
|
spec = get_contract_spec(ths_code)
|
||||||
|
mult = spec.get("mult") or 0
|
||||||
|
tick_raw = data.get("最小变动价位", "")
|
||||||
|
m = re.search(r"([\d.]+)", tick_raw)
|
||||||
|
if m and mult:
|
||||||
|
tick = float(m.group(1))
|
||||||
|
data["最小变动价位_hint"] = f"一手合约最小波动{round(tick * mult, 2)}元"
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_em_direct(em_symbol: str) -> dict[str, str]:
|
||||||
|
page_url = f"https://quote.eastmoney.com/qihuo/{em_symbol}.html"
|
||||||
|
r = requests.get(page_url, timeout=12)
|
||||||
|
r.encoding = r.apparent_encoding or "utf-8"
|
||||||
|
inner = None
|
||||||
|
for pat in [
|
||||||
|
r"futures_([A-Za-z0-9_]+)",
|
||||||
|
r"#(futures_[A-Za-z0-9_]+)",
|
||||||
|
r"/(futures_[A-Za-z0-9_]+)",
|
||||||
|
]:
|
||||||
|
m = re.search(pat, r.text)
|
||||||
|
if m:
|
||||||
|
inner = m.group(1).replace("futures_", "")
|
||||||
|
break
|
||||||
|
if not inner:
|
||||||
|
raise ValueError("无法解析东方财富合约标识")
|
||||||
|
|
||||||
|
info_url = f"https://futsse-static.eastmoney.com/redis?msgid={inner}_info"
|
||||||
|
r2 = requests.get(info_url, timeout=12)
|
||||||
|
payload = r2.json()
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("东方财富返回数据无效")
|
||||||
|
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for key, label in EM_LABEL_MAP.items():
|
||||||
|
val = _clean_value(payload.get(key))
|
||||||
|
if val:
|
||||||
|
out[label] = val
|
||||||
|
if not out:
|
||||||
|
raise ValueError("东方财富合约字段为空")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_em_akshare(em_symbol: str) -> dict[str, str]:
|
||||||
|
import akshare as ak
|
||||||
|
|
||||||
|
df = ak.futures_contract_detail_em(symbol=em_symbol)
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
label = _clean_value(row.get("item"))
|
||||||
|
val = _clean_value(row.get("value"))
|
||||||
|
if label and val:
|
||||||
|
if label == "跌涨停板幅度":
|
||||||
|
label = "涨跌停幅度"
|
||||||
|
if label == "最后交割日":
|
||||||
|
label = "交割日期"
|
||||||
|
if label == "上市交易所":
|
||||||
|
label = "交易所"
|
||||||
|
if label == "合约交割月份":
|
||||||
|
label = "合约月份"
|
||||||
|
if label == "最初交易保证金":
|
||||||
|
label = "最低交易保证金"
|
||||||
|
if label == "最小变动价格":
|
||||||
|
label = "最小变动价位"
|
||||||
|
out[label] = val
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_sina_direct(sina_symbol: str) -> dict[str, str]:
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
url = f"https://finance.sina.com.cn/futures/quotes/{sina_symbol}.shtml"
|
||||||
|
r = requests.get(url, timeout=12, headers={"Referer": "https://finance.sina.com.cn/"})
|
||||||
|
r.encoding = "gb2312"
|
||||||
|
tables = pd.read_html(StringIO(r.text))
|
||||||
|
if len(tables) < 7:
|
||||||
|
raise ValueError("新浪页面结构变化")
|
||||||
|
temp_df = tables[6]
|
||||||
|
parts = []
|
||||||
|
for ncol in [slice(0, 2), slice(2, 4), slice(4, None)]:
|
||||||
|
part = temp_df.iloc[:, ncol]
|
||||||
|
part.columns = ["item", "value"]
|
||||||
|
parts.append(part)
|
||||||
|
merged = pd.concat(parts, axis=0, ignore_index=True)
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for _, row in merged.iterrows():
|
||||||
|
label = _clean_value(row["item"])
|
||||||
|
val = _clean_value(row["value"])
|
||||||
|
if not label or not val or len(label) > 80 or "发帖" in val:
|
||||||
|
continue
|
||||||
|
out[label] = val
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_sina_akshare(sina_symbol: str) -> dict[str, str]:
|
||||||
|
import akshare as ak
|
||||||
|
|
||||||
|
df = ak.futures_contract_detail(symbol=sina_symbol)
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
label = _clean_value(row.get("item"))
|
||||||
|
val = _clean_value(row.get("value"))
|
||||||
|
if label and val and "发帖" not in val:
|
||||||
|
out[label] = val
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_profile(primary: dict[str, str], secondary: dict[str, str]) -> dict[str, str]:
|
||||||
|
merged = dict(secondary)
|
||||||
|
merged.update(primary)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def get_contract_profile(raw_symbol: str) -> Optional[dict]:
|
||||||
|
ths_code = _normalize_ths_code(raw_symbol)
|
||||||
|
if not ths_code:
|
||||||
|
return None
|
||||||
|
|
||||||
|
em_symbol = _to_em_page_symbol(ths_code)
|
||||||
|
sina_symbol = _to_sina_quote_symbol(ths_code)
|
||||||
|
data: dict[str, str] = {}
|
||||||
|
source_parts: list[str] = []
|
||||||
|
|
||||||
|
# 东方财富(字段与看盘软件简介接近)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
data = _fetch_em_akshare(em_symbol)
|
||||||
|
source_parts.append("东方财富")
|
||||||
|
except ImportError:
|
||||||
|
data = _fetch_em_direct(em_symbol)
|
||||||
|
source_parts.append("东方财富")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("eastmoney profile failed %s: %s", em_symbol, exc)
|
||||||
|
|
||||||
|
# 新浪补充交割地点、上市日期等
|
||||||
|
sina_data: dict[str, str] = {}
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
sina_data = _fetch_sina_akshare(sina_symbol)
|
||||||
|
except ImportError:
|
||||||
|
sina_data = _fetch_sina_direct(sina_symbol)
|
||||||
|
if sina_data:
|
||||||
|
source_parts.append("新浪")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("sina profile failed %s: %s", sina_symbol, exc)
|
||||||
|
|
||||||
|
if sina_data:
|
||||||
|
data = _merge_profile(data, sina_data)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
_add_computed_hints(ths_code, data)
|
||||||
|
rows = _rows_from_dict(data)
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ths_code": ths_code,
|
||||||
|
"symbol_name": data.get("交易品种", ""),
|
||||||
|
"exchange": data.get("交易所", ""),
|
||||||
|
"rows": rows,
|
||||||
|
"source": " + ".join(source_parts) if source_parts else "未知",
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
| 交易记录与复盘 | `/records` | 平仓记录 + 复盘上传与历史 |
|
| 交易记录与复盘 | `/records` | 平仓记录 + 复盘上传与历史 |
|
||||||
| 统计分析 | `/stats` | 胜率、手续费、盈亏汇总 |
|
| 统计分析 | `/stats` | 胜率、手续费、盈亏汇总 |
|
||||||
| 手续费配置 | `/fees` | 本地费率表与倍率 |
|
| 手续费配置 | `/fees` | 本地费率表与倍率 |
|
||||||
|
| 品种简介 | `/contract` | 合约规格查询 |
|
||||||
| 系统设置 | `/settings` | 资金、微信、改密码 |
|
| 系统设置 | `/settings` | 资金、微信、改密码 |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -215,6 +216,36 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 品种简介
|
||||||
|
|
||||||
|
**路径**:`/contract`
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
查询指定合约的**交易所规格说明**,展示风格与看盘软件「合约简介」类似:
|
||||||
|
|
||||||
|
- 交易品种、交易代码、交易单位、报价单位
|
||||||
|
- 最小变动价位(附一手最小波动估算)
|
||||||
|
- 最低交易保证金、涨跌停幅度
|
||||||
|
- 合约月份、交易时间、最后交易日、交割日期
|
||||||
|
- 交割方式、交割地点、交割品级、上市日期、交易所
|
||||||
|
|
||||||
|
### 使用
|
||||||
|
|
||||||
|
1. 导航进入「品种简介」
|
||||||
|
2. 输入中文品种名或同花顺合约代码(如 `螺纹钢`、`rb2510`)
|
||||||
|
3. 从联想列表选择或点击「查询」
|
||||||
|
|
||||||
|
### 数据来源
|
||||||
|
|
||||||
|
- 主数据:**东方财富** 合约详情接口
|
||||||
|
- 补充:**新浪财经** 合约页(交割地点、上市日期等)
|
||||||
|
- 若已安装 AKShare,优先走 AKShare 封装;否则直接请求上述数据源
|
||||||
|
|
||||||
|
API:`GET /api/contract_profile?symbol=rb2510` 返回 JSON。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 系统设置
|
## 系统设置
|
||||||
|
|
||||||
**路径**:`/settings`
|
**路径**:`/settings`
|
||||||
@@ -301,6 +332,7 @@ qihuo/
|
|||||||
├── contract_specs.py # 合约乘数、保证金比例
|
├── contract_specs.py # 合约乘数、保证金比例
|
||||||
├── fee_specs.py # 手续费计算
|
├── fee_specs.py # 手续费计算
|
||||||
├── fee_sync.py # AKShare 费率同步
|
├── fee_sync.py # AKShare 费率同步
|
||||||
|
├── contract_profile.py # 品种/合约简介查询
|
||||||
├── kline_chart.py # 复盘 K 线图
|
├── kline_chart.py # 复盘 K 线图
|
||||||
├── data/fee_rates.json # 默认费率表
|
├── data/fee_rates.json # 默认费率表
|
||||||
├── reset_admin.py # 重置管理员密码
|
├── reset_admin.py # 重置管理员密码
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
(function () {
|
||||||
|
var form = document.getElementById('contract-search-form');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
var wrap = form.querySelector('.symbol-wrap');
|
||||||
|
var hidden = wrap && wrap.querySelector('input[name="symbol"]');
|
||||||
|
var visible = form.querySelector('#contract-symbol-input');
|
||||||
|
|
||||||
|
// 带 symbol 参数进入时,显示合约代码
|
||||||
|
if (hidden && hidden.value && visible && !visible.value) {
|
||||||
|
visible.value = hidden.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', function () {
|
||||||
|
if (!hidden || !visible) return;
|
||||||
|
var v = visible.value.trim();
|
||||||
|
// 若未从下拉选择,尝试用输入框内容(支持直接输入 rb2510)
|
||||||
|
if (!hidden.value && v) {
|
||||||
|
var m = v.match(/([A-Za-z]+\d{3,4})/);
|
||||||
|
hidden.value = m ? m[1] : v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -372,6 +372,14 @@
|
|||||||
.btn-verify:disabled{opacity:.45;cursor:not-allowed}
|
.btn-verify:disabled{opacity:.45;cursor:not-allowed}
|
||||||
.badge.result-manual{background:var(--dir-bg);color:var(--accent)}
|
.badge.result-manual{background:var(--dir-bg);color:var(--accent)}
|
||||||
.badge.result-external{background:var(--expired-bg);color:var(--expired-text)}
|
.badge.result-external{background:var(--expired-bg);color:var(--expired-text)}
|
||||||
|
.profile-page .profile-head{display:flex;align-items:center;gap:.65rem;flex-wrap:wrap;margin:1rem 0 .75rem;font-size:.9rem}
|
||||||
|
.profile-page .profile-source{font-size:.72rem;color:var(--text-muted)}
|
||||||
|
.profile-spec{max-width:820px;border:1px solid var(--card-border);border-radius:10px;background:var(--card-inner);padding:.25rem .85rem}
|
||||||
|
.profile-row{display:grid;grid-template-columns:minmax(120px,28%) 1fr;gap:.5rem 1rem;padding:.6rem 0;border-bottom:1px solid var(--table-border);align-items:start}
|
||||||
|
.profile-row:last-child{border-bottom:none}
|
||||||
|
.profile-label{color:var(--text-muted);font-size:.84rem;line-height:1.4}
|
||||||
|
.profile-value{color:var(--text-primary);font-size:.86rem;line-height:1.5;word-break:break-word}
|
||||||
|
.profile-hint{color:var(--planned-text);font-size:.74rem;margin-top:.25rem;line-height:1.35}
|
||||||
.calc-readonly{background:var(--calc-bg);color:var(--accent)}
|
.calc-readonly{background:var(--calc-bg);color:var(--accent)}
|
||||||
@media(max-width:1100px){
|
@media(max-width:1100px){
|
||||||
.split-grid{grid-template-columns:1fr}
|
.split-grid{grid-template-columns:1fr}
|
||||||
@@ -406,6 +414,7 @@
|
|||||||
<a href="{{ url_for('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a>
|
<a href="{{ url_for('records') }}" class="{% if request.endpoint in ('records', 'trades') %}active{% endif %}">交易记录与复盘</a>
|
||||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
<a href="{{ url_for('stats') }}" class="{% if request.endpoint == 'stats' %}active{% endif %}">统计分析</a>
|
||||||
<a href="{{ url_for('fees') }}" class="{% if request.endpoint == 'fees' %}active{% endif %}">手续费配置</a>
|
<a href="{{ url_for('fees') }}" class="{% if request.endpoint == 'fees' %}active{% endif %}">手续费配置</a>
|
||||||
|
<a href="{{ url_for('contract_profile_page') }}" class="{% if request.endpoint == 'contract_profile_page' %}active{% endif %}">品种简介</a>
|
||||||
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
|
<a href="{{ url_for('settings') }}" class="{% if request.endpoint == 'settings' %}active{% endif %}">系统设置</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}品种简介 - 国内期货监控系统{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card profile-page">
|
||||||
|
<h2>品种简介</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="contract-search-form" class="form-row" method="get" action="{{ url_for('contract_profile_page') }}">
|
||||||
|
<div class="symbol-wrap" style="flex:1;min-width:220px;max-width:360px">
|
||||||
|
<input type="text" class="symbol-input" id="contract-symbol-input"
|
||||||
|
placeholder="输入品种或合约,如 螺纹钢 / rb2510" autocomplete="off" required>
|
||||||
|
<input type="hidden" name="symbol" id="contract-symbol-hidden" value="{{ symbol or '' }}">
|
||||||
|
<div class="symbol-dropdown"></div>
|
||||||
|
<div class="symbol-selected"></div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">查询</button>
|
||||||
|
</form>
|
||||||
|
<p class="hint">展示交易所合约规格:交易单位、最小变动、保证金、交割规则等(数据来源:东方财富 / 新浪)。</p>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="flash" style="margin-top:1rem">{{ error }}</div>
|
||||||
|
{% elif profile %}
|
||||||
|
<div class="profile-head">
|
||||||
|
<strong>{{ profile.symbol_name or profile.ths_code }}</strong>
|
||||||
|
<span class="text-muted">{{ profile.ths_code }}</span>
|
||||||
|
{% if profile.exchange %}<span class="badge active">{{ profile.exchange }}</span>{% endif %}
|
||||||
|
<span class="profile-source">来源:{{ profile.source }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-spec">
|
||||||
|
{% for row in profile.rows %}
|
||||||
|
<div class="profile-row">
|
||||||
|
<div class="profile-label">{{ row.label }}</div>
|
||||||
|
<div class="profile-value">
|
||||||
|
{{ row.value }}
|
||||||
|
{% if row.hint %}<div class="profile-hint">{{ row.hint }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% elif symbol %}
|
||||||
|
<p class="text-muted" style="margin-top:1rem">未查询到该合约简介,请检查合约代码是否正确。</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extra_js %}
|
||||||
|
<script src="{{ url_for('static', filename='js/contract.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user