Files
crypto_monitor/focus_chart_lib.py
dekun 93e148a3e7 fix: unify order/key focus K-line theme, PnL, RR and exchange price tick
Share focus_chart templates and APIs across four instances; align chart Y-axis, price lines and meta bar with exchange symbol precision and live unrealized PnL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 16:45:51 +08:00

188 lines
6.0 KiB
Python

"""实盘/关键位放大 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