Type/to search

通用网格策略(数学自适应版v4.1)

Common strategy
Created: 2026-07-14 23:45:28
Last modified: 18 hours ago
2
Follow
494
Followers

数学自适应动态网格策略(V4.2)

一句话简介

一个运行在合约市场的动态移动网格策略:在传统网格"低买高卖、高卖低买"的基础上,叠加了一整套在线递归数学估计层(Kalman + RLS-OU + 跨资产协整),让区间宽度、方向、移动、止盈、风控全部由市场数据自适应决定,而不是靠人为写死的固定窗口。


核心理念

传统网格的痛点是"参数拍脑袋、单边行情被套、震荡里赚不够"。本策略的设计哲学是:

时间尺度是"估计出来的输出",不是"人为输入的窗口"。

所有关键状态(趋势、波动、均值回归半衰期、均衡幅度)都在每根 K 线在线更新,记忆长度随信噪比与回归速度自适应伸缩,没有任何固定 N 根回看窗口。


四种方向模式

模式行为
自动 (推荐)由体制判定动态切换:涨势→做多、跌势→做空、震荡→双向。带迟滞+冷却,旧方向持仓不暴力平仓,靠趋势闸门停加仓 + 止盈衰减自然退出。
仅做多价格回落到格价挂买多,涨一格止盈
仅做空价格上涨到格价挂卖空,跌一格止盈
多空都做以区间中线为界,下半做多、上半做空

数学自适应层(V4 核心)

① Kalman 局部水平+斜率

逐根估计瞬时趋势斜率瞬时波动 σ,观测噪声在线自适应,得出无量纲趋势强度 z = 斜率/波动

② RLS-AR(1) / OU 均值回归

作用在协整残差 ε(而非原始价格)上,估计回归半衰期 H均衡标准差 σ_eq。区间宽度由 σ_eq 决定,波动大就自动放宽、波动小就收窄。

③ 跨资产协整确认(可选)

配置参考合约后,用 β 聚合估计(抗 Epps 高频异步偏差)区分:

  • 系统性趋势(被参考确认)→ 拦住逆势单
  • 特质性噪声(目标自己在动、参考没跟)→ 照常收割

并自带参考休市/冻结检测,正股休市时段自动回退到目标自身趋势判断。


五道风控闸门(开新仓前逐一检查)

  1. 趋势闸门:确认不利趋势 → 停开逆势单
  2. 净敞口硬上界:净方向名义价值达上限 → 停开(防无限接飞刀)
  3. 库存偏移天花板(Avellaneda-Stoikov 离散版):多头堆积→有效上沿自动下压;空头镜像同理
  4. 回撤熔断:权益从峰值回撤超阈值 → 停开
  5. 止盈衰减退出:持仓龄超过 k×半衰期仍未回归 → 止盈价向现价连续衰减,主动周转资金(逆势体制下时钟×2 加速),不死等、也不二元止损

动态区间管理

  • 有利方向突破 → 区间跟随移动(持仓不平仓,继续等止盈)
  • 不利方向超出 → 区间不动、不撤单,等待价格回归
  • σ 漂移重建:无突破的震荡中,宽度偏离目标 [0.75, 1.33] 且冷却期过 → 以现价为锚重建区间
  • 手动移动:状态栏提供「降低下沿」「升高上沿」按钮

资金利用(V4.2 最大化格数)

在两个约束下取格数最大值(短板原则):

  1. 格距 ≥ 双边手续费 × 覆盖倍数(保证每格盈利覆盖手续费)
  2. 每格资金 ≥ 交易所最小下单量 / 最小名义价值

每格均等分配资金,保证每格都能成交。


主要参数速览

分类参数
基础方向模式、初始价格、区间宽度%、移动幅度%、触发偏移%、杠杆、手续费率、费用覆盖倍数
数学层估计器热身根数、参考合约、Kalman 响应性、波动自适应速率、趋势闸门阈值、OU 记忆、宽度 σ 倍数/上下限
风控资金投入系数、最大净敞口、库存天花板强度、回撤熔断%、退出半衰期倍数、β 聚合根数

适用与提示

  • 适用:合约永续市场,尤其震荡+间歇趋势交替的品种
  • 推荐:优先用「自动」方向模式,让体制判定接管方向
  • 跨资产:有强相关标的时配置参考合约,可显著改善单边行情下的抗套能力
  • 风险:网格+杠杆策略在极端单边行情仍有风险,务必设置合理的净敞口上限与回撤熔断

⚠️ 本策略仅供学习与研究,不构成投资建议。实盘前请充分回测并理解各参数含义。

Source
Python
'''backtest
start: 2026-05-07 17:00:00
end: 2026-05-19 14:53:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_OKX","currency":"SPACEX_USDT","balance":300}]
args: [["INIT_PRICE",3000],["GRID_WIDTH_PCT",30]]
'''

"""
通用网格策略(动态移动版)
========================================================

【方向模式说明】
  - 仅做多(long):价格低于格子价挂买多,涨到上一格止盈
  - 仅做空(short):价格高于格子价挂卖空,跌到下一格止盈
  - 多空都做(both):以区间中线为界,下半做多、上半做空

【百分比参数说明】
  - GRID_WIDTH_PCT: 区间宽度占价格的百分比,如 5 表示区间宽 = 价格 × 5%
  - SHIFT_STEP_PCT: 每次移动占价格的百分比,如 0.5 表示每步 = 价格 × 0.5%
  - BREAKOUT_TRIGGER_PCT: 触发移动的超出百分比,0 = 恰好碰到即触发
"""

import json
import math

# ═══════════════════════════════════════════
#  状态常量
# ═══════════════════════════════════════════
STATE_ACTIVE    = "ACTIVE"
STATE_SHIFTING  = "SHIFTING"
STATE_WAITING   = "WAITING"   # 新增:价格超出区间,等待回归

# ═══════════════════════════════════════════
#  全局状态
# ═══════════════════════════════════════════
state             = STATE_ACTIVE

range_low         = 0.0
range_high        = 0.0
grid_prices       = []
grid_usdt         = []

grids             = []

shift_count       = 0
shift_down_count  = 0
shift_history     = []

empty_bars_count  = 0

Funding           = 0.0
last_closed_ktime = 0

positions_cache   = []
assets = {"USDT": {"total_balance": 0, "margin_balance": 0}}
symbol        = ""
base_currency = ""
swap_code     = ""
direction     = "long"   # 当前生效方向(auto模式下由体制判定动态切换)
direction_mode = "long"  # 用户选择的模式: long/short/both/auto

# V4.1 自动方向状态
_dir_switch_count   = 0
_last_dir_switch_bar = 0


# ═══════════════════════════════════════════
#  百分比转绝对值辅助
# ═══════════════════════════════════════════

def pct_to_abs(pct, ref_price):
    """将百分比参数转换为绝对值"""
    return ref_price * pct / 100.0


# ═══════════════════════════════════════════════════════════════
#  V4.0 数学自适应层  —— 递归估计,无任何固定回看窗口
# ---------------------------------------------------------------
#  设计原则:
#    * 时间尺度是"估计出来的输出",不是"人为输入的窗口"。
#    * 所有状态逐根 K 线在线更新(Kalman / RLS),
#      记忆随信噪比与回归速度自适应,而非固定 N 根。
#    * 三个估计器:
#        ① Kalman 局部水平+斜率  → 瞬时趋势 slope、瞬时波动 σ(无窗口)
#        ② RLS-AR(1) 遗忘因子自洽 → OU 半衰期 H、均衡幅度 σ_eq(定区间/退出)
#        ③ 库存感知风控         → 净敞口上界 + 保留价偏移(动态天花板)
# ═══════════════════════════════════════════════════════════════

# ── 估计器状态(全局,逐根更新)
_est_last_time = 0
_est_count     = 0

# ① Kalman:状态 x=[level, slope],作用在【价格】上。可复用状态字典。
def new_kf():
    return {"l": 0.0, "s": 0.0, "p00": 1.0, "p01": 0.0, "p10": 0.0, "p11": 1.0,
            "R": 0.0, "init": False, "sigma": 0.0, "z": 0.0, "price": 0.0}

_kf_target = new_kf()   # 目标标的的 Kalman
_kf_refs   = []         # 参考标的的 Kalman 列表(延迟初始化)
_ref_symbols = []       # 参考合约代码列表(init 时从 REF_SYMBOLS 解析)

# 跨资产:目标对各参考的回归系数 β_j(收益空间,EWMA 递归,无窗口)
_ref_cov   = []         # EWMA[ r_target · r_ref_j ]
_ref_var   = []         # EWMA[ r_ref_j² ]
_ref_beta  = []         # β_j = cov/var
_ref_prevc = []         # 各参考上一根收盘
_tgt_prevc = 0.0        # 目标上一根收盘

_trend_z_sys = 0.0      # 系统性(被参考确认的)趋势强度,带符号
_sys_share   = 0.0      # 系统性占比 |系统漂移|/|目标总漂移| ∈[0,1]
_refs_ok     = False    # 参考数据是否可用
_ref_fresh   = []       # V4.1: 各参考是否"新鲜"(未因正股休市而冻结)
_bar_ms      = 60000    # K线周期毫秒(从数据推断)

# V4.1 β聚合估计:粗时间尺度累积收益再更新协方差,抗高频异步(Epps)偏差
_agg_rt  = []           # 各参考对应的目标累积log收益
_agg_rj  = []           # 各参考自身累积log收益
_agg_cnt = []           # 累积根数

# V4.1 协整残差:ε_t = Σ(r_target − Σβ_j·r_ref_j),OU/AR(1) 拟合对象
_eps      = 0.0
_eps_prev = None

# V4.1 硬编码工程常量(不暴露为参数,减少自由度)
REF_STALE_BARS   = 3      # 参考K线落后目标超过该根数 → 判定休市/冻结
REBUILD_DEV_LO   = 0.75   # 当前宽度/目标宽度低于此 → 触发重建(拉伸)
REBUILD_DEV_HI   = 1.33   # 高于此 → 触发重建(压缩)
REBUILD_COOLDOWN = 30     # 两次σ自适应重建之间至少间隔的K线根数
HL_MIN_BARS      = 10.0   # 退出判据允许的半衰期下限(根)
HL_MAX_BARS      = 1440.0 # 上限(根)
HL_FALLBACK      = 240.0  # 半衰期不可估(趋势/单位根)时的退出时钟兜底

_ou_hl_valid      = 0.0   # 最近一次有效半衰期(根)
_last_rebuild_bar = 0     # 上次区间重建时的K线计数

# ② RLS-AR(1):作用在协整残差 ε 上(V4.1,不再拟合原始价格)
#    ε 是平稳的(协整残差),b 远离单位根,σ_eq/半衰期数值稳定
_rls_a  = 0.0
_rls_b  = 0.0
_rls_p00 = 1e6
_rls_p01 = 0.0
_rls_p10 = 0.0
_rls_p11 = 1e6
_rls_resid_var = 0.0
_rls_init = False

# 对外暴露的估计结果
_ou_half_life = 0.0    # 半衰期(根),0=未知
_ou_sigma_eq  = 0.0    # 均衡标准差(价格单位)
_trend_z      = 0.0    # 趋势强度 = 斜率/波动,带符号(无量纲)
_vol_sigma    = 0.0    # 瞬时波动(价格单位)

_equity_peak  = 0.0


