Label night-session products and hide day-only symbols at night.
Mark tradable varieties with a night tag; during 21:00-02:30 filter out index futures and other products without night sessions from symbol picker and recommend list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
-1
@@ -17,7 +17,7 @@ from flask import flash, jsonify, redirect, render_template, request, url_for, R
|
|||||||
from contract_specs import calc_position_metrics, get_contract_spec
|
from contract_specs import calc_position_metrics, get_contract_spec
|
||||||
from fee_specs import calc_fee_breakdown
|
from fee_specs import calc_fee_breakdown
|
||||||
from kline_stream import sse_format
|
from kline_stream import sse_format
|
||||||
from market_sessions import is_trading_session
|
from market_sessions import is_night_trading_session, is_trading_session
|
||||||
from position_sizing import (
|
from position_sizing import (
|
||||||
MODE_AMOUNT,
|
MODE_AMOUNT,
|
||||||
MODE_FIXED,
|
MODE_FIXED,
|
||||||
@@ -1493,6 +1493,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"trading_mode_label": trading_mode_label(get_setting),
|
"trading_mode_label": trading_mode_label(get_setting),
|
||||||
"risk_status": risk,
|
"risk_status": risk,
|
||||||
"trading_session": is_trading_session(),
|
"trading_session": is_trading_session(),
|
||||||
|
"night_session": is_night_trading_session(),
|
||||||
"pending_order_timeout_min": get_pending_order_timeout_min(get_setting),
|
"pending_order_timeout_min": get_pending_order_timeout_min(get_setting),
|
||||||
"sync_state": trading_state.sync_state,
|
"sync_state": trading_state.sync_state,
|
||||||
"sync_label": trading_state.sync_label(),
|
"sync_label": trading_state.sync_label(),
|
||||||
@@ -1636,6 +1637,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
ctp_auto_connect=is_ctp_auto_connect_enabled(get_setting),
|
ctp_auto_connect=is_ctp_auto_connect_enabled(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"),
|
||||||
|
night_session=is_night_trading_session(),
|
||||||
product_categories=PRODUCT_CATEGORIES,
|
product_categories=PRODUCT_CATEGORIES,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ def is_trading_session(now: Optional[datetime] = None) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_night_trading_session(now: Optional[datetime] = None) -> bool:
|
||||||
|
"""当前是否处于夜盘时段(21:00–02:30,且整体仍在交易时段内)。"""
|
||||||
|
if not is_trading_session(now):
|
||||||
|
return False
|
||||||
|
d = now or datetime.now(TZ)
|
||||||
|
if d.tzinfo is None:
|
||||||
|
d = d.replace(tzinfo=TZ)
|
||||||
|
else:
|
||||||
|
d = d.astimezone(TZ)
|
||||||
|
t = d.hour * 60 + d.minute
|
||||||
|
return t >= 21 * 60 or t < 2 * 60 + 30
|
||||||
|
|
||||||
|
|
||||||
def _session_open_allowed(day: datetime, hour: int, minute: int) -> bool:
|
def _session_open_allowed(day: datetime, hour: int, minute: int) -> bool:
|
||||||
wd = day.weekday()
|
wd = day.weekday()
|
||||||
if (hour, minute) == (9, 0) or (hour, minute) == (13, 30):
|
if (hour, minute) == (9, 0) or (hour, minute) == (13, 30):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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, product_category
|
from symbols import PRODUCTS, product_category, product_has_night_session
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ def assess_product_for_capital(
|
|||||||
"margin_one_lot": None,
|
"margin_one_lot": None,
|
||||||
"max_lots": 0,
|
"max_lots": 0,
|
||||||
"risk_one_lot_1pct": None,
|
"risk_one_lot_1pct": None,
|
||||||
|
"has_night_session": product_has_night_session(product),
|
||||||
}
|
}
|
||||||
|
|
||||||
margin_one = p * mult * margin_rate
|
margin_one = p * mult * margin_rate
|
||||||
@@ -125,6 +126,7 @@ def assess_product_for_capital(
|
|||||||
"roundtrip_fee_one_lot": fee_info["total_fee"],
|
"roundtrip_fee_one_lot": fee_info["total_fee"],
|
||||||
"status": status,
|
"status": status,
|
||||||
"status_label": label,
|
"status_label": label,
|
||||||
|
"has_night_session": product_has_night_session(product),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -167,6 +169,7 @@ def list_product_recommendations(
|
|||||||
"status_label": "计算失败",
|
"status_label": "计算失败",
|
||||||
"main_code": "",
|
"main_code": "",
|
||||||
"max_lots": 0,
|
"max_lots": 0,
|
||||||
|
"has_night_session": product_has_night_session(product),
|
||||||
}
|
}
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=10) as pool:
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
||||||
|
|||||||
+4
-1
@@ -181,9 +181,12 @@ def enrich_recommend_rows(
|
|||||||
row["status_label"] = "资金不足"
|
row["status_label"] = "资金不足"
|
||||||
if not row.get("category"):
|
if not row.get("category"):
|
||||||
row["category"] = product_category(row.get("ths") or "")
|
row["category"] = product_category(row.get("ths") or "")
|
||||||
|
from symbols import enrich_recommend_row
|
||||||
|
row = enrich_recommend_row(row)
|
||||||
_attach_turnover(row)
|
_attach_turnover(row)
|
||||||
enriched.append(row)
|
enriched.append(row)
|
||||||
return enriched
|
from symbols import filter_for_trading_session
|
||||||
|
return filter_for_trading_session(enriched)
|
||||||
|
|
||||||
|
|
||||||
def filter_recommend_by_sizing(
|
def filter_recommend_by_sizing(
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("192.168.8.21", username="root", password="woaini88", timeout=15)
|
||||||
|
for cmd in [
|
||||||
|
"ls -la /opt/qihuo/market_sessions.py",
|
||||||
|
"head -5 /opt/qihuo/market_sessions.py",
|
||||||
|
"cd /opt/qihuo && /opt/qihuo/venv/bin/python -c \"import market_sessions; print(market_sessions.is_night_trading_session())\"",
|
||||||
|
]:
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
print(">>>", cmd)
|
||||||
|
print(o.read().decode())
|
||||||
|
print(e.read().decode())
|
||||||
|
c.close()
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"""Deploy night session symbol filter."""
|
||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
FILES = [
|
||||||
|
"market_sessions.py",
|
||||||
|
"symbols.py",
|
||||||
|
"product_recommend.py",
|
||||||
|
"recommend_store.py",
|
||||||
|
"install_trading.py",
|
||||||
|
"templates/trade.html",
|
||||||
|
"static/js/symbol.js",
|
||||||
|
"static/js/trade.js",
|
||||||
|
"static/css/base.css",
|
||||||
|
]
|
||||||
|
|
||||||
|
VERIFY = r"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/opt/qihuo")
|
||||||
|
from market_sessions import is_night_trading_session, is_trading_session
|
||||||
|
from symbols import product_has_night_session, list_recommended_symbols_grouped, enrich_recommend_row
|
||||||
|
|
||||||
|
print("trading", is_trading_session(), "night", is_night_trading_session())
|
||||||
|
print("IF night", product_has_night_session("IF"))
|
||||||
|
print("ag night", product_has_night_session("ag"))
|
||||||
|
print("jd night", product_has_night_session("jd"))
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
enrich_recommend_row({"ths": "IF", "name": "沪深300", "status": "ok", "max_lots": 1}),
|
||||||
|
enrich_recommend_row({"ths": "ag", "name": "白银", "status": "ok", "max_lots": 1}),
|
||||||
|
]
|
||||||
|
groups = list_recommended_symbols_grouped(rows)
|
||||||
|
items = [i for g in groups for i in g.get("items", [])]
|
||||||
|
ths_set = {i.get("ths_code", "")[:2].lower() for i in items}
|
||||||
|
print("group ths prefixes", ths_set)
|
||||||
|
if is_night_trading_session() and any(x.startswith("if") for x in ths_set):
|
||||||
|
print("FAIL IF shown during night")
|
||||||
|
elif is_night_trading_session() and not any(x.startswith("ag") for x in ths_set):
|
||||||
|
print("FAIL ag missing during night")
|
||||||
|
else:
|
||||||
|
print("VERIFY PASS")
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("192.168.8.21", username="root", password="woaini88", timeout=15)
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
for rel in FILES:
|
||||||
|
sftp.put(str(root / rel), f"/opt/qihuo/{rel.replace(chr(92), '/')}")
|
||||||
|
print("uploaded", rel)
|
||||||
|
sftp.close()
|
||||||
|
for cmd in ("cd /opt/qihuo && pm2 restart qihuo", "sleep 3"):
|
||||||
|
print(">>>", cmd)
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
print(o.read().decode("utf-8", errors="replace"))
|
||||||
|
err = e.read().decode("utf-8", errors="replace")
|
||||||
|
if err.strip():
|
||||||
|
print(err.strip())
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
with sftp.open("/tmp/verify_night.py", "w") as f:
|
||||||
|
f.write(VERIFY)
|
||||||
|
sftp.close()
|
||||||
|
_, o, e = c.exec_command("cd /opt/qihuo && /opt/qihuo/venv/bin/python /tmp/verify_night.py")
|
||||||
|
print(o.read().decode("utf-8", errors="replace"))
|
||||||
|
print(e.read().decode("utf-8", errors="replace"))
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -281,6 +281,11 @@ font-size:.68rem;padding:.1rem .35rem;border-radius:4px;
|
|||||||
background:rgba(255,107,122,.15);color:inherit;font-weight:600;
|
background:rgba(255,107,122,.15);color:inherit;font-weight:600;
|
||||||
}
|
}
|
||||||
html[data-theme="light"] .near-expiry-tag{background:rgba(220,38,38,.12)}
|
html[data-theme="light"] .near-expiry-tag{background:rgba(220,38,38,.12)}
|
||||||
|
.night-session-tag{
|
||||||
|
display:inline-block;margin-left:4px;padding:0 5px;border-radius:4px;font-size:.7rem;font-weight:600;
|
||||||
|
color:#7dd3fc;background:rgba(56,189,248,.15);vertical-align:middle;line-height:1.3
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .night-session-tag{color:#0369a1;background:rgba(14,165,233,.12)}
|
||||||
.symbol-group-head{
|
.symbol-group-head{
|
||||||
padding:.4rem .85rem;font-size:.72rem;font-weight:600;
|
padding:.4rem .85rem;font-size:.72rem;font-weight:600;
|
||||||
color:var(--text-muted);background:var(--card-inner);
|
color:var(--text-muted);background:var(--card-inner);
|
||||||
|
|||||||
+4
-1
@@ -106,9 +106,12 @@
|
|||||||
div.classList.add('near-expiry');
|
div.classList.add('near-expiry');
|
||||||
}
|
}
|
||||||
var label = item.display || (item.name + ' ' + item.ths_code);
|
var label = item.display || (item.name + ' ' + item.ths_code);
|
||||||
if (item.near_expiry) {
|
if item.near_expiry) {
|
||||||
label += ' <span class="near-expiry-tag">临期</span>';
|
label += ' <span class="near-expiry-tag">临期</span>';
|
||||||
}
|
}
|
||||||
|
if (item.has_night_session) {
|
||||||
|
label += ' <span class="night-session-tag">夜盘</span>';
|
||||||
|
}
|
||||||
div.innerHTML = label +
|
div.innerHTML = label +
|
||||||
'<div class="sub">' + formatSub(item) + '</div>';
|
'<div class="sub">' + formatSub(item) + '</div>';
|
||||||
div.addEventListener('mousedown', function (e) {
|
div.addEventListener('mousedown', function (e) {
|
||||||
|
|||||||
+3
-2
@@ -1405,16 +1405,17 @@
|
|||||||
var code = r.main_code || r.ths || '';
|
var code = r.main_code || r.ths || '';
|
||||||
var nameCls = r.trend_transition ? ' class="trend-name"' : '';
|
var nameCls = r.trend_transition ? ' class="trend-name"' : '';
|
||||||
var name = r.name || '';
|
var name = r.name || '';
|
||||||
|
var nightTag = r.has_night_session ? ' <span class="night-session-tag">夜盘</span>' : '';
|
||||||
if (marketNavEnabled && r.main_code) {
|
if (marketNavEnabled && r.main_code) {
|
||||||
var href = '/market?symbol=' + encodeURIComponent(r.main_code) + '&period=d';
|
var href = '/market?symbol=' + encodeURIComponent(r.main_code) + '&period=d';
|
||||||
return (
|
return (
|
||||||
'<td><a href="' + href + '" class="rec-market-link" title="查看日线 K 线">' +
|
'<td><a href="' + href + '" class="rec-market-link" title="查看日线 K 线">' +
|
||||||
'<strong' + nameCls + '>' + name + '</strong> ' +
|
'<strong' + nameCls + '>' + name + nightTag + '</strong> ' +
|
||||||
'<span class="text-accent">' + r.main_code + '</span></a></td>'
|
'<span class="text-accent">' + r.main_code + '</span></a></td>'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
'<td><strong' + nameCls + '>' + name + '</strong> ' +
|
'<td><strong' + nameCls + '>' + name + nightTag + '</strong> ' +
|
||||||
'<span class="text-accent">' + code + '</span></td>'
|
'<span class="text-accent">' + code + '</span></td>'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-5
@@ -87,6 +87,38 @@ PRODUCT_CATEGORIES = ["贵金属", "有色金属", "黑色金属", "能源化工
|
|||||||
for _p in PRODUCTS:
|
for _p in PRODUCTS:
|
||||||
_p["category"] = PRODUCT_CATEGORY_MAP.get(_p["ths"], "其他")
|
_p["category"] = PRODUCT_CATEGORY_MAP.get(_p["ths"], "其他")
|
||||||
|
|
||||||
|
# 无夜盘品种(日盘-only):中金所股指、大商所鸡蛋/生猪等
|
||||||
|
NO_NIGHT_SESSION_THS = frozenset({"IF", "IH", "IC", "IM", "jd", "lh"})
|
||||||
|
|
||||||
|
|
||||||
|
def product_has_night_session(ths_or_product) -> bool:
|
||||||
|
"""品种是否参与夜盘交易。"""
|
||||||
|
if isinstance(ths_or_product, dict):
|
||||||
|
ths = (ths_or_product.get("ths") or "").strip()
|
||||||
|
else:
|
||||||
|
ths = (ths_or_product or "").strip()
|
||||||
|
if not ths:
|
||||||
|
return True
|
||||||
|
m = re.match(r"^([A-Za-z]+)", ths)
|
||||||
|
letters = m.group(1) if m else ths
|
||||||
|
return letters not in NO_NIGHT_SESSION_THS and letters.upper() not in NO_NIGHT_SESSION_THS
|
||||||
|
|
||||||
|
|
||||||
|
def filter_for_trading_session(rows: list[dict]) -> list[dict]:
|
||||||
|
"""夜盘时段隐藏无夜盘品种。"""
|
||||||
|
from market_sessions import is_night_trading_session
|
||||||
|
|
||||||
|
if not is_night_trading_session():
|
||||||
|
return rows
|
||||||
|
out: list[dict] = []
|
||||||
|
for row in rows:
|
||||||
|
if row.get("has_night_session") is False:
|
||||||
|
continue
|
||||||
|
ths = row.get("ths") or row.get("ths_code") or ""
|
||||||
|
if row.get("has_night_session") is True or product_has_night_session(ths):
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def product_category(ths: str) -> str:
|
def product_category(ths: str) -> str:
|
||||||
return PRODUCT_CATEGORY_MAP.get((ths or "").strip(), "其他")
|
return PRODUCT_CATEGORY_MAP.get((ths or "").strip(), "其他")
|
||||||
@@ -341,15 +373,22 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if best:
|
if best:
|
||||||
|
best = _enrich_item(best, product)
|
||||||
_MAIN_CACHE[cache_key] = (now, best)
|
_MAIN_CACHE[cache_key] = (now, best)
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
def _enrich_item(item: dict) -> dict:
|
def _enrich_item(item: dict, product: Optional[dict] = None) -> dict:
|
||||||
out = dict(item)
|
out = dict(item)
|
||||||
if not out.get("input_label"):
|
if not out.get("input_label"):
|
||||||
out["input_label"] = f"{out.get('name', '')} {out.get('ths_code', '')}".strip()
|
out["input_label"] = f"{out.get('name', '')} {out.get('ths_code', '')}".strip()
|
||||||
out["near_expiry"] = is_near_expiry_main(out.get("ths_code", ""))
|
out["near_expiry"] = is_near_expiry_main(out.get("ths_code", ""))
|
||||||
|
if product is None and out.get("ths_code"):
|
||||||
|
product = _product_for_contract_code(out["ths_code"])
|
||||||
|
if product is not None:
|
||||||
|
out["has_night_session"] = product_has_night_session(product)
|
||||||
|
elif "has_night_session" not in out:
|
||||||
|
out["has_night_session"] = product_has_night_session(out.get("ths_code") or "")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -365,7 +404,7 @@ def refresh_main_index():
|
|||||||
try:
|
try:
|
||||||
main = fut.result()
|
main = fut.result()
|
||||||
if main:
|
if main:
|
||||||
new_idx[product["sina"]] = _enrich_item(main)
|
new_idx[product["sina"]] = _enrich_item(main, product)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
with _main_index_lock:
|
with _main_index_lock:
|
||||||
@@ -389,7 +428,7 @@ def _start_warm_thread():
|
|||||||
def _stub_main_contract(product: dict) -> dict:
|
def _stub_main_contract(product: dict) -> dict:
|
||||||
"""缓存未就绪时的快速占位(当月合约),避免首次打开搜索为空。"""
|
"""缓存未就绪时的快速占位(当月合约),避免首次打开搜索为空。"""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return _enrich_item(_make_symbol_item(product, today.year, today.month, 0))
|
return _enrich_item(_make_symbol_item(product, today.year, today.month, 0), product)
|
||||||
|
|
||||||
|
|
||||||
def _product_matches(product: dict, q_lower: str) -> bool:
|
def _product_matches(product: dict, q_lower: str) -> bool:
|
||||||
@@ -428,12 +467,16 @@ def search_symbols(query: str) -> list:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
q_lower = q.lower()
|
q_lower = q.lower()
|
||||||
|
from market_sessions import is_night_trading_session
|
||||||
|
night_only = is_night_trading_session()
|
||||||
with _main_index_lock:
|
with _main_index_lock:
|
||||||
index = dict(_main_index)
|
index = dict(_main_index)
|
||||||
index_ready = bool(index)
|
index_ready = bool(index)
|
||||||
|
|
||||||
scored: list[tuple[int, dict]] = []
|
scored: list[tuple[int, dict]] = []
|
||||||
for p in PRODUCTS:
|
for p in PRODUCTS:
|
||||||
|
if night_only and not product_has_night_session(p):
|
||||||
|
continue
|
||||||
if not _product_matches(p, q_lower):
|
if not _product_matches(p, q_lower):
|
||||||
continue
|
continue
|
||||||
main = index.get(p["sina"])
|
main = index.get(p["sina"])
|
||||||
@@ -444,6 +487,7 @@ def search_symbols(query: str) -> list:
|
|||||||
|
|
||||||
scored.sort(key=lambda x: -x[0])
|
scored.sort(key=lambda x: -x[0])
|
||||||
results = [item for _, item in scored[:12]]
|
results = [item for _, item in scored[:12]]
|
||||||
|
results = filter_for_trading_session(results)
|
||||||
|
|
||||||
if not results and len(q) >= 3:
|
if not results and len(q) >= 3:
|
||||||
codes = ths_to_codes(q)
|
codes = ths_to_codes(q)
|
||||||
@@ -460,10 +504,19 @@ def search_symbols(query: str) -> list:
|
|||||||
"display": f"{name} ({codes['ths_code']})",
|
"display": f"{name} ({codes['ths_code']})",
|
||||||
"volume": raw.get("volume", 0) if raw else 0,
|
"volume": raw.get("volume", 0) if raw else 0,
|
||||||
}))
|
}))
|
||||||
|
results = filter_for_trading_session(results)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_recommend_row(row: dict) -> dict:
|
||||||
|
"""补全推荐行字段(含是否夜盘)。"""
|
||||||
|
out = dict(row)
|
||||||
|
ths = out.get("ths") or ""
|
||||||
|
out["has_night_session"] = product_has_night_session(ths)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
_THS_TO_PRODUCT = {p["ths"]: p for p in PRODUCTS}
|
_THS_TO_PRODUCT = {p["ths"]: p for p in PRODUCTS}
|
||||||
for _p in PRODUCTS:
|
for _p in PRODUCTS:
|
||||||
_THS_TO_PRODUCT.setdefault(_p["ths"].lower(), _p)
|
_THS_TO_PRODUCT.setdefault(_p["ths"].lower(), _p)
|
||||||
@@ -531,7 +584,7 @@ def _item_from_recommend_row(row: dict, product: dict) -> Optional[dict]:
|
|||||||
}
|
}
|
||||||
if max_lots is not None:
|
if max_lots is not None:
|
||||||
item["max_lots"] = max_lots
|
item["max_lots"] = max_lots
|
||||||
return _enrich_item(item)
|
return _enrich_item(item, product)
|
||||||
|
|
||||||
with _main_index_lock:
|
with _main_index_lock:
|
||||||
main = _main_index.get(product["sina"])
|
main = _main_index.get(product["sina"])
|
||||||
@@ -539,7 +592,7 @@ def _item_from_recommend_row(row: dict, product: dict) -> Optional[dict]:
|
|||||||
item = dict(main)
|
item = dict(main)
|
||||||
if max_lots is not None:
|
if max_lots is not None:
|
||||||
item["max_lots"] = max_lots
|
item["max_lots"] = max_lots
|
||||||
return _enrich_item(item)
|
return _enrich_item(item, product)
|
||||||
|
|
||||||
item = _stub_main_contract(product)
|
item = _stub_main_contract(product)
|
||||||
if max_lots is not None:
|
if max_lots is not None:
|
||||||
@@ -563,6 +616,10 @@ def list_recommended_symbols_grouped(recommend_rows: list[dict]) -> list[dict]:
|
|||||||
product = _product_for_ths(ths_key)
|
product = _product_for_ths(ths_key)
|
||||||
if not product:
|
if not product:
|
||||||
continue
|
continue
|
||||||
|
if not product_has_night_session(product):
|
||||||
|
from market_sessions import is_night_trading_session
|
||||||
|
if is_night_trading_session():
|
||||||
|
continue
|
||||||
seen.add(ths_key)
|
seen.add(ths_key)
|
||||||
item = _item_from_recommend_row(row, product)
|
item = _item_from_recommend_row(row, product)
|
||||||
if not item:
|
if not item:
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="hint">最大手数 = floor(权益 × 保证金上限 <strong>{{ max_margin_pct }}%</strong> ÷ 1手保证金);当前权益 <strong class="text-accent" id="rec-capital">{{ '%.2f'|format(capital) }}</strong> 元。
|
<p class="hint">最大手数 = floor(权益 × 保证金上限 <strong>{{ max_margin_pct }}%</strong> ÷ 1手保证金);当前权益 <strong class="text-accent" id="rec-capital">{{ '%.2f'|format(capital) }}</strong> 元。
|
||||||
{% if sizing_mode == 'fixed' %}仅显示最大手数 ≥ <strong>{{ fixed_lots }}</strong> 手的品种。{% endif %}
|
{% if sizing_mode == 'fixed' %}仅显示最大手数 ≥ <strong>{{ fixed_lots }}</strong> 手的品种。{% endif %}
|
||||||
|
{% if night_session %}<span class="text-muted">当前为夜盘时段,品种下拉与下表仅显示有夜盘品种;带「夜盘」标记。</span>{% else %}<span class="text-muted">有夜盘交易的品种带「夜盘」标记。</span>{% endif %}
|
||||||
保证金优先读取 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>
|
||||||
@@ -173,11 +174,11 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if r.main_code and nav_items.market %}
|
{% if r.main_code and nav_items.market %}
|
||||||
<a href="{{ url_for('market_page', symbol=r.main_code, period='d') }}" class="rec-market-link" title="查看日线 K 线">
|
<a href="{{ url_for('market_page', symbol=r.main_code, period='d') }}" class="rec-market-link" title="查看日线 K 线">
|
||||||
<strong class="{% if r.trend_transition %}trend-name{% endif %}">{{ r.name }}</strong>
|
<strong class="{% if r.trend_transition %}trend-name{% endif %}">{{ r.name }}</strong>{% if r.has_night_session %} <span class="night-session-tag">夜盘</span>{% endif %}
|
||||||
<span class="text-accent">{{ r.main_code }}</span>
|
<span class="text-accent">{{ r.main_code }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong class="{% if r.trend_transition %}trend-name{% endif %}">{{ r.name }}</strong>
|
<strong class="{% if r.trend_transition %}trend-name{% endif %}">{{ r.name }}</strong>{% if r.has_night_session %} <span class="night-session-tag">夜盘</span>{% endif %}
|
||||||
<span class="text-accent">{{ r.main_code or r.ths }}</span>
|
<span class="text-accent">{{ r.main_code or r.ths }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user