9875ee6d44
- 止盈止损改为程序本地监控,触发后市价平仓(含跳空) - 交易前30分钟后台自动连接 CTP - 保证金占用上限默认30%,可在系统设置修改 - K线标准蜡烛图红跌绿涨,费率表全宽固定表头 - 品种推荐按保证金比例×总资金计算推荐手数 Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""按账户资金推荐可交易品种(期货核心筛选)。"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import Callable, Optional
|
|
|
|
from contract_specs import get_contract_spec
|
|
from fee_specs import calc_fee_breakdown
|
|
from symbols import PRODUCTS
|
|
|
|
|
|
def _letters_from_ths(ths_code: str) -> str:
|
|
import re
|
|
m = re.match(r"^([A-Za-z]+)", (ths_code or "").strip())
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
def assess_product_for_capital(
|
|
product: dict,
|
|
capital: float,
|
|
price: Optional[float],
|
|
*,
|
|
max_margin_pct: float = 30.0,
|
|
default_stop_ticks: int = 20,
|
|
reward_risk_ratio: float = 2.0,
|
|
trading_mode: str = "simulation",
|
|
) -> dict:
|
|
"""评估单品种在当前资金下是否可交易。"""
|
|
ths = product.get("ths") or ""
|
|
name = product.get("name") or ths
|
|
exchange = product.get("exchange") or ""
|
|
spec = get_contract_spec(ths + "8888")
|
|
mult = spec["mult"]
|
|
margin_rate = spec["margin_rate"]
|
|
tick = float(spec.get("tick_size") or 1.0)
|
|
p = float(price) if price and price > 0 else 0.0
|
|
cap = float(capital or 0)
|
|
margin_pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
|
|
|
if p <= 0:
|
|
return {
|
|
"ths": ths,
|
|
"name": name,
|
|
"exchange": exchange,
|
|
"status": "no_price",
|
|
"status_label": "暂无行情",
|
|
"min_capital_one_lot": None,
|
|
"margin_one_lot": None,
|
|
"recommended_lots": 0,
|
|
"risk_one_lot_1pct": None,
|
|
}
|
|
|
|
margin_one = p * mult * margin_rate
|
|
min_capital = margin_one / (margin_pct / 100.0) if margin_pct > 0 else margin_one
|
|
margin_budget = cap * margin_pct / 100.0 if cap > 0 else 0.0
|
|
recommended_lots = int(math.floor(margin_budget / margin_one)) if margin_one > 0 and margin_budget > 0 else 0
|
|
stop_dist = tick * default_stop_ticks
|
|
risk_one_lot = stop_dist * mult
|
|
risk_pct_1lot = (risk_one_lot / cap * 100) if cap > 0 else 999.0
|
|
ref_sl = round(p - stop_dist, 4)
|
|
ref_tp = round(p + stop_dist * reward_risk_ratio, 4)
|
|
fee_ths = ths + "8888"
|
|
fee_info = calc_fee_breakdown(
|
|
fee_ths, p, p, 1.0, open_time="", close_time="", trading_mode=trading_mode,
|
|
)
|
|
|
|
can_margin = recommended_lots >= 1
|
|
can_risk = cap > 0 and risk_one_lot <= cap * 0.01
|
|
|
|
if can_margin and can_risk:
|
|
status, label = "ok", f"推荐 {recommended_lots} 手"
|
|
elif can_margin:
|
|
status, label = "margin_ok", f"可开 {recommended_lots} 手·止损偏宽"
|
|
else:
|
|
status, label = "blocked", "资金不足"
|
|
|
|
return {
|
|
"ths": ths,
|
|
"name": name,
|
|
"exchange": exchange,
|
|
"price": round(p, 4),
|
|
"mult": mult,
|
|
"tick_size": tick,
|
|
"margin_one_lot": round(margin_one, 2),
|
|
"min_capital_one_lot": round(min_capital, 2),
|
|
"recommended_lots": recommended_lots,
|
|
"margin_budget": round(margin_budget, 2),
|
|
"max_margin_pct": margin_pct,
|
|
"risk_one_lot_1pct": round(risk_one_lot, 2),
|
|
"risk_pct_1lot_at_1pct_rule": round(risk_pct_1lot, 2),
|
|
"ref_stop_loss": ref_sl,
|
|
"ref_take_profit": ref_tp,
|
|
"open_fee_one_lot": fee_info["open_fee"],
|
|
"roundtrip_fee_one_lot": fee_info["total_fee"],
|
|
"status": status,
|
|
"status_label": label,
|
|
}
|
|
|
|
|
|
def list_product_recommendations(
|
|
capital: float,
|
|
quote_fn: Callable[[str], Optional[dict]],
|
|
*,
|
|
max_margin_pct: float = 30.0,
|
|
trading_mode: str = "simulation",
|
|
) -> list[dict]:
|
|
"""扫描全部品种并排序:推荐 > 可开 > 不足。quote_fn(品种代码) -> {price, ths_code, ...}"""
|
|
|
|
def _one(product: dict) -> dict:
|
|
ths = product["ths"]
|
|
quote = quote_fn(ths) or {}
|
|
price = quote.get("price")
|
|
row = assess_product_for_capital(
|
|
product, capital, price,
|
|
max_margin_pct=max_margin_pct,
|
|
trading_mode=trading_mode,
|
|
)
|
|
main_code = (quote.get("ths_code") or "").strip()
|
|
row["main_code"] = main_code
|
|
return row
|
|
|
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
|
rows = list(pool.map(_one, PRODUCTS))
|
|
order = {"ok": 0, "margin_ok": 1, "blocked": 2, "no_price": 3}
|
|
rows.sort(key=lambda r: (order.get(r["status"], 9), -(r.get("recommended_lots") or 0)))
|
|
return rows
|