Limit tradable products to four varieties for accounts at or below 100k.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -789,7 +789,13 @@ def logout():
|
|||||||
@login_required
|
@login_required
|
||||||
def api_symbol_search():
|
def api_symbol_search():
|
||||||
q = request.args.get("q", "")
|
q = request.args.get("q", "")
|
||||||
return jsonify(search_symbols(q))
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
from trading_context import get_account_capital
|
||||||
|
capital = get_account_capital(conn, get_setting)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return jsonify(search_symbols(q, capital=capital))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/symbols/mains")
|
@app.route("/api/symbols/mains")
|
||||||
|
|||||||
+11
-4
@@ -28,10 +28,7 @@ from position_sizing import (
|
|||||||
calc_order_tick_metrics,
|
calc_order_tick_metrics,
|
||||||
normalize_sizing_mode,
|
normalize_sizing_mode,
|
||||||
)
|
)
|
||||||
from recommend_store import (
|
from product_recommend import assert_product_allowed_for_capital, is_small_account, SMALL_ACCOUNT_SCOPE_LABEL
|
||||||
recommend_payload,
|
|
||||||
refresh_recommend_cache,
|
|
||||||
)
|
|
||||||
from recommend_stream import recommend_hub, schedule_recommend_refresh, start_recommend_worker
|
from recommend_stream import recommend_hub, schedule_recommend_refresh, start_recommend_worker
|
||||||
from position_stream import position_hub, start_position_worker
|
from position_stream import position_hub, start_position_worker
|
||||||
from ctp_settings import is_ctp_auto_connect_enabled
|
from ctp_settings import is_ctp_auto_connect_enabled
|
||||||
@@ -1645,6 +1642,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
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(),
|
night_session=is_night_trading_session(),
|
||||||
|
small_account_scope=is_small_account(capital),
|
||||||
|
small_account_scope_label=SMALL_ACCOUNT_SCOPE_LABEL,
|
||||||
product_categories=PRODUCT_CATEGORIES,
|
product_categories=PRODUCT_CATEGORIES,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
@@ -2148,6 +2147,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
if err:
|
if err:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": err}), 403
|
return jsonify({"ok": False, "error": err}), 403
|
||||||
|
scope_err = assert_product_allowed_for_capital(sym, _capital(conn))
|
||||||
|
if scope_err:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": False, "error": scope_err}), 403
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
if not ctp_st.get("connected"):
|
if not ctp_st.get("connected"):
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -2503,6 +2506,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": err}), 403
|
return jsonify({"ok": False, "error": err}), 403
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
|
scope_err = assert_product_allowed_for_capital(sym, capital)
|
||||||
|
if scope_err:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": False, "error": scope_err}), 403
|
||||||
codes = ths_to_codes(sym)
|
codes = ths_to_codes(sym)
|
||||||
price = fetch_price(sym, codes.get("market_code", "") if codes else "", codes.get("sina_code", "") if codes else "")
|
price = fetch_price(sym, codes.get("market_code", "") if codes else "", codes.get("sina_code", "") if codes else "")
|
||||||
plan, perr = compute_trend_plan_futures(
|
plan, perr = compute_trend_plan_futures(
|
||||||
|
|||||||
+65
-1
@@ -18,6 +18,52 @@ from symbols import PRODUCTS, product_category, product_has_night_session
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 权益低于该值时,仅允许下列品种(可开仓列表、品种下拉、开仓报单)
|
||||||
|
SMALL_ACCOUNT_CAPITAL_MAX = 100_000.0
|
||||||
|
SMALL_ACCOUNT_PRODUCT_THS = frozenset({"c", "m", "MA", "rb"})
|
||||||
|
SMALL_ACCOUNT_SCOPE_LABEL = "玉米、豆粕、甲醇、螺纹钢"
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_product_ths(ths: str) -> str:
|
||||||
|
import re
|
||||||
|
s = (ths or "").strip()
|
||||||
|
m = re.match(r"^([A-Za-z]+)", s)
|
||||||
|
return m.group(1) if m else s
|
||||||
|
|
||||||
|
|
||||||
|
def is_small_account(capital: float) -> bool:
|
||||||
|
cap = float(capital or 0)
|
||||||
|
return 0 < cap <= SMALL_ACCOUNT_CAPITAL_MAX
|
||||||
|
|
||||||
|
|
||||||
|
def product_in_small_account_whitelist(ths_or_product) -> bool:
|
||||||
|
if isinstance(ths_or_product, dict):
|
||||||
|
key = (ths_or_product.get("ths") or "").strip()
|
||||||
|
else:
|
||||||
|
key = normalize_product_ths(str(ths_or_product or ""))
|
||||||
|
if not key:
|
||||||
|
return False
|
||||||
|
root = normalize_product_ths(key)
|
||||||
|
if root in SMALL_ACCOUNT_PRODUCT_THS:
|
||||||
|
return True
|
||||||
|
upper = root.upper()
|
||||||
|
return upper in {x.upper() for x in SMALL_ACCOUNT_PRODUCT_THS}
|
||||||
|
|
||||||
|
|
||||||
|
def assert_product_allowed_for_capital(ths: str, capital: float) -> Optional[str]:
|
||||||
|
"""小账户品种白名单校验;通过返回 None。"""
|
||||||
|
if not is_small_account(capital):
|
||||||
|
return None
|
||||||
|
if product_in_small_account_whitelist(ths):
|
||||||
|
return None
|
||||||
|
return f"权益 10 万以下仅可交易:{SMALL_ACCOUNT_SCOPE_LABEL}"
|
||||||
|
|
||||||
|
|
||||||
|
def filter_products_for_capital(products: list[dict], capital: float) -> list[dict]:
|
||||||
|
if not is_small_account(capital):
|
||||||
|
return list(products)
|
||||||
|
return [p for p in products if product_in_small_account_whitelist(p)]
|
||||||
|
|
||||||
|
|
||||||
def _attach_turnover(row: dict) -> None:
|
def _attach_turnover(row: dict) -> None:
|
||||||
"""成交额 = 昨日成交量(手) × 昨收 × 合约乘数。"""
|
"""成交额 = 昨日成交量(手) × 昨收 × 合约乘数。"""
|
||||||
@@ -60,6 +106,23 @@ def assess_product_for_capital(
|
|||||||
cap = float(capital or 0)
|
cap = float(capital or 0)
|
||||||
margin_pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
margin_pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
||||||
|
|
||||||
|
if is_small_account(cap) and not product_in_small_account_whitelist(product):
|
||||||
|
return {
|
||||||
|
"ths": ths,
|
||||||
|
"name": name,
|
||||||
|
"exchange": exchange,
|
||||||
|
"category": category,
|
||||||
|
"mult": spec["mult"],
|
||||||
|
"tick_size": tick,
|
||||||
|
"status": "blocked",
|
||||||
|
"status_label": f"10万以下限{SMALL_ACCOUNT_SCOPE_LABEL}",
|
||||||
|
"min_capital_one_lot": None,
|
||||||
|
"margin_one_lot": None,
|
||||||
|
"max_lots": 0,
|
||||||
|
"risk_one_lot_1pct": None,
|
||||||
|
"has_night_session": product_has_night_session(product),
|
||||||
|
}
|
||||||
|
|
||||||
if p <= 0:
|
if p <= 0:
|
||||||
return {
|
return {
|
||||||
"ths": ths,
|
"ths": ths,
|
||||||
@@ -173,5 +236,6 @@ def list_product_recommendations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=10) as pool:
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
||||||
rows = list(pool.map(_one, PRODUCTS))
|
products = filter_products_for_capital(PRODUCTS, capital)
|
||||||
|
rows = list(pool.map(_one, products))
|
||||||
return sort_recommend_by_trend(rows)
|
return sort_recommend_by_trend(rows)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Deploy small-account product whitelist."""
|
||||||
|
import paramiko, sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
files = ["product_recommend.py", "symbols.py", "app.py", "install_trading.py", "templates/trade.html"]
|
||||||
|
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()
|
||||||
|
_, o, _ = c.exec_command("cd /opt/qihuo && pm2 restart qihuo")
|
||||||
|
print(o.read().decode("utf-8", errors="replace"))
|
||||||
|
c.close()
|
||||||
+12
-2
@@ -461,20 +461,25 @@ def _match_score(product: dict, q_lower: str) -> int:
|
|||||||
return 10
|
return 10
|
||||||
|
|
||||||
|
|
||||||
def search_symbols(query: str) -> list:
|
def search_symbols(query: str, *, capital: float | None = None) -> list:
|
||||||
q = query.strip()
|
q = query.strip()
|
||||||
if not q:
|
if not q:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
q_lower = q.lower()
|
q_lower = q.lower()
|
||||||
from market_sessions import is_night_trading_session
|
from market_sessions import is_night_trading_session
|
||||||
|
from product_recommend import filter_products_for_capital, is_small_account
|
||||||
|
|
||||||
night_only = is_night_trading_session()
|
night_only = is_night_trading_session()
|
||||||
|
product_pool = PRODUCTS
|
||||||
|
if capital is not None and is_small_account(capital):
|
||||||
|
product_pool = filter_products_for_capital(PRODUCTS, capital)
|
||||||
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 product_pool:
|
||||||
if night_only and not product_has_night_session(p):
|
if night_only and not product_has_night_session(p):
|
||||||
continue
|
continue
|
||||||
if not _product_matches(p, q_lower):
|
if not _product_matches(p, q_lower):
|
||||||
@@ -492,6 +497,11 @@ def search_symbols(query: str) -> list:
|
|||||||
if not results and len(q) >= 3:
|
if not results and len(q) >= 3:
|
||||||
codes = ths_to_codes(q)
|
codes = ths_to_codes(q)
|
||||||
if codes:
|
if codes:
|
||||||
|
product = _product_for_contract_code(codes["ths_code"])
|
||||||
|
if capital is not None and is_small_account(capital):
|
||||||
|
from product_recommend import is_small_account, product_in_small_account_whitelist
|
||||||
|
if not product or not product_in_small_account_whitelist(product):
|
||||||
|
return results
|
||||||
raw = fetch_raw_for_volume(codes["sina_code"])
|
raw = fetch_raw_for_volume(codes["sina_code"])
|
||||||
name = raw["name"] if raw else q
|
name = raw["name"] if raw else q
|
||||||
results.append(_enrich_item({
|
results.append(_enrich_item({
|
||||||
|
|||||||
@@ -133,7 +133,8 @@
|
|||||||
<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 %}
|
{% if small_account_scope %}<span class="text-muted">权益 10 万以下仅显示并可交易:{{ small_account_scope_label }}。</span>{% endif %}
|
||||||
|
{% if night_session %}<span class="text-muted">当前为夜盘时段,品种下拉与下表仅显示有夜盘品种;带「夜盘」标记。</span>{% elif not small_account_scope %}<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user