Add industry filter to recommendations and fix verify button width.
Show category, turnover, and per-industry counts; clarify volume is in lots. Prevent trade-save button from stretching full column width. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-1
@@ -62,7 +62,7 @@ from strategy.strategy_roll_lib import preview_roll
|
|||||||
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot
|
||||||
from strategy.strategy_trend_lib import compute_trend_plan_futures, trend_dca_level_reached
|
from strategy.strategy_trend_lib import compute_trend_plan_futures, trend_dca_level_reached
|
||||||
from strategy.strategy_snapshot_lib import STRATEGY_ROLL, STRATEGY_TREND
|
from strategy.strategy_snapshot_lib import STRATEGY_ROLL, STRATEGY_TREND
|
||||||
from symbols import ths_to_codes, resolve_main_contract, PRODUCTS
|
from symbols import ths_to_codes, resolve_main_contract, PRODUCTS, PRODUCT_CATEGORIES
|
||||||
from trading_context import (
|
from trading_context import (
|
||||||
TRADING_MODE_LIVE,
|
TRADING_MODE_LIVE,
|
||||||
TRADING_MODE_SIM,
|
TRADING_MODE_SIM,
|
||||||
@@ -1126,6 +1126,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
pending_order_timeout_min=get_pending_order_timeout_min(get_setting),
|
pending_order_timeout_min=get_pending_order_timeout_min(get_setting),
|
||||||
recommend_rows=rec_cache.get("rows") or [],
|
recommend_rows=rec_cache.get("rows") or [],
|
||||||
recommend_updated_at=rec_cache.get("updated_at"),
|
recommend_updated_at=rec_cache.get("updated_at"),
|
||||||
|
product_categories=PRODUCT_CATEGORIES,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+18
-1
@@ -9,11 +9,23 @@ from typing import Callable, Optional
|
|||||||
from contract_specs import get_contract_spec
|
from contract_specs import get_contract_spec
|
||||||
from fee_specs import calc_fee_breakdown
|
from fee_specs import calc_fee_breakdown
|
||||||
from recommend_trend import analyze_product_daily, sort_recommend_by_trend
|
from recommend_trend import analyze_product_daily, sort_recommend_by_trend
|
||||||
from symbols import PRODUCTS
|
from symbols import PRODUCTS, product_category
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_turnover(row: dict) -> None:
|
||||||
|
"""成交额 = 昨日成交量(手) × 昨收 × 合约乘数。"""
|
||||||
|
try:
|
||||||
|
vol = float(row.get("volume") or 0)
|
||||||
|
price = float(row.get("prev_close") or row.get("price") or 0)
|
||||||
|
mult = float(row.get("mult") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return
|
||||||
|
if vol > 0 and price > 0 and mult > 0:
|
||||||
|
row["turnover"] = round(vol * price * mult, 2)
|
||||||
|
|
||||||
|
|
||||||
def _letters_from_ths(ths_code: str) -> str:
|
def _letters_from_ths(ths_code: str) -> str:
|
||||||
import re
|
import re
|
||||||
m = re.match(r"^([A-Za-z]+)", (ths_code or "").strip())
|
m = re.match(r"^([A-Za-z]+)", (ths_code or "").strip())
|
||||||
@@ -34,6 +46,7 @@ def assess_product_for_capital(
|
|||||||
ths = product.get("ths") or ""
|
ths = product.get("ths") or ""
|
||||||
name = product.get("name") or ths
|
name = product.get("name") or ths
|
||||||
exchange = product.get("exchange") or ""
|
exchange = product.get("exchange") or ""
|
||||||
|
category = product.get("category") or product_category(ths)
|
||||||
spec = get_contract_spec(ths + "8888")
|
spec = get_contract_spec(ths + "8888")
|
||||||
mult = spec["mult"]
|
mult = spec["mult"]
|
||||||
margin_rate = spec["margin_rate"]
|
margin_rate = spec["margin_rate"]
|
||||||
@@ -47,6 +60,7 @@ def assess_product_for_capital(
|
|||||||
"ths": ths,
|
"ths": ths,
|
||||||
"name": name,
|
"name": name,
|
||||||
"exchange": exchange,
|
"exchange": exchange,
|
||||||
|
"category": category,
|
||||||
"status": "no_price",
|
"status": "no_price",
|
||||||
"status_label": "暂无行情",
|
"status_label": "暂无行情",
|
||||||
"min_capital_one_lot": None,
|
"min_capital_one_lot": None,
|
||||||
@@ -87,6 +101,7 @@ def assess_product_for_capital(
|
|||||||
"ths": ths,
|
"ths": ths,
|
||||||
"name": name,
|
"name": name,
|
||||||
"exchange": exchange,
|
"exchange": exchange,
|
||||||
|
"category": category,
|
||||||
"price": round(p, 4),
|
"price": round(p, 4),
|
||||||
"mult": mult,
|
"mult": mult,
|
||||||
"tick_size": tick,
|
"tick_size": tick,
|
||||||
@@ -129,6 +144,7 @@ def list_product_recommendations(
|
|||||||
row["main_code"] = main_code
|
row["main_code"] = main_code
|
||||||
if main_code:
|
if main_code:
|
||||||
row.update(analyze_product_daily(main_code))
|
row.update(analyze_product_daily(main_code))
|
||||||
|
_attach_turnover(row)
|
||||||
return row
|
return row
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("recommend product failed %s: %s", ths, exc)
|
logger.warning("recommend product failed %s: %s", ths, exc)
|
||||||
@@ -136,6 +152,7 @@ def list_product_recommendations(
|
|||||||
"ths": ths,
|
"ths": ths,
|
||||||
"name": product.get("name") or ths,
|
"name": product.get("name") or ths,
|
||||||
"exchange": product.get("exchange") or "",
|
"exchange": product.get("exchange") or "",
|
||||||
|
"category": product.get("category") or product_category(ths),
|
||||||
"status": "no_price",
|
"status": "no_price",
|
||||||
"status_label": "计算失败",
|
"status_label": "计算失败",
|
||||||
"main_code": "",
|
"main_code": "",
|
||||||
|
|||||||
+21
-1
@@ -8,8 +8,9 @@ from datetime import datetime
|
|||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from fee_specs import ensure_fee_rates_schema
|
from fee_specs import ensure_fee_rates_schema
|
||||||
from product_recommend import list_product_recommendations
|
from product_recommend import _attach_turnover, list_product_recommendations
|
||||||
from recommend_trend import sort_recommend_by_trend
|
from recommend_trend import sort_recommend_by_trend
|
||||||
|
from symbols import product_category
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -53,6 +54,18 @@ def rows_missing_daily_stats(rows: list[dict]) -> bool:
|
|||||||
return any("gap" not in r for r in rows)
|
return any("gap" not in r for r in rows)
|
||||||
|
|
||||||
|
|
||||||
|
def rows_missing_category(rows: list[dict]) -> bool:
|
||||||
|
if not rows:
|
||||||
|
return False
|
||||||
|
return any("category" not in r for r in rows)
|
||||||
|
|
||||||
|
|
||||||
|
def rows_missing_turnover(rows: list[dict]) -> bool:
|
||||||
|
if not rows:
|
||||||
|
return False
|
||||||
|
return any("turnover" not in r for r in rows)
|
||||||
|
|
||||||
|
|
||||||
def recommend_cache_needs_refresh(
|
def recommend_cache_needs_refresh(
|
||||||
cached: dict,
|
cached: dict,
|
||||||
*,
|
*,
|
||||||
@@ -68,6 +81,10 @@ def recommend_cache_needs_refresh(
|
|||||||
return True
|
return True
|
||||||
if rows_missing_daily_stats(rows):
|
if rows_missing_daily_stats(rows):
|
||||||
return True
|
return True
|
||||||
|
if rows_missing_category(rows):
|
||||||
|
return True
|
||||||
|
if rows_missing_turnover(rows):
|
||||||
|
return True
|
||||||
if float(capital or 0) > 0 and not rows:
|
if float(capital or 0) > 0 and not rows:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -128,6 +145,9 @@ def enrich_recommend_rows(
|
|||||||
elif lots < 1 and status in ("ok", "margin_ok"):
|
elif lots < 1 and status in ("ok", "margin_ok"):
|
||||||
row["status"] = "blocked"
|
row["status"] = "blocked"
|
||||||
row["status_label"] = "资金不足"
|
row["status_label"] = "资金不足"
|
||||||
|
if not row.get("category"):
|
||||||
|
row["category"] = product_category(row.get("ths") or "")
|
||||||
|
_attach_turnover(row)
|
||||||
enriched.append(row)
|
enriched.append(row)
|
||||||
return enriched
|
return enriched
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ def compute_daily_quote_stats(bars: list) -> dict:
|
|||||||
"yesterday_change_pct": round(y_change_pct, 2) if y_change_pct is not None else None,
|
"yesterday_change_pct": round(y_change_pct, 2) if y_change_pct is not None else None,
|
||||||
"yesterday_amplitude_pct": round(y_amp, 2) if y_amp is not None else None,
|
"yesterday_amplitude_pct": round(y_amp, 2) if y_amp is not None else None,
|
||||||
"volume": int(vol) if vol is not None else None,
|
"volume": int(vol) if vol is not None else None,
|
||||||
|
"volume_unit": "lot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
.rec-sort-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.45rem .65rem;margin-bottom:.55rem;font-size:.78rem}
|
.rec-sort-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.45rem .65rem;margin-bottom:.55rem;font-size:.78rem}
|
||||||
.rec-sort-bar label{color:var(--text-muted);white-space:nowrap}
|
.rec-sort-bar label{color:var(--text-muted);white-space:nowrap}
|
||||||
.rec-sort-bar select{padding:.35rem .5rem;font-size:.78rem;min-width:7rem}
|
.rec-sort-bar select{padding:.35rem .5rem;font-size:.78rem;min-width:7rem}
|
||||||
|
.rec-stats{
|
||||||
|
font-size:.78rem;color:var(--text-muted);margin-bottom:.45rem;line-height:1.5;
|
||||||
|
}
|
||||||
|
.rec-stats strong{color:var(--accent);font-weight:600}
|
||||||
.rec-sort-dir-btn{
|
.rec-sort-dir-btn{
|
||||||
border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);
|
border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);
|
||||||
padding:.3rem .55rem;border-radius:6px;cursor:pointer;font-size:.78rem;min-width:2rem;
|
padding:.3rem .55rem;border-radius:6px;cursor:pointer;font-size:.78rem;min-width:2rem;
|
||||||
|
|||||||
+99
-6
@@ -33,8 +33,11 @@
|
|||||||
var recRowsRaw = [];
|
var recRowsRaw = [];
|
||||||
var recSortKey = 'trend';
|
var recSortKey = 'trend';
|
||||||
var recSortDesc = true;
|
var recSortDesc = true;
|
||||||
var REC_SORT_CACHE = 'qihuo_rec_sort_v1';
|
var recIndustryFilter = '';
|
||||||
var REC_COLSPAN = 14;
|
var REC_SORT_CACHE = 'qihuo_rec_sort_v2';
|
||||||
|
var REC_INDUSTRY_CACHE = 'qihuo_rec_industry_v1';
|
||||||
|
var REC_COLSPAN = 16;
|
||||||
|
var productCategories = window.PRODUCT_CATEGORIES || [];
|
||||||
var POS_CACHE_KEY = 'qihuo_trading_live_v3';
|
var POS_CACHE_KEY = 'qihuo_trading_live_v3';
|
||||||
|
|
||||||
function runWhenReady(fn) {
|
function runWhenReady(fn) {
|
||||||
@@ -1107,6 +1110,10 @@
|
|||||||
if (p.key) recSortKey = p.key;
|
if (p.key) recSortKey = p.key;
|
||||||
if (typeof p.desc === 'boolean') recSortDesc = p.desc;
|
if (typeof p.desc === 'boolean') recSortDesc = p.desc;
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
|
try {
|
||||||
|
var ind = sessionStorage.getItem(REC_INDUSTRY_CACHE);
|
||||||
|
if (ind != null) recIndustryFilter = ind;
|
||||||
|
} catch (e2) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveRecSortPrefs() {
|
function saveRecSortPrefs() {
|
||||||
@@ -1115,11 +1122,62 @@
|
|||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveRecIndustryPref() {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(REC_INDUSTRY_CACHE, recIndustryFilter || '');
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
function syncRecSortUi() {
|
function syncRecSortUi() {
|
||||||
var sel = document.getElementById('rec-sort-key');
|
var sel = document.getElementById('rec-sort-key');
|
||||||
var btn = document.getElementById('rec-sort-dir');
|
var btn = document.getElementById('rec-sort-dir');
|
||||||
|
var indSel = document.getElementById('rec-industry-filter');
|
||||||
if (sel) sel.value = recSortKey;
|
if (sel) sel.value = recSortKey;
|
||||||
if (btn) btn.textContent = recSortDesc ? '↓' : '↑';
|
if (btn) btn.textContent = recSortDesc ? '↓' : '↑';
|
||||||
|
if (indSel) indSel.value = recIndustryFilter || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRecommendRows(rows) {
|
||||||
|
if (!recIndustryFilter) return (rows || []).slice();
|
||||||
|
return (rows || []).filter(function (r) {
|
||||||
|
return (r.category || '') === recIndustryFilter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function countByCategory(rows) {
|
||||||
|
var counts = {};
|
||||||
|
(rows || []).forEach(function (r) {
|
||||||
|
var cat = r.category || '其他';
|
||||||
|
counts[cat] = (counts[cat] || 0) + 1;
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecStats(allRows, visibleRows) {
|
||||||
|
var el = document.getElementById('rec-stats');
|
||||||
|
if (!el) return;
|
||||||
|
var total = (allRows || []).length;
|
||||||
|
var shown = (visibleRows || []).length;
|
||||||
|
if (!total) {
|
||||||
|
el.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var parts = [];
|
||||||
|
if (recIndustryFilter) {
|
||||||
|
parts.push('筛选 <strong>' + shown + '</strong> / 共 ' + total + ' 个品种');
|
||||||
|
} else {
|
||||||
|
parts.push('共 <strong>' + total + '</strong> 个品种');
|
||||||
|
}
|
||||||
|
var order = productCategories.length ? productCategories.slice() : [];
|
||||||
|
var counts = countByCategory(recIndustryFilter ? visibleRows : allRows);
|
||||||
|
Object.keys(counts).forEach(function (k) {
|
||||||
|
if (order.indexOf(k) < 0) order.push(k);
|
||||||
|
});
|
||||||
|
var breakdown = order.filter(function (cat) { return counts[cat]; }).map(function (cat) {
|
||||||
|
return cat + ' ' + counts[cat];
|
||||||
|
});
|
||||||
|
if (breakdown.length) parts.push(breakdown.join(' · '));
|
||||||
|
el.innerHTML = parts.join(' · ');
|
||||||
}
|
}
|
||||||
|
|
||||||
var TREND_SORT_RANK = { break_long: 0, break_short: 0, long: 1, short: 2, range: 3, '': 9 };
|
var TREND_SORT_RANK = { break_long: 0, break_short: 0, long: 1, short: 2, range: 3, '': 9 };
|
||||||
@@ -1165,6 +1223,15 @@
|
|||||||
return String(Math.round(n));
|
return String(Math.round(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtRecTurnover(v) {
|
||||||
|
if (v === null || v === undefined) return '—';
|
||||||
|
var n = Number(v);
|
||||||
|
if (!isFinite(n)) return '—';
|
||||||
|
if (n >= 1e8) return (n / 1e8).toFixed(2) + '亿';
|
||||||
|
if (n >= 1e4) return (n / 1e4).toFixed(1) + '万';
|
||||||
|
return String(Math.round(n));
|
||||||
|
}
|
||||||
|
|
||||||
function changeCellHtml(r) {
|
function changeCellHtml(r) {
|
||||||
if (r.yesterday_change == null) return '—';
|
if (r.yesterday_change == null) return '—';
|
||||||
var ch = Number(r.yesterday_change);
|
var ch = Number(r.yesterday_change);
|
||||||
@@ -1206,7 +1273,10 @@
|
|||||||
function renderRecommendRows(rows) {
|
function renderRecommendRows(rows) {
|
||||||
if (!recommendList) return;
|
if (!recommendList) return;
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
var emptyMsg = recIndustryFilter
|
||||||
|
? '当前行业下暂无推荐品种'
|
||||||
|
: '当前资金下暂无推荐品种(每日后台刷新)';
|
||||||
|
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">' + emptyMsg + '</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
recommendList.innerHTML = rows.map(function (r) {
|
recommendList.innerHTML = rows.map(function (r) {
|
||||||
@@ -1217,6 +1287,7 @@
|
|||||||
'<tr class="' + rowCls + '">' +
|
'<tr class="' + rowCls + '">' +
|
||||||
'<td><strong' + nameCls + '>' + (r.name || '') + '</strong> <span class="text-accent">' + (r.main_code || r.ths || '') + '</span></td>' +
|
'<td><strong' + nameCls + '>' + (r.name || '') + '</strong> <span class="text-accent">' + (r.main_code || r.ths || '') + '</span></td>' +
|
||||||
'<td>' + (r.exchange || '') + '</td>' +
|
'<td>' + (r.exchange || '') + '</td>' +
|
||||||
|
'<td>' + (r.category || '—') + '</td>' +
|
||||||
'<td>' + trendBadgeHtml(r) + '</td>' +
|
'<td>' + trendBadgeHtml(r) + '</td>' +
|
||||||
'<td>' + gapBadgeHtml(r) + '</td>' +
|
'<td>' + gapBadgeHtml(r) + '</td>' +
|
||||||
'<td>' + (r.price != null ? r.price : '—') + '</td>' +
|
'<td>' + (r.price != null ? r.price : '—') + '</td>' +
|
||||||
@@ -1225,6 +1296,7 @@
|
|||||||
'<td>' + changeCellHtml(r) + '</td>' +
|
'<td>' + changeCellHtml(r) + '</td>' +
|
||||||
'<td>' + (r.yesterday_amplitude_pct != null ? r.yesterday_amplitude_pct + '%' : '—') + '</td>' +
|
'<td>' + (r.yesterday_amplitude_pct != null ? r.yesterday_amplitude_pct + '%' : '—') + '</td>' +
|
||||||
'<td>' + fmtRecVolume(r.volume) + '</td>' +
|
'<td>' + fmtRecVolume(r.volume) + '</td>' +
|
||||||
|
'<td>' + fmtRecTurnover(r.turnover) + '</td>' +
|
||||||
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot + (r.margin_source === 'ctp' ? ' <span class="text-muted">(柜台)</span>' : '') : '—') + '</td>' +
|
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot + (r.margin_source === 'ctp' ? ' <span class="text-muted">(柜台)</span>' : '') : '—') + '</td>' +
|
||||||
'<td>' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '</td>' +
|
'<td>' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '</td>' +
|
||||||
'<td>' + (r.max_lots != null && r.max_lots > 0 ? r.max_lots : '—') + '</td>' +
|
'<td>' + (r.max_lots != null && r.max_lots > 0 ? r.max_lots : '—') + '</td>' +
|
||||||
@@ -1234,6 +1306,13 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRecommendTable() {
|
||||||
|
var filtered = filterRecommendRows(recRowsRaw);
|
||||||
|
var sorted = sortRecommendRows(filtered);
|
||||||
|
updateRecStats(recRowsRaw, sorted);
|
||||||
|
renderRecommendRows(sorted);
|
||||||
|
}
|
||||||
|
|
||||||
function renderRecommendations(data) {
|
function renderRecommendations(data) {
|
||||||
if (!recommendList || !data) return;
|
if (!recommendList || !data) return;
|
||||||
updateRecommendMaxMaps(data);
|
updateRecommendMaxMaps(data);
|
||||||
@@ -1247,9 +1326,10 @@
|
|||||||
recRowsRaw = rows.slice();
|
recRowsRaw = rows.slice();
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||||
|
updateRecStats([], []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderRecommendRows(sortRecommendRows(recRowsRaw));
|
renderRecommendTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initRecommendSortControls() {
|
function initRecommendSortControls() {
|
||||||
@@ -1257,11 +1337,19 @@
|
|||||||
syncRecSortUi();
|
syncRecSortUi();
|
||||||
var sel = document.getElementById('rec-sort-key');
|
var sel = document.getElementById('rec-sort-key');
|
||||||
var btn = document.getElementById('rec-sort-dir');
|
var btn = document.getElementById('rec-sort-dir');
|
||||||
|
var indSel = document.getElementById('rec-industry-filter');
|
||||||
|
if (indSel) {
|
||||||
|
indSel.addEventListener('change', function () {
|
||||||
|
recIndustryFilter = indSel.value || '';
|
||||||
|
saveRecIndustryPref();
|
||||||
|
renderRecommendTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (sel) {
|
if (sel) {
|
||||||
sel.addEventListener('change', function () {
|
sel.addEventListener('change', function () {
|
||||||
recSortKey = sel.value || 'trend';
|
recSortKey = sel.value || 'trend';
|
||||||
saveRecSortPrefs();
|
saveRecSortPrefs();
|
||||||
renderRecommendRows(sortRecommendRows(recRowsRaw));
|
renderRecommendTable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (btn) {
|
if (btn) {
|
||||||
@@ -1269,9 +1357,10 @@
|
|||||||
recSortDesc = !recSortDesc;
|
recSortDesc = !recSortDesc;
|
||||||
saveRecSortPrefs();
|
saveRecSortPrefs();
|
||||||
syncRecSortUi();
|
syncRecSortUi();
|
||||||
renderRecommendRows(sortRecommendRows(recRowsRaw));
|
renderRecommendTable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (recRowsRaw.length) updateRecStats(recRowsRaw, filterRecommendRows(recRowsRaw));
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectRecommendStream() {
|
function connectRecommendStream() {
|
||||||
@@ -1373,6 +1462,10 @@
|
|||||||
initCtpOnLoad();
|
initCtpOnLoad();
|
||||||
connectRecommendStream();
|
connectRecommendStream();
|
||||||
initRecommendSortControls();
|
initRecommendSortControls();
|
||||||
|
if (window.__RECOMMEND_ROWS__ && window.__RECOMMEND_ROWS__.length) {
|
||||||
|
recRowsRaw = window.__RECOMMEND_ROWS__.slice();
|
||||||
|
renderRecommendTable();
|
||||||
|
}
|
||||||
fetch('/api/recommend/list')
|
fetch('/api/recommend/list')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) { if (data.ok) renderRecommendations(data); })
|
.then(function (data) { if (data.ok) renderRecommendations(data); })
|
||||||
|
|||||||
+22
@@ -65,6 +65,28 @@ PRODUCTS = [
|
|||||||
{"name": "中证1000", "ths": "IM", "sina": "IM", "exchange": "中金所", "ex": "CFFEX"},
|
{"name": "中证1000", "ths": "IM", "sina": "IM", "exchange": "中金所", "ex": "CFFEX"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
PRODUCT_CATEGORY_MAP = {
|
||||||
|
"ag": "贵金属", "au": "贵金属",
|
||||||
|
"cu": "有色金属", "al": "有色金属", "zn": "有色金属", "pb": "有色金属", "ni": "有色金属", "sn": "有色金属",
|
||||||
|
"rb": "黑色金属", "hc": "黑色金属", "ss": "黑色金属", "i": "黑色金属", "j": "黑色金属", "jm": "黑色金属",
|
||||||
|
"SF": "黑色金属", "SM": "黑色金属",
|
||||||
|
"sc": "能源化工", "fu": "能源化工", "bu": "能源化工", "ru": "能源化工", "sp": "能源化工",
|
||||||
|
"l": "能源化工", "pp": "能源化工", "v": "能源化工", "eg": "能源化工", "eb": "能源化工", "pg": "能源化工",
|
||||||
|
"MA": "能源化工", "TA": "能源化工", "SA": "能源化工", "UR": "能源化工", "FG": "能源化工",
|
||||||
|
"m": "农产品", "y": "农产品", "p": "农产品", "c": "农产品", "cs": "农产品", "jd": "农产品", "lh": "农产品",
|
||||||
|
"RM": "农产品", "OI": "农产品", "SR": "农产品", "CF": "农产品", "AP": "农产品", "CJ": "农产品", "PK": "农产品",
|
||||||
|
"IF": "金融期货", "IH": "金融期货", "IC": "金融期货", "IM": "金融期货",
|
||||||
|
}
|
||||||
|
PRODUCT_CATEGORIES = ["贵金属", "有色金属", "黑色金属", "能源化工", "农产品", "金融期货"]
|
||||||
|
|
||||||
|
for _p in PRODUCTS:
|
||||||
|
_p["category"] = PRODUCT_CATEGORY_MAP.get(_p["ths"], "其他")
|
||||||
|
|
||||||
|
|
||||||
|
def product_category(ths: str) -> str:
|
||||||
|
return PRODUCT_CATEGORY_MAP.get((ths or "").strip(), "其他")
|
||||||
|
|
||||||
|
|
||||||
EXCHANGE_ORDER = ["上期所", "上期能源", "大商所", "郑商所", "中金所"]
|
EXCHANGE_ORDER = ["上期所", "上期能源", "大商所", "郑商所", "中金所"]
|
||||||
_MAIN_CACHE: dict[str, tuple[float, dict]] = {}
|
_MAIN_CACHE: dict[str, tuple[float, dict]] = {}
|
||||||
_CACHE_TTL = 300
|
_CACHE_TTL = 300
|
||||||
|
|||||||
+1
-1
@@ -433,7 +433,7 @@
|
|||||||
.records-trade-card{overflow:visible}
|
.records-trade-card{overflow:visible}
|
||||||
.records-trade-card .card-body{overflow:visible}
|
.records-trade-card .card-body{overflow:visible}
|
||||||
.trade-actions{display:flex;gap:.35rem;flex-wrap:nowrap;align-items:center;min-width:230px;white-space:nowrap}
|
.trade-actions{display:flex;gap:.35rem;flex-wrap:nowrap;align-items:center;min-width:230px;white-space:nowrap}
|
||||||
.trade-actions a,.trade-actions button{flex-shrink:0;white-space:nowrap;font-size:.72rem;padding:.3rem .55rem;border-radius:6px;text-decoration:none;border:none;cursor:pointer}
|
.trade-actions a,.trade-actions button{flex-shrink:0;white-space:nowrap;font-size:.72rem;padding:.3rem .55rem;border-radius:6px;text-decoration:none;border:none;cursor:pointer;width:auto;min-width:0}
|
||||||
.btn-fill{background:var(--dir-bg);color:var(--accent)}
|
.btn-fill{background:var(--dir-bg);color:var(--accent)}
|
||||||
.btn-verify{background:var(--nav-active);color:#fff}
|
.btn-verify{background:var(--nav-active);color:#fff}
|
||||||
.btn-verify:disabled{opacity:.45;cursor:not-allowed}
|
.btn-verify:disabled{opacity:.45;cursor:not-allowed}
|
||||||
|
|||||||
+16
-4
@@ -121,8 +121,16 @@
|
|||||||
保证金优先读取 CTP 柜台合约信息。
|
保证金优先读取 CTP 柜台合约信息。
|
||||||
{% if recommend_updated_at %}<span class="text-muted">每日后台更新 · 最近 {{ recommend_updated_at }}</span>{% else %}<span class="text-muted" id="rec-updated">等待今日后台刷新…</span>{% endif %}
|
{% if recommend_updated_at %}<span class="text-muted">每日后台更新 · 最近 {{ recommend_updated_at }}</span>{% else %}<span class="text-muted" id="rec-updated">等待今日后台刷新…</span>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p class="trend-hint">走势:近一周日线,近3日重叠≥70%为震荡;跳空=今日开盘 vs 昨日收盘。支持按走势/跳空/成交量/振幅排序。</p>
|
<p class="trend-hint">走势:近一周日线,近3日重叠≥70%为震荡;跳空=今日开盘 vs 昨日收盘。成交量为昨日成交手数,成交额=成交量×昨收×合约乘数。支持按走势/跳空/成交量/振幅排序,可按行业筛选。</p>
|
||||||
|
<div class="rec-stats" id="rec-stats"></div>
|
||||||
<div class="rec-sort-bar">
|
<div class="rec-sort-bar">
|
||||||
|
<label for="rec-industry-filter">行业</label>
|
||||||
|
<select id="rec-industry-filter">
|
||||||
|
<option value="" selected>全部</option>
|
||||||
|
{% for cat in product_categories %}
|
||||||
|
<option value="{{ cat }}">{{ cat }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
<label for="rec-sort-key">排序</label>
|
<label for="rec-sort-key">排序</label>
|
||||||
<select id="rec-sort-key">
|
<select id="rec-sort-key">
|
||||||
<option value="trend" selected>走势</option>
|
<option value="trend" selected>走势</option>
|
||||||
@@ -136,9 +144,9 @@
|
|||||||
<table class="trade-table" id="recommend-table">
|
<table class="trade-table" id="recommend-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>品种</th><th>交易所</th><th>走势</th><th>是否跳空</th>
|
<th>品种</th><th>交易所</th><th>行业</th><th>走势</th><th>是否跳空</th>
|
||||||
<th>参考价</th><th>昨日收盘</th><th>今日开盘</th>
|
<th>参考价</th><th>昨日收盘</th><th>今日开盘</th>
|
||||||
<th>昨日涨跌</th><th>昨日振幅</th><th>成交量</th>
|
<th>昨日涨跌</th><th>昨日振幅</th><th>成交量(手)</th><th>成交额</th>
|
||||||
<th>1手保证金</th><th>1手手续费</th><th>最大手数</th><th>状态</th>
|
<th>1手保证金</th><th>1手手续费</th><th>最大手数</th><th>状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -148,6 +156,7 @@
|
|||||||
<tr class="rec-{{ r.status }}{% if r.trend_transition %} rec-trend-break{% endif %}">
|
<tr class="rec-{{ r.status }}{% if r.trend_transition %} rec-trend-break{% endif %}">
|
||||||
<td><strong class="{% if r.trend_transition %}trend-name{% endif %}">{{ r.name }}</strong> <span class="text-accent">{{ r.main_code or r.ths }}</span></td>
|
<td><strong class="{% if r.trend_transition %}trend-name{% endif %}">{{ r.name }}</strong> <span class="text-accent">{{ r.main_code or r.ths }}</span></td>
|
||||||
<td>{{ r.exchange }}</td>
|
<td>{{ r.exchange }}</td>
|
||||||
|
<td>{{ r.category or '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if r.trend_label and r.trend_label != '—' %}
|
{% if r.trend_label and r.trend_label != '—' %}
|
||||||
<span class="badge trend-badge {% if r.trend in ('break_long', 'break_short') %}break{% elif r.trend == 'long' %}profit{% elif r.trend == 'short' %}loss{% else %}planned{% endif %}" title="{% if r.trend_overlap_pct is not none %}近3日重叠 {{ r.trend_overlap_pct }}%{% endif %}">
|
<span class="badge trend-badge {% if r.trend in ('break_long', 'break_short') %}break{% elif r.trend == 'long' %}profit{% elif r.trend == 'short' %}loss{% else %}planned{% endif %}" title="{% if r.trend_overlap_pct is not none %}近3日重叠 {{ r.trend_overlap_pct }}%{% endif %}">
|
||||||
@@ -172,6 +181,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{% if r.yesterday_amplitude_pct is not none %}{{ '%.2f'|format(r.yesterday_amplitude_pct) }}%{% else %}—{% endif %}</td>
|
<td>{% if r.yesterday_amplitude_pct is not none %}{{ '%.2f'|format(r.yesterday_amplitude_pct) }}%{% else %}—{% endif %}</td>
|
||||||
<td>{% if r.volume is not none %}{{ r.volume }}{% else %}—{% endif %}</td>
|
<td>{% if r.volume is not none %}{{ r.volume }}{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if r.turnover is not none %}{{ '%.0f'|format(r.turnover) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if r.margin_one_lot %}{{ r.margin_one_lot }}{% if r.margin_source == 'ctp' %} <span class="text-muted">(柜台)</span>{% endif %}{% else %}—{% endif %}</td>
|
<td>{% if r.margin_one_lot %}{{ r.margin_one_lot }}{% if r.margin_source == 'ctp' %} <span class="text-muted">(柜台)</span>{% endif %}{% else %}—{% endif %}</td>
|
||||||
<td>{% if r.open_fee_one_lot is defined and r.open_fee_one_lot is not none %}{{ r.open_fee_one_lot }}{% else %}—{% endif %}</td>
|
<td>{% if r.open_fee_one_lot is defined and r.open_fee_one_lot is not none %}{{ r.open_fee_one_lot }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if r.max_lots is not none and r.max_lots > 0 %}{{ r.max_lots }}{% else %}—{% endif %}</td>
|
<td>{% if r.max_lots is not none and r.max_lots > 0 %}{{ r.max_lots }}{% else %}—{% endif %}</td>
|
||||||
@@ -179,7 +189,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="14" class="empty-hint">等待今日后台刷新推荐…</td></tr>
|
<tr><td colspan="16" class="empty-hint">等待今日后台刷新推荐…</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -193,6 +203,8 @@
|
|||||||
window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
|
window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
|
||||||
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
|
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
|
||||||
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
|
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
|
||||||
|
window.PRODUCT_CATEGORIES = {{ product_categories | default([]) | tojson }};
|
||||||
|
window.__RECOMMEND_ROWS__ = {{ recommend_rows | default([]) | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
|
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user