def kf_step(st, y):
    """Kalman 局部水平+趋势,逐点更新一个状态字典。观测噪声 R 在线自适应,无窗口。"""
    st["price"] = y
    if not st["init"]:
        st["l"] = y
        st["s"] = 0.0
        st["R"] = max((y * 0.0005) ** 2, 1e-12)   # 初始波动先验≈5bp
        st["init"] = True
        return

    R = st["R"] if st["R"] > 0 else 1e-12
    q_s = KF_Q_RATIO * R
    q_l = KF_Q_RATIO * R * 0.25

    l_pred = st["l"] + st["s"]
    s_pred = st["s"]
    pp00 = st["p00"] + st["p01"] + st["p10"] + st["p11"] + q_l
    pp01 = st["p01"] + st["p11"]
    pp10 = st["p10"] + st["p11"]
    pp11 = st["p11"] + q_s

    e = y - l_pred
    S = pp00 + R
    if S <= 0:
        S = 1e-12
    k0 = pp00 / S
    k1 = pp10 / S
    st["l"] = l_pred + k0 * e
    st["s"] = s_pred + k1 * e
    st["p00"] = (1 - k0) * pp00
    st["p01"] = (1 - k0) * pp01
    st["p10"] = pp10 - k1 * pp00
    st["p11"] = pp11 - k1 * pp01

    st["R"] = st["R"] + KF_ADAPT * (e * e - st["R"])
    if st["R"] < 1e-12:
        st["R"] = 1e-12

    st["sigma"] = math.sqrt(st["R"])
    st["z"]     = st["s"] / (st["sigma"] + 1e-12)


def ref_n():
    """启用的参考标的数量(由 REF_SYMBOLS 解析而来)。"""
    return len(_ref_symbols)


def get_ref_records(j):
    """
    取第 j 个参考标的的 K 线:直接用同一交易所对象传入参考合约代码,
    无需配置附加交易所。FMZ: exchange.GetRecords(symbol)。
    """
    try:
        return exchange.GetRecords(_ref_symbols[j])
    except Exception:
        try:
            return exchange.GetRecords(_ref_symbols[j], PERIOD_M1)
        except Exception:
            return None


def _rls_update(y_prev, y):
    """
    RLS-AR(1) 估计均值回归,作用在协整残差 ε(log空间)上。
    遗忘因子由估出的半衰期自洽给出,无固定窗口。
    V4.1: σ_eq 单位是 log 收益(≈分数偏离),使用时乘价格换算。
    """
    global _rls_a, _rls_b, _rls_p00, _rls_p01, _rls_p10, _rls_p11
    global _rls_resid_var, _rls_init, _ou_half_life, _ou_sigma_eq, _ou_hl_valid

    if not _rls_init:
        _rls_a = 0.0
        _rls_b = 1.0
        _rls_init = True
        return

    # 遗忘因子:记忆 ≈ OU_MEMORY 个半衰期;半衰期未知时用温和默认
    if _ou_half_life > 0:
        mem = max(OU_MEMORY * _ou_half_life, 5.0)
        ff  = math.exp(-1.0 / mem)
    else:
        ff = 0.99
    ff = min(max(ff, 0.90), 0.9995)

    phi0, phi1 = 1.0, y_prev
    # Pphi = P·phi
    pph0 = _rls_p00 * phi0 + _rls_p01 * phi1
    pph1 = _rls_p10 * phi0 + _rls_p11 * phi1
    denom = ff + phi0 * pph0 + phi1 * pph1
    if denom <= 0:
        denom = 1e-12
    k0 = pph0 / denom
    k1 = pph1 / denom
    e = y - (_rls_a * phi0 + _rls_b * phi1)
    _rls_a += k0 * e
    _rls_b += k1 * e
    # P = (P - K·Pphi^T)/ff
    _rls_p00 = (_rls_p00 - k0 * pph0) / ff
    _rls_p01 = (_rls_p01 - k0 * pph1) / ff
    _rls_p10 = (_rls_p10 - k1 * pph0) / ff
    _rls_p11 = (_rls_p11 - k1 * pph1) / ff

    _rls_resid_var = _rls_resid_var + KF_ADAPT * (e * e - _rls_resid_var)

    b = _rls_b
    if 0.0 < b < 0.9999:
        theta = -math.log(b)
        _ou_half_life = math.log(2.0) / theta if theta > 0 else 0.0
        if (1 - b * b) > 1e-9 and _rls_resid_var > 0:
            _ou_sigma_eq = math.sqrt(_rls_resid_var / (1 - b * b))
        if 1.0 <= _ou_half_life <= 1e5:
            _ou_hl_valid = _ou_half_life   # 记住最近一次可信半衰期,供退出时钟用
    else:
        # b≈1或更大:无回归(趋势/单位根)→ 半衰期未知,退出时钟用 _ou_hl_valid/兜底
        _ou_half_life = 0.0


def update_estimators():
    """每轮主循环调用:把新收盘 K 线逐根喂给递归估计器(目标 + 参考)。"""
    global _est_last_time, _est_count, _trend_z, _vol_sigma, _bar_ms
    global _kf_refs, _ref_cov, _ref_var, _ref_beta, _ref_prevc
    global _trend_z_sys, _sys_share, _refs_ok, _ref_fresh
    global _agg_rt, _agg_rj, _agg_cnt, _eps, _eps_prev

    try:
        records = exchange.GetRecords()
    except Exception as e:
        Log(f"估计器取K线异常: {e}")
        return
    if not records or len(records) < 2:
        return

    closed = records[:-1]           # 最后一根可能未收盘,跳过
    new_bars = [r for r in closed if r["Time"] > _est_last_time]
    if not new_bars:
        return

    if len(closed) >= 2:
        d = closed[-1]["Time"] - closed[-2]["Time"]
        if d > 0:
            _bar_ms = d

    nref = ref_n()
    ref_maps = []
    if nref > 0:
        if len(_kf_refs) < nref:    # 惰性初始化参考状态
            _kf_refs   = [new_kf() for _ in range(nref)]
            _ref_cov   = [0.0] * nref
            _ref_var   = [1e-12] * nref
            _ref_beta  = [0.0] * nref
            _ref_prevc = [0.0] * nref
            _ref_fresh = [False] * nref
            _agg_rt    = [0.0] * nref
            _agg_rj    = [0.0] * nref
            _agg_cnt   = [0] * nref
        ok_all = True
        tgt_latest = closed[-1]["Time"]
        for j in range(nref):
            rr = get_ref_records(j)
            if not rr or len(rr) < 2:
                ok_all = False
                ref_maps.append({})
                _ref_fresh[j] = False
            else:
                ref_closed = rr[:-1]
                ref_maps.append({b["Time"]: b["Close"] for b in ref_closed})
                # V4.1 休市/冻结检测:参考最新已收盘K线落后目标超过 REF_STALE_BARS 根
                # → 该参考正股休市或数据冻结,其"零收益"不代表市场信息,判 stale
                lag_ms = tgt_latest - ref_closed[-1]["Time"]
                _ref_fresh[j] = lag_ms <= REF_STALE_BARS * _bar_ms
        _refs_ok = ok_all
    else:
        _refs_ok = False

    prev_close = None
    idx = closed.index(new_bars[0])
    if idx > 0:
        prev_close = closed[idx - 1]["Close"]

    agg_n = max(int(BETA_AGG_BARS), 1)

    for r in new_bars:
        c = r["Close"]
        t = r["Time"]
        kf_step(_kf_target, c)

        r_t = 0.0
        if prev_close is not None and prev_close > 0 and c > 0:
            r_t = math.log(c / prev_close)

        # ── 跨资产:参考 Kalman + β聚合估计(粗尺度,抗Epps偏差)
        beta_ret_sys = 0.0     # 本根K线的系统性(β加权参考)log收益
        used_ref     = False
        if nref > 0 and prev_close is not None:
            for j in range(nref):
                rc = ref_maps[j].get(t)
                if rc is None or rc <= 0:
                    continue
                if _ref_prevc[j] > 0 and rc != _ref_prevc[j]:
                    # rc == prev 视为冻结bar,跳过β累积(不让休市零收益把β拖向0)
                    r_j = math.log(rc / _ref_prevc[j])
                    _agg_rt[j]  += r_t
                    _agg_rj[j]  += r_j
                    _agg_cnt[j] += 1
                    if _agg_cnt[j] >= agg_n:
                        # 聚合收益上更新 EWMA 协方差 → β_j
                        art, arj = _agg_rt[j], _agg_rj[j]
                        _ref_cov[j] += KF_ADAPT * (art * arj - _ref_cov[j])
                        _ref_var[j] += KF_ADAPT * (arj * arj - _ref_var[j])
                        _ref_beta[j] = _ref_cov[j] / _ref_var[j] if _ref_var[j] > 1e-12 else 0.0
                        _agg_rt[j] = _agg_rj[j] = 0.0
                        _agg_cnt[j] = 0
                    # 残差增量只用"新鲜"参考的系统性部分
                    if _ref_fresh[j]:
                        beta_ret_sys += _ref_beta[j] * r_j
                        used_ref = True
                kf_step(_kf_refs[j], rc)
                _ref_prevc[j] = rc

        # ── V4.1 协整残差累积 + OU/AR(1)(拟合ε而非原始价格)
        if prev_close is not None:
            d_eps = r_t - beta_ret_sys if used_ref else r_t
            _eps += d_eps
            if _eps_prev is not None:
                _rls_update(_eps_prev, _eps)
            _eps_prev = _eps

        prev_close = c
        _est_count += 1
        _est_last_time = t

    # 目标自身趋势
    _vol_sigma = _kf_target["sigma"]
    _trend_z   = _kf_target["z"]

    # 系统性(被参考确认的)趋势:分数漂移空间 ĝ_sys = Σ β_j · g_ref_j
    # V4.1: 只累加"新鲜"参考;全部休市时由 gate_trend_z 回退自身 z
    fresh_any = nref > 0 and _refs_ok and any(_ref_fresh)
    if fresh_any:
        pt = _kf_target["price"]
        g_t = (_kf_target["s"] / pt) if pt > 0 else 0.0
        g_sys = 0.0
        for j in range(nref):
            pj = _kf_refs[j]["price"]
            if pj > 0 and _ref_fresh[j]:
                g_sys += _ref_beta[j] * (_kf_refs[j]["s"] / pj)
        sig_frac = (_vol_sigma / pt) if pt > 0 else 1e-9
        _trend_z_sys = g_sys / (sig_frac + 1e-12)
        _sys_share   = abs(g_sys) / (abs(g_t) + 1e-12)
    else:
        _trend_z_sys = _trend_z
        _sys_share   = 1.0


def estimators_ready():
    return _est_count >= EST_WARMUP


def gate_trend_z():
    """
    趋势闸门用的趋势强度:
      启用参考且数据可用 → 用【系统性】z_sys(被三星/海力士等确认的部分);
      否则退回目标自身 z。
    这样:目标跌但参考没跟(特质性噪声)→z_sys≈0→不拦,照收割;
          目标跌且参考同步(系统性真趋势)→z_sys显著→拦住逆势单。
    """
    # V4.1: 参考必须"新鲜"才可信——正股休市时段 z_sys≈0 是假信号,
    # 此时回退目标自身趋势 z,避免休市窗口里闸门失效接系统性飞刀
    if ref_n() > 0 and _refs_ok and any(_ref_fresh) and estimators_ready():
        return _trend_z_sys
    return _trend_z


def half_life_for_exit():
    """退出时钟用的半衰期(根)。当前不可估时用最近有效值,再不行用兜底,并夹在合理区间。"""
    h = _ou_half_life if _ou_half_life > 0 else (_ou_hl_valid if _ou_hl_valid > 0 else HL_FALLBACK)
    return min(max(h, HL_MIN_BARS), HL_MAX_BARS)


