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 %} + +