refactor: unify three-exchange instance UI with shared templates
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,33 +1,63 @@
|
||||
"""Build embed_page_fragment.html from gate index.html."""
|
||||
"""Build embed_page_fragment.html from lib/instance/templates/index.html."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
src_lines = (ROOT / "crypto_monitor_gate" / "templates" / "index.html").read_text(
|
||||
encoding="utf-8"
|
||||
).splitlines()
|
||||
SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||
OUT = ROOT / "lib" / "instance" / "templates" / "embed_page_fragment.html"
|
||||
|
||||
# 1-based line numbers from index.html
|
||||
macro_body = src_lines[243:262] # {% macro %} … {% endmacro %}
|
||||
grid_inner = src_lines[328:736] # inside .grid (exclude outer wrapper)
|
||||
stats_block = src_lines[738:772]
|
||||
GRID_START = ' <div class="grid">'
|
||||
STATS_START = ' <div class="card full stats-card'
|
||||
|
||||
out_lines = [
|
||||
"{# Hub iframe tab fragment — shared via embed_templates #}",
|
||||
*macro_body,
|
||||
'<div class="grid">',
|
||||
*grid_inner,
|
||||
"</div>",
|
||||
*stats_block,
|
||||
]
|
||||
|
||||
out_dir = ROOT / "lib" / "instance" / "templates"
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
text = "\n".join(out_lines) + "\n"
|
||||
text = text.replace(
|
||||
"{% include 'order_monitor_rule_tips_gate.html' %}",
|
||||
"{% include order_rule_tips_tpl %}",
|
||||
)
|
||||
(out_dir / "embed_page_fragment.html").write_text(text, encoding="utf-8")
|
||||
print("wrote", out_dir / "embed_page_fragment.html", "lines", len(out_lines))
|
||||
def _slice_between(lines: list[str], start: str, end: str | None) -> list[str]:
|
||||
try:
|
||||
i = next(idx for idx, line in enumerate(lines) if line == start)
|
||||
except StopIteration:
|
||||
raise SystemExit(f"marker not found: {start!r}")
|
||||
if end is None:
|
||||
return lines[i:]
|
||||
try:
|
||||
j = next(idx for idx, line in enumerate(lines[i + 1 :], i + 1) if line.startswith(end))
|
||||
except StopIteration:
|
||||
raise SystemExit(f"end marker not found: {end!r}")
|
||||
return lines[i:j]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = SRC.read_text(encoding="utf-8").splitlines()
|
||||
macro_start = next(i for i, l in enumerate(lines) if l.startswith("{% macro period_stats"))
|
||||
macro_end = next(i for i, l in enumerate(lines) if l.strip() == "{% endmacro %}")
|
||||
macro_body = lines[macro_start : macro_end + 1]
|
||||
|
||||
grid_block = _slice_between(lines, GRID_START, STATS_START)
|
||||
# strip outer .grid wrapper; fragment adds its own
|
||||
if grid_block and grid_block[0] == GRID_START:
|
||||
grid_block = grid_block[1:]
|
||||
if grid_block and grid_block[-1].strip() == "</div>":
|
||||
# only remove closing div if it closes .grid (heuristic: last line before stats)
|
||||
pass
|
||||
|
||||
stats_block = _slice_between(lines, STATS_START, " </div>")
|
||||
|
||||
out_lines = [
|
||||
"{# Hub iframe tab fragment — shared via embed_templates #}",
|
||||
*macro_body,
|
||||
'<div class="grid">',
|
||||
*grid_block,
|
||||
"</div>",
|
||||
*stats_block,
|
||||
]
|
||||
text = "\n".join(out_lines).rstrip() + "\n"
|
||||
if "order_rule_tips_tpl" not in text:
|
||||
text = text.replace(
|
||||
"{% include 'order_monitor_rule_tips_binance.html' %}",
|
||||
"{% include order_rule_tips_tpl %}",
|
||||
)
|
||||
OUT.write_text(text, encoding="utf-8")
|
||||
print("wrote", OUT, "lines", len(out_lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从 binance index.html 生成三所共用的 lib/instance/templates/index.html。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||
OUT = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||
|
||||
TRANSFER_BLOCK = """ <details class="tip-collapse transfer-rule-collapse">
|
||||
<summary class="tip-collapse-summary">划转规则说明</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;将 {{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入、超出划回 {{ auto_transfer_from }};<strong>持仓中不划转</strong>并微信通知)
|
||||
</div>
|
||||
</details>
|
||||
<form action="/manual_transfer" method="post" class="form-row">
|
||||
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
|
||||
<select name="from_account">
|
||||
<option value="funding" {% if auto_transfer_from == 'funding' %}selected{% endif %}>from: funding</option>
|
||||
<option value="swap" {% if auto_transfer_from == 'swap' %}selected{% endif %}>from: swap</option>
|
||||
<option value="spot" {% if auto_transfer_from == 'spot' %}selected{% endif %}>from: spot</option>
|
||||
</select>
|
||||
<select name="to_account">
|
||||
<option value="swap" {% if auto_transfer_to == 'swap' %}selected{% endif %}>to: swap</option>
|
||||
<option value="funding" {% if auto_transfer_to == 'funding' %}selected{% endif %}>to: funding</option>
|
||||
<option value="spot" {% if auto_transfer_to == 'spot' %}selected{% endif %}>to: spot</option>
|
||||
</select>
|
||||
<button type="submit">手动划转</button>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
|
||||
# 外链 CSS 替代内联 style
|
||||
text = re.sub(
|
||||
r" <style>.*?</style>\n",
|
||||
' <link rel="stylesheet" href="/static/instance_page.css?v=1">\n',
|
||||
text,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# 顶栏:划转 + 可选 open guard
|
||||
text = text.replace(
|
||||
' <div class="rule-tip">实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8)</div>\n',
|
||||
" {% include 'instance_top_bar.html' %}\n",
|
||||
)
|
||||
|
||||
# 规则条动态 include
|
||||
text = text.replace(
|
||||
"{% include 'order_monitor_rule_tips_binance.html' %}",
|
||||
"{% include order_rule_tips_tpl %}",
|
||||
)
|
||||
|
||||
# 下单面板内划转块移除(已上移到顶栏)
|
||||
if TRANSFER_BLOCK in text:
|
||||
text = text.replace(TRANSFER_BLOCK, "", 1)
|
||||
|
||||
# 孤儿仓恢复 banner
|
||||
orphan_block = """ {% if not order and orphan_live_positions %}
|
||||
{% set o = orphan_live_positions[0] %}
|
||||
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:block;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8">
|
||||
<strong>检测到交易所仍有持仓,本地无对应监控单</strong>
|
||||
<span style="margin-left:8px">{{ o.exchange_symbol or o.symbol }} · {{ '多' if o.direction == 'long' else '空' }}</span>
|
||||
<form action="/recover_orphan_order" method="post" style="display:inline;margin-left:12px">
|
||||
<input type="hidden" name="symbol" value="{{ o.symbol }}">
|
||||
<input type="hidden" name="direction" value="{{ o.direction }}">
|
||||
<button type="submit" style="padding:4px 10px;font-size:.82rem">恢复监控</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:none;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8"></div>
|
||||
{% endif %}"""
|
||||
wrapped = "{% if ui_orphan_recovery_enabled %}\n" + orphan_block + "\n {% endif %}"
|
||||
text = text.replace(orphan_block, wrapped, 1)
|
||||
|
||||
# refreshAccountSnapshot:采用 OKX 版 open_guard 逻辑
|
||||
old_can_trade = """ let canTradeText = "可开仓";
|
||||
if (!data.can_trade) {
|
||||
const parts = [];
|
||||
if (data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason) {
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
const ac = Number(data.active_count || 0);
|
||||
const max = Number(data.max_active_positions || {{ max_active_positions }});
|
||||
if (ac >= max) parts.push(`持仓 ${ac}/${max}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
if (!parts.length) parts.push(`未到北京时间 {{ reset_hour }}:00`);
|
||||
else parts.push(`或未到北京时间 {{ reset_hour }}:00`);
|
||||
canTradeText = `不可开仓(${parts.join(";")})`;
|
||||
}"""
|
||||
new_can_trade = """ let canTradeText = "可开仓";
|
||||
if(!data.can_trade){
|
||||
const parts = [];
|
||||
if (data.risk_status && data.risk_status.can_trade === false && data.risk_status.reason) {
|
||||
parts.push(data.risk_status.reason);
|
||||
}
|
||||
if((data.active_count||0) >= (data.max_active_positions||{{ max_active_positions }})) parts.push(`持仓 ${data.active_count}/${data.max_active_positions}`);
|
||||
const hard = Number(data.daily_open_hard_limit != null ? data.daily_open_hard_limit : {{ daily_open_hard_limit }});
|
||||
const opens = Number(data.opens_today);
|
||||
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
|
||||
if(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
|
||||
canTradeText = parts.length ? `不可开仓(${parts.join(";")})` : "不可开仓";
|
||||
}"""
|
||||
text = text.replace(old_can_trade, new_can_trade, 1)
|
||||
|
||||
guard_sync = """ const allowEl = document.getElementById("allow-open-before-reset");
|
||||
const guardStatus = document.getElementById("open-guard-status");
|
||||
const resetH = data.reset_hour != null ? data.reset_hour : {{ reset_hour }};
|
||||
if(allowEl && typeof data.open_guard_enabled !== "undefined"){
|
||||
allowEl.checked = !data.open_guard_enabled;
|
||||
}
|
||||
if(guardStatus && typeof data.open_guard_enabled !== "undefined"){
|
||||
guardStatus.innerText = data.open_guard_enabled
|
||||
? `已限制:${resetH}:00 前不可开仓`
|
||||
: `已放开:${resetH}:00 前允许开仓`;
|
||||
}"""
|
||||
insert_after = """ if(tip){
|
||||
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${openCntTxt ? openCntTxt + ";" : ""}${canTradeText}${avail};人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1`;
|
||||
}
|
||||
}).catch(()=>{});"""
|
||||
if guard_sync not in text:
|
||||
text = text.replace(
|
||||
insert_after,
|
||||
insert_after.replace(" }).catch(()=>{});", guard_sync + "\n }).catch(()=>{});"),
|
||||
1,
|
||||
)
|
||||
|
||||
open_guard_js = """
|
||||
const allowOpenBeforeResetEl = document.getElementById("allow-open-before-reset");
|
||||
if(allowOpenBeforeResetEl){
|
||||
allowOpenBeforeResetEl.addEventListener("change", function(){
|
||||
const allow = !!this.checked;
|
||||
fetch("/api/settings/open_guard", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({enabled: !allow}),
|
||||
}).then(r=>r.json()).then(data=>{
|
||||
if(!data.ok){ alert(data.msg || "保存失败"); return; }
|
||||
refreshAccountSnapshot();
|
||||
}).catch(()=>alert("保存失败"));
|
||||
});
|
||||
}
|
||||
"""
|
||||
marker = "const orderSymbolEl = document.getElementById(\"order-symbol\");"
|
||||
if "allowOpenBeforeResetEl" not in text:
|
||||
text = text.replace(marker, "{% if ui_open_guard_enabled %}" + open_guard_js + "{% endif %}\n" + marker, 1)
|
||||
|
||||
orphan_fn_guard = "{% if ui_orphan_recovery_enabled %}\n renderOrphanRecoverBanner(data.orphan_live_positions);\n {% endif %}"
|
||||
text = text.replace(
|
||||
" renderOrphanRecoverBanner(data.orphan_live_positions);",
|
||||
orphan_fn_guard,
|
||||
)
|
||||
|
||||
header = "{# 三所共用 standalone 主页 — 由 scripts/build_unified_index.py 生成,勿手改三所副本 #}\n"
|
||||
if not text.startswith("{# 三所共用"):
|
||||
text = header + text
|
||||
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(text, encoding="utf-8")
|
||||
print("wrote", OUT, "lines", len(text.splitlines()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user