def effective_width_abs(ref_price):
    """
    区间宽度:由 OU 均衡幅度 σ_eq 决定(覆盖 ±(WIDTH_SIGMA_MULT/2)·σ_eq)。
    V4.1: σ_eq 是残差ε的log单位(≈分数偏离),乘价格换算成绝对宽度。
    σ_eq 由数据递归估出,随波动/回归速度自适应——无固定窗口。
    未热身或异常时回退固定百分比。
    """
    if estimators_ready() and _ou_sigma_eq > 0:
        w = WIDTH_SIGMA_MULT * _ou_sigma_eq * ref_price
        floor_w = pct_to_abs(GRID_WIDTH_PCT, ref_price) * WIDTH_FLOOR_FRAC
        cap_w   = pct_to_abs(GRID_WIDTH_PCT, ref_price) * WIDTH_CAP_MULT
        return min(max(w, floor_w), cap_w)
    if estimators_ready() and _vol_sigma > 0:
        # σ_eq 不可用时,退用瞬时波动构造带宽
        return max(WIDTH_SIGMA_MULT * _vol_sigma, pct_to_abs(GRID_WIDTH_PCT, ref_price) * WIDTH_FLOOR_FRAC)
    return pct_to_abs(GRID_WIDTH_PCT, ref_price)


def regime_label_text():
    if not estimators_ready():
        return "🌡️ 估计器热身中"
    gz = gate_trend_z()
    ref_live = ref_n() > 0 and _refs_ok and any(_ref_fresh)
    tag = "系统性" if ref_live else ("自身(参考休市)" if ref_n() > 0 else "自身")
    if gz <= -TREND_Z_THRESHOLD:
        return f"📉 {tag}下跌趋势(z={gz:.2f})"
    if gz >= TREND_Z_THRESHOLD:
        return f"📈 {tag}上涨趋势(z={gz:.2f})"
    # 目标自身在动但未被参考确认 → 判为可收割的特质性波动
    if ref_live and abs(_trend_z) >= TREND_Z_THRESHOLD:
        return f"🟰 特质波动(自身z={_trend_z:.2f}/系统z={gz:.2f} 未确认→收割)"
    return f"🟰 震荡收割(z={gz:.2f})"


def effective_ceiling():
    """
    库存感知的动态天花板(Avellaneda-Stoikov 保留价偏移的离散版):
    多头库存越大,有效上沿越往下压——高位不再加仓,天花板自动下调。
    """
    if INVENTORY_SKEW <= 0 or MAX_NET_EXPOSURE_MULT <= 0 or range_high <= range_low:
        return range_high
    long_amt  = assets[base_currency].get("long_amount", 0)
    short_amt = assets[base_currency].get("short_amount", 0)
    price     = get_price()
    ct_val    = assets[base_currency]["ctVal"]
    net_usdt  = (long_amt - short_amt) * ct_val * price
    limit     = Funding * MAX_NET_EXPOSURE_MULT
    if limit <= 0:
        return range_high
    inv_ratio = max(0.0, net_usdt / limit)          # 0~1(多头方向)
    inv_ratio = min(inv_ratio, 1.0)
    drop = INVENTORY_SKEW * inv_ratio * (range_high - range_low)
    return range_high - drop


def effective_floor():
    """
    V4.1 空头镜像:空头库存越大,有效下沿越往上抬——低位不再加空。
    short/both/auto 模式下与 effective_ceiling 对称。
    """
    if INVENTORY_SKEW <= 0 or MAX_NET_EXPOSURE_MULT <= 0 or range_high <= range_low:
        return range_low
    long_amt  = assets[base_currency].get("long_amount", 0)
    short_amt = assets[base_currency].get("short_amount", 0)
    price     = get_price()
    ct_val    = assets[base_currency]["ctVal"]
    net_usdt  = (long_amt - short_amt) * ct_val * price
    limit     = Funding * MAX_NET_EXPOSURE_MULT
    if limit <= 0:
        return range_low
    inv_ratio = max(0.0, -net_usdt / limit)         # 0~1(空头方向)
    inv_ratio = min(inv_ratio, 1.0)
    lift = INVENTORY_SKEW * inv_ratio * (range_high - range_low)
    return range_low + lift


def can_open(side, grid_price):
    """
    是否允许在该方向、该价位开【新仓】。多道数学闸门:
      1) 趋势闸门(Kalman):确认不利趋势→停开逆势单
      2) 净敞口硬上界(γ):防无限接飞刀爆仓
      3) 库存偏移天花板:多头堆积→高位停开(动态下调最高点)
      4) 回撤熔断:兜底
    返回 (bool, reason)
    """
    if estimators_ready():
        gz = gate_trend_z()
        if side == "long" and gz <= -TREND_Z_THRESHOLD:
            return False, "trend"
        if side == "short" and gz >= TREND_Z_THRESHOLD:
            return False, "trend"

    if MAX_NET_EXPOSURE_MULT > 0:
        long_amt  = assets[base_currency].get("long_amount", 0)
        short_amt = assets[base_currency].get("short_amount", 0)
        price     = get_price()
        ct_val    = assets[base_currency]["ctVal"]
        net_usdt  = (long_amt - short_amt) * ct_val * price
        limit     = Funding * MAX_NET_EXPOSURE_MULT
        if side == "long" and net_usdt >= limit:
            return False, "risk"
        if side == "short" and -net_usdt >= limit:
            return False, "risk"

    if side == "long" and grid_price > effective_ceiling():
        return False, "skew"
    if side == "short" and grid_price < effective_floor():
        return False, "skew"

    if check_drawdown_breaker():
        return False, "dd"

    return True, ""


def check_drawdown_breaker():
    global _equity_peak
    if MAX_DRAWDOWN_PCT <= 0:
        return False
    eq = assets["USDT"].get("equity", Funding)
    if eq > _equity_peak:
        _equity_peak = eq
    if _equity_peak > 0 and (eq - _equity_peak) / _equity_peak * 100 <= -MAX_DRAWDOWN_PCT:
        return True
    return False


# ═══════════════════════════════════════════════════════════════
#  V4.1 ① k×H 止盈衰减退出:持仓龄超过 K_EXIT_HALFLIFE 个半衰期仍未回归
#  → 回归假设变质,止盈价从原格价向"当前价+覆盖手续费"连续衰减,
#  主动周转资金,不死等、也不二元止损。逆势体制下衰减时钟×2加速。
# ═══════════════════════════════════════════════════════════════

def apply_tp_decay():
    if K_EXIT_HALFLIFE <= 0 or not estimators_ready() or len(grids) == 0:
        return
    H = half_life_for_exit()
    k_bars = K_EXIT_HALFLIFE * H
    price  = get_price()
    if price <= 0:
        return
    step = (range_high - range_low) / max(len(grids), 1)
    gz   = gate_trend_z()
    fee_cover = FEE_RATE * 2 * 2   # 退出下限仍覆盖双边手续费×2

    for i, g in enumerate(grids):
        if g["status"] != "pending_close" or g.get("buy_contracts", 0) <= 0:
            continue
        ob = g.get("open_bar")
        if ob is None:
            g["open_bar"] = _est_count
            continue
        g_dir = g.get("grid_dir", "long")
        age = _est_count - ob
        # 体制逆势 → 变质更可信,等效持仓龄加倍
        adverse = ((g_dir == "long" and gz <= -TREND_Z_THRESHOLD) or
                   (g_dir == "short" and gz >= TREND_Z_THRESHOLD))
        eff_age = age * (2.0 if adverse else 1.0)
        if eff_age <= k_bars:
            continue
        frac = min(1.0, (eff_age - k_bars) / max(k_bars, 1.0))  # 再过k×H衰减到底

        tp0 = g.get("tp0") or g.get("tp_price") or (grid_prices[i + 1] if g_dir == "long" else grid_prices[i])
        cur = g.get("tp_price") or tp0
        if g_dir == "long":
            floor_tp = price * (1 + fee_cover)
            if tp0 <= floor_tp:
                continue
            target = tp0 - frac * (tp0 - floor_tp)
            moved  = cur - target
        else:
            floor_tp = price * (1 - fee_cover)
            if tp0 >= floor_tp:
                continue
            target = tp0 + frac * (floor_tp - tp0)
            moved  = target - cur
        # 阈值:至少移动半格才改单,避免高频撤挂
        if moved < max(0.5 * step, 10 ** (-assets[base_currency]["PricePrecision"])):
            continue

        Log(f"[TP衰减] 格{i}({g_dir}) 持仓龄{age}根>({K_EXIT_HALFLIFE}×H={k_bars:.0f}根)"
            f"{'×逆势加速' if adverse else ''}  止盈 {cur} → {fp(target)} (原{tp0})")
        cancel_order_safe(g.get("sell_oid"))
        g["sell_oid"]  = None
        g["tp_target"] = fp(target)
        Sleep(200)
        _place_grid_close(i, g["buy_contracts"], g_dir, price_override=fp(target))


# ═══════════════════════════════════════════════════════════════
#  V4.1 ② σ_eq 漂移触发区间重建:无突破的震荡里宽度也随波动收放。
#  当前宽度/目标宽度偏离 [0.75, 1.33] 且冷却期已过 → 以当前价为锚重建。
#  做多锚上沿=现价(区间只在现价下方买),天花板随之灵活下调/上调。
# ═══════════════════════════════════════════════════════════════

def check_width_rebuild():
    global state, _last_rebuild_bar, shift_count
    if not estimators_ready() or state != STATE_ACTIVE or len(grids) == 0:
        return False
    if _est_count - _last_rebuild_bar < REBUILD_COOLDOWN:
        return False
    price = get_price()
    if price <= 0 or range_high <= range_low:
        return False
    # 价格在区间外时交给突破移动/等待回归逻辑,此处不抢
    if price > range_high or price < range_low:
        return False
    target_w = effective_width_abs(price)
    cur_w    = range_high - range_low
    if target_w <= 0:
        return False
    ratio = cur_w / target_w
    if REBUILD_DEV_LO <= ratio <= REBUILD_DEV_HI:
        return False

    if direction == "long":
        new_high = price
        new_low  = price - target_w
    elif direction == "short":
        new_low  = price
        new_high = price + target_w
    else:
        new_low  = price - target_w / 2.0
        new_high = price + target_w / 2.0

    Log("=" * 60)
    Log(f"★ σ自适应区间重建!宽度比={ratio:.2f} (当前{cur_w:.4f} / 目标{target_w:.4f})")
    Log(f"  旧区间: {range_low} ~ {range_high}")
    Log(f"  新区间: {fp(new_low)} ~ {fp(new_high)}  σ_eq={_ou_sigma_eq:.6f} H={_ou_half_life:.0f}根")
    Log("=" * 60)

    state = STATE_SHIFTING
    cancel_open_orders_only()
    Sleep(500)
    shift_history.append({
        "n": f"σ{_est_count}", "dir": "⇄σ重建",
        "price": round(price, 8),
        "old_low": range_low, "old_high": range_high,
        "new_low": fp(new_low), "new_high": fp(new_high),
    })
    for _ in range(5):
        if update_account():
            break
        Sleep(2000)
    activate_grids_keep_positions(fp(new_low), fp(new_high))
    _last_rebuild_bar = _est_count
    return True


# ═══════════════════════════════════════════════════════════════
#  V4.1 ③ 自动方向:体制判定 → 网格方向动态切换(带迟滞+冷却)
#    上涨趋势(z ≥ +T)   → long 网格(回调买入顺势收割)
#    下跌趋势(z ≤ −T)   → short 网格(反弹卖出顺势收割)
#    震荡(|z| < T/2)    → both 网格(双向收割)
#  迟滞:进入趋势用 ±T,退回震荡用 ±T/2,避免阈值附近来回抖动。
#  切换只影响【新开仓】的方向布局;旧方向持仓保留,
#  由趋势闸门停止加仓 + TP衰减(逆势×2加速)自然退出——连续过渡,无暴力平仓。
# ═══════════════════════════════════════════════════════════════

DIR_Z_EXIT_FRAC = 0.5   # 退出趋势体制的迟滞系数(T/2)

