策略源码
'''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: [["SYMBOL","SPACEX_USDT.swap"],["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" # runtime direction
# ═══════════════════════════════════════════
# 百分比转绝对值辅助
# ═══════════════════════════════════════════
def pct_to_abs(pct, ref_price):
"""将百分比参数转换为绝对值"""
return ref_price * pct / 100.0
# ═══════════════════════════════════════════
# 精度 / 换算
# ═══════════════════════════════════════════
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():
"""
【V4.1 新增】只撤销开仓挂单(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" # 回归区间后 _try_reopen 会自动重挂
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}),请手动检查!")
# ═══════════════════════════════════════════
# 动态格数计算
# ═══════════════════════════════════════════
def calc_grids(r_low, r_high):
price = get_price()
range_width = r_high - r_low
total_position_usdt = Funding * LEVERAGE / 2
min_step_pct = FEE_RATE * 2 * FEE_PROFIT_MULTI
min_step_abs = price * min_step_pct
min_step_abs = max(min_step_abs, 10 ** (-assets[base_currency]["PricePrecision"]))
max_by_range = int(range_width / min_step_abs)
min_qty = get_min_qty()
ct_val = assets[base_currency]["ctVal"]
total_contracts = total_position_usdt / (ct_val * price)
max_by_capital = int(total_contracts / min_qty) if min_qty > 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)
dists = [r_high - prices[i] for i in range(n)]
total_dist = sum(dists)
usdt_list = []
for i in range(n):
u = total_position_usdt * dists[i] / total_dist if total_dist > 0 else total_position_usdt / n
usdt_list.append(round(u, 4))
Log(f"动态格数计算: 区间宽度={range_width:.4f} 最小格距={min_step_abs:.4f}")
Log(f" 理论格数={max_by_range} 资金格数={max_by_capital} 实际格数={n}")
Log(f" 总仓位={total_position_usdt:.2f}U 总张数≈{total_contracts:.4f} 最小单张={min_qty} 格距={step:.4f}")
Log(f" 最小格金额={min(usdt_list):.2f}U 最大格金额={max(usdt_list):.2f}U 合计={sum(usdt_list):.2f}U")
return prices, usdt_list
# ═══════════════════════════════════════════
# 激活格子
# ═══════════════════════════════════════════
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":
if grid_low < price:
grids[i]["grid_dir"] = "long"
_place_grid_open(i, "long")
else:
grids[i]["grid_dir"] = "long"
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):
"""
【V4.1 新增】移动区间时保留现有持仓的版本。
- 对已持仓格子(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"],
})
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,先全部标空
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"]
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
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):
"""挂止盈平仓单(做多=卖平,做空=买平)"""
n = len(grids)
if i >= n:
return
g = grids[i]
if grid_dir == "long":
sell_price = grid_prices[i + 1]
oid = place_limit_close_long(sell_price, contracts,
label=f"格{i} 平多@{sell_price}")
else:
buy_price = grid_prices[i]
oid = place_limit_close_short(buy_price, contracts,
label=f"格{i} 平空@{buy_price}")
if oid:
g["sell_oid"] = oid
g["buy_contracts"] = contracts
g["status"] = "pending_close"
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():
"""
【V4.2 改动】
只在"有利方向"触发区间移动,不利方向完全不动、不撤单:
- 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 = pct_to_abs(GRID_WIDTH_PCT, 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 = pct_to_abs(GRID_WIDTH_PCT, 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):
"""
【V4.1】执行区间移动:只撤开仓单,保留持仓,用新区间重建格子。
"""
global shift_count, state
# 1. 只撤开仓挂单,止盈单保留
Log("步骤1: 撤销开仓挂单(止盈单和持仓保留)...")
cancel_open_orders_only()
Sleep(500)
# 2. 记录历史
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,
})
# 3. 刷新账户
Log("步骤2: 刷新账户数据...")
for _ in range(5):
if update_account():
break
Sleep(2000)
# 4. 用保留持仓的方式激活新区间
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 = pct_to_abs(GRID_WIDTH_PCT, 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)
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 = pct_to_abs(GRID_WIDTH_PCT, 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)
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
_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"
_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"):
_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":
if grid_prices[i] < price:
_place_grid_open(i, "long")
else:
if grid_prices[i + 1] > price:
_place_grid_open(i, "short")
# ═══════════════════════════════════════════
# 初始化
# ═══════════════════════════════════════════
def init():
global symbol, base_currency, Funding, INIT_FUNDING, swap_code, direction
direction = DIRECTION.strip().lower() if DIRECTION else "long"
if direction not in ("long", "short", "both"):
direction = "long"
Log(f"通用网格策略(V4.1-不强制平仓版)启动 方向模式={direction}")
exchange.SetMarginLevel(SYMBOL, LEVERAGE)
if SYMBOL and SYMBOL.strip():
symbol = SYMBOL.strip()
if symbol.endswith(".swap"):
symbol = symbol[:-5]
exchange.SetCurrency(symbol)
else:
symbol = exchange.GetCurrency()
base_currency = symbol.split("_")[0]
swap_code = symbol + ".swap"
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)
# 判断是否超出区间(区分有利/不利方向)
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)
main = {
"type": "table",
"title": f"通用网格(V4.1不强制平仓) | {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}%"],
]
}
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": "⚠️ 持仓待挂止盈",
}.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()
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 = pct_to_abs(GRID_WIDTH_PCT, initial_high)
initial_low = fp(initial_high - width_abs)
initial_high = fp(initial_high)
else:
if INIT_PRICE > 0:
initial_low = INIT_PRICE
Log(f"[做空] 使用用户设定的初始下沿: {initial_low}")
else:
initial_low = price_now
Log(f"[做空] 初始下沿 = 当前价格: {initial_low}")
width_abs = pct_to_abs(GRID_WIDTH_PCT, initial_low)
initial_high = fp(initial_low + width_abs)
initial_low = fp(initial_low)
Log("=" * 60)
Log(f"策略: 通用网格(V4.1-不强制平仓版) 交易对={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 = pct_to_abs(GRID_WIDTH_PCT, 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_low = round(initial_low - shift_abs, 8)
width_abs = pct_to_abs(GRID_WIDTH_PCT, initial_low)
initial_high = round(initial_low + 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 = pct_to_abs(GRID_WIDTH_PCT, 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 = pct_to_abs(GRID_WIDTH_PCT, 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
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
sync_grid_orders()
refresh_tables()
Sleep(POLL_INTERVAL)