通用网格策略(数学自适应版v4.1)
2
Follow
494
Followers
数学自适应动态网格策略(V4.2)
一句话简介
一个运行在合约市场的动态移动网格策略:在传统网格"低买高卖、高卖低买"的基础上,叠加了一整套在线递归数学估计层(Kalman + RLS-OU + 跨资产协整),让区间宽度、方向、移动、止盈、风控全部由市场数据自适应决定,而不是靠人为写死的固定窗口。
核心理念
传统网格的痛点是"参数拍脑袋、单边行情被套、震荡里赚不够"。本策略的设计哲学是:
时间尺度是"估计出来的输出",不是"人为输入的窗口"。
所有关键状态(趋势、波动、均值回归半衰期、均衡幅度)都在每根 K 线在线更新,记忆长度随信噪比与回归速度自适应伸缩,没有任何固定 N 根回看窗口。
四种方向模式
| 模式 | 行为 |
|---|---|
| 自动 (推荐) | 由体制判定动态切换:涨势→做多、跌势→做空、震荡→双向。带迟滞+冷却,旧方向持仓不暴力平仓,靠趋势闸门停加仓 + 止盈衰减自然退出。 |
| 仅做多 | 价格回落到格价挂买多,涨一格止盈 |
| 仅做空 | 价格上涨到格价挂卖空,跌一格止盈 |
| 多空都做 | 以区间中线为界,下半做多、上半做空 |
数学自适应层(V4 核心)
① Kalman 局部水平+斜率
逐根估计瞬时趋势斜率与瞬时波动 σ,观测噪声在线自适应,得出无量纲趋势强度 z = 斜率/波动。
② RLS-AR(1) / OU 均值回归
作用在协整残差 ε(而非原始价格)上,估计回归半衰期 H 与均衡标准差 σ_eq。区间宽度由 σ_eq 决定,波动大就自动放宽、波动小就收窄。
③ 跨资产协整确认(可选)
配置参考合约后,用 β 聚合估计(抗 Epps 高频异步偏差)区分:
- 系统性趋势(被参考确认)→ 拦住逆势单
- 特质性噪声(目标自己在动、参考没跟)→ 照常收割
并自带参考休市/冻结检测,正股休市时段自动回退到目标自身趋势判断。
五道风控闸门(开新仓前逐一检查)
- 趋势闸门:确认不利趋势 → 停开逆势单
- 净敞口硬上界:净方向名义价值达上限 → 停开(防无限接飞刀)
- 库存偏移天花板(Avellaneda-Stoikov 离散版):多头堆积→有效上沿自动下压;空头镜像同理
- 回撤熔断:权益从峰值回撤超阈值 → 停开
- 止盈衰减退出:持仓龄超过 k×半衰期仍未回归 → 止盈价向现价连续衰减,主动周转资金(逆势体制下时钟×2 加速),不死等、也不二元止损
动态区间管理
- 有利方向突破 → 区间跟随移动(持仓不平仓,继续等止盈)
- 不利方向超出 → 区间不动、不撤单,等待价格回归
- σ 漂移重建:无突破的震荡中,宽度偏离目标 [0.75, 1.33] 且冷却期过 → 以现价为锚重建区间
- 手动移动:状态栏提供「降低下沿」「升高上沿」按钮
资金利用(V4.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
Commands
Related strategies
Comment
All comments (0)
No data
- 1