def desired_direction():
    """根据体制判定期望的网格方向。带迟滞:以当前 direction 为状态。"""
    gz = gate_trend_z()
    t_in  = TREND_Z_THRESHOLD
    t_out = TREND_Z_THRESHOLD * DIR_Z_EXIT_FRAC

    if direction == "long":
        if gz <= -t_in:
            return "short"          # 直接翻转:跌势确认
        if gz < t_out:
            return "both"           # 涨势衰竭 → 退回震荡双向
        return "long"
    elif direction == "short":
        if gz >= t_in:
            return "long"
        if gz > -t_out:
            return "both"
        return "short"
    else:  # both
        if gz >= t_in:
            return "long"
        if gz <= -t_in:
            return "short"
        return "both"


def check_direction_switch():
    """auto 模式下检测并执行方向切换:重建网格(保留持仓),锚定当前价。"""
    global direction, state, _dir_switch_count, _last_dir_switch_bar, _last_rebuild_bar

    if direction_mode != "auto" or not estimators_ready() or state != STATE_ACTIVE:
        return False
    if _est_count - _last_dir_switch_bar < REBUILD_COOLDOWN:
        return False
    want = desired_direction()
    if want == direction:
        return False

    price = get_price()
    if price <= 0:
        return False
    target_w = effective_width_abs(price)
    if target_w <= 0:
        return False

    old_dir = direction
    if want == "long":
        new_high, new_low = price, price - target_w
    elif want == "short":
        new_low, new_high = price, price + target_w
    else:
        new_low  = price - target_w / 2.0
        new_high = price + target_w / 2.0

    gz = gate_trend_z()
    Log("=" * 60)
    Log(f"★ 自动方向切换!体制z={gz:+.3f}  {old_dir} → {want}")
    Log(f"  新区间: {fp(new_low)} ~ {fp(new_high)}  (锚定现价{price})")
    Log(f"  旧方向持仓保留:停止加仓,TP衰减(逆势×2)负责退出")
    Log("=" * 60)

    direction = want
    state = STATE_SHIFTING
    cancel_open_orders_only()
    Sleep(500)
    _dir_switch_count += 1
    shift_history.append({
        "n": f"D{_dir_switch_count}", "dir": f"⇄转向 {old_dir}→{want} (z={gz:+.2f})",
        "price": round(price, 8),
        "old_low": range_low, "old_high": range_high,
        "new_low": fp(new_low), "new_high": fp(new_high),
    })
    for _ in range(5):
        if update_account():
            break
        Sleep(2000)
    activate_grids_keep_positions(fp(new_low), fp(new_high))
    _last_dir_switch_bar = _est_count
    _last_rebuild_bar    = _est_count
    return True


# ═══════════════════════════════════════════
#  精度 / 换算
# ═══════════════════════════════════════════

def fp(price):
    try:
        return round(price, assets[base_currency]["PricePrecision"])
    except:
        return price


def fa(amount):
    try:
        if amount <= 0:
            return 0
        r = round(amount, assets[base_currency]["AmountPrecision"])
        return r if r > 0 else 0
    except:
        return 0


def usdt_to_contracts(usdt, price):
    try:
        return usdt / (assets[base_currency]["ctVal"] * price)
    except:
        return 0


def contracts_to_usdt(contracts, price):
    try:
        return contracts * assets[base_currency]["ctVal"] * price
    except:
        return 0


def get_min_qty():
    try:
        return assets[base_currency]["MinQty"]
    except:
        return 0.001


def get_price():
    return assets[base_currency]["price"]


# ═══════════════════════════════════════════
#  账户 / 行情
# ═══════════════════════════════════════════

def update_price():
    ticker = exchange.GetTicker(swap_code)
    if not ticker:
        return False
    assets[base_currency]["price"] = ticker.Last
    return True


def update_account():
    try:
        acc = exchange.GetAccount()
        Sleep(500)
        if not acc:
            return False
        assets["USDT"]["equity"]         = acc.Equity
        assets["USDT"]["total_balance"]  = acc.Balance + acc.FrozenBalance
        assets["USDT"]["margin_balance"] = acc.Balance
        global Funding
        Funding = acc.Equity
        long_amt, long_hp, long_pft = _get_position(PD_LONG)
        short_amt, short_hp, short_pft = _get_position(PD_SHORT)
        assets[base_currency]["long_amount"]   = long_amt
        assets[base_currency]["long_price"]    = long_hp
        assets[base_currency]["long_profit"]   = long_pft
        assets[base_currency]["short_amount"]  = short_amt
        assets[base_currency]["short_price"]   = short_hp
        assets[base_currency]["short_profit"]  = short_pft
        assets[base_currency]["amount"]        = long_amt + short_amt
        assets[base_currency]["unrealised_profit"] = long_pft + short_pft
        global positions_cache
        try:
            positions_cache = exchange.GetPositions(swap_code) or []
            Sleep(500)
        except Exception:
            positions_cache = []
        return True
    except Exception as e:
        Log(f"更新账户异常: {e}")
        return False


def _get_position(pos_type=PD_LONG):
    """获取指定方向的持仓"""
    try:
        positions = exchange.GetPositions(swap_code)
        Sleep(500)
        if not positions:
            return 0, 0, 0
        for pos in positions:
            if pos["Amount"] > 0 and pos["Type"] == pos_type:
                return pos["Amount"], pos["Price"], pos["Profit"]
        return 0, 0, 0
    except Exception as e:
        Log(f"获取持仓异常: {e}")
        return 0, 0, 0


def get_total_position():
    long_amt, _, _ = _get_position(PD_LONG)
    short_amt, _, _ = _get_position(PD_SHORT)
    return long_amt + short_amt


# ═══════════════════════════════════════════
#  订单操作
# ═══════════════════════════════════════════

def cancel_all_orders(sc):
    try:
        orders = exchange.GetOrders(sc)
        if orders:
            for o in orders:
                exchange.CancelOrder(o["Id"])
                Sleep(100)
    except Exception as e:
        Log(f"撤单异常: {e}")


def cancel_open_orders_only():
    """
    只撤销开仓挂单(pending_buy 状态的格子),
    保留止盈挂单(pending_close),持仓继续持有。
    """
    cancelled = 0
    for i, g in enumerate(grids):
        if g["status"] == "pending_buy" and g.get("buy_oid"):
            cancel_order_safe(g["buy_oid"])
            g["buy_oid"] = None
            g["status"]  = "empty"
            cancelled += 1
    if cancelled > 0:
        Log(f"[撤开仓单] 共撤销 {cancelled} 个开仓挂单,止盈单和持仓保持不动")


def cancel_order_safe(oid):
    if not oid:
        return
    try:
        exchange.CancelOrder(oid)
        Sleep(100)
    except Exception as e:
        Log(f"撤单{oid}异常: {e}")


def check_order_status(oid):
    if oid is None:
        return "unknown", 0, 0
    try:
        order = exchange.GetOrder(oid)
        if not order:
            return "unknown", 0, 0
        status = order.get("Status", -1)
        deal   = order.get("DealAmount", 0) or 0
        avg_px = order.get("AvgPrice", 0) or order.get("Price", 0) or 0
        if status == 1:
            return "filled", deal, avg_px
        elif status == 2:
            return "cancelled", deal, avg_px
        elif status in (0, 3):
            return "active", deal, avg_px
        else:
            return "unknown", deal, avg_px
    except Exception as e:
        Log(f"查询订单{oid}异常: {e}")
        return "unknown", 0, 0


def place_limit_buy(buy_price, usdt_amount, label=""):
    """开多单(限价买入开仓)"""
    raw       = usdt_to_contracts(usdt_amount, buy_price)
    contracts = fa(raw)
    if contracts <= 0:
        return None

    min_qty      = assets[base_currency]["MinQty"]
    ct_val       = assets[base_currency]["ctVal"]
    min_notional = assets[base_currency].get("MinNotional", 5)
    amt_prec     = assets[base_currency]["AmountPrecision"]

    if contracts < min_qty:
        return "skip_min_detail", raw, min_qty

    notional = contracts * ct_val * buy_price
    if notional < min_notional:
        step = 10 ** (-amt_prec)
        contracts_needed = math.ceil(min_notional / (ct_val * buy_price) / step) * step
        contracts_needed = round(contracts_needed, amt_prec)
        if contracts_needed < min_qty:
            contracts_needed = min_qty
        if contracts_needed * ct_val * buy_price < min_notional:
            contracts_needed = round(contracts_needed + step, amt_prec)
        Log(f"[{label}] 名义价值{notional:.2f}U < {min_notional}U,"
            f"ceil补足: {contracts}张 → {contracts_needed}张 "
            f"({contracts_needed * ct_val * buy_price:.2f}U)")
        contracts = contracts_needed

    price_f = fp(buy_price)
    order_u = contracts * ct_val * buy_price
    Log(f"[{label}] 限价买开多 @{price_f}  {contracts}张  ≈{order_u:.2f}U")
    oid = exchange.CreateOrder(swap_code, "buy", price_f, contracts)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_sell_open(sell_price, usdt_amount, label=""):
    """开空单(限价卖出开仓)"""
    raw       = usdt_to_contracts(usdt_amount, sell_price)
    contracts = fa(raw)
    if contracts <= 0:
        return None

    min_qty      = assets[base_currency]["MinQty"]
    ct_val       = assets[base_currency]["ctVal"]
    min_notional = assets[base_currency].get("MinNotional", 5)
    amt_prec     = assets[base_currency]["AmountPrecision"]

    if contracts < min_qty:
        return "skip_min_detail", raw, min_qty

    notional = contracts * ct_val * sell_price
    if notional < min_notional:
        step = 10 ** (-amt_prec)
        contracts_needed = math.ceil(min_notional / (ct_val * sell_price) / step) * step
        contracts_needed = round(contracts_needed, amt_prec)
        if contracts_needed < min_qty:
            contracts_needed = min_qty
        if contracts_needed * ct_val * sell_price < min_notional:
            contracts_needed = round(contracts_needed + step, amt_prec)
        Log(f"[{label}] 名义价值{notional:.2f}U < {min_notional}U,"
            f"ceil补足: {contracts}张 → {contracts_needed}张 "
            f"({contracts_needed * ct_val * sell_price:.2f}U)")
        contracts = contracts_needed

    price_f = fp(sell_price)
    order_u = contracts * ct_val * sell_price
    Log(f"[{label}] 限价卖开空 @{price_f}  {contracts}张  ≈{order_u:.2f}U")
    oid = exchange.CreateOrder(swap_code, "sell", price_f, contracts)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_close_long(sell_price, contracts, label=""):
    """平多单(限价卖出平仓)"""
    fc = fa(contracts)
    if fc <= 0:
        return None
    price_f = fp(sell_price)
    Log(f"[{label}] 限价平多 @{price_f}  {fc}张")
    oid = exchange.CreateOrder(swap_code, "closebuy", price_f, fc)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_close_short(buy_price, contracts, label=""):
    """平空单(限价买入平仓)"""
    fc = fa(contracts)
    if fc <= 0:
        return None
    price_f = fp(buy_price)
    Log(f"[{label}] 限价平空 @{price_f}  {fc}张")
    oid = exchange.CreateOrder(swap_code, "closesell", price_f, fc)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def close_all_positions_market():
    """平掉所有持仓(多头+空头)—— 仅在手动触发时使用"""
    for attempt in range(10):
        long_amt, _, _ = _get_position(PD_LONG)
        short_amt, _, _ = _get_position(PD_SHORT)
        if long_amt <= 0 and short_amt <= 0:
            Log(f"[平仓] 持仓已清空(第{attempt + 1}次确认)")
            return
        price = get_price()
        if long_amt > 0:
            sell_p = fp(price * (1 - 0.001))
            fc = fa(long_amt)
            Log(f"[平仓] 第{attempt + 1}次平多: {fc}张 @{sell_p}")
            exchange.CreateOrder(swap_code, "closebuy", sell_p, fc)
            Sleep(1000)
        if short_amt > 0:
            buy_p = fp(price * (1 + 0.001))
            fc = fa(short_amt)
            Log(f"[平仓] 第{attempt + 1}次平空: {fc}张 @{buy_p}")
            exchange.CreateOrder(swap_code, "closesell", buy_p, fc)
            Sleep(1000)
        Sleep(1500)

    long_final, _, _ = _get_position(PD_LONG)
    short_final, _, _ = _get_position(PD_SHORT)
    if long_final > 0 or short_final > 0:
        Log(f"警告: 平仓重试10次后仍有持仓(多{long_final}/空{short_final}),请手动检查!")


