diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index 1194d47..18e2517 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -290,7 +290,7 @@ def resolve_path(path_value):
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "crypto_monitor_2026_secret_key")
-from lib.instance.instance_embed_lib import attach_embed_templates
+from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled
attach_embed_templates(app, _REPO_ROOT)
@@ -6510,7 +6510,11 @@ def render_main_page(page="trade", embed_mode=None):
options_trading_usdc = None
options_funding_usdc = None
options_funding_usdt = None
- if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
+ if (
+ OKX_OPTIONS_ENABLED
+ and exchange_options.apiKey
+ and embed_mode != "fragment"
+ ):
try:
from lib.exchange.okx_options_lib import (
fetch_options_funding_usdc,
@@ -6601,22 +6605,29 @@ def render_main_page(page="trade", embed_mode=None):
hard_limit=DAILY_OPEN_HARD_LIMIT,
extra_blocks=not risk_status.get("can_trade", True),
)
- key_rule_ctx = key_monitor_rule_template_context(
- kline_timeframe=KLINE_TIMEFRAME,
- key_breakout_amp_min_pct=KEY_BREAKOUT_AMP_MIN_PCT,
- key_volume_ma_bars=KEY_VOLUME_MA_BARS,
- key_volume_ratio_min=KEY_VOLUME_RATIO_MIN,
- key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
- key_daily_volume_rank_max=KEY_DAILY_VOLUME_RANK_MAX,
- key_confirm_breakout_bar=KEY_CONFIRM_BREAKOUT_BAR,
- key_confirm_bar=KEY_CONFIRM_BAR,
- key_alert_max_times=KEY_ALERT_MAX_TIMES,
- key_alert_interval_minutes=KEY_ALERT_INTERVAL_MINUTES,
- key_stop_outside_breakout_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
- key_trend_stop_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
- false_breakout_validity_hours=FALSE_BREAKOUT_VALIDITY_HOURS,
- trigger_entry_validity_hours=TRIGGER_ENTRY_VALIDITY_HOURS,
- )
+ key_rule_ctx = {}
+ if page in ("key_monitor", "trade") or page in (
+ "strategy",
+ "strategy_trend",
+ "strategy_roll",
+ "strategy_records",
+ ):
+ key_rule_ctx = key_monitor_rule_template_context(
+ kline_timeframe=KLINE_TIMEFRAME,
+ key_breakout_amp_min_pct=KEY_BREAKOUT_AMP_MIN_PCT,
+ key_volume_ma_bars=KEY_VOLUME_MA_BARS,
+ key_volume_ratio_min=KEY_VOLUME_RATIO_MIN,
+ key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
+ key_daily_volume_rank_max=KEY_DAILY_VOLUME_RANK_MAX,
+ key_confirm_breakout_bar=KEY_CONFIRM_BREAKOUT_BAR,
+ key_confirm_bar=KEY_CONFIRM_BAR,
+ key_alert_max_times=KEY_ALERT_MAX_TIMES,
+ key_alert_interval_minutes=KEY_ALERT_INTERVAL_MINUTES,
+ key_stop_outside_breakout_pct=KEY_STOP_OUTSIDE_BREAKOUT_PCT,
+ key_trend_stop_outside_pct=KEY_TREND_STOP_OUTSIDE_PCT,
+ false_breakout_validity_hours=FALSE_BREAKOUT_VALIDITY_HOURS,
+ trigger_entry_validity_hours=TRIGGER_ENTRY_VALIDITY_HOURS,
+ )
strategy_extra = {}
if plan.strategy:
from lib.strategy.strategy_ui import strategy_render_extras
@@ -6768,36 +6779,54 @@ def index():
@app.route("/key_monitor")
@login_required
def key_monitor_page():
+ redir = redirect_to_embed_shell_if_enabled("key_monitor")
+ if redir is not None:
+ return redir
return render_main_page("key_monitor")
@app.route("/trade")
@login_required
def trade_page():
+ redir = redirect_to_embed_shell_if_enabled("trade")
+ if redir is not None:
+ return redir
return render_main_page("trade")
@app.route("/records")
@login_required
def records_page():
+ redir = redirect_to_embed_shell_if_enabled("records")
+ if redir is not None:
+ return redir
return render_main_page("records")
@app.route("/stats")
@login_required
def stats_page():
+ redir = redirect_to_embed_shell_if_enabled("stats")
+ if redir is not None:
+ return redir
return render_main_page("stats")
@app.route("/settings")
@login_required
def settings_page():
+ redir = redirect_to_embed_shell_if_enabled("settings")
+ if redir is not None:
+ return redir
return render_main_page("settings")
@app.route("/options")
@login_required
def options_main_page():
+ redir = redirect_to_embed_shell_if_enabled("options")
+ if redir is not None:
+ return redir
return render_main_page("options")
diff --git a/lib/common/static/instance_embed.js b/lib/common/static/instance_embed.js
index b3deb0a..1e7aafd 100644
--- a/lib/common/static/instance_embed.js
+++ b/lib/common/static/instance_embed.js
@@ -7,6 +7,7 @@
trade: "/trade",
strategy: "/strategy",
strategy_records: "/strategy/records",
+ options: "/options",
records: "/records",
stats: "/stats",
settings: "/settings",
@@ -14,6 +15,7 @@
let navToken = 0;
let loadingTab = false;
+ const tabHtmlCache = new Map();
/** 自带校验后 form.submit() 的表单,勿在捕获阶段再 fetch 一份(会双发 POST) */
const CUSTOM_SUBMIT_FORM_IDS = new Set(["add-order-form", "key-form", "roll-form"]);
@@ -112,20 +114,61 @@
});
}
+ function embedPageUrl(tab) {
+ const qs = listWindowQueryString();
+ let url = "/api/embed/page/" + encodeURIComponent(tab);
+ const parts = [];
+ if (qs) parts.push(qs);
+ parts.push("embed=1");
+ return url + "?" + parts.join("&");
+ }
+
+ async function fetchTabHtml(tab) {
+ const r = await fetch(embedPageUrl(tab), {
+ credentials: "same-origin",
+ headers: { "X-Instance-Soft-Nav": "1" },
+ });
+ const j = await r.json();
+ if (!j.ok || !j.html) throw new Error(j.msg || "加载失败");
+ return j.html;
+ }
+
+ function warmTabCache(tab) {
+ if (!tab || tabHtmlCache.has(tab)) return;
+ fetchTabHtml(tab)
+ .then((html) => {
+ tabHtmlCache.set(tab, html);
+ })
+ .catch(() => {});
+ }
+
+ function clearTabCache() {
+ tabHtmlCache.clear();
+ }
+
async function loadTab(tab, opts) {
const options = opts || {};
if (!tab || loadingTab) return;
const token = ++navToken;
+ const force = !!options.force;
+ const cached = !force && tabHtmlCache.get(tab);
+
+ if (cached) {
+ injectFragment(cached);
+ setNavActive(tab);
+ if (!options.skipUrl) syncUrl(tab, !!options.replace);
+ runPageInit(tab);
+ warmTabCache(tab);
+ return;
+ }
+
loadingTab = true;
setRootLoading(true);
try {
- const qs = listWindowQueryString();
- const url = "/api/embed/page/" + encodeURIComponent(tab) + (qs ? "?" + qs : "");
- const r = await fetch(url, { credentials: "same-origin" });
+ const html = await fetchTabHtml(tab);
if (token !== navToken) return;
- const j = await r.json();
- if (!j.ok || !j.html) throw new Error(j.msg || "加载失败");
- injectFragment(j.html);
+ tabHtmlCache.set(tab, html);
+ injectFragment(html);
setNavActive(tab);
if (!options.skipUrl) syncUrl(tab, !!options.replace);
runPageInit(tab);
@@ -146,7 +189,8 @@
}
function reloadCurrentTab() {
- return loadTab(getTab(), { replace: true, skipUrl: true });
+ tabHtmlCache.delete(getTab());
+ return loadTab(getTab(), { replace: true, skipUrl: true, force: true });
}
function postFormAndReload(form, label) {
@@ -172,6 +216,7 @@
function patchApplyListWindow() {
if (typeof global.applyListWindow !== "function") return;
global.applyListWindow = function embedApplyListWindow() {
+ clearTabCache();
const qs = listWindowQueryString();
const tab = getTab();
const q = new URLSearchParams(qs);
@@ -239,6 +284,9 @@
function bindNav() {
document.querySelectorAll(".embed-top-nav [data-embed-tab]").forEach((a) => {
+ a.addEventListener("mouseenter", () => {
+ warmTabCache(a.getAttribute("data-embed-tab"));
+ });
a.addEventListener("click", (ev) => {
ev.preventDefault();
const tab = a.getAttribute("data-embed-tab");
@@ -268,6 +316,7 @@
reloadCurrentTab,
getTab,
postFormAndReload,
+ clearTabCache,
};
if (document.readyState === "loading") {
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index ce781cf..f99644a 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -4,10 +4,12 @@
const root = document.getElementById("options-root");
if (!root) return;
+ const panelCache = (window.__optionsPanelCache = window.__optionsPanelCache || {});
+
const state = {
underlying: root.dataset.defaultUnderly || "ETH",
optType: "C",
- chain: null,
+ chain: panelCache.chain || null,
selectedInst: null,
};
@@ -254,6 +256,9 @@
return;
}
state.chain = d;
+ panelCache.chain = d;
+ panelCache.underlying = state.underlying;
+ panelCache.optType = state.optType;
state.selectedInst = null;
parkOrderPanel();
renderExpiries();
@@ -457,6 +462,28 @@
refreshHistory();
}
+ function bootOptionsPanel() {
+ updateSizeInputs();
+ const hasCache =
+ panelCache.chain &&
+ panelCache.underlying === state.underlying &&
+ panelCache.optType === state.optType;
+ if (hasCache) {
+ state.chain = panelCache.chain;
+ renderExpiries();
+ renderStrikes();
+ refreshAllPositions();
+ requestAnimationFrame(function () {
+ loadChain();
+ });
+ return;
+ }
+ requestAnimationFrame(function () {
+ loadChain();
+ refreshAllPositions();
+ });
+ }
+
document.querySelectorAll(".opt-uly-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".opt-uly-btn").forEach(function (b) { b.classList.remove("active"); });
@@ -495,7 +522,5 @@
});
});
- updateSizeInputs();
- loadChain();
- refreshAllPositions();
+ bootOptionsPanel();
})();
diff --git a/lib/instance/instance_embed_lib.py b/lib/instance/instance_embed_lib.py
index ad4bc11..5d57bfe 100644
--- a/lib/instance/instance_embed_lib.py
+++ b/lib/instance/instance_embed_lib.py
@@ -72,6 +72,22 @@ def embed_shell_enabled() -> bool:
return (os.getenv("HUB_EMBED_SHELL") or "1").strip().lower() in ("1", "true", "yes", "on")
+def redirect_to_embed_shell_if_enabled(page: str):
+ """直连 /trade 等整页路由时,重定向到 embed 壳(顶栏常驻、tab 软切换)。"""
+ from flask import redirect, request, urlencode
+
+ if not embed_shell_enabled():
+ return None
+ if (request.args.get("embed") or "").strip() == "1":
+ return None
+ if (request.path or "").rstrip("/") == "/embed":
+ return None
+ q = {k: v for k, v in request.args.items()}
+ q["tab"] = page
+ q["embed"] = "1"
+ return redirect("/embed?" + urlencode(q))
+
+
def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
"""embed=1 打开时:/trade → /embed?tab=trade&embed=1"""
if not embed_shell_enabled():
diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html
index 5327a69..1aa7ef1 100644
--- a/lib/instance/templates/embed_shell.html
+++ b/lib/instance/templates/embed_shell.html
@@ -90,6 +90,6 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
{% include 'embed_boot_scripts.html' %}
-
+