feat(hub): trend plan breakeven and stop from monitor fullscreen
Proxy /api/hub/trend/stop and breakeven to instances; enable offset input and actions in hub UI. Add horizontal padding on strategy records page. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7727,6 +7727,8 @@ try:
|
|||||||
"add_key": add_key,
|
"add_key": add_key,
|
||||||
"preview_trend_pullback": preview_trend_pullback,
|
"preview_trend_pullback": preview_trend_pullback,
|
||||||
"execute_trend_pullback": execute_trend_pullback,
|
"execute_trend_pullback": execute_trend_pullback,
|
||||||
|
"stop_trend_pullback": stop_trend_pullback,
|
||||||
|
"trend_pullback_breakeven": trend_pullback_breakeven,
|
||||||
},
|
},
|
||||||
ohlcv_fn=_hub_fetch_ohlcv,
|
ohlcv_fn=_hub_fetch_ohlcv,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -158,6 +158,29 @@ def _invoke_view(view_name: str, path: str, form=None) -> dict:
|
|||||||
return {"ok": ok, "messages": msgs}
|
return {"ok": ok, "messages": msgs}
|
||||||
|
|
||||||
|
|
||||||
|
def _invoke_view_get(view_name: str, path: str) -> dict:
|
||||||
|
views = _ctx().get("views") or {}
|
||||||
|
view = views.get(view_name)
|
||||||
|
if not view:
|
||||||
|
return {"ok": False, "messages": [f"未配置视图 {view_name}"]}
|
||||||
|
with current_app.test_request_context(path, method="GET"):
|
||||||
|
session["logged_in"] = True
|
||||||
|
try:
|
||||||
|
view()
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "messages": [str(e)]}
|
||||||
|
try:
|
||||||
|
msgs = [str(x) for x in get_flashed_messages()]
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "messages": [f"读取提示信息失败: {e}"]}
|
||||||
|
ok = True
|
||||||
|
for m in msgs:
|
||||||
|
if any(k in m for k in _FAIL_HINTS):
|
||||||
|
ok = False
|
||||||
|
break
|
||||||
|
return {"ok": ok, "messages": msgs}
|
||||||
|
|
||||||
|
|
||||||
def _hub_json(view_name: str, path: str, form=None):
|
def _hub_json(view_name: str, path: str, form=None):
|
||||||
try:
|
try:
|
||||||
return jsonify(_invoke_view(view_name, path, form=form))
|
return jsonify(_invoke_view(view_name, path, form=form))
|
||||||
@@ -445,6 +468,31 @@ def register_hub_routes(app):
|
|||||||
return jsonify({"ok": False, "msg": "预览不存在或已过期"}), 404
|
return jsonify({"ok": False, "msg": "预览不存在或已过期"}), 404
|
||||||
return jsonify({"ok": True, "preview": preview})
|
return jsonify({"ok": True, "preview": preview})
|
||||||
|
|
||||||
|
@app.route("/api/hub/trend/stop/<int:pid>", methods=["POST"])
|
||||||
|
@_hub_auth_required
|
||||||
|
def api_hub_trend_stop(pid):
|
||||||
|
if not _ctx().get("has_trend"):
|
||||||
|
return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400
|
||||||
|
return jsonify(_invoke_view_get("stop_trend_pullback", f"/stop_trend_pullback/{pid}"))
|
||||||
|
|
||||||
|
@app.route("/api/hub/trend/breakeven/<int:pid>", methods=["POST"])
|
||||||
|
@_hub_auth_required
|
||||||
|
def api_hub_trend_breakeven(pid):
|
||||||
|
if not _ctx().get("has_trend"):
|
||||||
|
return jsonify({"ok": False, "msg": "该实例无趋势回调"}), 400
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
raw = (request.form.get("breakeven_offset_pct") or body.get("breakeven_offset_pct") or "").strip()
|
||||||
|
form = {}
|
||||||
|
if raw != "":
|
||||||
|
form["breakeven_offset_pct"] = raw
|
||||||
|
return jsonify(
|
||||||
|
_invoke_view(
|
||||||
|
"trend_pullback_breakeven",
|
||||||
|
f"/trend_pullback_breakeven/{pid}",
|
||||||
|
form=form,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@app.route("/hub-sso")
|
@app.route("/hub-sso")
|
||||||
def hub_sso_login():
|
def hub_sso_login():
|
||||||
"""中控签发的临时链接:写入 session 后跳转,直链访问仍走 /login。"""
|
"""中控签发的临时链接:写入 session 后跳转,直链访问仍走 /login。"""
|
||||||
|
|||||||
@@ -1124,6 +1124,77 @@ class PlaceTpslBody(BaseModel):
|
|||||||
contracts: float | None = None
|
contracts: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class TrendPlanActionBody(BaseModel):
|
||||||
|
plan_id: int
|
||||||
|
breakeven_offset_pct: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _flask_hub_messages(parsed: dict | None) -> tuple[bool, str]:
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
return False, "实例返回无效"
|
||||||
|
msgs = list(parsed.get("messages") or [])
|
||||||
|
if parsed.get("msg"):
|
||||||
|
msgs.insert(0, str(parsed["msg"]))
|
||||||
|
if parsed.get("error"):
|
||||||
|
msgs.append(str(parsed["error"]))
|
||||||
|
ok = parsed.get("ok") is not False
|
||||||
|
if parsed.get("ok") is True:
|
||||||
|
ok = True
|
||||||
|
elif parsed.get("ok") is False:
|
||||||
|
ok = False
|
||||||
|
else:
|
||||||
|
for m in msgs:
|
||||||
|
if any(
|
||||||
|
k in str(m)
|
||||||
|
for k in ("失败", "错误", "无法", "缺少", "过期", "未找到", "不允许", "异常")
|
||||||
|
):
|
||||||
|
ok = False
|
||||||
|
break
|
||||||
|
text = ";".join(str(x) for x in msgs if x) or ("成功" if ok else "操作失败")
|
||||||
|
return ok, text
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/trend/{exchange_id}/stop")
|
||||||
|
async def api_trend_plan_stop(exchange_id: str, body: TrendPlanActionBody):
|
||||||
|
ex = _find_exchange(exchange_id)
|
||||||
|
if not ex or not ex.get("enabled"):
|
||||||
|
raise HTTPException(status_code=404, detail="账户未启用")
|
||||||
|
if "trend" not in (ex.get("capabilities") or []):
|
||||||
|
raise HTTPException(status_code=400, detail="该账户未启用趋势计划监控")
|
||||||
|
pid = int(body.plan_id)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
parsed = await _fetch_flask_json(
|
||||||
|
client, ex, f"/api/hub/trend/stop/{pid}", method="POST"
|
||||||
|
)
|
||||||
|
ok, text = _flask_hub_messages(parsed)
|
||||||
|
_schedule_board_refresh()
|
||||||
|
return {"ok": ok, "message": text, "payload": parsed}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/trend/{exchange_id}/breakeven")
|
||||||
|
async def api_trend_plan_breakeven(exchange_id: str, body: TrendPlanActionBody):
|
||||||
|
ex = _find_exchange(exchange_id)
|
||||||
|
if not ex or not ex.get("enabled"):
|
||||||
|
raise HTTPException(status_code=404, detail="账户未启用")
|
||||||
|
if "trend" not in (ex.get("capabilities") or []):
|
||||||
|
raise HTTPException(status_code=400, detail="该账户未启用趋势计划监控")
|
||||||
|
pid = int(body.plan_id)
|
||||||
|
data = {}
|
||||||
|
if body.breakeven_offset_pct is not None:
|
||||||
|
data["breakeven_offset_pct"] = str(body.breakeven_offset_pct)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
parsed = await _fetch_flask_json(
|
||||||
|
client,
|
||||||
|
ex,
|
||||||
|
f"/api/hub/trend/breakeven/{pid}",
|
||||||
|
method="POST",
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
ok, text = _flask_hub_messages(parsed)
|
||||||
|
_schedule_board_refresh()
|
||||||
|
return {"ok": ok, "message": text, "payload": parsed}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/orders/{exchange_id}/cancel")
|
@app.post("/api/orders/{exchange_id}/cancel")
|
||||||
async def api_cancel_order(exchange_id: str, body: CancelOrderBody):
|
async def api_cancel_order(exchange_id: str, body: CancelOrderBody):
|
||||||
ex = _find_exchange(exchange_id)
|
ex = _find_exchange(exchange_id)
|
||||||
|
|||||||
@@ -1676,6 +1676,7 @@ body.market-chart-fs-open {
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: var(--plan-be-btn-bg);
|
background: var(--plan-be-btn-bg);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
border: 1px solid var(--plan-be-input-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -1683,6 +1684,15 @@ body.market-chart-fs-open {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hub-trend-plan-card button.hub-plan-be-btn {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-trend-plan-card .hub-plan-be-input:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.hub-trend-plan-card .hub-plan-be-btn--static {
|
.hub-trend-plan-card .hub-plan-be-btn--static {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1426,6 +1426,22 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
box.querySelectorAll(".btn-hub-trend-stop").forEach((btn) => {
|
||||||
|
btn.onclick = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
hubTrendPlanStop(btn.dataset.exId, btn.dataset.planId);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
box.querySelectorAll(".btn-hub-trend-be").forEach((btn) => {
|
||||||
|
btn.onclick = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
const card = btn.closest(".hub-trend-plan-card");
|
||||||
|
const inp = card ? card.querySelector(".hub-plan-be-input") : null;
|
||||||
|
hubTrendPlanBreakeven(btn.dataset.exId, btn.dataset.planId, inp);
|
||||||
|
};
|
||||||
|
});
|
||||||
box.querySelectorAll(".btn-close-ex").forEach((btn) => {
|
box.querySelectorAll(".btn-close-ex").forEach((btn) => {
|
||||||
btn.onclick = () => closeOne(btn.dataset.id);
|
btn.onclick = () => closeOne(btn.dataset.id);
|
||||||
});
|
});
|
||||||
@@ -1668,18 +1684,27 @@
|
|||||||
? `≈${fmt(t.plan_margin_capital, 2)}U`
|
? `≈${fmt(t.plan_margin_capital, 2)}U`
|
||||||
: "—";
|
: "—";
|
||||||
const levTxt = t.leverage != null && t.leverage !== "" ? `${esc(t.leverage)}x` : "—";
|
const levTxt = t.leverage != null && t.leverage !== "" ? `${esc(t.leverage)}x` : "—";
|
||||||
const bePct =
|
const bePctDefault =
|
||||||
t.breakeven_offset_pct != null && t.breakeven_offset_pct !== ""
|
t.breakeven_default_offset_pct != null && t.breakeven_default_offset_pct !== ""
|
||||||
? esc(t.breakeven_offset_pct)
|
? t.breakeven_default_offset_pct
|
||||||
: "0.3";
|
: t.breakeven_offset_pct != null && t.breakeven_offset_pct !== ""
|
||||||
|
? t.breakeven_offset_pct
|
||||||
|
: "0.3";
|
||||||
const exId = exchangeRow && exchangeRow.id != null ? esc(exchangeRow.id) : "";
|
const exId = exchangeRow && exchangeRow.id != null ? esc(exchangeRow.id) : "";
|
||||||
const canOpen = !!(exchangeRow && (exchangeRow.flask_url_browser || exchangeRow.flask_url));
|
const planId = esc(t.id);
|
||||||
const endBtn = canOpen
|
const caps = (exchangeRow && exchangeRow.capabilities) || [];
|
||||||
? `<a href="#" class="btn-close-plan btn-open-instance" data-ex-id="${exId}" data-next="/stop_trend_pullback/${esc(t.id)}" data-confirm="结束计划:市价平仓并撤掉该合约全部挂单,确定?">结束计划</a>`
|
const flaskOk =
|
||||||
|
exchangeRow && exchangeRow.flask_ok !== false && (exchangeRow.hub_monitor || {}).ok !== false;
|
||||||
|
const canHubTrend = !!(flaskOk && caps.includes("trend") && exId && planId);
|
||||||
|
const beAppliedFlag = !!t.breakeven_applied;
|
||||||
|
const endBtn = canHubTrend
|
||||||
|
? `<button type="button" class="btn-close-plan btn-hub-trend-stop" data-ex-id="${exId}" data-plan-id="${planId}">结束计划</button>`
|
||||||
: "";
|
: "";
|
||||||
const beBtn = canOpen
|
const beBtn = canHubTrend && !beAppliedFlag
|
||||||
? `<a href="#" class="hub-plan-be-btn btn-open-instance" data-ex-id="${exId}" data-next="/strategy">保本移交下单监控</a>`
|
? `<button type="button" class="hub-plan-be-btn btn-hub-trend-be" data-ex-id="${exId}" data-plan-id="${planId}">保本移交下单监控</button>`
|
||||||
: `<span class="hub-plan-be-btn hub-plan-be-btn--static">保本移交下单监控</span>`;
|
: beAppliedFlag
|
||||||
|
? ""
|
||||||
|
: `<span class="hub-plan-be-btn hub-plan-be-btn--static">保本移交下单监控</span>`;
|
||||||
const beApplied =
|
const beApplied =
|
||||||
t.breakeven_applied
|
t.breakeven_applied
|
||||||
? `<span class="hub-plan-be-done">已保本 ${esc(String(t.breakeven_applied_at || "").slice(0, 16))}</span>`
|
? `<span class="hub-plan-be-done">已保本 ${esc(String(t.breakeven_applied_at || "").slice(0, 16))}</span>`
|
||||||
@@ -1718,7 +1743,7 @@
|
|||||||
<div class="plan-card-meta hub-plan-breakeven-row">
|
<div class="plan-card-meta hub-plan-breakeven-row">
|
||||||
<label class="hub-plan-be-label">
|
<label class="hub-plan-be-label">
|
||||||
保本移交 偏移%
|
保本移交 偏移%
|
||||||
<input type="number" disabled value="${bePct}" class="hub-plan-be-input" />
|
<input type="number" min="0" step="0.01" value="${esc(bePctDefault)}" class="hub-plan-be-input" data-ex-id="${exId}" data-plan-id="${planId}" ${canHubTrend && !beAppliedFlag ? "" : "disabled"} />
|
||||||
</label>
|
</label>
|
||||||
${beBtn}
|
${beBtn}
|
||||||
${beApplied}
|
${beApplied}
|
||||||
@@ -2446,6 +2471,63 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hubTrendPlanStop(exchangeId, planId) {
|
||||||
|
if (!exchangeId || !planId) {
|
||||||
|
showToast("缺少交易所或计划 ID", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm("结束计划:市价平仓并撤掉该合约全部挂单,确定?")) return;
|
||||||
|
try {
|
||||||
|
const r = await apiFetch("/api/trend/" + encodeURIComponent(exchangeId) + "/stop", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ plan_id: Number(planId) }),
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
showToast(j.message || (j.ok ? "已结束趋势回调计划" : "结束失败"), !j.ok);
|
||||||
|
if (j.ok) refreshMonitorBoardNow();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(String(e), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hubTrendPlanBreakeven(exchangeId, planId, inputEl) {
|
||||||
|
if (!exchangeId || !planId) {
|
||||||
|
showToast("缺少交易所或计划 ID", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = inputEl ? String(inputEl.value || "").trim() : "";
|
||||||
|
let pct = null;
|
||||||
|
if (raw !== "") {
|
||||||
|
pct = Number(raw);
|
||||||
|
if (!Number.isFinite(pct) || pct < 0) {
|
||||||
|
showToast("保本偏移% 须为非负数", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"确认保本?将结束本趋势计划,持仓移交「下单监控」,并在交易所挂保本止损与计划止盈;后续平仓写入交易记录。"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const body = { plan_id: Number(planId) };
|
||||||
|
if (pct != null) body.breakeven_offset_pct = pct;
|
||||||
|
const r = await apiFetch("/api/trend/" + encodeURIComponent(exchangeId) + "/breakeven", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
showToast(j.message || (j.ok ? "保本移交成功" : "保本移交失败"), !j.ok);
|
||||||
|
if (j.ok) refreshMonitorBoardNow();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(String(e), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function closeOnePosition(exchangeId, symbol, side) {
|
async function closeOnePosition(exchangeId, symbol, side) {
|
||||||
const label = `${symbol} · ${side}`;
|
const label = `${symbol} · ${side}`;
|
||||||
if (!confirm(`确认对该账户市价平仓:${label}?`)) return;
|
if (!confirm(`确认对该账户市价平仓:${label}?`)) return;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ Chrome **桌面快捷方式**图标来自站点 `favicon` / `manifest`(已配
|
|||||||
| 功能 | 说明 |
|
| 功能 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
|
| **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
|
||||||
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**、**风险%**、**程序监控·止盈价**、**盈亏比**,与实例策略页一致);独立卡片:**关键位**、**下单监控**、**趋势回调**(单计划 **两列**:左=币种基本信息与 3×2 指标,右=**补仓计划明细**,底=保本移交 + 快照可用/计划保证金/杠杆;字段与实例 `/strategy` 一致,结束/保本在实例操作)、**顺势加仓** |
|
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**、**风险%**、**程序监控·止盈价**、**盈亏比**,与实例策略页一致);独立卡片:**关键位**、**下单监控**、**趋势回调**(单计划 **两列**:左=币种基本信息与 3×2 指标,右=**补仓计划明细**,底=**保本偏移%** 可编辑 + **保本移交** / **结束计划**(中控直接调实例,与 `/strategy` 一致)、快照可用/计划保证金/杠杆)、**顺势加仓** |
|
||||||
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |
|
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |
|
||||||
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
|
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
|
||||||
| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`、`cancel-symbol` |
|
| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`、`cancel-symbol` |
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{% set mf = money_fmt|default(funds_fmt) %}
|
{% set mf = money_fmt|default(funds_fmt) %}
|
||||||
<style>
|
<style>
|
||||||
.strategy-records-page{padding:4px 0 20px}
|
.strategy-records-page{
|
||||||
|
padding:10px clamp(14px,2.2vw,22px) 22px;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
.strategy-records-page h2{margin:0 0 8px;color:#dbe4ff}
|
.strategy-records-page h2{margin:0 0 8px;color:#dbe4ff}
|
||||||
.strategy-records-tip{font-size:.76rem;color:#8892b0;line-height:1.55;margin-bottom:12px}
|
.strategy-records-tip{font-size:.76rem;color:#8892b0;line-height:1.55;margin-bottom:12px}
|
||||||
.sr-filters{display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center;padding:12px 14px;background:#141a2a;border:1px solid #2a3150;border-radius:10px;margin-bottom:16px}
|
.sr-filters{display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center;padding:12px 14px;background:#141a2a;border:1px solid #2a3150;border-radius:10px;margin-bottom:16px}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ def install_strategy_trend(app: Flask, repo_root: str, app_module: Any = None, *
|
|||||||
app.extensions["strategy_trend_cfg"] = cfg
|
app.extensions["strategy_trend_cfg"] = cfg
|
||||||
register_trend_routes(app, cfg)
|
register_trend_routes(app, cfg)
|
||||||
_patch_hub_monitor_enrich(app, cfg)
|
_patch_hub_monitor_enrich(app, cfg)
|
||||||
|
_patch_hub_trend_views(app)
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def _trend_ctx():
|
def _trend_ctx():
|
||||||
@@ -391,6 +392,23 @@ def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict:
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_hub_trend_views(app: Flask) -> None:
|
||||||
|
"""将趋势回调路由注册进 HUB_CTX.views,供中控 /api/hub/trend/* 调用。"""
|
||||||
|
ctx = dict(app.config.get("HUB_CTX") or {})
|
||||||
|
views = dict(ctx.get("views") or {})
|
||||||
|
for name in (
|
||||||
|
"preview_trend_pullback",
|
||||||
|
"execute_trend_pullback",
|
||||||
|
"stop_trend_pullback",
|
||||||
|
"trend_pullback_breakeven",
|
||||||
|
):
|
||||||
|
vf = app.view_functions.get(name)
|
||||||
|
if vf is not None:
|
||||||
|
views[name] = vf
|
||||||
|
ctx["views"] = views
|
||||||
|
app.config["HUB_CTX"] = ctx
|
||||||
|
|
||||||
|
|
||||||
def _patch_hub_monitor_enrich(app: Flask, cfg: dict) -> None:
|
def _patch_hub_monitor_enrich(app: Flask, cfg: dict) -> None:
|
||||||
ctx = dict(app.config.get("HUB_CTX") or {})
|
ctx = dict(app.config.get("HUB_CTX") or {})
|
||||||
prev = ctx.get("enrich_monitor")
|
prev = ctx.get("enrich_monitor")
|
||||||
@@ -465,7 +483,12 @@ def enrich_trend_plan(cfg: dict, row) -> dict:
|
|||||||
d["floating_pnl"] = d["floating_mark"] = None
|
d["floating_pnl"] = d["floating_mark"] = None
|
||||||
from strategy_snapshot_lib import attach_trend_dca_levels
|
from strategy_snapshot_lib import attach_trend_dca_levels
|
||||||
|
|
||||||
return attach_trend_dca_levels(d)
|
d = attach_trend_dca_levels(d)
|
||||||
|
try:
|
||||||
|
d["breakeven_default_offset_pct"] = float(cfg.get("breakeven_offset_pct", 0.3))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
d["breakeven_default_offset_pct"] = 0.3
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
def _weighted_avg(old_avg, old_amt, fill_px, add_amt):
|
def _weighted_avg(old_avg, old_amt, fill_px, add_amt):
|
||||||
|
|||||||
Reference in New Issue
Block a user