# ═══════════════════════════════════════════
#  动态格数计算(V4.2 最大化格数版)
# ═══════════════════════════════════════════

def calc_grids(r_low, r_high):
    """
    【V4.2 资金利用率优化】
    策略:格数尽可能多,每格资金压到交易所允许的最小值。
    约束优先级:
      1. 格距 >= 双边手续费 × FEE_PROFIT_MULTI(保证每格盈利覆盖手续费)
      2. 每格资金 >= max(min_qty × ctVal × price, min_notional)(保证能下单)
      3. 取两个约束中格数较小值(短板原则)
    每格分配相同资金(均等分配),不再按距离加权,保证每格都能成交。
    """
    price    = get_price()
    range_width = r_high - r_low

    ct_val       = assets[base_currency]["ctVal"]
    min_qty      = assets[base_currency]["MinQty"]
    amt_prec     = assets[base_currency]["AmountPrecision"]
    price_prec   = assets[base_currency]["PricePrecision"]
    min_notional = assets[base_currency].get("MinNotional", 5)

    # ─── 约束1:格距必须覆盖双边手续费
    min_step_pct = FEE_RATE * 2 * FEE_PROFIT_MULTI
    min_step_abs = max(
        price * min_step_pct,
        10 ** (-price_prec)
    )
    max_by_range = int(range_width / min_step_abs)

    # ─── 约束2:每格最小资金 = max(最小张数对应资金, 交易所最小名义价值)
    #     这是真正能下单的资金下限,不能再低
    min_usdt_per_grid = max(
        min_qty * ct_val * price,
        min_notional
    )
    # V4.0: 去掉写死的 /2;安全系数控制总投入,both 模式内部再对半
    cap_frac = MAX_CAPITAL_FRAC / 2.0 if direction == "both" else MAX_CAPITAL_FRAC
    total_position_usdt = Funding * LEVERAGE * cap_frac
    max_by_capital = int(total_position_usdt / min_usdt_per_grid) if min_usdt_per_grid > 0 else max_by_range

    n = min(max_by_range, max_by_capital)
    if n < 1:
        Log(f"警告: 区间太窄或资金不足,无法建立有效格子 "
            f"(by_range={max_by_range}, by_capital={max_by_capital})")
        return [], []

    step   = range_width / n
    prices = [fp(r_low + i * step) for i in range(n + 1)]
    prices[-1] = fp(r_high)

    # ─── 均等分配资金(每格相同),保证每格都能成交最小手数
    usdt_per_grid = total_position_usdt / n
    # 若均分后低于最小下单资金,则向上取整到最小值
    usdt_per_grid = max(usdt_per_grid, min_usdt_per_grid)
    usdt_list = [round(usdt_per_grid, 4)] * n

    actual_total = usdt_per_grid * n
    Log(f"动态格数计算(V4.2): 区间宽={range_width:.4f}  格距={step:.4f}  最小格距={min_step_abs:.6f}")
    Log(f"  费率约束格数={max_by_range}  资金约束格数={max_by_capital}  实际格数={n}")
    Log(f"  账户资金={Funding:.2f}U  杠杆仓位={total_position_usdt:.2f}U  每格={usdt_per_grid:.2f}U  合计={actual_total:.2f}U")
    Log(f"  每格最小下单资金={min_usdt_per_grid:.2f}U  (min_qty={min_qty}张 × ctVal={ct_val} × 价格={price:.2f})")
    return prices, usdt_list


# ═══════════════════════════════════════════
#  清算价格(直接从币安接口获取)
# ═══════════════════════════════════════════

def get_liquidation_prices():
    """
    从 GetPositions 的 Info 字段读取清算价格。
    无需额外 API 权限,FMZ 封装接口直接可用。

    返回 dict:
      long_liq:  多头持仓清算价(无持仓返回 None)
      short_liq: 空头持仓清算价(无持仓返回 None)
    """
    try:
        positions = exchange.GetPositions(swap_code)
        if not positions:
            return {"long_liq": None, "short_liq": None}

        long_liq  = None
        short_liq = None

        for pos in positions:
            amt = pos.get("Amount", 0) or 0
            if amt <= 0:
                continue
            info = pos.get("Info") or {}
            liq_px = float(info.get("liquidationPrice", 0) or 0)
            if liq_px <= 0:
                continue
            pos_type = pos.get("Type", -1)   # PD_LONG=0, PD_SHORT=1
            if pos_type == 0:
                long_liq = liq_px
            elif pos_type == 1:
                short_liq = liq_px

        return {"long_liq": long_liq, "short_liq": short_liq}

    except Exception as e:
        Log(f"获取清算价失败: {e}")
        return {"long_liq": None, "short_liq": None}


# ═══════════════════════════════════════════
#  激活格子
# ═══════════════════════════════════════════

def activate_grids(r_low, r_high):
    global state, grids, grid_prices, grid_usdt, range_low, range_high

    range_low  = r_low
    range_high = r_high

    prices, usdt_list = calc_grids(r_low, r_high)
    if not prices:
        raise Exception("格子计算失败,请检查区间设置和资金")

    grid_prices = prices
    grid_usdt   = usdt_list
    n           = len(usdt_list)

    Log(f"区间激活: 下沿={r_low} 上沿={r_high} 格数={n} 方向={direction}")
    for i in range(n):
        Log(f"  格{i}: 下@{prices[i]} ↔ 上@{prices[i + 1]}  {usdt_list[i]:.2f}U")

    grids = [{"buy_oid": None, "sell_oid": None, "status": "empty",
              "buy_contracts": 0, "filled_price": 0, "grid_dir": direction} for _ in range(n)]

    price = get_price()
    mid_price = (r_low + r_high) / 2.0

    for i in range(n):
        grid_low  = prices[i]
        grid_high = prices[i + 1]

        if direction == "long":
            grids[i]["grid_dir"] = "long"
            if grid_low < price and grid_low <= r_high:
                _place_grid_open(i, "long")
            else:
                grids[i]["status"] = "skip_above"

        elif direction == "short":
            if grid_high > price:
                grids[i]["grid_dir"] = "short"
                _place_grid_open(i, "short")
            else:
                grids[i]["grid_dir"] = "short"
                grids[i]["status"] = "skip_below"

        else:  # both
            if grid_high <= mid_price:
                grids[i]["grid_dir"] = "long"
                if grid_low < price:
                    _place_grid_open(i, "long")
                else:
                    grids[i]["status"] = "skip_above"
            else:
                grids[i]["grid_dir"] = "short"
                if grid_high > price:
                    _place_grid_open(i, "short")
                else:
                    grids[i]["status"] = "skip_below"

    state = STATE_ACTIVE


def activate_grids_keep_positions(r_low, r_high):
    """
    移动区间时保留现有持仓的版本。
    - 对已持仓格子(pending_close / holding_no_close):保留止盈单,不动
    - 对空格子:按新区间重新挂开仓单
    """
    global state, grids, grid_prices, grid_usdt, range_low, range_high

    old_holding = []
    for g in grids:
        if g["status"] in ("pending_close", "holding_no_close") and g.get("buy_contracts", 0) > 0:
            old_holding.append({
                "buy_contracts": g["buy_contracts"],
                "filled_price":  g["filled_price"],
                "sell_oid":      g.get("sell_oid"),
                "grid_dir":      g.get("grid_dir", direction),
                "status":        g["status"],
                "open_bar":      g.get("open_bar"),     # V4.1: 持仓龄/TP衰减状态跨重建保留
                "tp0":           g.get("tp0"),
                "tp_price":      g.get("tp_price"),
                "tp_target":     g.get("tp_target"),
            })

    range_low  = r_low
    range_high = r_high

    prices, usdt_list = calc_grids(r_low, r_high)
    if not prices:
        raise Exception("格子计算失败,请检查区间设置和资金")

    grid_prices = prices
    grid_usdt   = usdt_list
    n           = len(usdt_list)

    Log(f"区间激活(保留持仓): 下沿={r_low} 上沿={r_high} 格数={n}  现有持仓={len(old_holding)}格继续持有")

    grids = [{"buy_oid": None, "sell_oid": None, "status": "empty",
              "buy_contracts": 0, "filled_price": 0, "grid_dir": direction} for _ in range(n)]

    for idx, hold in enumerate(old_holding):
        if idx >= n:
            Log(f"警告: 旧持仓格{idx}超出新格数{n},该持仓止盈单将被忽略(持仓仍在,需手动处理)")
            break
        grids[idx]["buy_contracts"] = hold["buy_contracts"]
        grids[idx]["filled_price"]  = hold["filled_price"]
        grids[idx]["sell_oid"]      = hold["sell_oid"]
        grids[idx]["grid_dir"]      = hold["grid_dir"]
        grids[idx]["status"]        = hold["status"]
        grids[idx]["open_bar"]      = hold.get("open_bar")
        grids[idx]["tp0"]           = hold.get("tp0")
        grids[idx]["tp_price"]      = hold.get("tp_price")
        grids[idx]["tp_target"]     = hold.get("tp_target")
        Log(f"  恢复持仓 → 格{idx}: {hold['buy_contracts']}张 @{hold['filled_price']}  状态={hold['status']}")

    price     = get_price()
    mid_price = (r_low + r_high) / 2.0

    for i in range(n):
        g = grids[i]
        if g["status"] in ("pending_close", "holding_no_close"):
            continue

        grid_low  = prices[i]
        grid_high = prices[i + 1]

        if direction == "long":
            g["grid_dir"] = "long"
            if grid_low < price:
                _place_grid_open(i, "long")
            else:
                g["status"] = "skip_above"

        elif direction == "short":
            g["grid_dir"] = "short"
            if grid_high > price:
                _place_grid_open(i, "short")
            else:
                g["status"] = "skip_below"

        else:  # both
            if grid_high <= mid_price:
                g["grid_dir"] = "long"
                if grid_low < price:
                    _place_grid_open(i, "long")
                else:
                    g["status"] = "skip_above"
            else:
                g["grid_dir"] = "short"
                if grid_high > price:
                    _place_grid_open(i, "short")
                else:
                    g["status"] = "skip_below"

    state = STATE_ACTIVE


def _place_grid_open(i, grid_dir):
    """挂开仓单(做多=买开,做空=卖开)"""
    n = len(grids)
    if i >= n:
        return
    g = grids[i]
    if g["status"] in ("holding", "pending_sell", "pending_buy", "pending_close"):
        return

    # ── V4.0 开仓数学闸门:趋势 / 净敞口 / 库存天花板 / 熔断
    gate_price = grid_prices[i] if grid_dir == "long" else grid_prices[i + 1]
    ok, reason = can_open(grid_dir, gate_price)
    if not ok:
        g["status"] = {"trend": "skip_trend", "risk": "skip_risk",
                       "skew": "skip_skew", "dd": "skip_dd"}.get(reason, "skip_risk")
        return

    if grid_dir == "long":
        result = place_limit_buy(grid_prices[i], grid_usdt[i],
                                 label=f"格{i} 多@{grid_prices[i]}")
    else:
        result = place_limit_sell_open(grid_prices[i + 1], grid_usdt[i],
                                       label=f"格{i} 空@{grid_prices[i+1]}")

    if isinstance(result, tuple) and result[0] == "skip_min_detail":
        _, raw_contracts, min_qty = result
        g["status"]   = "skip_min"
        g["skip_raw"] = round(raw_contracts, 6)
        g["skip_min"] = min_qty
    elif result:
        g["buy_oid"] = result
        g["status"]  = "pending_buy"
    else:
        g["status"]  = "skip_min"


