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:
|
try:
|
||||||
payload = _refresh_trading_live_snapshot(fast=fast)
|
payload = _refresh_trading_live_snapshot(fast=fast)
|
||||||
position_hub.broadcast("positions", payload)
|
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:
|
except Exception as exc:
|
||||||
logger.debug("push position snapshot: %s", exc)
|
logger.debug("push position snapshot: %s", exc)
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ def assess_product_for_capital(
|
|||||||
trading_mode: str = "simulation",
|
trading_mode: str = "simulation",
|
||||||
ctp_connected: bool = True,
|
ctp_connected: bool = True,
|
||||||
main_code: str = "",
|
main_code: str = "",
|
||||||
|
margin_used: float = 0.0,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""评估单品种在当前资金下是否可交易。"""
|
"""评估单品种在当前资金下是否可交易。"""
|
||||||
ths = product.get("ths") or ""
|
ths = product.get("ths") or ""
|
||||||
@@ -225,6 +226,7 @@ def assess_product_for_capital(
|
|||||||
margin_one = p * mult * margin_rate
|
margin_one = p * mult * margin_rate
|
||||||
min_capital = margin_one / (margin_pct / 100.0) if margin_pct > 0 else margin_one
|
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 = 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
|
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
|
stop_dist = tick * default_stop_ticks
|
||||||
risk_one_lot = stop_dist * mult
|
risk_one_lot = stop_dist * mult
|
||||||
@@ -287,6 +289,7 @@ def list_product_recommendations(
|
|||||||
max_margin_pct: float = 30.0,
|
max_margin_pct: float = 30.0,
|
||||||
trading_mode: str = "simulation",
|
trading_mode: str = "simulation",
|
||||||
ctp_connected: bool = True,
|
ctp_connected: bool = True,
|
||||||
|
margin_used: float = 0.0,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""扫描全部品种并排序:可开且纪律友好 > 可开 > 不足。quote_fn(品种代码) -> {price, ths_code, ...}"""
|
"""扫描全部品种并排序:可开且纪律友好 > 可开 > 不足。quote_fn(品种代码) -> {price, ths_code, ...}"""
|
||||||
|
|
||||||
@@ -302,6 +305,7 @@ def list_product_recommendations(
|
|||||||
trading_mode=trading_mode,
|
trading_mode=trading_mode,
|
||||||
ctp_connected=ctp_connected,
|
ctp_connected=ctp_connected,
|
||||||
main_code=main_code,
|
main_code=main_code,
|
||||||
|
margin_used=margin_used,
|
||||||
)
|
)
|
||||||
row["main_code"] = main_code
|
row["main_code"] = main_code
|
||||||
if main_code:
|
if main_code:
|
||||||
|
|||||||
+61
-3
@@ -117,17 +117,61 @@ def _ctp_connected_for_mode(trading_mode: str) -> bool:
|
|||||||
return False
|
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(
|
def enrich_recommend_rows(
|
||||||
rows: list[dict],
|
rows: list[dict],
|
||||||
capital: float,
|
capital: float,
|
||||||
*,
|
*,
|
||||||
max_margin_pct: float = 30.0,
|
max_margin_pct: float = 30.0,
|
||||||
trading_mode: str = "simulation",
|
trading_mode: str = "simulation",
|
||||||
|
margin_used: float = 0.0,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""用当前权益与保证金比例补算最大可开手数(兼容旧缓存)。"""
|
"""用当前权益与保证金比例补算最大可开手数(兼容旧缓存)。"""
|
||||||
cap = float(capital or 0)
|
cap = float(capital or 0)
|
||||||
pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
budget_info = margin_budget_info(cap, max_margin_pct, margin_used)
|
||||||
budget = cap * pct / 100.0 if cap > 0 else 0.0
|
pct = budget_info["max_margin_pct"]
|
||||||
|
budget = budget_info["margin_budget_remaining"]
|
||||||
ctp_connected = _ctp_connected_for_mode(trading_mode)
|
ctp_connected = _ctp_connected_for_mode(trading_mode)
|
||||||
enriched: list[dict] = []
|
enriched: list[dict] = []
|
||||||
for raw in rows:
|
for raw in rows:
|
||||||
@@ -172,6 +216,8 @@ def enrich_recommend_rows(
|
|||||||
row["max_lots"] = lots
|
row["max_lots"] = lots
|
||||||
row.pop("recommended_lots", None)
|
row.pop("recommended_lots", None)
|
||||||
row["margin_budget"] = round(budget, 2)
|
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
|
row["max_margin_pct"] = pct
|
||||||
status = row.get("status") or ""
|
status = row.get("status") or ""
|
||||||
if lots >= 1 and status in ("ok", "margin_ok"):
|
if lots >= 1 and status in ("ok", "margin_ok"):
|
||||||
@@ -181,6 +227,8 @@ def enrich_recommend_rows(
|
|||||||
)
|
)
|
||||||
if row.get("margin_source") == "ctp":
|
if row.get("margin_source") == "ctp":
|
||||||
row["status_label"] += f"({src}保证金)"
|
row["status_label"] += f"({src}保证金)"
|
||||||
|
if budget_info["margin_used"] > 0:
|
||||||
|
row["status_label"] += "·扣持仓"
|
||||||
elif lots < 1 and status in ("ok", "margin_ok"):
|
elif lots < 1 and status in ("ok", "margin_ok"):
|
||||||
row["status"] = "blocked"
|
row["status"] = "blocked"
|
||||||
row["status_label"] = "资金不足"
|
row["status_label"] = "资金不足"
|
||||||
@@ -214,17 +262,24 @@ def refresh_recommend_cache(
|
|||||||
*,
|
*,
|
||||||
trading_mode: str = "simulation",
|
trading_mode: str = "simulation",
|
||||||
max_margin_pct: float = 30.0,
|
max_margin_pct: float = 30.0,
|
||||||
|
margin_used: float | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""后台拉行情、筛选并写入数据库。"""
|
"""后台拉行情、筛选并写入数据库。"""
|
||||||
ensure_recommend_tables(conn)
|
ensure_recommend_tables(conn)
|
||||||
ensure_fee_rates_schema(conn)
|
ensure_fee_rates_schema(conn)
|
||||||
ctp_connected = _ctp_connected_for_mode(trading_mode)
|
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(
|
all_rows = list_product_recommendations(
|
||||||
capital,
|
capital,
|
||||||
quote_fn,
|
quote_fn,
|
||||||
max_margin_pct=max_margin_pct,
|
max_margin_pct=max_margin_pct,
|
||||||
trading_mode=trading_mode,
|
trading_mode=trading_mode,
|
||||||
ctp_connected=ctp_connected,
|
ctp_connected=ctp_connected,
|
||||||
|
margin_used=used,
|
||||||
)
|
)
|
||||||
rows = filter_affordable_recommendations(all_rows)
|
rows = filter_affordable_recommendations(all_rows)
|
||||||
if not rows and float(capital or 0) > 0:
|
if not rows and float(capital or 0) > 0:
|
||||||
@@ -293,11 +348,14 @@ def recommend_payload(
|
|||||||
payload = load_recommend_cache(conn)
|
payload = load_recommend_cache(conn)
|
||||||
cap = float(live_capital or 0)
|
cap = float(live_capital or 0)
|
||||||
pct = max(1.0, min(100.0, float(max_margin_pct or 30.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["capital"] = cap
|
||||||
payload["max_margin_pct"] = pct
|
payload["max_margin_pct"] = pct
|
||||||
|
payload.update(budget_info)
|
||||||
rows = payload.get("rows") or []
|
rows = payload.get("rows") or []
|
||||||
rows = enrich_recommend_rows(
|
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 = filter_rows_for_account_scope(
|
||||||
rows, cap, ctp_connected=_ctp_connected_for_mode(trading_mode),
|
rows, cap, ctp_connected=_ctp_connected_for_mode(trading_mode),
|
||||||
|
|||||||
+24
-4
@@ -38,6 +38,7 @@
|
|||||||
var recommendMaxByProduct = {};
|
var recommendMaxByProduct = {};
|
||||||
var recommendMaxByCode = {};
|
var recommendMaxByCode = {};
|
||||||
var recRowsRaw = [];
|
var recRowsRaw = [];
|
||||||
|
var recMeta = {};
|
||||||
var recSortKey = 'trend';
|
var recSortKey = 'trend';
|
||||||
var recSortDesc = true;
|
var recSortDesc = true;
|
||||||
var recIndustryFilter = '';
|
var recIndustryFilter = '';
|
||||||
@@ -1454,7 +1455,7 @@
|
|||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRecStats(allRows, visibleRows) {
|
function updateRecStats(allRows, visibleRows, meta) {
|
||||||
var el = document.getElementById('rec-stats');
|
var el = document.getElementById('rec-stats');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
var total = (allRows || []).length;
|
var total = (allRows || []).length;
|
||||||
@@ -1464,6 +1465,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var parts = [];
|
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) {
|
if (recIndustryFilter) {
|
||||||
parts.push('筛选 <strong>' + shown + '</strong> / 共 ' + total + ' 个品种');
|
parts.push('筛选 <strong>' + shown + '</strong> / 共 ' + total + ' 个品种');
|
||||||
} else {
|
} else {
|
||||||
@@ -1641,15 +1648,28 @@
|
|||||||
function renderRecommendTable() {
|
function renderRecommendTable() {
|
||||||
var filtered = filterRecommendRows(recRowsRaw);
|
var filtered = filterRecommendRows(recRowsRaw);
|
||||||
var sorted = sortRecommendRows(filtered);
|
var sorted = sortRecommendRows(filtered);
|
||||||
updateRecStats(recRowsRaw, sorted);
|
updateRecStats(recRowsRaw, sorted, recMeta);
|
||||||
renderRecommendRows(sorted);
|
renderRecommendRows(sorted);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecommendations(data) {
|
function renderRecommendations(data) {
|
||||||
if (!recommendList || !data) return;
|
if (!recommendList || !data) return;
|
||||||
updateRecommendMaxMaps(data);
|
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');
|
var recCap = document.getElementById('rec-capital');
|
||||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
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');
|
var recUpdated = document.getElementById('rec-updated');
|
||||||
if (recUpdated && data.updated_at) {
|
if (recUpdated && data.updated_at) {
|
||||||
recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at;
|
recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at;
|
||||||
@@ -1658,7 +1678,7 @@
|
|||||||
recRowsRaw = rows.slice();
|
recRowsRaw = rows.slice();
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||||
updateRecStats([], []);
|
updateRecStats([], [], recMeta);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderRecommendTable();
|
renderRecommendTable();
|
||||||
@@ -1692,7 +1712,7 @@
|
|||||||
renderRecommendTable();
|
renderRecommendTable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (recRowsRaw.length) updateRecStats(recRowsRaw, filterRecommendRows(recRowsRaw));
|
if (recRowsRaw.length) updateRecStats(recRowsRaw, filterRecommendRows(recRowsRaw), recMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectRecommendStream() {
|
function connectRecommendStream() {
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
<div class="card trade-card trade-card-full" id="recommend">
|
<div class="card trade-card trade-card-full" id="recommend">
|
||||||
<h2>可开仓品种</h2>
|
<h2>可开仓品种</h2>
|
||||||
<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(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 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_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 %}
|
{% if small_account_margin_rec %}<span class="text-muted">{{ small_account_margin_rec.label }}。</span>{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user