Add key-level auto trade, AI analysis, and trading UX improvements.
Key monitors use 5m close triggers with WeChat alerts and box/convergence auto orders; add pending-order worker, structured WeChat notify, AI settings/messages, session clock, CTP margin sizing, and dual-layer position limits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+121
-22
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from typing import Optional
|
||||
|
||||
from contract_specs import get_contract_spec
|
||||
from contract_specs import get_contract_spec, margin_one_lot
|
||||
|
||||
MODE_FIXED = "fixed"
|
||||
MODE_AMOUNT = "amount"
|
||||
@@ -57,37 +57,56 @@ def calc_lots_by_amount(
|
||||
capital: float = 0.0,
|
||||
max_lots: Optional[int] = None,
|
||||
max_margin_pct: float = 30.0,
|
||||
) -> tuple[Optional[int], Optional[str]]:
|
||||
"""固定金额:按止损距离将金额换算为手数。"""
|
||||
trading_mode: str | None = None,
|
||||
) -> tuple[Optional[int], Optional[str], dict]:
|
||||
"""固定金额:先按止损距离算手数,再按保证金上限收紧。返回 (手数, 错误, 详情)。"""
|
||||
info: dict = {
|
||||
"lots_by_risk": 0,
|
||||
"lots_by_margin": None,
|
||||
"capped_by": None,
|
||||
}
|
||||
try:
|
||||
entry_f = float(entry)
|
||||
sl_f = float(stop_loss)
|
||||
budget = float(amount)
|
||||
cap = float(capital or 0)
|
||||
except (TypeError, ValueError):
|
||||
return None, "参数格式错误"
|
||||
return None, "参数格式错误", info
|
||||
if entry_f <= 0 or budget <= 0:
|
||||
return None, "入场价或固定金额无效"
|
||||
return None, "入场价或固定金额无效", info
|
||||
per_lot_risk, err = _per_lot_risk(entry_f, sl_f, direction, ths_code)
|
||||
if err:
|
||||
return None, err
|
||||
return None, err, info
|
||||
lots = int(math.floor(budget / per_lot_risk))
|
||||
info["lots_by_risk"] = lots
|
||||
if lots < 1:
|
||||
return None, f"按固定金额 {budget:.0f} 元,当前止损距离下不足 1 手"
|
||||
return None, f"按固定金额 {budget:.0f} 元,当前止损距离下不足 1 手", info
|
||||
if cap > 0:
|
||||
spec = get_contract_spec(ths_code)
|
||||
margin_per_lot = entry_f * spec["mult"] * spec["margin_rate"]
|
||||
margin_per_lot, _src = margin_one_lot(
|
||||
ths_code, entry_f, direction=direction, trading_mode=trading_mode,
|
||||
)
|
||||
if margin_per_lot <= 0:
|
||||
spec = get_contract_spec(ths_code)
|
||||
margin_per_lot = entry_f * spec["mult"] * spec["margin_rate"]
|
||||
margin_cap = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
||||
max_by_margin = (
|
||||
int(math.floor(cap * margin_cap / 100.0 / margin_per_lot))
|
||||
if margin_per_lot > 0 else lots
|
||||
)
|
||||
info["lots_by_margin"] = max_by_margin
|
||||
info["margin_per_lot"] = round(margin_per_lot, 2)
|
||||
info["max_margin_pct"] = margin_cap
|
||||
if max_by_margin < 1:
|
||||
return None, f"按保证金上限 {margin_cap:g}%,当前不足 1 手"
|
||||
return None, f"按保证金上限 {margin_cap:g}%,当前不足 1 手", info
|
||||
if max_by_margin < lots:
|
||||
info["capped_by"] = "margin"
|
||||
lots = min(lots, max_by_margin)
|
||||
cap_lots = max_lots if max_lots is not None else DEFAULT_MAX_ORDER_LOTS
|
||||
lots = min(lots, cap_lots)
|
||||
return lots, None
|
||||
if lots > cap_lots:
|
||||
lots = cap_lots
|
||||
info["capped_by"] = info.get("capped_by") or "max_lots"
|
||||
info["lots"] = lots
|
||||
return lots, None, info
|
||||
|
||||
|
||||
def calc_lots_by_risk(
|
||||
@@ -100,6 +119,7 @@ def calc_lots_by_risk(
|
||||
*,
|
||||
max_lots: Optional[int] = None,
|
||||
max_margin_pct: float = 30.0,
|
||||
trading_mode: str | None = None,
|
||||
) -> tuple[Optional[int], Optional[str]]:
|
||||
"""策略等场景:按权益百分比风险预算换算手数。"""
|
||||
try:
|
||||
@@ -110,13 +130,22 @@ def calc_lots_by_risk(
|
||||
if cap <= 0 or rp <= 0:
|
||||
return None, "资金或风险比例无效"
|
||||
budget = cap * rp / 100.0
|
||||
return calc_lots_by_amount(
|
||||
lots, err, info = calc_lots_by_amount(
|
||||
entry, stop_loss, direction, budget, ths_code,
|
||||
capital=cap, max_lots=max_lots, max_margin_pct=max_margin_pct,
|
||||
trading_mode=trading_mode,
|
||||
)
|
||||
return lots, err
|
||||
|
||||
|
||||
def calc_order_tick_metrics(ths_code: str, lots: float, price: Optional[float] = None) -> dict:
|
||||
def calc_order_tick_metrics(
|
||||
ths_code: str,
|
||||
lots: float,
|
||||
price: Optional[float] = None,
|
||||
*,
|
||||
direction: str = "long",
|
||||
trading_mode: str | None = None,
|
||||
) -> dict:
|
||||
"""下单区展示:最小变动价位、每跳盈亏、保证金等。"""
|
||||
spec = get_contract_spec(ths_code)
|
||||
mult = int(spec["mult"])
|
||||
@@ -127,7 +156,22 @@ def calc_order_tick_metrics(ths_code: str, lots: float, price: Optional[float] =
|
||||
tick_value_total = round(tick_value_per_lot * lots_i, 2)
|
||||
prec = price_precision_from_tick(tick)
|
||||
mark = float(price) if price else 0.0
|
||||
margin_per_lot = round(mark * mult * margin_rate, 2) if mark > 0 else None
|
||||
margin_per_lot = None
|
||||
margin_source = "estimate"
|
||||
if mark > 0:
|
||||
margin_per_lot, margin_source, spec_used = margin_one_lot(
|
||||
ths_code, mark, direction=direction, trading_mode=trading_mode,
|
||||
)
|
||||
if spec_used.get("mult"):
|
||||
mult = int(spec_used["mult"])
|
||||
if spec_used.get("tick_size"):
|
||||
tick = float(spec_used["tick_size"])
|
||||
tick_value_per_lot = round(tick * mult, 4)
|
||||
tick_value_total = round(tick_value_per_lot * lots_i, 2)
|
||||
prec = price_precision_from_tick(tick)
|
||||
if margin_per_lot <= 0:
|
||||
margin_per_lot = round(mark * mult * margin_rate, 2)
|
||||
margin_source = "estimate"
|
||||
margin_total = round(margin_per_lot * lots_i, 2) if margin_per_lot else None
|
||||
return {
|
||||
"mult": mult,
|
||||
@@ -139,6 +183,7 @@ def calc_order_tick_metrics(ths_code: str, lots: float, price: Optional[float] =
|
||||
"margin_per_lot": margin_per_lot,
|
||||
"margin_total": margin_total,
|
||||
"margin_rate": margin_rate,
|
||||
"margin_source": margin_source,
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +194,8 @@ def calc_margin_usage_pct(
|
||||
extra_symbol: str = "",
|
||||
extra_lots: int = 0,
|
||||
extra_price: float = 0,
|
||||
extra_direction: str = "long",
|
||||
trading_mode: str | None = None,
|
||||
) -> float:
|
||||
"""当前持仓 + 拟开仓占权益的保证金比例(%)。"""
|
||||
cap = float(capital or 0)
|
||||
@@ -159,13 +206,65 @@ def calc_margin_usage_pct(
|
||||
lots = int(p.get("lots") or 0)
|
||||
if lots <= 0:
|
||||
continue
|
||||
sym = (p.get("symbol") or "").strip()
|
||||
entry = float(p.get("avg_price") or p.get("entry_price") or 0)
|
||||
if entry <= 0:
|
||||
ctp_margin = float(p.get("margin") or 0)
|
||||
if ctp_margin > 0:
|
||||
total += ctp_margin
|
||||
continue
|
||||
spec = get_contract_spec(sym)
|
||||
total += entry * spec["mult"] * lots * spec["margin_rate"]
|
||||
sym = (p.get("symbol") or p.get("symbol_code") or "").strip()
|
||||
entry = float(p.get("avg_price") or p.get("entry_price") or 0)
|
||||
direction = (p.get("direction") or "long").strip().lower()
|
||||
if entry <= 0 or not sym:
|
||||
continue
|
||||
per_lot, _, _ = margin_one_lot(
|
||||
sym, entry, direction=direction, trading_mode=trading_mode,
|
||||
)
|
||||
if per_lot <= 0:
|
||||
spec = get_contract_spec(sym)
|
||||
per_lot = entry * spec["mult"] * spec["margin_rate"]
|
||||
total += per_lot * lots
|
||||
if extra_symbol and extra_lots > 0 and extra_price > 0:
|
||||
spec = get_contract_spec(extra_symbol)
|
||||
total += extra_price * spec["mult"] * extra_lots * spec["margin_rate"]
|
||||
per_lot, _, _ = margin_one_lot(
|
||||
extra_symbol, extra_price, direction=extra_direction, trading_mode=trading_mode,
|
||||
)
|
||||
if per_lot <= 0:
|
||||
spec = get_contract_spec(extra_symbol)
|
||||
per_lot = extra_price * spec["mult"] * spec["margin_rate"]
|
||||
total += per_lot * extra_lots
|
||||
return round(total / cap * 100.0, 2)
|
||||
|
||||
|
||||
def cap_lots_for_margin_budget(
|
||||
positions: list[dict],
|
||||
capital: float,
|
||||
symbol: str,
|
||||
direction: str,
|
||||
price: float,
|
||||
desired_lots: int,
|
||||
max_margin_pct: float,
|
||||
trading_mode: str | None = None,
|
||||
) -> tuple[int, float]:
|
||||
"""在保证金上限内,返回可加仓手数及占用比例。"""
|
||||
desired = max(0, int(desired_lots or 0))
|
||||
if desired <= 0:
|
||||
return 0, calc_margin_usage_pct(positions, capital, trading_mode=trading_mode)
|
||||
for lots in range(desired, 0, -1):
|
||||
usage = calc_margin_usage_pct(
|
||||
positions,
|
||||
capital,
|
||||
extra_symbol=symbol,
|
||||
extra_lots=lots,
|
||||
extra_price=price,
|
||||
extra_direction=direction,
|
||||
trading_mode=trading_mode,
|
||||
)
|
||||
if usage <= max_margin_pct:
|
||||
return lots, usage
|
||||
return 0, calc_margin_usage_pct(
|
||||
positions,
|
||||
capital,
|
||||
extra_symbol=symbol,
|
||||
extra_lots=desired,
|
||||
extra_price=price,
|
||||
extra_direction=direction,
|
||||
trading_mode=trading_mode,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user