def _place_grid_close(i, contracts, grid_dir, price_override=None):
    """
    挂止盈平仓单(做多=卖平,做空=买平)。
    V4.1: price_override 支持止盈价衰减;未指定时优先用 g["tp_target"]
    (衰减后目标),保证 holding_no_close 重试不会把TP弹回原格价。
    """
    n = len(grids)
    if i >= n:
        return
    g = grids[i]

    if price_override is None:
        price_override = g.get("tp_target")

    if grid_dir == "long":
        sell_price = price_override if price_override else grid_prices[i + 1]
        oid = place_limit_close_long(sell_price, contracts,
                                     label=f"格{i} 平多@{sell_price}")
    else:
        buy_price = price_override if price_override else grid_prices[i]
        oid = place_limit_close_short(buy_price, contracts,
                                      label=f"格{i} 平空@{buy_price}")

    tp_px = sell_price if grid_dir == "long" else buy_price
    if oid:
        g["sell_oid"]      = oid
        g["buy_contracts"] = contracts
        g["status"]        = "pending_close"
        g["tp_price"]      = tp_px
        if not g.get("tp0"):
            g["tp0"] = tp_px          # 记住原始止盈价,衰减从它出发单调进行
        if g.get("open_bar") is None:
            g["open_bar"] = _est_count
    else:
        g["status"]        = "holding_no_close"
        g["buy_contracts"] = contracts
        g["sell_oid"]      = None
        Log(f"格{i} 止盈挂单失败,标记 holding_no_close,下轮重试")


# ═══════════════════════════════════════════
#  区间突破检测 & 移动
# ═══════════════════════════════════════════

def check_breakout_and_shift():
    """
    只在"有利方向"触发区间移动,不利方向完全不动、不撤单:
      - long : 涨破上沿 -> 上移(有利);跌破下沿 -> 忽略
      - short: 跌破下沿 -> 下移(有利);涨破上沿 -> 忽略
      - both : 双向均触发移动
    移动时不强制平仓,只撤开仓挂单,持仓继续持有。
    """
    global range_low, range_high, shift_count, state

    price = get_price()

    ref_price      = (range_low + range_high) / 2.0
    trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref_price)

    price_above = price >= range_high + trigger_offset
    price_below = price <= range_low  - trigger_offset

    if direction == "long":
        if price_above:
            return _do_shift_up(price)
        if price_below:
            Log(f"[做多] 价格{price}跌破下沿{range_low}(不利方向),不移动不撤单,等待回归")
            return False

    elif direction == "short":
        if price_below:
            return _do_shift_down_auto(price)
        if price_above:
            Log(f"[做空] 价格{price}涨破上沿{range_high}(不利方向),不移动不撤单,等待回归")
            return False

    else:  # both
        if price_above:
            return _do_shift_up(price)
        if price_below:
            return _do_shift_down_auto(price)

    return False


def _do_shift_up(price):
    global range_low, range_high, shift_count, state

    old_low  = range_low
    old_high = range_high
    new_high = range_high
    new_low  = range_low
    steps    = 0

    while True:
        ref = (new_low + new_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        if price < new_high + trigger_offset:
            break
        shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)
        if shift_abs <= 0:
            break
        new_high  = round(new_high + shift_abs, 8)
        width_abs = effective_width_abs(new_high)
        new_low   = round(new_high - width_abs, 8)
        steps    += 1
        if steps > 500:
            Log("警告: 上移计算超过500步")
            break

    Log("=" * 60)
    Log(f"★ 区间上移触发!当前价={price}  旧上沿={old_high}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {fp(new_low)} ~ {fp(new_high)}  (共上移{steps}步)")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING
    _execute_shift_keep_positions(old_low, old_high, fp(new_low), fp(new_high), steps, "↑上移", price)
    return True


def _do_shift_down_auto(price):
    global range_low, range_high, shift_count, state

    old_low  = range_low
    old_high = range_high
    new_low  = range_low
    new_high = range_high
    steps    = 0

    while True:
        ref = (new_low + new_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        if price > new_low - trigger_offset:
            break
        shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)
        if shift_abs <= 0:
            break
        new_low   = round(new_low - shift_abs, 8)
        width_abs = effective_width_abs(new_low)
        new_high  = round(new_low + width_abs, 8)
        steps    += 1
        if steps > 500:
            Log("警告: 下移计算超过500步")
            break

    Log("=" * 60)
    Log(f"★ 区间下移触发!当前价={price}  旧下沿={old_low}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {fp(new_low)} ~ {fp(new_high)}  (共下移{steps}步)")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING
    _execute_shift_keep_positions(old_low, old_high, fp(new_low), fp(new_high), steps, "↓下移(自动)", price)
    return True


def _execute_shift_keep_positions(old_low, old_high, new_low, new_high, steps, dir_label, price):
    global shift_count, state, _last_rebuild_bar
    _last_rebuild_bar = _est_count   # V4.1: 与σ重建共享冷却,避免移动后立刻再重建

    Log("步骤1: 撤销开仓挂单(止盈单和持仓保留)...")
    cancel_open_orders_only()
    Sleep(500)

    shift_count += 1
    shift_history.append({
        "n":        shift_count,
        "dir":      dir_label,
        "price":    round(price, 8),
        "old_low":  old_low,
        "old_high": old_high,
        "new_low":  new_low,
        "new_high": new_high,
    })

    Log("步骤2: 刷新账户数据...")
    for _ in range(5):
        if update_account():
            break
        Sleep(2000)

    Log("步骤3: 激活新区间格子(保留持仓)...")
    activate_grids_keep_positions(new_low, new_high)
    Log(f"★ 区间移动完成!累计移动 {shift_count} 次,持仓继续持有中")


# ═══════════════════════════════════════════
#  区间手动下移/上移
# ═══════════════════════════════════════════

def shift_range_down():
    global range_low, range_high, shift_down_count, state

    price    = get_price()
    old_low  = range_low
    old_high = range_high

    ref = (range_low + range_high) / 2.0
    shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)

    new_high = fp(round(range_high - shift_abs, 8))
    width_abs = effective_width_abs(new_high)
    new_low  = fp(round(new_high - width_abs, 8))

    Log("=" * 60)
    Log(f"★ 手动降低下沿触发!当前价={price}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {new_low} ~ {new_high}  (下移≈{shift_abs:.4f})")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING

    cancel_open_orders_only()
    Sleep(500)

    shift_down_count += 1
    shift_history.append({
        "n":        f"↓{shift_down_count}",
        "dir":      "↓手动下移",
        "price":    round(price, 8),
        "old_low":  old_low,
        "old_high": old_high,
        "new_low":  new_low,
        "new_high": new_high,
    })

    for _ in range(5):
        if update_account():
            break
        Sleep(2000)

    activate_grids_keep_positions(new_low, new_high)
    global _last_rebuild_bar
    _last_rebuild_bar = _est_count
    Log(f"★ 手动区间下移完成!累计手动下移 {shift_down_count} 次,持仓继续持有中")
    return True


def shift_range_up():
    global range_low, range_high, shift_count, state

    price    = get_price()
    old_low  = range_low
    old_high = range_high

    ref = (range_low + range_high) / 2.0
    shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)

    new_high = fp(round(range_high + shift_abs, 8))
    width_abs = effective_width_abs(new_high)
    new_low  = fp(round(new_high - width_abs, 8))

    Log("=" * 60)
    Log(f"★ 手动升高上沿触发!当前价={price}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {new_low} ~ {new_high}  (上移≈{shift_abs:.4f})")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING

    cancel_open_orders_only()
    Sleep(500)

    shift_count += 1
    shift_history.append({
        "n":        shift_count,
        "dir":      "↑手动上移",
        "price":    round(price, 8),
        "old_low":  old_low,
        "old_high": old_high,
        "new_low":  new_low,
        "new_high": new_high,
    })

    for _ in range(5):
        if update_account():
            break
        Sleep(2000)

    activate_grids_keep_positions(new_low, new_high)
    global _last_rebuild_bar
    _last_rebuild_bar = _est_count
    Log(f"★ 手动区间上移完成!累计上移 {shift_count} 次,持仓继续持有中")
    return True


# ═══════════════════════════════════════════
#  交互命令处理
# ═══════════════════════════════════════════

def handle_command():
    try:
        cmd = GetCommand()
        if not cmd:
            return False

        Log(f"收到交互命令: {cmd}")

        if cmd == "shift_down":
            Log("用户点击【降低下沿】按钮")
            shift_range_down()
            refresh_tables()
            return True
        elif cmd == "shift_up":
            Log("用户点击【升高上沿】按钮")
            shift_range_up()
            refresh_tables()
            return True

        Log(f"未知命令: {cmd}")
        return False
    except Exception as e:
        Log(f"处理命令异常: {e}")
        return False


# ═══════════════════════════════════════════
#  格子状态机
# ═══════════════════════════════════════════

def sync_grid_orders():
    global empty_bars_count
    price = get_price()
    n     = len(grids)
    if n == 0:
        return

    for i in range(n):
        g       = grids[i]
        g_dir   = g.get("grid_dir", "") or direction

        if g["status"] == "pending_buy":
            st, deal, avg_px = check_order_status(g["buy_oid"])
            if st == "filled":
                contracts = fa(deal) if deal > 0 else fa(usdt_to_contracts(grid_usdt[i], grid_prices[i]))
                if g_dir == "long":
                    filled_px = avg_px if avg_px > 0 else grid_prices[i]
                else:
                    filled_px = avg_px if avg_px > 0 else grid_prices[i + 1]
                Log(f"格{i}({g_dir}) 开仓成交 均价={filled_px}  实际{contracts}张")
                g["buy_oid"]       = None
                g["buy_contracts"] = contracts
                g["filled_price"]  = filled_px
                g["open_bar"]      = _est_count   # V4.1: 持仓龄时钟(以K线根数计)
                g["tp0"]           = None
                g["tp_target"]     = None
                _place_grid_close(i, contracts, g_dir)
            elif st == "cancelled":
                Log(f"格{i} 开仓单已撤销,重置为 empty")
                g["buy_oid"] = None
                g["status"]  = "empty"

        elif g["status"] == "pending_close":
            st, deal, avg_px = check_order_status(g["sell_oid"])
            if st == "filled":
                if g_dir == "long":
                    filled_px = avg_px if avg_px > 0 else grid_prices[i + 1]
                else:
                    filled_px = avg_px if avg_px > 0 else grid_prices[i]
                Log(f"格{i}({g_dir}) 止盈成交 均价={filled_px} → 重置")
                g["sell_oid"]      = None
                g["buy_contracts"] = 0
                g["filled_price"]  = 0
                g["status"]        = "empty"
                g["open_bar"]      = None   # V4.1: 清空TP衰减状态
                g["tp0"]           = None
                g["tp_price"]      = None
                g["tp_target"]     = None
                _try_reopen(i, g_dir, price)
            elif st == "cancelled":
                Log(f"格{i} 止盈单已撤销,重新挂止盈单")
                g["sell_oid"] = None
                contracts = g.get("buy_contracts", 0)
                if contracts > 0:
                    _place_grid_close(i, contracts, g_dir)
                else:
                    g["status"] = "empty"

        elif g["status"] in ("empty", "skip_min", "skip_above", "skip_below",
                              "skip_trend", "skip_risk", "skip_skew", "skip_dd"):
            _try_reopen(i, g_dir, price)

        elif g["status"] == "holding_no_close":
            contracts = g.get("buy_contracts", 0)
            if contracts > 0:
                Log(f"格{i} holding_no_close 重试挂止盈单")
                _place_grid_close(i, contracts, g_dir)
            else:
                g["status"] = "empty"

    if EMPTY_TIMEOUT_BARS > 0:
        total_amt   = get_total_position()
        has_holding = any(g["status"] == "pending_close" for g in grids)
        if total_amt <= 0 and not has_holding:
            empty_bars_count += 1
        else:
            empty_bars_count = 0


