diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py
index 961f7ed..07bfadf 100644
--- a/crypto_monitor_binance/app.py
+++ b/crypto_monitor_binance/app.py
@@ -6549,39 +6549,51 @@ def api_order_kline():
"volume": float(bar[5]),
})
+ from focus_chart_lib import (
+ build_order_kline_order_payload,
+ load_swap_positions_for_order_kline,
+ metrics_for_order_item,
+ )
+
current_price = get_price(order_item["symbol"])
- margin = float(order_item.get("margin_capital") or 0)
- leverage = float(order_item.get("leverage") or 0)
- entry = float(order_item.get("trigger_price") or 0)
- float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
- float_pct = round((float_pnl / margin * 100), 2) if margin > 0 else 0
+ positions = load_swap_positions_for_order_kline(
+ exchange,
+ private_configured=exchange_private_api_configured(),
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+ ex_metrics = metrics_for_order_item(
+ order_item,
+ positions,
+ resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
+ select_live_fn=_select_live_position_row,
+ parse_metrics_fn=parse_ccxt_position_metrics,
+ )
+ order_payload = build_order_kline_order_payload(
+ order_item,
+ ticker_price=current_price,
+ format_price_fn=format_price_for_symbol,
+ calc_pnl_fn=calc_pnl,
+ calc_rr_ratio_fn=calc_rr_ratio,
+ ex_metrics=ex_metrics,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
return jsonify({
"ok": True,
"timeframe": timeframe,
"limit": limit,
- "order": {
- "id": order_item["id"],
- "symbol": order_item["symbol"],
- "direction": order_item.get("direction") or "long",
- "trigger_price": order_item.get("trigger_price"),
- "stop_loss": order_item.get("stop_loss"),
- "take_profit": order_item.get("take_profit"),
- "trigger_price_display": format_price_for_symbol(exchange_symbol, order_item.get("trigger_price")),
- "stop_loss_display": format_price_for_symbol(exchange_symbol, order_item.get("stop_loss")),
- "take_profit_display": format_price_for_symbol(exchange_symbol, order_item.get("take_profit")),
- "margin_capital": order_item.get("margin_capital"),
- "leverage": order_item.get("leverage"),
- "position_ratio": order_item.get("position_ratio"),
- "rr_ratio": order_item.get("rr_ratio"),
- "breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
- "current_price": round(float(current_price), 8) if current_price else None,
- "current_price_display": format_price_for_symbol(exchange_symbol, current_price) if current_price else None,
- "float_pnl": round(float(float_pnl), FUNDS_DECIMALS),
- "float_pct": float_pct,
- },
+ "order": order_payload,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
@@ -6675,8 +6687,6 @@ def api_key_kline():
"direction": key_row["direction"] or "long",
"upper": upper,
"lower": lower,
- "upper_display": format_price_for_symbol(exchange_symbol, upper) if upper is not None else None,
- "lower_display": format_price_for_symbol(exchange_symbol, lower) if lower is not None else None,
"notification_count": int(key_row["notification_count"] or 0),
"upper_diff": upper_diff,
"upper_pct": upper_pct,
@@ -6684,16 +6694,35 @@ def api_key_kline():
"lower_pct": lower_pct,
}
+ from focus_chart_lib import enrich_key_kline_response
+
+ price_display, key_info = enrich_key_kline_response(
+ symbol=symbol,
+ current_price=current_price,
+ key_info=key_info,
+ format_price_fn=format_price_for_symbol,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+
return jsonify({
"ok": True,
"symbol": symbol,
"timeframe": timeframe,
"limit": limit,
"current_price": round(float(current_price), 8) if current_price is not None else None,
- "current_price_display": format_price_for_symbol(exchange_symbol, current_price) if current_price is not None else None,
+ "current_price_display": price_display,
"key_monitor": key_info,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
diff --git a/crypto_monitor_binance/templates/key_focus_v2.html b/crypto_monitor_binance/templates/key_focus_v2.html
deleted file mode 100644
index 2a80064..0000000
--- a/crypto_monitor_binance/templates/key_focus_v2.html
+++ /dev/null
@@ -1,278 +0,0 @@
-
-
-
-
-
-
- {{ exchange_display }} | 关键位放大
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
关键位放大(可输入币种){{ exchange_display }}
-
-
最近刷新:--
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/crypto_monitor_binance/templates/order_focus_v2.html b/crypto_monitor_binance/templates/order_focus_v2.html
deleted file mode 100644
index 57c16d2..0000000
--- a/crypto_monitor_binance/templates/order_focus_v2.html
+++ /dev/null
@@ -1,231 +0,0 @@
-
-
-
-
-
-
- {{ exchange_display }} | 实盘下单放大
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
实盘下单放大(100根K线){{ exchange_display }}
-
-
最近刷新:--
-
- {% if orders %}
-
-
-
-
-
-
-
-
- {% else %}
-
当前没有激活订单,无法展示放大K线。
- {% endif %}
-
-
- {% if orders %}
-
-
-
- {% endif %}
-
-
-{% if orders %}
-
-
-{% endif %}
-
-
-
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py
index 135b425..ae0ab44 100644
--- a/crypto_monitor_gate/app.py
+++ b/crypto_monitor_gate/app.py
@@ -6551,40 +6551,51 @@ def api_order_kline():
"volume": float(bar[5]),
})
- current_price = get_price(order_item["symbol"])
- margin = float(order_item.get("margin_capital") or 0)
- leverage = float(order_item.get("leverage") or 0)
- entry = float(order_item.get("trigger_price") or 0)
- float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
- float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0
+ from focus_chart_lib import (
+ build_order_kline_order_payload,
+ load_swap_positions_for_order_kline,
+ metrics_for_order_item,
+ )
+
+ current_price = get_price(order_item["symbol"])
+ positions = load_swap_positions_for_order_kline(
+ exchange,
+ private_configured=exchange_private_api_configured(),
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+ ex_metrics = metrics_for_order_item(
+ order_item,
+ positions,
+ resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
+ select_live_fn=_select_live_position_row,
+ parse_metrics_fn=parse_ccxt_position_metrics,
+ )
+ order_payload = build_order_kline_order_payload(
+ order_item,
+ ticker_price=current_price,
+ format_price_fn=format_price_for_symbol,
+ calc_pnl_fn=calc_pnl,
+ calc_rr_ratio_fn=calc_rr_ratio,
+ ex_metrics=ex_metrics,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
- sym = order_item["symbol"]
return jsonify({
"ok": True,
"timeframe": timeframe,
"limit": limit,
- "order": {
- "id": order_item["id"],
- "symbol": sym,
- "direction": order_item.get("direction") or "long",
- "trigger_price": order_item.get("trigger_price"),
- "stop_loss": order_item.get("stop_loss"),
- "take_profit": order_item.get("take_profit"),
- "trigger_price_display": format_price_for_symbol(sym, order_item.get("trigger_price")),
- "stop_loss_display": format_price_for_symbol(sym, order_item.get("stop_loss")),
- "take_profit_display": format_price_for_symbol(sym, order_item.get("take_profit")),
- "margin_capital": order_item.get("margin_capital"),
- "leverage": order_item.get("leverage"),
- "position_ratio": order_item.get("position_ratio"),
- "rr_ratio": order_item.get("rr_ratio"),
- "breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
- "current_price": round(float(current_price), 8) if current_price else None,
- "current_price_display": format_price_for_symbol(sym, current_price) if current_price else None,
- "float_pnl": round(float(float_pnl), 2),
- "float_pct": float_pct,
- },
+ "order": order_payload,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
@@ -6685,15 +6696,35 @@ def api_key_kline():
"lower_pct": lower_pct,
}
+ from focus_chart_lib import enrich_key_kline_response
+
+ price_display, key_info = enrich_key_kline_response(
+ symbol=symbol,
+ current_price=current_price,
+ key_info=key_info,
+ format_price_fn=format_price_for_symbol,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+
return jsonify({
"ok": True,
"symbol": symbol,
"timeframe": timeframe,
"limit": limit,
"current_price": round(float(current_price), 8) if current_price is not None else None,
+ "current_price_display": price_display,
"key_monitor": key_info,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
diff --git a/crypto_monitor_gate/templates/key_focus_v2.html b/crypto_monitor_gate/templates/key_focus_v2.html
deleted file mode 100644
index 92814dc..0000000
--- a/crypto_monitor_gate/templates/key_focus_v2.html
+++ /dev/null
@@ -1,278 +0,0 @@
-
-
-
-
-
-
- {{ exchange_display }} | 关键位放大
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
关键位放大(可输入币种){{ exchange_display }}
-
-
最近刷新:--
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/crypto_monitor_gate/templates/order_focus_v2.html b/crypto_monitor_gate/templates/order_focus_v2.html
deleted file mode 100644
index 57c16d2..0000000
--- a/crypto_monitor_gate/templates/order_focus_v2.html
+++ /dev/null
@@ -1,231 +0,0 @@
-
-
-
-
-
-
- {{ exchange_display }} | 实盘下单放大
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
实盘下单放大(100根K线){{ exchange_display }}
-
-
最近刷新:--
-
- {% if orders %}
-
-
-
-
-
-
-
-
- {% else %}
-
当前没有激活订单,无法展示放大K线。
- {% endif %}
-
-
- {% if orders %}
-
-
-
- {% endif %}
-
-
-{% if orders %}
-
-
-{% endif %}
-
-
-
diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py
index e391fa2..73a5099 100644
--- a/crypto_monitor_gate_bot/app.py
+++ b/crypto_monitor_gate_bot/app.py
@@ -6290,35 +6290,51 @@ def api_order_kline():
"volume": float(bar[5]),
})
+ from focus_chart_lib import (
+ build_order_kline_order_payload,
+ load_swap_positions_for_order_kline,
+ metrics_for_order_item,
+ )
+
current_price = get_price(order_item["symbol"])
- margin = float(order_item.get("margin_capital") or 0)
- leverage = float(order_item.get("leverage") or 0)
- entry = float(order_item.get("trigger_price") or 0)
- float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
- float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0
+ positions = load_swap_positions_for_order_kline(
+ exchange,
+ private_configured=exchange_private_api_configured(),
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+ ex_metrics = metrics_for_order_item(
+ order_item,
+ positions,
+ resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
+ select_live_fn=_select_live_position_row,
+ parse_metrics_fn=parse_ccxt_position_metrics,
+ )
+ order_payload = build_order_kline_order_payload(
+ order_item,
+ ticker_price=current_price,
+ format_price_fn=format_price_for_symbol,
+ calc_pnl_fn=calc_pnl,
+ calc_rr_ratio_fn=calc_rr_ratio,
+ ex_metrics=ex_metrics,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
return jsonify({
"ok": True,
"timeframe": timeframe,
"limit": limit,
- "order": {
- "id": order_item["id"],
- "symbol": order_item["symbol"],
- "direction": order_item.get("direction") or "long",
- "trigger_price": order_item.get("trigger_price"),
- "stop_loss": order_item.get("stop_loss"),
- "take_profit": order_item.get("take_profit"),
- "margin_capital": order_item.get("margin_capital"),
- "leverage": order_item.get("leverage"),
- "position_ratio": order_item.get("position_ratio"),
- "rr_ratio": order_item.get("rr_ratio"),
- "breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
- "current_price": round(float(current_price), 8) if current_price else None,
- "float_pnl": round(float(float_pnl), 6),
- "float_pct": float_pct,
- },
+ "order": order_payload,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
@@ -6419,15 +6435,35 @@ def api_key_kline():
"lower_pct": lower_pct,
}
+ from focus_chart_lib import enrich_key_kline_response
+
+ price_display, key_info = enrich_key_kline_response(
+ symbol=symbol,
+ current_price=current_price,
+ key_info=key_info,
+ format_price_fn=format_price_for_symbol,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+
return jsonify({
"ok": True,
"symbol": symbol,
"timeframe": timeframe,
"limit": limit,
"current_price": round(float(current_price), 8) if current_price is not None else None,
+ "current_price_display": price_display,
"key_monitor": key_info,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
diff --git a/crypto_monitor_gate_bot/templates/key_focus_v2.html b/crypto_monitor_gate_bot/templates/key_focus_v2.html
deleted file mode 100644
index 92814dc..0000000
--- a/crypto_monitor_gate_bot/templates/key_focus_v2.html
+++ /dev/null
@@ -1,278 +0,0 @@
-
-
-
-
-
-
- {{ exchange_display }} | 关键位放大
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
关键位放大(可输入币种){{ exchange_display }}
-
-
最近刷新:--
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/crypto_monitor_gate_bot/templates/order_focus_v2.html b/crypto_monitor_gate_bot/templates/order_focus_v2.html
deleted file mode 100644
index 95412ef..0000000
--- a/crypto_monitor_gate_bot/templates/order_focus_v2.html
+++ /dev/null
@@ -1,231 +0,0 @@
-
-
-
-
-
-
- {{ exchange_display }} | 实盘下单放大
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
实盘下单放大(100根K线){{ exchange_display }}
-
-
最近刷新:--
-
- {% if orders %}
-
-
-
-
-
-
-
-
- {% else %}
-
当前没有激活订单,无法展示放大K线。
- {% endif %}
-
-
- {% if orders %}
-
-
-
- {% endif %}
-
-
-{% if orders %}
-
-
-{% endif %}
-
-
-
diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index 282521a..253f2aa 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -6148,34 +6148,51 @@ def api_order_kline():
"volume": float(bar[5]),
})
+ from focus_chart_lib import (
+ build_order_kline_order_payload,
+ load_swap_positions_for_order_kline,
+ metrics_for_order_item,
+ )
+
current_price = get_price(order_item["symbol"])
- margin = float(order_item.get("margin_capital") or 0)
- leverage = float(order_item.get("leverage") or 0)
- entry = float(order_item.get("trigger_price") or 0)
- float_pnl = calc_pnl(order_item.get("direction") or "long", entry, current_price, margin, leverage) if current_price else 0
- float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0
+ positions = load_swap_positions_for_order_kline(
+ exchange,
+ private_configured=exchange_private_api_configured(),
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+ ex_metrics = metrics_for_order_item(
+ order_item,
+ positions,
+ resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
+ select_live_fn=_select_live_position_row,
+ parse_metrics_fn=parse_ccxt_position_metrics,
+ )
+ order_payload = build_order_kline_order_payload(
+ order_item,
+ ticker_price=current_price,
+ format_price_fn=format_price_for_symbol,
+ calc_pnl_fn=calc_pnl,
+ calc_rr_ratio_fn=calc_rr_ratio,
+ ex_metrics=ex_metrics,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
return jsonify({
"ok": True,
"timeframe": timeframe,
"limit": limit,
- "order": {
- "id": order_item["id"],
- "symbol": order_item["symbol"],
- "direction": order_item.get("direction") or "long",
- "trigger_price": order_item.get("trigger_price"),
- "stop_loss": order_item.get("stop_loss"),
- "take_profit": order_item.get("take_profit"),
- "margin_capital": order_item.get("margin_capital"),
- "leverage": order_item.get("leverage"),
- "position_ratio": order_item.get("position_ratio"),
- "rr_ratio": order_item.get("rr_ratio"),
- "current_price": round(float(current_price), 8) if current_price else None,
- "float_pnl": round(float(float_pnl), 6),
- "float_pct": float_pct,
- },
+ "order": order_payload,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
@@ -6275,15 +6292,35 @@ def api_key_kline():
"lower_pct": lower_pct,
}
+ from focus_chart_lib import enrich_key_kline_response
+
+ price_display, key_info = enrich_key_kline_response(
+ symbol=symbol,
+ current_price=current_price,
+ key_info=key_info,
+ format_price_fn=format_price_for_symbol,
+ )
+
+ from focus_chart_lib import kline_api_price_fields
+
+ price_fields = kline_api_price_fields(
+ exchange,
+ exchange_symbol,
+ candles,
+ ensure_markets_fn=ensure_markets_loaded,
+ )
+
return jsonify({
"ok": True,
"symbol": symbol,
"timeframe": timeframe,
"limit": limit,
"current_price": round(float(current_price), 8) if current_price is not None else None,
+ "current_price_display": price_display,
"key_monitor": key_info,
"candles": candles,
"updated_at": app_now_str(),
+ **price_fields,
})
diff --git a/crypto_monitor_okx/templates/key_focus_v2.html b/crypto_monitor_okx/templates/key_focus_v2.html
deleted file mode 100644
index 22fa40c..0000000
--- a/crypto_monitor_okx/templates/key_focus_v2.html
+++ /dev/null
@@ -1,277 +0,0 @@
-
-
-
-
-
-
- 关键位放大 | K线查看
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/crypto_monitor_okx/templates/order_focus_v2.html b/crypto_monitor_okx/templates/order_focus_v2.html
deleted file mode 100644
index 79ae52e..0000000
--- a/crypto_monitor_okx/templates/order_focus_v2.html
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
-
- 实盘下单放大 | 100根K线
-
-
-
-
-
-
-
-
-
-
-
-
返回首页
-
实盘下单放大(100根K线)
-
-
最近刷新:--
-
- {% if orders %}
-
-
-
-
-
-
-
-
- {% else %}
-
当前没有激活订单,无法展示放大K线。
- {% endif %}
-
-
- {% if orders %}
-
-
-
- {% endif %}
-
-
-{% if orders %}
-
-
-{% endif %}
-
-
-
diff --git a/focus_chart_lib.py b/focus_chart_lib.py
new file mode 100644
index 0000000..4da1cf5
--- /dev/null
+++ b/focus_chart_lib.py
@@ -0,0 +1,187 @@
+"""实盘/关键位放大 K 线:订单元数据与交易所浮盈、价格展示精度。"""
+from __future__ import annotations
+
+from typing import Any, Callable, Optional
+
+from hub_ohlcv_lib import (
+ normalize_price_tick,
+ price_tick_from_market,
+ round_ohlcv_bars_to_tick,
+)
+from order_monitor_display_lib import (
+ apply_order_live_price_display,
+ apply_order_price_display_fields,
+)
+
+
+def resolve_kline_price_tick(
+ exchange: Any,
+ exchange_symbol: str,
+ *,
+ ensure_markets_fn: Callable[[], None],
+) -> Optional[float]:
+ """交易所最小价格变动单位,供 lightweight-charts 右侧刻度与标记线对齐。"""
+ if not exchange_symbol:
+ return None
+ try:
+ ensure_markets_fn()
+ return normalize_price_tick(price_tick_from_market(exchange, exchange_symbol))
+ except Exception:
+ return None
+
+
+def align_candles_to_price_tick(
+ candles: list[dict[str, Any]],
+ price_tick: Optional[float],
+) -> None:
+ if price_tick is not None and candles:
+ round_ohlcv_bars_to_tick(candles, price_tick)
+
+
+def kline_api_price_fields(
+ exchange: Any,
+ exchange_symbol: str,
+ candles: list[dict[str, Any]],
+ *,
+ ensure_markets_fn: Callable[[], None],
+) -> dict[str, Any]:
+ tick = resolve_kline_price_tick(
+ exchange, exchange_symbol, ensure_markets_fn=ensure_markets_fn
+ )
+ align_candles_to_price_tick(candles, tick)
+ return {"price_tick": tick}
+
+
+def load_swap_positions_for_order_kline(
+ exchange: Any,
+ *,
+ private_configured: bool,
+ ensure_markets_fn: Callable[[], None],
+ settle: str = "usdt",
+) -> list:
+ if not private_configured:
+ return []
+ try:
+ ensure_markets_fn()
+ try:
+ return exchange.fetch_positions(None, {"settle": settle}) or []
+ except Exception:
+ return exchange.fetch_positions() or []
+ except Exception:
+ return []
+
+
+def metrics_for_order_item(
+ order_item: dict[str, Any],
+ positions: list,
+ *,
+ resolve_ex_sym_fn: Callable[[Any], str],
+ select_live_fn: Callable[[list, str, str], Any],
+ parse_metrics_fn: Callable[..., Optional[dict]],
+) -> Optional[dict]:
+ if not positions:
+ return None
+ ex_sym = resolve_ex_sym_fn(order_item)
+ direction = order_item.get("direction") or "long"
+ prow = select_live_fn(positions, ex_sym, direction)
+ if not prow:
+ return None
+ lev = order_item.get("leverage")
+ return parse_metrics_fn(prow, order_leverage=lev)
+
+
+def build_order_kline_order_payload(
+ order_item: dict[str, Any],
+ *,
+ ticker_price: Any,
+ format_price_fn: Callable[[Any, Any], str],
+ calc_pnl_fn: Callable[..., float],
+ calc_rr_ratio_fn: Callable[..., Optional[float]],
+ ex_metrics: Optional[dict] = None,
+) -> dict[str, Any]:
+ sym = order_item.get("symbol") or ""
+ direction = order_item.get("direction") or "long"
+ margin = float(order_item.get("margin_capital") or 0)
+ leverage = float(order_item.get("leverage") or 0)
+ entry = float(order_item.get("trigger_price") or 0)
+
+ float_pnl = 0.0
+ float_pct = 0.0
+ if ticker_price and entry > 0:
+ float_pnl = float(
+ calc_pnl_fn(direction, entry, ticker_price, margin, leverage)
+ )
+ float_pct = round((float_pnl / margin * 100), 4) if margin > 0 else 0.0
+
+ px_for_fmt = ticker_price
+ mark_raw = None
+ if ex_metrics and ex_metrics.get("mark_price") is not None:
+ mark_raw = ex_metrics["mark_price"]
+ try:
+ px_for_fmt = float(mark_raw)
+ except (TypeError, ValueError):
+ pass
+
+ if ex_metrics and ex_metrics.get("unrealized_pnl") is not None:
+ float_pnl = round(float(ex_metrics["unrealized_pnl"]), 2)
+ denom = ex_metrics.get("initial_margin") or margin
+ float_pct = (
+ round((float_pnl / float(denom)) * 100, 4)
+ if denom and float(denom) > 0
+ else float_pct
+ )
+
+ payload: dict[str, Any] = {
+ "id": order_item["id"],
+ "symbol": sym,
+ "direction": direction,
+ "trigger_price": order_item.get("trigger_price"),
+ "stop_loss": order_item.get("stop_loss"),
+ "take_profit": order_item.get("take_profit"),
+ "trigger_price_display": format_price_fn(sym, order_item.get("trigger_price")),
+ "stop_loss_display": format_price_fn(sym, order_item.get("stop_loss")),
+ "take_profit_display": format_price_fn(sym, order_item.get("take_profit")),
+ "margin_capital": order_item.get("margin_capital"),
+ "leverage": order_item.get("leverage"),
+ "position_ratio": order_item.get("position_ratio"),
+ "breakeven_enabled": bool(int(order_item.get("breakeven_enabled") or 0)),
+ "current_price": round(float(px_for_fmt), 8) if px_for_fmt is not None else None,
+ "float_pnl": round(float(float_pnl), 2),
+ "float_pct": float_pct,
+ }
+ apply_order_price_display_fields(
+ payload,
+ direction=direction,
+ entry_price=order_item.get("trigger_price"),
+ initial_stop_loss=order_item.get("initial_stop_loss"),
+ stop_loss=order_item.get("stop_loss"),
+ take_profit=order_item.get("take_profit"),
+ calc_rr_ratio_fn=calc_rr_ratio_fn,
+ )
+ apply_order_live_price_display(
+ payload,
+ sym,
+ ticker_price,
+ mark_raw,
+ format_price_fn,
+ )
+ payload["current_price_display"] = payload.get("price_display") or (
+ format_price_fn(sym, px_for_fmt) if px_for_fmt is not None else None
+ )
+ return payload
+
+
+def enrich_key_kline_response(
+ *,
+ symbol: str,
+ current_price: Any,
+ key_info: Optional[dict[str, Any]],
+ format_price_fn: Callable[[Any, Any], str],
+) -> tuple[Any, Optional[dict[str, Any]]]:
+ price_display = format_price_fn(symbol, current_price) if current_price is not None else None
+ if key_info is None:
+ return price_display, None
+ enriched = dict(key_info)
+ enriched["upper_display"] = format_price_fn(symbol, key_info.get("upper"))
+ enriched["lower_display"] = format_price_fn(symbol, key_info.get("lower"))
+ return price_display, enriched
diff --git a/hub_bridge.py b/hub_bridge.py
index 8d1533b..93618f9 100644
--- a/hub_bridge.py
+++ b/hub_bridge.py
@@ -51,6 +51,8 @@ def install_instance_theme_static(app) -> None:
assets = {
"instance_theme.js": "application/javascript; charset=utf-8",
"instance_theme.css": "text/css; charset=utf-8",
+ "focus_chart_page.js": "application/javascript; charset=utf-8",
+ "focus_chart_page.css": "text/css; charset=utf-8",
}
for name, mime in assets.items():
diff --git a/static/focus_chart_page.css b/static/focus_chart_page.css
new file mode 100644
index 0000000..2f3966c
--- /dev/null
+++ b/static/focus_chart_page.css
@@ -0,0 +1,221 @@
+/* 实盘/关键位放大页:与 instance_theme 联动,高对比 meta + 主题感知图表区 */
+body.focus-page {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ padding: 14px;
+ margin: 0;
+ background: var(--focus-bg, #0b0d14);
+ color: var(--focus-fg, #eaeaea);
+}
+
+html[data-theme="light"] body.focus-page {
+ --focus-bg: #eef3f8;
+ --focus-fg: #142232;
+ --focus-card-bg: #fff;
+ --focus-card-border: #b8c8d8;
+ --focus-meta-bg: #fff;
+ --focus-meta-border: #9eb4c8;
+ --focus-meta-label: #2a4a66;
+ --focus-meta-value: #0a1628;
+ --focus-status: #4a6078;
+ --focus-chart-bg: #f0f4f9;
+ --focus-chart-border: #b8c8d8;
+ --focus-btn-bg: #fff;
+ --focus-btn-fg: #006e9a;
+ --focus-btn-border: rgba(0, 95, 140, 0.22);
+ --focus-input-bg: #fff;
+ --focus-input-fg: #142232;
+ --focus-input-border: #b8c8d8;
+ --focus-title: #0a1628;
+ --focus-pnl-up: #0a7a3d;
+ --focus-pnl-down: #c62828;
+ --focus-dir-short: #b71c1c;
+ --focus-dir-long: #0a7a3d;
+}
+
+html[data-theme="dark"] body.focus-page {
+ --focus-bg: #0b0d14;
+ --focus-fg: #eaeaea;
+ --focus-card-bg: #121726;
+ --focus-card-border: #2a3150;
+ --focus-meta-bg: #141b2f;
+ --focus-meta-border: #3d4f72;
+ --focus-meta-label: #c8d8f0;
+ --focus-meta-value: #f0f4ff;
+ --focus-status: #95a2c2;
+ --focus-chart-bg: #0f1320;
+ --focus-chart-border: #2a3150;
+ --focus-btn-bg: #151a2a;
+ --focus-btn-fg: #8fc8ff;
+ --focus-btn-border: #304164;
+ --focus-input-bg: #1a1a29;
+ --focus-input-fg: #fff;
+ --focus-input-border: #2e2e45;
+ --focus-title: #dbe4ff;
+ --focus-pnl-up: #3ddc84;
+ --focus-pnl-down: #ff7070;
+ --focus-dir-short: #ff8a80;
+ --focus-dir-long: #69f0ae;
+}
+
+body.focus-page * {
+ box-sizing: border-box;
+}
+
+.focus-page .container {
+ width: min(98vw, 1900px);
+ margin: 0 auto;
+}
+
+.focus-page .card {
+ background: var(--focus-card-bg);
+ border-radius: 10px;
+ padding: 12px;
+ border: 1px solid var(--focus-card-border);
+ margin-bottom: 12px;
+}
+
+.focus-page .row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.focus-page .btn {
+ padding: 7px 10px;
+ border-radius: 8px;
+ text-decoration: none;
+ border: 1px solid var(--focus-btn-border);
+ background: var(--focus-btn-bg);
+ color: var(--focus-btn-fg);
+ cursor: pointer;
+}
+
+.focus-page .btn:hover {
+ filter: brightness(1.06);
+}
+
+.focus-page select,
+.focus-page input,
+.focus-page button {
+ padding: 8px 10px;
+ border-radius: 8px;
+ border: 1px solid var(--focus-input-border);
+ background: var(--focus-input-bg);
+ color: var(--focus-input-fg);
+}
+
+.focus-page .focus-title {
+ color: var(--focus-title);
+ font-weight: 700;
+}
+
+.focus-page .meta {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.focus-page .meta-item {
+ background: var(--focus-meta-bg);
+ border: 1px solid var(--focus-meta-border);
+ border-radius: 8px;
+ padding: 10px 10px 9px;
+}
+
+.focus-page .meta-item .k {
+ font-size: 0.78rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ color: var(--focus-meta-label);
+}
+
+.focus-page .meta-item .v {
+ font-size: 1.02rem;
+ font-weight: 600;
+ margin-top: 5px;
+ word-break: break-all;
+ color: var(--focus-meta-value);
+}
+
+.focus-page .meta-item--emph {
+ border-width: 2px;
+ border-color: var(--focus-meta-label);
+}
+
+.focus-page .meta-item--emph .k {
+ font-size: 0.82rem;
+ font-weight: 700;
+}
+
+.focus-page .meta-item--emph .v {
+ font-size: 1.12rem;
+ font-weight: 800;
+}
+
+.focus-page .meta-item--pnl .v {
+ font-size: 1.14rem;
+ font-weight: 800;
+ letter-spacing: 0.01em;
+}
+
+.focus-page .meta-pnl-up {
+ color: var(--focus-pnl-up) !important;
+}
+
+.focus-page .meta-pnl-down {
+ color: var(--focus-pnl-down) !important;
+}
+
+.focus-page .meta-dir-long {
+ color: var(--focus-dir-long) !important;
+}
+
+.focus-page .meta-dir-short {
+ color: var(--focus-dir-short) !important;
+}
+
+.focus-page .status {
+ font-size: 0.84rem;
+ color: var(--focus-status);
+}
+
+.focus-page .status.err {
+ color: var(--focus-pnl-down);
+}
+
+.focus-page #chart-wrap {
+ height: 560px;
+ background: var(--focus-chart-bg);
+ border: 1px solid var(--focus-chart-border);
+ border-radius: 10px;
+ padding: 8px;
+}
+
+.focus-page #chart {
+ width: 100%;
+ height: 100%;
+}
+
+.focus-page .empty {
+ padding: 18px;
+ color: var(--focus-status);
+}
+
+.focus-page .exchange-tag {
+ font-size: 0.72rem;
+ font-weight: 600;
+ color: #b8f5d0;
+ background: #14241e;
+ border: 1px solid #2d6a4f;
+ padding: 4px 10px;
+ border-radius: 999px;
+ margin-left: 8px;
+}
+
+html[data-theme="light"] .focus-page .exchange-tag {
+ color: #0a5c38;
+ background: #e8f5ee;
+ border-color: #7bc9a0;
+}
diff --git a/static/focus_chart_page.js b/static/focus_chart_page.js
new file mode 100644
index 0000000..ded6202
--- /dev/null
+++ b/static/focus_chart_page.js
@@ -0,0 +1,401 @@
+/**
+ * 实盘/关键位放大 K 线:交易所 tick 精度、主题感知图表、高对比 meta。
+ */
+(function (global) {
+ "use strict";
+
+ let activePriceTick = null;
+
+ function currentTheme() {
+ return document.documentElement.getAttribute("data-theme") === "light"
+ ? "light"
+ : "dark";
+ }
+
+ function chartTheme(theme) {
+ if (theme === "light") {
+ return {
+ layout: { background: { color: "#f0f4f9" }, textColor: "#142232" },
+ grid: { vertLines: { color: "#d0dae4" }, horzLines: { color: "#d0dae4" } },
+ rightPriceScale: { borderColor: "#b8c8d8" },
+ timeScale: { borderColor: "#b8c8d8" },
+ candle: {
+ upColor: "#0a7a3d",
+ downColor: "#c62828",
+ wickUpColor: "#0a7a3d",
+ wickDownColor: "#c62828",
+ },
+ };
+ }
+ return {
+ layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
+ grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
+ rightPriceScale: { borderColor: "#2a3150" },
+ timeScale: { borderColor: "#2a3150" },
+ candle: {
+ upColor: "#4cd97f",
+ downColor: "#ff6666",
+ wickUpColor: "#4cd97f",
+ wickDownColor: "#ff6666",
+ },
+ };
+ }
+
+ const SAFE_PRICE_FORMAT = { type: "price", precision: 4, minMove: 0.0001 };
+
+ function decimalsFromTick(tick) {
+ if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return null;
+ const minMove = Number(tick);
+ if (minMove >= 1) return 0;
+ const raw = String(minMove);
+ const sci = raw.match(/e-(\d+)/i);
+ if (sci) return Math.min(12, parseInt(sci[1], 10));
+ const fixed = minMove.toFixed(12);
+ const frac = fixed.split(".")[1] || "";
+ const trimmed = frac.replace(/0+$/, "");
+ if (trimmed.length) return Math.min(12, trimmed.length);
+ return Math.max(0, Math.min(12, Math.round(-Math.log10(minMove))));
+ }
+
+ function tickToPriceFormat(tick) {
+ try {
+ if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) {
+ return { type: "price", precision: 2, minMove: 0.01 };
+ }
+ const minMove = Number(tick);
+ let prec = decimalsFromTick(minMove);
+ if (prec == null || prec < 0) prec = 4;
+ prec = Math.min(12, Math.max(0, Math.floor(prec)));
+ return { type: "price", precision: prec, minMove: minMove };
+ } catch (_) {
+ return SAFE_PRICE_FORMAT;
+ }
+ }
+
+ function roundToTick(v, tick) {
+ if (v == null || Number.isNaN(Number(v))) return v;
+ const n = Number(v);
+ if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return n;
+ const t = Number(tick);
+ const rounded = Math.round(n / t) * t;
+ const dec = decimalsFromTick(t);
+ if (dec == null) return rounded;
+ return parseFloat(rounded.toFixed(dec));
+ }
+
+ function fmtPriceByTick(v, tick) {
+ if (v == null || Number.isNaN(Number(v))) return "-";
+ const n = Number(roundToTick(v, tick));
+ if (n === 0) return "0";
+ const dec = decimalsFromTick(tick);
+ if (dec != null) return n.toFixed(dec);
+ const av = Math.abs(n);
+ let d = 8;
+ if (av >= 10000) d = 2;
+ else if (av >= 100) d = 3;
+ else if (av >= 1) d = 4;
+ else if (av >= 0.01) d = 6;
+ const text = n.toFixed(d);
+ return text.includes(".") ? text.replace(/\.?0+$/, "") : text;
+ }
+
+ function setActivePriceTick(tick) {
+ activePriceTick =
+ tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0
+ ? null
+ : Number(tick);
+ }
+
+ function formatSigned(v, digits) {
+ digits = digits === undefined ? 2 : digits;
+ if (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-";
+ const n = Number(v);
+ const sign = n > 0 ? "+" : "";
+ return sign + n.toFixed(digits);
+ }
+
+ function formatSignedPrice(v) {
+ if (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-";
+ const n = Number(v);
+ const body = fmtPriceByTick(Math.abs(n), activePriceTick);
+ if (body === "-") return "-";
+ return (n > 0 ? "+" : n < 0 ? "-" : "") + body;
+ }
+
+ function formatRrRatio(rr) {
+ if (rr === null || typeof rr === "undefined") return "-:1";
+ const n = Number(rr);
+ if (Number.isNaN(n)) return "-:1";
+ const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2)));
+ return body + ":1";
+ }
+
+ function displayPrice(orderOrData, field, rawField) {
+ const dispKey = field + "_display";
+ if (orderOrData && orderOrData[dispKey] && orderOrData[dispKey] !== "-") {
+ return String(orderOrData[dispKey]);
+ }
+ const raw = orderOrData ? orderOrData[rawField || field] : null;
+ if (raw === null || typeof raw === "undefined" || Number.isNaN(Number(raw))) return "-";
+ return fmtPriceByTick(raw, activePriceTick);
+ }
+
+ function lineTitle(label, display) {
+ const d = display && display !== "-" ? display : "";
+ return d ? label + " " + d : label;
+ }
+
+ function paintOrderMeta(order) {
+ const symEl = document.getElementById("m-symbol");
+ const dirEl = document.getElementById("m-direction");
+ const pnlEl = document.getElementById("m-pnl");
+ if (symEl) symEl.textContent = order.symbol || "-";
+ if (dirEl) {
+ const isShort = order.direction === "short";
+ dirEl.textContent = isShort ? "做空" : "做多";
+ dirEl.className = "v " + (isShort ? "meta-dir-short" : "meta-dir-long");
+ }
+ const set = function (id, text) {
+ const el = document.getElementById(id);
+ if (el) el.textContent = text;
+ };
+ set("m-entry", displayPrice(order, "trigger_price"));
+ set("m-sl", displayPrice(order, "stop_loss"));
+ set("m-tp", displayPrice(order, "take_profit"));
+ set("m-rr", formatRrRatio(order.rr_ratio));
+ set(
+ "m-breakeven",
+ order.breakeven_enabled === false || order.breakeven_enabled === 0 ? "关闭" : "开启"
+ );
+ set(
+ "m-price",
+ order.current_price_display ||
+ order.price_display ||
+ displayPrice(order, "current_price")
+ );
+ if (pnlEl) {
+ pnlEl.textContent =
+ formatSigned(order.float_pnl, 2) +
+ "U (" +
+ formatSigned(order.float_pct, 2) +
+ "%)";
+ pnlEl.className = "v";
+ const pnl = Number(order.float_pnl || 0);
+ if (pnl > 0) pnlEl.classList.add("meta-pnl-up");
+ else if (pnl < 0) pnlEl.classList.add("meta-pnl-down");
+ }
+ }
+
+ function paintKeyMeta(data) {
+ const key = data.key_monitor || null;
+ const symEl = document.getElementById("m-symbol");
+ if (symEl) symEl.textContent = data.symbol || "-";
+ const set = function (id, text) {
+ const el = document.getElementById(id);
+ if (el) el.textContent = text;
+ };
+ set(
+ "m-price",
+ data.current_price_display || displayPrice(data, "current_price")
+ );
+ const dirEl = document.getElementById("m-direction");
+ if (!key) {
+ set("m-type", "未匹配到关键位");
+ set("m-direction", "-");
+ if (dirEl) dirEl.className = "v";
+ set("m-upper", "-");
+ set("m-lower", "-");
+ set("m-updiff", "-");
+ set("m-lowdiff", "-");
+ return;
+ }
+ set("m-type", key.monitor_type || "-");
+ if (dirEl) {
+ const isShort = key.direction === "short";
+ dirEl.textContent = isShort ? "做空" : "做多";
+ dirEl.className = "v " + (isShort ? "meta-dir-short" : "meta-dir-long");
+ }
+ set("m-upper", key.upper_display || displayPrice(key, "upper"));
+ set("m-lower", key.lower_display || displayPrice(key, "lower"));
+ if (activePriceTick != null) {
+ set(
+ "m-updiff",
+ formatSignedPrice(key.upper_diff) +
+ " (" +
+ formatSigned(key.upper_pct, 2) +
+ "%)"
+ );
+ set(
+ "m-lowdiff",
+ formatSignedPrice(key.lower_diff) +
+ " (" +
+ formatSigned(key.lower_pct, 2) +
+ "%)"
+ );
+ } else {
+ set(
+ "m-updiff",
+ formatSigned(key.upper_diff, 4) + " (" + formatSigned(key.upper_pct, 2) + "%)"
+ );
+ set(
+ "m-lowdiff",
+ formatSigned(key.lower_diff, 4) + " (" + formatSigned(key.lower_pct, 2) + "%)"
+ );
+ }
+ }
+
+ function applyPriceFormatToSeries(series, pf) {
+ if (!series || !series.applyOptions) return;
+ try {
+ series.applyOptions({ priceFormat: pf });
+ } catch (_) {
+ try {
+ series.applyOptions({ priceFormat: SAFE_PRICE_FORMAT });
+ } catch (_2) {}
+ }
+ }
+
+ function createFocusChart(host) {
+ if (!global.LightweightCharts) return null;
+ const th = chartTheme(currentTheme());
+ const chart = global.LightweightCharts.createChart(host, {
+ layout: th.layout,
+ grid: th.grid,
+ rightPriceScale: th.rightPriceScale,
+ timeScale: Object.assign({ timeVisible: true, secondsVisible: false }, th.timeScale),
+ crosshair: { mode: 0 },
+ localization: {
+ priceFormatter: function (p) {
+ return fmtPriceByTick(p, activePriceTick);
+ },
+ },
+ });
+ let candleSeries = null;
+
+ function applyChartPriceFormat() {
+ let pf = SAFE_PRICE_FORMAT;
+ try {
+ pf = tickToPriceFormat(activePriceTick);
+ } catch (_) {
+ pf = SAFE_PRICE_FORMAT;
+ }
+ applyPriceFormatToSeries(candleSeries, pf);
+ try {
+ chart.applyOptions({
+ localization: {
+ priceFormatter: function (p) {
+ return fmtPriceByTick(p, activePriceTick);
+ },
+ },
+ });
+ } catch (_) {}
+ }
+
+ function setPriceTick(tick) {
+ setActivePriceTick(tick);
+ applyChartPriceFormat();
+ }
+
+ const opts = Object.assign({ borderVisible: false }, th.candle);
+ if (typeof chart.addCandlestickSeries === "function") {
+ candleSeries = chart.addCandlestickSeries(opts);
+ } else if (
+ typeof chart.addSeries === "function" &&
+ global.LightweightCharts.CandlestickSeries
+ ) {
+ candleSeries = chart.addSeries(global.LightweightCharts.CandlestickSeries, opts);
+ }
+ applyChartPriceFormat();
+
+ const priceLines = [];
+ function resetPriceLines() {
+ if (!candleSeries) return;
+ priceLines.forEach(function (line) {
+ try {
+ candleSeries.removePriceLine(line);
+ } catch (_) {}
+ });
+ priceLines.length = 0;
+ }
+ function addLine(price, title, color) {
+ if (!candleSeries || price === null || typeof price === "undefined") return;
+ const p = Number(roundToTick(price, activePriceTick));
+ if (Number.isNaN(p) || p <= 0) return;
+ priceLines.push(
+ candleSeries.createPriceLine({
+ price: p,
+ color: color,
+ lineWidth: 1,
+ lineStyle: 0,
+ axisLabelVisible: true,
+ title: title,
+ })
+ );
+ }
+ function applyTheme() {
+ const t = chartTheme(currentTheme());
+ chart.applyOptions({
+ layout: t.layout,
+ grid: t.grid,
+ rightPriceScale: t.rightPriceScale,
+ timeScale: t.timeScale,
+ localization: {
+ priceFormatter: function (p) {
+ return fmtPriceByTick(p, activePriceTick);
+ },
+ },
+ });
+ if (candleSeries && typeof candleSeries.applyOptions === "function") {
+ candleSeries.applyOptions(t.candle);
+ }
+ applyChartPriceFormat();
+ }
+ function resize() {
+ chart.applyOptions({ width: host.clientWidth, height: host.clientHeight });
+ }
+ global.addEventListener("resize", resize);
+ resize();
+ const obs = new MutationObserver(applyTheme);
+ obs.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+ return {
+ chart: chart,
+ candleSeries: candleSeries,
+ resetPriceLines: resetPriceLines,
+ addLine: addLine,
+ applyTheme: applyTheme,
+ setPriceTick: setPriceTick,
+ ensureSeries: function () {
+ if (candleSeries) return true;
+ const t = chartTheme(currentTheme());
+ const o = Object.assign({ borderVisible: false }, t.candle);
+ if (typeof chart.addCandlestickSeries === "function") {
+ candleSeries = chart.addCandlestickSeries(o);
+ } else if (
+ typeof chart.addSeries === "function" &&
+ global.LightweightCharts.CandlestickSeries
+ ) {
+ candleSeries = chart.addSeries(global.LightweightCharts.CandlestickSeries, o);
+ }
+ applyChartPriceFormat();
+ return !!candleSeries;
+ },
+ };
+ }
+
+ global.FocusChartPage = {
+ currentTheme: currentTheme,
+ chartTheme: chartTheme,
+ formatSigned: formatSigned,
+ formatRrRatio: formatRrRatio,
+ displayPrice: displayPrice,
+ lineTitle: lineTitle,
+ paintOrderMeta: paintOrderMeta,
+ paintKeyMeta: paintKeyMeta,
+ createFocusChart: createFocusChart,
+ setActivePriceTick: setActivePriceTick,
+ fmtPriceByTick: fmtPriceByTick,
+ };
+})(typeof window !== "undefined" ? window : globalThis);
diff --git a/strategy_templates/key_focus_v2.html b/strategy_templates/key_focus_v2.html
new file mode 100644
index 0000000..4229d15
--- /dev/null
+++ b/strategy_templates/key_focus_v2.html
@@ -0,0 +1,175 @@
+
+
+
+
+
+ {{ exchange_display }} | 关键位放大
+
+
+
+
+
+
+
+
+
+
返回首页
+
关键位放大(可输入币种){{ exchange_display }}
+
+
最近刷新:--
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/strategy_templates/order_focus_v2.html b/strategy_templates/order_focus_v2.html
new file mode 100644
index 0000000..48e4506
--- /dev/null
+++ b/strategy_templates/order_focus_v2.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+ {{ exchange_display }} | 实盘下单放大
+
+
+
+
+
+
+
+
+
+
返回首页
+
实盘下单放大(100根K线){{ exchange_display }}
+
+
最近刷新:--
+
+ {% if orders %}
+
+
+
+
+
+
+
+
+ {% else %}
+
当前没有激活订单,无法展示放大K线。
+ {% endif %}
+
+
+ {% if orders %}
+
+
+ {% endif %}
+
+
+{% if orders %}
+
+
+
+{% endif %}
+
+