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 fee_specs import calc_fee_breakdown
|
||||
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 (
|
||||
MODE_AMOUNT,
|
||||
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),
|
||||
"risk_status": risk,
|
||||
"trading_session": is_trading_session(),
|
||||
"night_session": is_night_trading_session(),
|
||||
"pending_order_timeout_min": get_pending_order_timeout_min(get_setting),
|
||||
"sync_state": trading_state.sync_state,
|
||||
"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),
|
||||
recommend_rows=rec_cache.get("rows") or [],
|
||||
recommend_updated_at=rec_cache.get("updated_at"),
|
||||
night_session=is_night_trading_session(),
|
||||
product_categories=PRODUCT_CATEGORIES,
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -45,6 +45,19 @@ def is_trading_session(now: Optional[datetime] = None) -> bool:
|
||||
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:
|
||||
wd = day.weekday()
|
||||
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 fee_specs import calc_fee_breakdown
|
||||
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__)
|
||||
|
||||
@@ -74,6 +74,7 @@ def assess_product_for_capital(
|
||||
"margin_one_lot": None,
|
||||
"max_lots": 0,
|
||||
"risk_one_lot_1pct": None,
|
||||
"has_night_session": product_has_night_session(product),
|
||||
}
|
||||
|
||||
margin_one = p * mult * margin_rate
|
||||
@@ -125,6 +126,7 @@ def assess_product_for_capital(
|
||||
"roundtrip_fee_one_lot": fee_info["total_fee"],
|
||||
"status": status,
|
||||
"status_label": label,
|
||||
"has_night_session": product_has_night_session(product),
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +169,7 @@ def list_product_recommendations(
|
||||
"status_label": "计算失败",
|
||||
"main_code": "",
|
||||
"max_lots": 0,
|
||||
"has_night_session": product_has_night_session(product),
|
||||
}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as pool:
|
||||
|
||||
+4
-1
@@ -181,9 +181,12 @@ def enrich_recommend_rows(
|
||||
row["status_label"] = "资金不足"
|
||||
if not row.get("category"):
|
||||
row["category"] = product_category(row.get("ths") or "")
|
||||
from symbols import enrich_recommend_row
|
||||
row = enrich_recommend_row(row)
|
||||
_attach_turnover(row)
|
||||
enriched.append(row)
|
||||
return enriched
|
||||
from symbols import filter_for_trading_session
|
||||
return filter_for_trading_session(enriched)
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
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{
|
||||
padding:.4rem .85rem;font-size:.72rem;font-weight:600;
|
||||
color:var(--text-muted);background:var(--card-inner);
|
||||
|
||||
+4
-1
@@ -106,9 +106,12 @@
|
||||
div.classList.add('near-expiry');
|
||||
}
|
||||
var label = item.display || (item.name + ' ' + item.ths_code);
|
||||
if (item.near_expiry) {
|
||||
if item.near_expiry) {
|
||||
label += ' <span class="near-expiry-tag">临期</span>';
|
||||
}
|
||||
if (item.has_night_session) {
|
||||
label += ' <span class="night-session-tag">夜盘</span>';
|
||||
}
|
||||
div.innerHTML = label +
|
||||
'<div class="sub">' + formatSub(item) + '</div>';
|
||||
div.addEventListener('mousedown', function (e) {
|
||||
|
||||
+3
-2
@@ -1405,16 +1405,17 @@
|
||||
var code = r.main_code || r.ths || '';
|
||||
var nameCls = r.trend_transition ? ' class="trend-name"' : '';
|
||||
var name = r.name || '';
|
||||
var nightTag = r.has_night_session ? ' <span class="night-session-tag">夜盘</span>' : '';
|
||||
if (marketNavEnabled && r.main_code) {
|
||||
var href = '/market?symbol=' + encodeURIComponent(r.main_code) + '&period=d';
|
||||
return (
|
||||
'<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>'
|
||||
);
|
||||
}
|
||||
return (
|
||||
'<td><strong' + nameCls + '>' + name + '</strong> ' +
|
||||
'<td><strong' + nameCls + '>' + name + nightTag + '</strong> ' +
|
||||
'<span class="text-accent">' + code + '</span></td>'
|
||||
);
|
||||
}
|
||||
|
||||
+62
-5
@@ -87,6 +87,38 @@ PRODUCT_CATEGORIES = ["贵金属", "有色金属", "黑色金属", "能源化工
|
||||
for _p in PRODUCTS:
|
||||
_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:
|
||||
return PRODUCT_CATEGORY_MAP.get((ths or "").strip(), "其他")
|
||||
@@ -341,15 +373,22 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
|
||||
}
|
||||
|
||||
if best:
|
||||
best = _enrich_item(best, product)
|
||||
_MAIN_CACHE[cache_key] = (now, best)
|
||||
return best
|
||||
|
||||
|
||||
def _enrich_item(item: dict) -> dict:
|
||||
def _enrich_item(item: dict, product: Optional[dict] = None) -> dict:
|
||||
out = dict(item)
|
||||
if not out.get("input_label"):
|
||||
out["input_label"] = f"{out.get('name', '')} {out.get('ths_code', '')}".strip()
|
||||
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
|
||||
|
||||
|
||||
@@ -365,7 +404,7 @@ def refresh_main_index():
|
||||
try:
|
||||
main = fut.result()
|
||||
if main:
|
||||
new_idx[product["sina"]] = _enrich_item(main)
|
||||
new_idx[product["sina"]] = _enrich_item(main, product)
|
||||
except Exception:
|
||||
pass
|
||||
with _main_index_lock:
|
||||
@@ -389,7 +428,7 @@ def _start_warm_thread():
|
||||
def _stub_main_contract(product: dict) -> dict:
|
||||
"""缓存未就绪时的快速占位(当月合约),避免首次打开搜索为空。"""
|
||||
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:
|
||||
@@ -428,12 +467,16 @@ def search_symbols(query: str) -> list:
|
||||
return []
|
||||
|
||||
q_lower = q.lower()
|
||||
from market_sessions import is_night_trading_session
|
||||
night_only = is_night_trading_session()
|
||||
with _main_index_lock:
|
||||
index = dict(_main_index)
|
||||
index_ready = bool(index)
|
||||
|
||||
scored: list[tuple[int, dict]] = []
|
||||
for p in PRODUCTS:
|
||||
if night_only and not product_has_night_session(p):
|
||||
continue
|
||||
if not _product_matches(p, q_lower):
|
||||
continue
|
||||
main = index.get(p["sina"])
|
||||
@@ -444,6 +487,7 @@ def search_symbols(query: str) -> list:
|
||||
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
results = [item for _, item in scored[:12]]
|
||||
results = filter_for_trading_session(results)
|
||||
|
||||
if not results and len(q) >= 3:
|
||||
codes = ths_to_codes(q)
|
||||
@@ -460,10 +504,19 @@ def search_symbols(query: str) -> list:
|
||||
"display": f"{name} ({codes['ths_code']})",
|
||||
"volume": raw.get("volume", 0) if raw else 0,
|
||||
}))
|
||||
results = filter_for_trading_session(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}
|
||||
for _p in PRODUCTS:
|
||||
_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:
|
||||
item["max_lots"] = max_lots
|
||||
return _enrich_item(item)
|
||||
return _enrich_item(item, product)
|
||||
|
||||
with _main_index_lock:
|
||||
main = _main_index.get(product["sina"])
|
||||
@@ -539,7 +592,7 @@ def _item_from_recommend_row(row: dict, product: dict) -> Optional[dict]:
|
||||
item = dict(main)
|
||||
if max_lots is not None:
|
||||
item["max_lots"] = max_lots
|
||||
return _enrich_item(item)
|
||||
return _enrich_item(item, product)
|
||||
|
||||
item = _stub_main_contract(product)
|
||||
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)
|
||||
if not product:
|
||||
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)
|
||||
item = _item_from_recommend_row(row, product)
|
||||
if not item:
|
||||
|
||||
@@ -133,6 +133,7 @@
|
||||
<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> 元。
|
||||
{% 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 柜台合约信息。
|
||||
{% if recommend_updated_at %}<span class="text-muted">每日后台更新 · 最近 {{ recommend_updated_at }}</span>{% else %}<span class="text-muted" id="rec-updated">等待今日后台刷新…</span>{% endif %}
|
||||
</p>
|
||||
@@ -173,11 +174,11 @@
|
||||
<td>
|
||||
{% 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 线">
|
||||
<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>
|
||||
</a>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user