def _try_reopen(i, g_dir, price):
    if g_dir == "long":
        # 做多:挂单价=格子下沿,必须在区间内且当前价高于挂单价(等待价格回落成交)
        # 同时限制挂单价不能超过 range_high,避免价格突破上沿时在上沿外乱挂
        if grid_prices[i] < price and grid_prices[i] <= range_high:
            _place_grid_open(i, "long")
    else:
        # 做空:挂单价=格子上沿,必须在区间内且当前价低于挂单价
        if grid_prices[i + 1] > price and grid_prices[i + 1] >= range_low:
            _place_grid_open(i, "short")


# ═══════════════════════════════════════════
#  初始化
# ═══════════════════════════════════════════

def init():
    global symbol, base_currency, Funding, INIT_FUNDING, swap_code, direction, direction_mode

    direction_mode = DIRECTION.strip().lower() if DIRECTION else "auto"
    if direction_mode not in ("long", "short", "both", "auto"):
        direction_mode = "auto"
    # auto: 热身期先按中性双向布网,体制确认后由 check_direction_switch 接管
    direction = "both" if direction_mode == "auto" else direction_mode

    Log(f"通用网格策略(V4.1-数学自适应版)启动  方向模式={direction_mode}"
        f"{'(初始按both布网,体制确认后自动切换)' if direction_mode == 'auto' else ''}")

    # 直接从交易所获取当前交易对,无需 SYMBOL 参数
    # GetCurrency() 返回如 "BTC_USDT.SWAP",统一处理成 "BTC_USDT"
    raw       = exchange.GetCurrency()
    symbol    = raw.upper().replace(".SWAP", "").replace(".swap", "")  # → "BTC_USDT"
    swap_code = symbol + ".swap"                                        # → "BTC_USDT.swap"

    exchange.SetMarginLevel(swap_code, LEVERAGE)

    base_currency = symbol.split("_")[0]   # → "BTC"

    # V4.0: 解析参考合约(跨资产确认),统一成 .swap 代码
    global _ref_symbols
    _ref_symbols = []
    if REF_SYMBOLS and REF_SYMBOLS.strip():
        for raw in REF_SYMBOLS.split(","):
            s = raw.strip().upper().replace(".SWAP", "")
            if s:
                _ref_symbols.append(s + ".swap")
    if _ref_symbols:
        Log(f"跨资产参考合约({len(_ref_symbols)}): {_ref_symbols}")

    Log(f"交易对: {symbol}  合约: {swap_code}")

    ticker = exchange.GetTicker(swap_code)
    if not ticker:
        raise Exception(f"无法获取行情: {swap_code}")

    data = exchange.GetMarkets().get(swap_code)
    if not data:
        raise Exception(f"无法获取市场信息: {swap_code}")

    assets[base_currency] = {
        "amount": 0, "long_amount": 0, "long_price": 0, "long_profit": 0,
        "short_amount": 0, "short_price": 0, "short_profit": 0,
        "hold_price": 0, "price": ticker.Last, "unrealised_profit": 0,
        "AmountPrecision": data["AmountPrecision"],
        "PricePrecision":  data["PricePrecision"],
        "MinQty":          data["MinQty"],
        "ctVal":           data["CtVal"],
        "MinNotional":     data.get("MinNotional", 5),
    }

    cancel_all_orders(swap_code)
    acc = exchange.GetAccount()
    if acc:
        INIT_FUNDING = acc.Equity
        Funding      = INIT_FUNDING

    Log(f"账户资金={Funding:.2f}U  杠杆={LEVERAGE}x")
    Log(f"手续费=万{FEE_RATE * 10000:.0f}  格距覆盖费{FEE_PROFIT_MULTI}倍")
    Log(f"最小下单张数={get_min_qty()}  合约面值={assets[base_currency]['ctVal']}")
    Log(f"区间宽度={GRID_WIDTH_PCT}%  移动幅度={SHIFT_STEP_PCT}%  触发偏移={BREAKOUT_TRIGGER_PCT}%")


# ═══════════════════════════════════════════
#  状态面板
# ═══════════════════════════════════════════

def refresh_tables():
    try:
        price  = get_price()
        bal    = assets["USDT"]["total_balance"]
        equity = assets["USDT"].get("equity", bal)
        unreal = assets[base_currency]["unrealised_profit"]
        profit  = round(equity - INIT_FUNDING, 4)
        ppct    = round(profit / INIT_FUNDING * 100, 2) if INIT_FUNDING else 0
        n       = len(grids)
        pending = sum(1 for g in grids if g["status"] == "pending_buy")
        holding = sum(1 for g in grids if g["status"] in ("pending_close", "holding_no_close"))

        long_amt  = assets[base_currency].get("long_amount", 0)
        short_amt = assets[base_currency].get("short_amount", 0)

        dist_to_upper = round(range_high - price, 4)
        dist_to_lower = round(price - range_low, 4)

        dir_label_map = {"long": "🟢做多", "short": "🔴做空", "both": "🔵双向"}
        dir_label = dir_label_map.get(direction, direction)
        if direction_mode == "auto":
            dir_label = f"🤖自动[{dir_label} 已转向{_dir_switch_count}次]"

        ref_price      = (range_low + range_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref_price)
        above_range    = price >= range_high + trigger_offset
        below_range    = price <= range_low  - trigger_offset

        unfavorable_wait = (
            (direction == "long"  and below_range) or
            (direction == "short" and above_range)
        )

        if state == STATE_SHIFTING:
            state_label = "🔄 区间移动中..."
        elif unfavorable_wait:
            state_label = (f"⏳ 不利方向超出区间,挂单保留等待回归 ({n}格 持仓{holding}) {dir_label}")
        elif above_range or below_range:
            state_label = (f"🔄 有利方向突破,区间即将移动 ({n}格 持仓{holding}) {dir_label}")
        else:
            state_label = f"✅ 网格运行 ({n}格 开仓挂单{pending} 持仓{holding}) {dir_label}"

        rng_w   = round(range_high - range_low, 4)
        rng_pct = round(rng_w / price * 100, 3) if price > 0 else 0

        ref = (range_low + range_high) / 2.0
        shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)

        # ── 清算价格(直接从币安接口获取,准确值)
        liq = get_liquidation_prices()
        long_liq_str  = f"⚠️ {liq['long_liq']}"  if liq.get("long_liq")  else "— (无多头持仓)"
        short_liq_str = f"⚠️ {liq['short_liq']}" if liq.get("short_liq") else "— (无空头持仓)"

        # ── V4.0 数学层状态
        ct_val_m      = assets[base_currency].get("ctVal", 1)
        net_exp_usdt  = (long_amt - short_amt) * ct_val_m * price
        net_exp_limit = Funding * MAX_NET_EXPOSURE_MULT if MAX_NET_EXPOSURE_MULT > 0 else 0

        main = {
            "type": "table",
            "title": f"通用网格(V4.2最大格数) | {symbol} | {state_label}",
            "cols": ["项目", "数值", "项目", "数值", "项目", "数值"],
            "rows": [
                ["状态", state_label, "账户权益", f"{equity:.4f}U",
                 "总盈亏", f"{profit:+.4f}U ({ppct:+.2f}%)"],
                ["当前价格", f"{price}", "多头持仓", f"{long_amt}张",
                 "空头持仓", f"{short_amt}张"],
                ["区间下沿", f"{range_low}", "区间上沿", f"{range_high}",
                 "区间宽度", f"{rng_w} ({rng_pct}%)"],
                ["距上沿", f"{dist_to_upper}", "距下沿", f"{dist_to_lower}",
                 "未实现盈亏", f"{unreal:.4f}U"],
                ["方向模式", dir_label,
                 "上移次数", f"{shift_count}次",
                 "手动下移", f"{shift_down_count}次"],
                ["宽度%", f"{GRID_WIDTH_PCT}%",
                 "移动%", f"{SHIFT_STEP_PCT}% (≈{shift_abs:.4f})",
                 "触发%", f"{BREAKOUT_TRIGGER_PCT}%"],
                ["多头清算价", long_liq_str,
                 "空头清算价", short_liq_str,
                 "格数", f"{n}格"],
                ["市场体制", regime_label_text(),
                 "半衰期H", (f"{_ou_half_life:.1f}根" if _ou_half_life > 0 else "—(无回归)"),
                 "均衡σ_eq", f"{_ou_sigma_eq*100:.4f}% (残差)"],
                ["趋势z/阈值", f"{_trend_z:+.3f} / ±{TREND_Z_THRESHOLD}",
                 "净敞口/上限", f"{net_exp_usdt:+.1f} / ±{net_exp_limit:.1f}U",
                 "动态天花板", f"{fp(effective_ceiling())}"],
                ["跨资产确认", (f"参考{ref_n()}个 "
                              f"{'✅' + str(sum(1 for f in _ref_fresh if f)) + '个在市' if (_refs_ok and any(_ref_fresh)) else ('💤休市→用自身z' if _refs_ok else '⚠️无数据')}"
                              if ref_n() > 0 else "未启用"),
                 "系统性z/占比", f"{_trend_z_sys:+.3f} / {_sys_share*100:.0f}%",
                 "β(目标~参考)", (", ".join(f"{b:+.2f}" for b in _ref_beta) if _ref_beta else "—")],
                ["退出时钟", f"H_exit={half_life_for_exit():.0f}根 ×{K_EXIT_HALFLIFE}",
                 "残差ε", f"{(_eps - (_rls_a/(1-_rls_b) if 0<_rls_b<1 else _eps))*100:+.3f}%" if _eps_prev is not None else "—",
                 "宽度比(现/目标)", (f"{(range_high-range_low)/effective_width_abs(price):.2f}" if effective_width_abs(price) > 0 else "—")],
            ]
        }

        ct_val = assets[base_currency].get("ctVal", 1)

        total_hold_contracts = 0
        total_hold_cost      = 0.0
        grid_positions       = []

        for i, g in enumerate(grids):
            contracts = g.get("buy_contracts", 0)
            filled_px = g.get("filled_price", 0)
            g_dir     = g.get("grid_dir", "long")
            if contracts > 0 and filled_px > 0:
                total_hold_contracts += contracts
                total_hold_cost      += contracts * filled_px
                if g_dir == "long":
                    pnl     = contracts * ct_val * (price - filled_px)
                    pnl_pct = (price - filled_px) / filled_px * 100
                    tp_p    = grid_prices[i + 1] if i + 1 < len(grid_prices) else "-"
                else:
                    pnl     = contracts * ct_val * (filled_px - price)
                    pnl_pct = (filled_px - price) / filled_px * 100
                    tp_p    = grid_prices[i] if i < len(grid_prices) else "-"
                grid_positions.append([
                    f"格{i}({g_dir[0].upper()})",
                    f"{contracts}张",
                    f"{filled_px}",
                    f"{price}",
                    f"{pnl:+.4f}U",
                    f"{pnl_pct:+.2f}%",
                    f"{tp_p}",
                ])

        if total_hold_contracts > 0 and total_hold_cost > 0:
            avg_cost = total_hold_cost / total_hold_contracts
            grid_positions.append([
                "═ 合计 ═",
                f"{total_hold_contracts}张",
                f"{avg_cost:.4f}",
                f"{price}",
                f"{unreal:+.4f}U",
                "-",
                "-",
            ])
        else:
            grid_positions = [["无持仓", "-", "-", f"{price}", "-", "-", "-"]]

        pos_tbl = {
            "type": "table",
            "title": f"持仓汇总 | 实时价={price}",
            "cols": ["来源", "持仓数量", "持仓均价", "实时价格", "浮动盈亏", "盈亏比例", "止盈价"],
            "rows": grid_positions
        }

        def short_id(oid):
            return str(oid)[-8:] if oid else "-"

        grid_rows = []
        for i, g in enumerate(grids):
            buy_p     = grid_prices[i]     if i < len(grid_prices)     else 0
            sell_p    = grid_prices[i + 1] if i + 1 < len(grid_prices) else 0
            usdt      = grid_usdt[i]       if i < len(grid_usdt)       else 0
            contracts = g.get("buy_contracts", 0)
            filled_px = g.get("filled_price", 0)
            g_dir     = g.get("grid_dir", "long")

            status_cn = {
                "empty":            "⬜ 等待",
                "pending_buy":      "🟡 挂单开仓",
                "pending_close":    "🔵 持仓止盈中",
                "skip_above":       "⬆️ 等待(价格太低)",
                "skip_below":       "⬇️ 等待(价格太高)",
                "holding_no_close": "⚠️ 持仓待挂止盈",
                "skip_trend":       "🚫 逆势暂停开仓",
                "skip_risk":        "🛑 净敞口达上限",
                "skip_skew":        "🔻 库存天花板下调",
                "skip_dd":          "🧯 回撤熔断中",
            }.get(g["status"], g["status"])
            if g["status"] == "skip_min":
                skip_raw = g.get("skip_raw", 0)
                skip_min = g.get("skip_min", get_min_qty())
                status_cn = f"⛔ 不足最小({skip_raw}张 < {skip_min}张)"

            dir_icon = "🟢多" if g_dir == "long" else "🔴空"

            hold_p_str = f"{filled_px}" if (contracts > 0 and filled_px > 0) else "-"

            if contracts > 0 and filled_px > 0:
                if g_dir == "long":
                    pnl = contracts * ct_val * (price - filled_px)
                else:
                    pnl = contracts * ct_val * (filled_px - price)
                pnl_pct = pnl / (contracts * ct_val * filled_px) * 100 if filled_px > 0 else 0
                pnl_str = f"{pnl:+.2f}U ({pnl_pct:+.2f}%)"
            else:
                pnl_str = "-"

            buy_id_str  = f"开:{short_id(g.get('buy_oid'))}"  if g.get("buy_oid")  else "-"
            sell_id_str = f"平:{short_id(g.get('sell_oid'))}" if g.get("sell_oid") else "-"
            order_str   = " / ".join(x for x in [buy_id_str, sell_id_str] if x != "-") or "-"

            grid_rows.append([
                f"格{i}",
                dir_icon,
                f"{buy_p}",
                f"{sell_p}",
                f"{contracts}张" if contracts > 0 else "-",
                hold_p_str,
                pnl_str,
                order_str,
                status_cn,
            ])

        if not grid_rows:
            grid_rows = [["无格子", "-", "-", "-", "-", "-", "-", "-", "无"]]

        grid_tbl = {
            "type": "table",
            "title": f"格子明细 | 下沿={range_low}  上沿={range_high}",
            "cols": ["格子", "方向", "下价", "上价", "持仓量", "开仓价", "浮动盈亏", "挂单ID", "状态"],
            "rows": grid_rows
        }

        history_rows = []
        for h in shift_history[-8:][::-1]:
            history_rows.append([
                f"{h.get('dir', '↑上移')} 第{h['n']}次",
                f"{h['price']}",
                f"{h['old_low']} ~ {h['old_high']}",
                f"{h['new_low']} ~ {h['new_high']}",
            ])
        if not history_rows:
            history_rows = [["暂无", "-", "-", "-"]]

        history_tbl = {
            "type": "table",
            "title": f"区间移动记录(自动移动{shift_count}次 / 手动下移{shift_down_count}次)",
            "cols": ["操作", "触发价格", "旧区间", "新区间"],
            "rows": history_rows
        }

        LogProfit(profit, "&")

        btn_shift_down = {
            "type": "button",
            "cmd":  "shift_down",
            "name": f"降低下沿 (-{SHIFT_STEP_PCT}%)",
            "description": f"区间下移{SHIFT_STEP_PCT}%: 当前{range_low}~{range_high}"
        }
        btn_shift_up = {
            "type": "button",
            "cmd":  "shift_up",
            "name": f"升高上沿 (+{SHIFT_STEP_PCT}%)",
            "description": f"区间上移{SHIFT_STEP_PCT}%: 当前{range_low}~{range_high}"
        }

        LogStatus(
            "`" + json.dumps(main,           ensure_ascii=False) + "`\n"
            "`" + json.dumps(pos_tbl,        ensure_ascii=False) + "`\n"
            "`" + json.dumps(grid_tbl,       ensure_ascii=False) + "`\n"
            "`" + json.dumps(history_tbl,    ensure_ascii=False) + "`\n"
            "`" + json.dumps(btn_shift_down, ensure_ascii=False) + "`\n"
            "`" + json.dumps(btn_shift_up,   ensure_ascii=False) + "`"
        )
    except Exception as e:
        Log(f"状态面板异常: {e}")


