Deduct open position margin from recommend max lots.
Recalculate tradable symbol budgets from remaining margin after CTP usage and refresh the table on position updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1578,6 +1578,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
try:
|
||||
payload = _refresh_trading_live_snapshot(fast=fast)
|
||||
position_hub.broadcast("positions", payload)
|
||||
conn = get_db()
|
||||
try:
|
||||
rec = _recommend_payload(conn)
|
||||
recommend_hub.broadcast("recommend", {"ok": True, **rec})
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
logger.debug("push position snapshot: %s", exc)
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ def assess_product_for_capital(
|
||||
trading_mode: str = "simulation",
|
||||
ctp_connected: bool = True,
|
||||
main_code: str = "",
|
||||
margin_used: float = 0.0,
|
||||
) -> dict:
|
||||
"""评估单品种在当前资金下是否可交易。"""
|
||||
ths = product.get("ths") or ""
|
||||
@@ -225,6 +226,7 @@ def assess_product_for_capital(
|
||||
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
|
||||
margin_budget = max(0.0, margin_budget - max(0.0, float(margin_used or 0)))
|
||||
max_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
|
||||
@@ -287,6 +289,7 @@ def list_product_recommendations(
|
||||
max_margin_pct: float = 30.0,
|
||||
trading_mode: str = "simulation",
|
||||
ctp_connected: bool = True,
|
||||
margin_used: float = 0.0,
|
||||
) -> list[dict]:
|
||||
"""扫描全部品种并排序:可开且纪律友好 > 可开 > 不足。quote_fn(品种代码) -> {price, ths_code, ...}"""
|
||||
|
||||
@@ -302,6 +305,7 @@ def list_product_recommendations(
|
||||
trading_mode=trading_mode,
|
||||
ctp_connected=ctp_connected,
|
||||
main_code=main_code,
|
||||
margin_used=margin_used,
|
||||
)
|
||||
row["main_code"] = main_code
|
||||
if main_code:
|
||||
|
||||
+61
-3
@@ -117,17 +117,61 @@ def _ctp_connected_for_mode(trading_mode: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def recommend_margin_used(trading_mode: str) -> float:
|
||||
"""当前持仓已占用保证金(CTP 柜台优先)。"""
|
||||
if not _ctp_connected_for_mode(trading_mode):
|
||||
return 0.0
|
||||
try:
|
||||
from vnpy_bridge import ctp_account_margin_used, ctp_list_positions
|
||||
|
||||
used = ctp_account_margin_used(trading_mode)
|
||||
if used is not None and used > 0:
|
||||
return float(used)
|
||||
total = 0.0
|
||||
for p in ctp_list_positions(
|
||||
trading_mode, refresh_if_empty=False, refresh_margin=True,
|
||||
):
|
||||
m = float(p.get("margin") or 0)
|
||||
if m > 0:
|
||||
total += m
|
||||
return round(total, 2) if total > 0 else 0.0
|
||||
except Exception as exc:
|
||||
logger.debug("recommend_margin_used: %s", exc)
|
||||
return 0.0
|
||||
|
||||
|
||||
def margin_budget_info(
|
||||
capital: float,
|
||||
max_margin_pct: float,
|
||||
margin_used: float = 0.0,
|
||||
) -> dict[str, float]:
|
||||
"""保证金上限总额、已占用、剩余可开额度。"""
|
||||
cap = float(capital or 0)
|
||||
pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
||||
total = cap * pct / 100.0 if cap > 0 else 0.0
|
||||
used = max(0.0, float(margin_used or 0))
|
||||
remaining = max(0.0, total - used)
|
||||
return {
|
||||
"margin_budget_total": round(total, 2),
|
||||
"margin_used": round(used, 2),
|
||||
"margin_budget_remaining": round(remaining, 2),
|
||||
"max_margin_pct": pct,
|
||||
}
|
||||
|
||||
|
||||
def enrich_recommend_rows(
|
||||
rows: list[dict],
|
||||
capital: float,
|
||||
*,
|
||||
max_margin_pct: float = 30.0,
|
||||
trading_mode: str = "simulation",
|
||||
margin_used: float = 0.0,
|
||||
) -> list[dict]:
|
||||
"""用当前权益与保证金比例补算最大可开手数(兼容旧缓存)。"""
|
||||
cap = float(capital or 0)
|
||||
pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
||||
budget = cap * pct / 100.0 if cap > 0 else 0.0
|
||||
budget_info = margin_budget_info(cap, max_margin_pct, margin_used)
|
||||
pct = budget_info["max_margin_pct"]
|
||||
budget = budget_info["margin_budget_remaining"]
|
||||
ctp_connected = _ctp_connected_for_mode(trading_mode)
|
||||
enriched: list[dict] = []
|
||||
for raw in rows:
|
||||
@@ -172,6 +216,8 @@ def enrich_recommend_rows(
|
||||
row["max_lots"] = lots
|
||||
row.pop("recommended_lots", None)
|
||||
row["margin_budget"] = round(budget, 2)
|
||||
row["margin_budget_total"] = budget_info["margin_budget_total"]
|
||||
row["margin_used"] = budget_info["margin_used"]
|
||||
row["max_margin_pct"] = pct
|
||||
status = row.get("status") or ""
|
||||
if lots >= 1 and status in ("ok", "margin_ok"):
|
||||
@@ -181,6 +227,8 @@ def enrich_recommend_rows(
|
||||
)
|
||||
if row.get("margin_source") == "ctp":
|
||||
row["status_label"] += f"({src}保证金)"
|
||||
if budget_info["margin_used"] > 0:
|
||||
row["status_label"] += "·扣持仓"
|
||||
elif lots < 1 and status in ("ok", "margin_ok"):
|
||||
row["status"] = "blocked"
|
||||
row["status_label"] = "资金不足"
|
||||
@@ -214,17 +262,24 @@ def refresh_recommend_cache(
|
||||
*,
|
||||
trading_mode: str = "simulation",
|
||||
max_margin_pct: float = 30.0,
|
||||
margin_used: float | None = None,
|
||||
) -> list[dict]:
|
||||
"""后台拉行情、筛选并写入数据库。"""
|
||||
ensure_recommend_tables(conn)
|
||||
ensure_fee_rates_schema(conn)
|
||||
ctp_connected = _ctp_connected_for_mode(trading_mode)
|
||||
used = (
|
||||
float(margin_used)
|
||||
if margin_used is not None
|
||||
else recommend_margin_used(trading_mode)
|
||||
)
|
||||
all_rows = list_product_recommendations(
|
||||
capital,
|
||||
quote_fn,
|
||||
max_margin_pct=max_margin_pct,
|
||||
trading_mode=trading_mode,
|
||||
ctp_connected=ctp_connected,
|
||||
margin_used=used,
|
||||
)
|
||||
rows = filter_affordable_recommendations(all_rows)
|
||||
if not rows and float(capital or 0) > 0:
|
||||
@@ -293,11 +348,14 @@ def recommend_payload(
|
||||
payload = load_recommend_cache(conn)
|
||||
cap = float(live_capital or 0)
|
||||
pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
||||
used = recommend_margin_used(trading_mode)
|
||||
budget_info = margin_budget_info(cap, pct, used)
|
||||
payload["capital"] = cap
|
||||
payload["max_margin_pct"] = pct
|
||||
payload.update(budget_info)
|
||||
rows = payload.get("rows") or []
|
||||
rows = enrich_recommend_rows(
|
||||
rows, cap, max_margin_pct=pct, trading_mode=trading_mode,
|
||||
rows, cap, max_margin_pct=pct, trading_mode=trading_mode, margin_used=used,
|
||||
)
|
||||
rows = filter_rows_for_account_scope(
|
||||
rows, cap, ctp_connected=_ctp_connected_for_mode(trading_mode),
|
||||
|
||||
+24
-4
@@ -38,6 +38,7 @@
|
||||
var recommendMaxByProduct = {};
|
||||
var recommendMaxByCode = {};
|
||||
var recRowsRaw = [];
|
||||
var recMeta = {};
|
||||
var recSortKey = 'trend';
|
||||
var recSortDesc = true;
|
||||
var recIndustryFilter = '';
|
||||
@@ -1454,7 +1455,7 @@
|
||||
return counts;
|
||||
}
|
||||
|
||||
function updateRecStats(allRows, visibleRows) {
|
||||
function updateRecStats(allRows, visibleRows, meta) {
|
||||
var el = document.getElementById('rec-stats');
|
||||
if (!el) return;
|
||||
var total = (allRows || []).length;
|
||||
@@ -1464,6 +1465,12 @@
|
||||
return;
|
||||
}
|
||||
var parts = [];
|
||||
if (meta && meta.margin_used > 0) {
|
||||
parts.push(
|
||||
'持仓占用 <strong>' + fmtNum(meta.margin_used) + '</strong> 元,' +
|
||||
'剩余额度 <strong class="text-accent">' + fmtNum(meta.margin_budget_remaining) + '</strong> 元'
|
||||
);
|
||||
}
|
||||
if (recIndustryFilter) {
|
||||
parts.push('筛选 <strong>' + shown + '</strong> / 共 ' + total + ' 个品种');
|
||||
} else {
|
||||
@@ -1641,15 +1648,28 @@
|
||||
function renderRecommendTable() {
|
||||
var filtered = filterRecommendRows(recRowsRaw);
|
||||
var sorted = sortRecommendRows(filtered);
|
||||
updateRecStats(recRowsRaw, sorted);
|
||||
updateRecStats(recRowsRaw, sorted, recMeta);
|
||||
renderRecommendRows(sorted);
|
||||
}
|
||||
|
||||
function renderRecommendations(data) {
|
||||
if (!recommendList || !data) return;
|
||||
updateRecommendMaxMaps(data);
|
||||
recMeta = {
|
||||
margin_used: data.margin_used || 0,
|
||||
margin_budget_remaining: data.margin_budget_remaining,
|
||||
margin_budget_total: data.margin_budget_total
|
||||
};
|
||||
var recCap = document.getElementById('rec-capital');
|
||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
||||
var recMarginHint = document.getElementById('rec-margin-hint');
|
||||
if (recMarginHint) {
|
||||
if (recMeta.margin_used > 0) {
|
||||
recMarginHint.textContent = ' · 已扣持仓占用 ' + fmtNum(recMeta.margin_used) + ' 元';
|
||||
} else {
|
||||
recMarginHint.textContent = '';
|
||||
}
|
||||
}
|
||||
var recUpdated = document.getElementById('rec-updated');
|
||||
if (recUpdated && data.updated_at) {
|
||||
recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at;
|
||||
@@ -1658,7 +1678,7 @@
|
||||
recRowsRaw = rows.slice();
|
||||
if (!rows.length) {
|
||||
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||
updateRecStats([], []);
|
||||
updateRecStats([], [], recMeta);
|
||||
return;
|
||||
}
|
||||
renderRecommendTable();
|
||||
@@ -1692,7 +1712,7 @@
|
||||
renderRecommendTable();
|
||||
});
|
||||
}
|
||||
if (recRowsRaw.length) updateRecStats(recRowsRaw, filterRecommendRows(recRowsRaw));
|
||||
if (recRowsRaw.length) updateRecStats(recRowsRaw, filterRecommendRows(recRowsRaw), recMeta);
|
||||
}
|
||||
|
||||
function connectRecommendStream() {
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<div class="card trade-card trade-card-full" id="recommend">
|
||||
<h2>可开仓品种</h2>
|
||||
<div class="card-body">
|
||||
<p class="hint">最大手数 = floor(权益 × 保证金上限 <strong>{{ max_margin_pct }}%</strong> ÷ 1手保证金);当前权益 <strong class="text-accent" id="rec-capital">{{ '%.2f'|format(recommend_capital) }}</strong> 元。
|
||||
<p class="hint">最大手数 = floor((权益 × 保证金上限 <strong>{{ max_margin_pct }}%</strong> − 已占用保证金) ÷ 1手保证金);当前权益 <strong class="text-accent" id="rec-capital">{{ '%.2f'|format(recommend_capital) }}</strong> 元<span id="rec-margin-hint" class="text-muted"></span>。
|
||||
{% if sizing_mode == 'fixed' %}仅显示最大手数 ≥ <strong>{{ fixed_lots }}</strong> 手的品种。{% endif %}
|
||||
{% if small_account_scope %}<span class="text-muted">{{ small_account_scope_hint }}。</span>{% endif %}
|
||||
{% if small_account_margin_rec %}<span class="text-muted">{{ small_account_margin_rec.label }}。</span>{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user