diff --git a/install_trading.py b/install_trading.py index 3de840a..03ef6d9 100644 --- a/install_trading.py +++ b/install_trading.py @@ -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: diff --git a/market_sessions.py b/market_sessions.py index 237e242..9d613f7 100644 --- a/market_sessions.py +++ b/market_sessions.py @@ -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): diff --git a/product_recommend.py b/product_recommend.py index 398ccf5..ffceb2e 100644 --- a/product_recommend.py +++ b/product_recommend.py @@ -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: diff --git a/recommend_store.py b/recommend_store.py index 14dde65..133ec34 100644 --- a/recommend_store.py +++ b/recommend_store.py @@ -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( diff --git a/scripts/check_server_night.py b/scripts/check_server_night.py new file mode 100644 index 0000000..58e8656 --- /dev/null +++ b/scripts/check_server_night.py @@ -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() diff --git a/scripts/deploy_night_session.py b/scripts/deploy_night_session.py new file mode 100644 index 0000000..a5d7f85 --- /dev/null +++ b/scripts/deploy_night_session.py @@ -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() diff --git a/static/css/base.css b/static/css/base.css index 419c829..3f36545 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -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); diff --git a/static/js/symbol.js b/static/js/symbol.js index 6a123b9..42cdd30 100644 --- a/static/js/symbol.js +++ b/static/js/symbol.js @@ -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 += ' 临期'; } + if (item.has_night_session) { + label += ' 夜盘'; + } div.innerHTML = label + '
最大手数 = floor(权益 × 保证金上限 {{ max_margin_pct }}% ÷ 1手保证金);当前权益 {{ '%.2f'|format(capital) }} 元。 {% if sizing_mode == 'fixed' %}仅显示最大手数 ≥ {{ fixed_lots }} 手的品种。{% endif %} + {% if night_session %}当前为夜盘时段,品种下拉与下表仅显示有夜盘品种;带「夜盘」标记。{% else %}有夜盘交易的品种带「夜盘」标记。{% endif %} 保证金优先读取 CTP 柜台合约信息。 {% if recommend_updated_at %}每日后台更新 · 最近 {{ recommend_updated_at }}{% else %}等待今日后台刷新…{% endif %}
@@ -173,11 +174,11 @@