# ═══════════════════════════════════════════
#  主入口
# ═══════════════════════════════════════════

def main():

    SetErrorFilter(
        "502:|503:|tcp|character|unexpected|network|timeout|"
        "WSARecv|Connect|GetAddr|no such|reset|http|received|EOF|reused|Unknown"
    )

    init()

    while not update_account():
        Log("等待账户数据...")
        Sleep(3000)
    while not update_price():
        Log("等待行情数据...")
        Sleep(3000)

    price_now = get_price()

    # V4.0: 用历史 K 线一次性热身递归估计器,让初始区间就用上自适应宽度
    global _equity_peak
    _equity_peak = assets["USDT"].get("equity", Funding)
    for _ in range(3):
        try:
            update_estimators()
        except Exception as e:
            Log(f"估计器热身异常: {e}")
            break
    Log(f"[V4.0] 估计器热身: 已喂{_est_count}根  半衰期={_ou_half_life:.1f}根  "
        f"σ_eq={_ou_sigma_eq:.6f}  趋势z={_trend_z:.3f}  就绪={estimators_ready()}")

    if direction in ("long", "both"):
        if INIT_PRICE > 0:
            initial_high = INIT_PRICE
            Log(f"[做多/双向] 使用用户设定的初始上沿: {initial_high}")
        else:
            initial_high = price_now
            Log(f"[做多/双向] 初始上沿 = 当前价格: {initial_high}")
        width_abs    = effective_width_abs(initial_high)
        initial_low  = fp(initial_high - width_abs)
        initial_high = fp(initial_high)

    else:
        if INIT_PRICE > 0:
            initial_high = INIT_PRICE
            Log(f"[做空] 使用用户设定的初始上沿: {initial_high}")
        else:
            initial_high = price_now
            Log(f"[做空] 初始上沿 = 当前价格: {initial_high}")
        width_abs    = effective_width_abs(initial_high)
        initial_low  = fp(initial_high - width_abs)
        initial_high = fp(initial_high)

    Log("=" * 60)
    Log(f"策略: 通用网格(V4.2-最大格数版)  交易对={symbol}  方向={direction}  杠杆={LEVERAGE}x")
    Log(f"初始区间: {initial_low} ~ {initial_high}  宽度={round(initial_high - initial_low, 8)} ({GRID_WIDTH_PCT}%)")
    Log(f"移动幅度: {SHIFT_STEP_PCT}%/次  触发偏移: {BREAKOUT_TRIGGER_PCT}%")
    Log("=" * 60)

    align_steps = 0
    if direction in ("long", "both"):
        ref = (initial_low + initial_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        while price_now >= initial_high + trigger_offset:
            shift_abs    = pct_to_abs(SHIFT_STEP_PCT, ref)
            initial_high = round(initial_high + shift_abs, 8)
            width_abs    = effective_width_abs(initial_high)
            initial_low  = round(initial_high - width_abs, 8)
            ref          = (initial_low + initial_high) / 2.0
            trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
            align_steps += 1
            if align_steps > 500:
                break

    if direction in ("short", "both"):
        ref = (initial_low + initial_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        # 做空:价格低于下沿才下移跟随;价格高于上沿不动(不利方向,等回归)
        while price_now <= initial_low - trigger_offset:
            shift_abs    = pct_to_abs(SHIFT_STEP_PCT, ref)
            initial_high = round(initial_high - shift_abs, 8)
            width_abs    = effective_width_abs(initial_high)
            initial_low  = round(initial_high - width_abs, 8)
            ref          = (initial_low + initial_high) / 2.0
            trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
            align_steps += 1
            if align_steps > 500:
                break

    if align_steps > 0:
        initial_low  = fp(initial_low)
        initial_high = fp(initial_high)
        Log(f"启动价格({price_now})超出初始区间,自动调整{align_steps}步 → 区间 {initial_low}~{initial_high}")

    if direction in ("long", "both") and price_now >= initial_high:
        initial_high = price_now
        width_abs    = effective_width_abs(initial_high)
        initial_low  = fp(initial_high - width_abs)
        initial_high = fp(initial_high)
        Log(f"启动价格({price_now})超出上沿,直接重算区间 → {initial_low}~{initial_high}")

    if direction in ("short", "both") and price_now <= initial_low:
        initial_low  = price_now
        width_abs    = effective_width_abs(initial_low)
        initial_high = fp(initial_low + width_abs)
        initial_low  = fp(initial_low)
        Log(f"启动价格({price_now})低于下沿,直接重算区间 → {initial_low}~{initial_high}")

    activate_grids(initial_low, initial_high)

    while True:
        if not update_account():
            Sleep(5000)
            continue
        if not update_price():
            Sleep(5000)
            continue

        # V4.0: 逐根 K 线在线更新递归估计器(Kalman 趋势/波动 + RLS-OU)
        try:
            update_estimators()
        except Exception as e:
            Log(f"更新估计器异常: {e}")

        try:
            if handle_command():
                Sleep(POLL_INTERVAL)
                continue
        except Exception as e:
            Log(f"处理命令异常: {e}")

        try:
            if check_breakout_and_shift():
                refresh_tables()
                Sleep(POLL_INTERVAL)
                continue
        except Exception as e:
            Log(f"区间移动异常: {e},5秒后重试")
            Sleep(5000)
            continue

        # V4.1: auto模式——体制变化触发方向切换(优先于宽度重建)
        try:
            if check_direction_switch():
                refresh_tables()
                Sleep(POLL_INTERVAL)
                continue
        except Exception as e:
            Log(f"自动方向切换异常: {e}")

        # V4.1: 无突破的震荡中,σ_eq 漂移也触发区间压缩/拉伸
        try:
            if check_width_rebuild():
                refresh_tables()
                Sleep(POLL_INTERVAL)
                continue
        except Exception as e:
            Log(f"σ自适应重建异常: {e}")

        sync_grid_orders()

        # V4.1: k×H 止盈衰减——超龄持仓主动周转,不死等回归
        try:
            apply_tp_decay()
        except Exception as e:
            Log(f"TP衰减异常: {e}")

        refresh_tables()
        Sleep(POLL_INTERVAL)
Strategy parameters
Strategy parameters
Direction
Initial Price
Grid Width %
Shift Step %
Breakout Trigger %
Leverage
Fee Rate
Fee Profit Multi
Poll Interval
Empty Timeout Bars
KLine Period Sec
Initial Funding
Warmup Bars
Reference Symbols (Optional)
KF Responsiveness
Vol Adapt Rate
Trend Z
OU Memory
Width σ Mult
Width Floor
Width Cap
Capital Frac
Max Net Exposure
Inventory Skew
Max Drawdown
Exit k×H
Beta Agg Bars
Commands
Shift Down
shift_down
Shift Up
shift_up
Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)