1368 lines
54 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.sample.trend.strategy;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.hengyi.xbaseweb.common.result.ApiResult;
import com.sample.trend.config.CommonConstant;
import com.sample.trend.strategy.util.AtrCalculator;
import com.sample.trend.strategy.util.ScriptUtil;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import vip.uuquant.exhangeapi.core.WebSocketConnectionStatus;
import vip.uuquant.exhangeapi.core.WebSocketMessageHandler;
import vip.uuquant.exhangeapi.enums.*;
import vip.uuquant.exhangeapi.impl.bitget.BitGetClient;
import vip.uuquant.tradesystem.runner.enums.AlarmLevel;
import vip.uuquant.tradesystem.runner.enums.RobotChartType;
import vip.uuquant.tradesystem.runner.strategy.BaseStrategy;
import vip.uuquant.tradesystem.runner.vo.AccountVO;
import vip.uuquant.tradesystem.runner.vo.SymbolVO;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 基于 ATR 的动态网格策略(只做多)。
* <p>
* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。
* 下方买单始终最多挂 1 笔:成交后再挂下一格,不会批量铺满网格。
* 若现价相对挂单价上行超过间距 × 1.5,撤单并按现价重新挂靠下一格,避免买单悬空。
* 采用限价挂单买入,买单成交后在「入场价 + 间距」挂限价卖单止盈。
* 网格间距变化时不撤销已有挂单,已挂卖单价格保持不变。
*/
public class AutoGridStrategy extends BaseStrategy {
private static final String MARGIN_MODE = "crossed";
/**
* 现价高于挂单价超过「间距 × 该倍数」时,撤单重挂
*/
private static final BigDecimal BUY_REBASE_MULTIPLIER = new BigDecimal("1.5");
private final Logger logger = LoggerFactory.getLogger(AutoGridStrategy.class);
// 运行参数
private BigDecimal baseQuantity;
private BigDecimal atrMultiplier;
private String gridGrpScript;
private String klineInterval;
private int klinePeriod;
private int gridAdjustIntervalMinutes;
private BigDecimal minGap;
private BigDecimal maxGap;
private BigDecimal upperBound;
private BigDecimal lowerBound;
private boolean testMode;
private int leverage;
/**
* 最大持仓层数;达到后不再新建买单。待成交买单始终最多 1 笔。
*/
private int gridCount;
// 精度
private int pricePrecision = 2;
private int quantityPrecision = 4;
// 交易所
private BitGetClient bitGetClient;
// 网格状态
private volatile BigDecimal currentPrice;
private volatile BigDecimal currentAtr;
private volatile BigDecimal currentGridGap;
private volatile long lastGridAdjustTime;
private volatile boolean upperBoundPaused;
private volatile boolean lowerBoundPaused;
/**
* 待成交买单:key = 网格价格字符串
*/
private final Map<String, GridBuyOrder> pendingBuyOrders = new ConcurrentHashMap<>();
/**
* 待成交卖单:key = clientPositionId
*/
private final Map<String, GridSellOrder> pendingSellOrders = new ConcurrentHashMap<>();
/**
* 已成交待止盈的多头仓位
*/
private final List<GridPosition> openPositions = new CopyOnWriteArrayList<>();
/**
* 已处理过的买单成交 orderId,避免 fill / orders 双推重复开仓补单
*/
private final Set<String> processedBuyFillOrderIds = ConcurrentHashMap.newKeySet();
/**
* 已处理过的卖单成交 orderId
*/
private final Set<String> processedSellFillOrderIds = ConcurrentHashMap.newKeySet();
private final Object tradeLock = new Object();
// 图表
private Integer statusChart;
private Integer gridTableChart;
private Long robotId;
// ======================== 内部模型 ========================
private static class GridBuyOrder {
private final String clientOrderId;
private volatile String exchangeOrderId;
private final BigDecimal buyPrice;
GridBuyOrder(String clientOrderId, BigDecimal buyPrice) {
this.clientOrderId = clientOrderId;
this.buyPrice = buyPrice;
}
}
private static class GridSellOrder {
private final String clientOrderId;
private volatile String exchangeOrderId;
private final String clientPositionId;
private final BigDecimal sellPrice;
private final BigDecimal quantity;
GridSellOrder(String clientOrderId, String clientPositionId,
BigDecimal sellPrice, BigDecimal quantity) {
this.clientOrderId = clientOrderId;
this.clientPositionId = clientPositionId;
this.sellPrice = sellPrice;
this.quantity = quantity;
}
}
private static class GridPosition {
private final String clientPositionId;
private final BigDecimal entryPrice;
private final BigDecimal quantity;
/** 开仓时锁定的止盈价,不随 gridGap 变化 */
private final BigDecimal takeProfitPrice;
GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity,
BigDecimal takeProfitPrice) {
this.clientPositionId = clientPositionId;
this.entryPrice = entryPrice;
this.quantity = quantity;
this.takeProfitPrice = takeProfitPrice;
}
}
// ======================== WebSocket ========================
private final WebSocketMessageHandler webSocketMessageHandler = new WebSocketMessageHandler() {
@Override
public void onOpen(String connectionId, Response response) {
logger.info("WebSocket 连接成功:{}", connectionId);
if (connectionId.equals(bitGetClient.getPublicConnectionId())) {
bitGetClient.subscribeTicker(getSymbol());
} else {
// 私有频道必须等 login 成功回调后再订阅,否则会报 30004
bitGetClient.login();
}
}
@Override
public void onMessage(String connectionId, String message) {
// logger.debug("WebSocket 收到消息:{}", message);
if ("pong".equals(message)) {
return;
}
try {
JSONObject json = JSONObject.parseObject(message);
// 登录成功后再订阅 fill / orders
if ("login".equals(json.getString("event"))) {
Integer code = json.getInteger("code");
if (code != null && code == 0) {
logger.info("私有 WebSocket 登录成功,开始订阅成交与订单");
bitGetClient.subscribeFill();
bitGetClient.subscribeOrders();
} else {
logger.error("私有 WebSocket 登录失败:{}", message);
submitAlarm("WebSocket 登录失败", message, AlarmLevel.ERROR);
}
return;
}
if ("error".equals(json.getString("event"))) {
logger.error("WebSocket 返回错误:{}", message);
return;
}
if (!json.containsKey("action")) {
return;
}
String action = json.getString("action");
JSONObject arg = json.getJSONObject("arg");
if (arg == null) {
return;
}
String channel = arg.getString("channel");
JSONArray data = json.getJSONArray("data");
if (data == null || data.isEmpty()) {
return;
}
if ("ticker".equals(channel)) {
handleTicker(data.getJSONObject(0));
} else if ("fill".equals(channel)
&& ("snapshot".equals(action) || "update".equals(action))) {
for (int i = 0; i < data.size(); i++) {
handleFill(data.getJSONObject(i));
}
} else if ("orders".equals(channel)
&& ("snapshot".equals(action) || "update".equals(action))) {
for (int i = 0; i < data.size(); i++) {
handleOrderUpdate(data.getJSONObject(i));
}
}
} catch (Exception e) {
logger.error("WebSocket 消息处理异常", e);
}
}
@Override
public void onClosing(String connectionId, int code, String reason) {
logger.warn("WebSocket 关闭中:{} {} {}", connectionId, code, reason);
}
@Override
public void onClosed(String connectionId, int code, String reason) {
logger.warn("WebSocket 已关闭:{} {} {}", connectionId, code, reason);
}
@Override
public void onFailure(String connectionId, Throwable throwable, Response response) {
logger.error("WebSocket 异常:{}", connectionId, throwable);
}
@Override
public void onStatusChange(String connectionId, WebSocketConnectionStatus oldStatus,
WebSocketConnectionStatus newStatus) {
logger.info("WebSocket 状态变化:{} {} -> {}", connectionId, oldStatus, newStatus);
}
};
// ======================== 生命周期 ========================
@Override
public void init(JSONObject context) {
super.init(context);
logger.info("ATR 动态网格策略初始化");
tryParams("base_quantity", "atr_multiplier", "grid_grp_script", "kline_interval", "kline_period",
"grid_adjust_interval", "min_gap", "max_gap", "upper_bound", "lower_bound",
"grid_count", "test_mode");
baseQuantity = getRunParams().getBigDecimal("base_quantity");
atrMultiplier = getRunParams().getBigDecimal("atr_multiplier");
gridGrpScript = getRunParams().getString("grid_grp_script");
klineInterval = getRunParams().getString("kline_interval");
klinePeriod = getRunParams().getInteger("kline_period");
gridAdjustIntervalMinutes = getRunParams().getInteger("grid_adjust_interval");
minGap = getRunParams().getBigDecimal("min_gap");
maxGap = getRunParams().getBigDecimal("max_gap");
upperBound = getRunParams().getBigDecimal("upper_bound");
lowerBound = getRunParams().getBigDecimal("lower_bound");
testMode = getRunParams().getBooleanValue("test_mode");
leverage = getRunParams().getIntValue("leverage");
if (leverage <= 0) {
leverage = 10;
}
gridCount = getRunParams().getInteger("grid_count");
if (gridCount <= 0) {
throw new IllegalArgumentException("grid_count 必须大于 0");
}
if (lowerBound.compareTo(upperBound) >= 0) {
throw new IllegalArgumentException("lower_bound 必须小于 upper_bound");
}
SymbolVO symbolInfo = getSymbolInfo();
if (symbolInfo != null) {
if (symbolInfo.getPricePrecision() != null) {
pricePrecision = symbolInfo.getPricePrecision();
}
if (symbolInfo.getQuantityPrecision() != null) {
quantityPrecision = symbolInfo.getQuantityPrecision();
}
}
robotId = getRobotConfig().getRobotId();
initBitGetClient();
initCharts();
refreshGridGap(true);
syncOpenOrdersFromExchange();
pruneExcessPendingBuys();
replenishMissingSellOrders();
placeInitialBuyGrids();
createTask(this::syncAccount, 5, 600);
submitAlarm("策略初始化", String.format("ATR 动态网格策略启动,交易对:%s,初始网格间距:%s",
getSymbol(), formatPrice(currentGridGap)), AlarmLevel.INFO);
logger.info("策略初始化完成,网格间距:{},ATR:{}", currentGridGap, currentAtr);
}
@Override
public void run() {
if (currentPrice == null) {
fetchCurrentPriceFromRest();
}
if (currentPrice == null || currentGridGap == null) {
return;
}
try {
checkPriceBounds();
refreshGridGap(false);
replenishMissingSellOrders();
rebaseStalePendingBuy();
replenishBuyGrids();
updateDashboard();
} catch (Exception e) {
logger.error("策略运行异常", e);
}
}
@Override
public void notify(JSONObject data) {
logger.info("收到平台通知:{}", data);
}
@Override
public void destroy() {
logger.info("策略销毁");
try {
removeChart(null);
removeAllTask();
CommonConstant.webSocketManager.closeAll();
submitAlarm("策略销毁", "ATR 动态网格策略已停止", AlarmLevel.WARNING);
} catch (Exception e) {
logger.error("策略销毁异常", e);
}
}
// ======================== 初始化 ========================
private void initBitGetClient() {
bitGetClient = CommonConstant.exchangeFactory.createExchange(ExchangeType.BITGET);
bitGetClient.changeTestMode(testMode);
AccountVO account = getAccount();
bitGetClient.init(account.getApiKey(), account.getSecretKey(), account.getPassphrase(),
CommonConstant.httpClient, CommonConstant.webSocketManager);
try {
bitGetClient.createPublicWebsocket(webSocketMessageHandler);
bitGetClient.createPrivateWebsocket(webSocketMessageHandler);
} catch (Exception e) {
throw new RuntimeException("BitGet WebSocket 连接失败", e);
}
}
private void initCharts() {
statusChart = createChart(RobotChartType.TEXT, "网格状态", 1, 1, 24);
gridTableChart = createChart(RobotChartType.TABLE, "网格明细", 2, 2, 24);
}
// ======================== ATR 与网格间距 ========================
private void refreshGridGap(boolean force) {
long now = System.currentTimeMillis();
long intervalMs = gridAdjustIntervalMinutes * 60_000L;
if (!force && lastGridAdjustTime > 0 && now - lastGridAdjustTime < intervalMs) {
return;
}
try {
BigDecimal atr = fetchAtr();
if (atr == null) {
logger.warn("ATR 计算失败,数据不足");
return;
}
Map<String, Object> scriptVariables = buildGridScriptVariables(atr);
BigDecimal rawGap;
try {
rawGap = ScriptUtil.evalBigDecimal(gridGrpScript, scriptVariables);
} catch (Exception scriptException) {
logger.warn("网格表达式执行失败,回退默认公式:{}", scriptException.getMessage());
rawGap = null;
}
if (rawGap == null) {
rawGap = atr.multiply(atrMultiplier);
}
BigDecimal gap = rawGap.max(minGap).min(maxGap).setScale(pricePrecision, RoundingMode.HALF_UP);
BigDecimal oldGap = currentGridGap;
currentAtr = atr;
currentGridGap = gap;
lastGridAdjustTime = now;
if (oldGap != null && oldGap.compareTo(gap) != 0) {
logger.info("网格间距调整:{} -> {}(ATR={})", oldGap, gap, atr);
submitAlarm("网格间距调整",
String.format("间距 %s -> %s,ATR=%s,已有挂单及卖单价格保持不变",
formatPrice(oldGap), formatPrice(gap), formatPrice(atr)),
AlarmLevel.INFO);
}
} catch (Exception e) {
logger.error("刷新网格间距失败", e);
}
}
private Map<String, Object> buildGridScriptVariables(BigDecimal atr) {
Map<String, Object> variables = new java.util.HashMap<>();
variables.put("atr", atr == null ? null : atr.doubleValue());
variables.put("atrMultiplier", atrMultiplier == null ? null : atrMultiplier.doubleValue());
variables.put("baseQuantity", baseQuantity == null ? null : baseQuantity.doubleValue());
variables.put("minGap", minGap == null ? null : minGap.doubleValue());
variables.put("maxGap", maxGap == null ? null : maxGap.doubleValue());
variables.put("upperBound", upperBound == null ? null : upperBound.doubleValue());
variables.put("lowerBound", lowerBound == null ? null : lowerBound.doubleValue());
variables.put("gridCount", gridCount);
variables.put("leverage", leverage);
variables.put("pricePrecision", pricePrecision);
variables.put("quantityPrecision", quantityPrecision);
variables.put("klinePeriod", klinePeriod);
variables.put("gridAdjustIntervalMinutes", gridAdjustIntervalMinutes);
variables.put("testMode", testMode);
variables.put("currentPrice", currentPrice == null ? null : currentPrice.doubleValue());
return variables;
}
private BigDecimal fetchAtr() throws Exception {
JSONObject params = new JSONObject();
params.put("symbol", getPlainSymbol());
params.put("granularity", klineInterval);
params.put("limit", String.valueOf(klinePeriod + 2));
JSONArray klines = bitGetClient.getKLines(params);
if (klines == null || klines.isEmpty()) {
return null;
}
return AtrCalculator.calculate(klines, klinePeriod);
}
// ======================== 价格边界 ========================
private void checkPriceBounds() {
if (currentPrice == null) {
return;
}
if (!upperBoundPaused && currentPrice.compareTo(upperBound) > 0) {
upperBoundPaused = true;
submitAlarm("价格超上限",
String.format("当前价 %s 突破上限 %s,暂停新建买单", formatPrice(currentPrice), formatPrice(upperBound)),
AlarmLevel.WARNING);
} else if (upperBoundPaused && currentPrice.compareTo(upperBound) <= 0) {
upperBoundPaused = false;
submitAlarm("恢复交易", "价格回到上限以内,恢复新建买单", AlarmLevel.INFO);
}
if (!lowerBoundPaused && currentPrice.compareTo(lowerBound) < 0) {
lowerBoundPaused = true;
submitAlarm("价格破下限",
String.format("当前价 %s 跌破下限 %s,暂停新建买单", formatPrice(currentPrice), formatPrice(lowerBound)),
AlarmLevel.ERROR);
} else if (lowerBoundPaused && currentPrice.compareTo(lowerBound) >= 0) {
lowerBoundPaused = false;
submitAlarm("恢复交易", "价格回到下限以上,恢复新建买单", AlarmLevel.INFO);
}
}
private boolean canPlaceBuyOrders() {
return !upperBoundPaused && !lowerBoundPaused;
}
// ======================== 网格挂单 ========================
/**
* 初始化仅挂下方一格买单,与运行中补单逻辑一致。
*/
private void placeInitialBuyGrids() {
if (currentPrice == null || currentGridGap == null) {
fetchCurrentPriceFromRest();
}
replenishBuyGrids();
}
/**
* 无待成交买单时,在现价下方挂一格;已有挂单则不补,保证始终只有 1 笔买单。
*/
private void replenishBuyGrids() {
if (!canPlaceBuyOrders() || currentGridGap == null || currentPrice == null) {
return;
}
synchronized (tradeLock) {
if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) {
return;
}
BigDecimal nextBuyPrice = findNextAvailableBuyPrice(calcBuyPriceBelowCurrent());
if (nextBuyPrice != null) {
placeLimitBuyOrder(nextBuyPrice);
}
}
}
/**
* 价格单边上行时,下方买单会悬空永远不成交。
* 当 现价 - 挂单价 > 间距 × 1.5 时,撤单并按现价重新对齐挂单(例:1532 / 间距 5 → 1530)。
*/
private void rebaseStalePendingBuy() {
if (!canPlaceBuyOrders() || currentPrice == null || currentGridGap == null
|| currentGridGap.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
synchronized (tradeLock) {
if (pendingBuyOrders.isEmpty()) {
return;
}
GridBuyOrder pending = pendingBuyOrders.values().iterator().next();
BigDecimal distance = currentPrice.subtract(pending.buyPrice);
BigDecimal threshold = currentGridGap.multiply(BUY_REBASE_MULTIPLIER);
if (distance.compareTo(threshold) <= 0) {
return;
}
BigDecimal newPrice = findNextAvailableBuyPrice(calcBuyPriceBelowCurrent());
if (newPrice == null || newPrice.compareTo(pending.buyPrice) == 0) {
return;
}
try {
bitGetClient.cancelOrder(getSymbol(), pending.exchangeOrderId, pending.clientOrderId, null);
pendingBuyOrders.remove(priceKey(pending.buyPrice));
logger.info("买单距现价过远,撤单重挂:旧价={},现价={},间距={},阈值={},新价={}",
pending.buyPrice, currentPrice, currentGridGap, threshold, newPrice);
submitAlarm("买单追价重挂",
String.format("旧挂单价 %s,现价 %s,超过间距×1.5(%s),重挂至 %s",
formatPrice(pending.buyPrice), formatPrice(currentPrice),
formatPrice(threshold), formatPrice(newPrice)),
AlarmLevel.INFO);
placeLimitBuyOrder(newPrice);
} catch (Exception e) {
logger.error("买单追价重挂失败,旧价={}", pending.buyPrice, e);
submitAlarm("买单追价重挂失败",
String.format("旧价 %s:%s", formatPrice(pending.buyPrice), e.getMessage()),
AlarmLevel.ERROR);
}
}
}
/**
* 现价向下对齐到网格,确保严格低于现价。
*/
private BigDecimal calcBuyPriceBelowCurrent() {
if (currentPrice == null || currentGridGap == null
|| currentGridGap.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
BigDecimal nextBuyPrice = alignPriceDown(currentPrice, currentGridGap);
if (nextBuyPrice.compareTo(currentPrice) >= 0) {
nextBuyPrice = nextBuyPrice.subtract(currentGridGap);
}
return normalizePrice(nextBuyPrice);
}
/**
* 买单成交后,挂「成交价再下一格」的限价买单。
*/
private void placeNextBuyBelow(BigDecimal filledPrice) {
if (!canPlaceBuyOrders() || currentGridGap == null || filledPrice == null) {
return;
}
synchronized (tradeLock) {
if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) {
return;
}
BigDecimal nextBuyPrice = findNextAvailableBuyPrice(
normalizePrice(filledPrice.subtract(currentGridGap)));
if (nextBuyPrice != null) {
placeLimitBuyOrder(nextBuyPrice);
}
}
}
private void placeLimitBuyOrder(BigDecimal price) {
if (!pendingBuyOrders.isEmpty() || openPositions.size() >= gridCount) {
return;
}
BigDecimal normalizedPrice = normalizePrice(price);
if (normalizedPrice.compareTo(lowerBound) < 0 || normalizedPrice.compareTo(upperBound) > 0) {
return;
}
if (hasBuyOrderAt(normalizedPrice) || hasOpenPositionAtPrice(normalizedPrice)) {
return;
}
String clientOrderId = buildClientOrderId("BUY", normalizedPrice);
try {
JSONObject orderInfo = buildOrderRequest("buy", "open", "limit", normalizedPrice, baseQuantity);
orderInfo.put("clientOid", clientOrderId);
JSONObject result = bitGetClient.placeOrder(orderInfo);
String exchangeOrderId = result.getString("orderId");
GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOrderId, normalizedPrice);
gridBuyOrder.exchangeOrderId = exchangeOrderId;
pendingBuyOrders.put(priceKey(normalizedPrice), gridBuyOrder);
logger.info("挂买单成功:价格={},订单号={}", normalizedPrice, exchangeOrderId);
submitOrderSync(exchangeOrderId, clientOrderId, normalizedPrice, baseQuantity,
OrderSide.BUY, OrderStatus.NEW);
} catch (Exception e) {
logger.error("挂买单失败,价格={}", normalizedPrice, e);
submitAlarm("挂买单失败",
String.format("价格 %s:%s", formatPrice(normalizedPrice), e.getMessage()),
AlarmLevel.ERROR);
}
}
// ======================== 限价卖单止盈 ========================
/**
* 为缺少卖单的持仓补挂限价止盈单(启动恢复或卖单被撤时)。
*/
private void replenishMissingSellOrders() {
synchronized (tradeLock) {
for (GridPosition position : openPositions) {
if (!hasSellOrderFor(position.clientPositionId)) {
placeLimitSellOrder(position);
}
}
}
}
private void placeLimitSellOrder(GridPosition position) {
if (hasSellOrderFor(position.clientPositionId)) {
return;
}
BigDecimal sellPrice = position.takeProfitPrice;
if (sellPrice.compareTo(upperBound) > 0) {
logger.warn("止盈价 {} 超过上限 {},跳过挂卖单", sellPrice, upperBound);
return;
}
String clientOrderId = buildClientOrderId("SELL", position.entryPrice);
try {
JSONObject orderInfo = buildOrderRequest("buy", "close", "limit", sellPrice,
position.quantity);
orderInfo.put("clientOid", clientOrderId);
JSONObject result = bitGetClient.placeOrder(orderInfo);
String exchangeOrderId = result.getString("orderId");
GridSellOrder sellOrder = new GridSellOrder(clientOrderId, position.clientPositionId,
sellPrice, position.quantity);
sellOrder.exchangeOrderId = exchangeOrderId;
pendingSellOrders.put(position.clientPositionId, sellOrder);
logger.info("挂卖单成功:入场={},止盈价={},数量={},订单号={}",
position.entryPrice, sellPrice, position.quantity, exchangeOrderId);
submitOrderSync(exchangeOrderId, clientOrderId, sellPrice, position.quantity,
OrderSide.SELL, OrderStatus.NEW);
} catch (Exception e) {
logger.error("挂卖单失败,入场价={},止盈价={}", position.entryPrice, sellPrice, e);
submitAlarm("挂卖单失败",
String.format("入场 %s,止盈 %s:%s",
formatPrice(position.entryPrice), formatPrice(sellPrice), e.getMessage()),
AlarmLevel.ERROR);
}
}
private boolean hasSellOrderFor(String clientPositionId) {
return pendingSellOrders.containsKey(clientPositionId);
}
// ======================== WebSocket 事件处理 ========================
private void handleTicker(JSONObject ticker) {
String symbol = resolveSymbol(ticker);
if (!getPlainSymbol().equals(symbol)) {
return;
}
// BitGet ticker:优先 lastPr,兼容 last
BigDecimal lastPrice = firstPositive(
ticker.getBigDecimal("lastPr"),
ticker.getBigDecimal("last"),
ticker.getBigDecimal("markPrice"));
if (lastPrice != null) {
currentPrice = lastPrice;
}
}
private void handleFill(JSONObject fill) {
String symbol = resolveSymbol(fill);
if (!getPlainSymbol().equals(symbol)) {
return;
}
String side = fill.getString("side");
String tradeSide = fill.getString("tradeSide");
BigDecimal price = fill.getBigDecimal("price");
BigDecimal volume = fill.getBigDecimal("baseVolume");
String orderId = fill.getString("orderId");
String clientOid = fill.getString("clientOid");
if (isOpenBuy(side, tradeSide)) {
onBuyFilled(price, volume, orderId, clientOid, price);
} else if (isCloseBuy(side, tradeSide)) {
onSellFilled(price, volume, orderId, clientOid);
}
}
/**
* BitGet orders 推送用 instId,fill 推送用 symbol。
* 成交以 status=filled 为可靠信号(fill 频道可能收不到)。
*/
private void handleOrderUpdate(JSONObject order) {
String symbol = resolveSymbol(order);
if (!getPlainSymbol().equals(symbol)) {
return;
}
String status = order.getString("status");
String side = order.getString("side");
String tradeSide = order.getString("tradeSide");
String orderId = order.getString("orderId");
String clientOid = order.getString("clientOid");
BigDecimal orderPrice = order.getBigDecimal("price");
if ("cancelled".equals(status) || "canceled".equals(status)) {
if (isOpenBuy(side, tradeSide)) {
removePendingBuy(orderId, clientOid, orderPrice);
} else if (isCloseBuy(side, tradeSide)) {
removePendingSell(orderId, clientOid, orderPrice);
}
return;
}
if (!"filled".equals(status)) {
return;
}
BigDecimal fillPrice = firstPositive(
order.getBigDecimal("priceAvg"),
order.getBigDecimal("fillPrice"),
orderPrice);
BigDecimal volume = firstPositive(
order.getBigDecimal("accBaseVolume"),
order.getBigDecimal("baseVolume"),
order.getBigDecimal("size"));
if (isOpenBuy(side, tradeSide)) {
onBuyFilled(fillPrice, volume, orderId, clientOid, orderPrice);
} else if (isCloseBuy(side, tradeSide)) {
onSellFilled(fillPrice, volume, orderId, clientOid);
}
}
private void onBuyFilled(BigDecimal fillPrice, BigDecimal volume, String orderId,
String clientOid, BigDecimal orderPrice) {
if (orderId != null && !processedBuyFillOrderIds.add(orderId)) {
return;
}
if (volume == null || volume.compareTo(BigDecimal.ZERO) <= 0) {
if (orderId != null) {
processedBuyFillOrderIds.remove(orderId);
}
logger.warn("买单成交数量无效,orderId={},volume={}", orderId, volume);
return;
}
GridBuyOrder pending = removePendingBuy(orderId, clientOid, orderPrice);
if (pending == null) {
if (orderId != null) {
processedBuyFillOrderIds.remove(orderId);
}
logger.debug("忽略非本策略买单成交,orderId={},clientOid={}", orderId, clientOid);
return;
}
synchronized (tradeLock) {
BigDecimal gridPrice = pending.buyPrice;
if (hasOpenPositionAtPrice(gridPrice)) {
if (orderId != null) {
processedBuyFillOrderIds.remove(orderId);
}
logger.warn("该价位已有持仓,忽略重复买单成交,gridPrice={},orderId={}", gridPrice, orderId);
submitAlarm("重复开仓拦截",
String.format("价位 %s 已有持仓,忽略 orderId=%s", formatPrice(gridPrice), orderId),
AlarmLevel.WARNING);
return;
}
String clientPositionId = "POS_" + gridPrice.toPlainString();
if (currentGridGap == null) {
if (orderId != null) {
processedBuyFillOrderIds.remove(orderId);
}
logger.warn("网格间距未就绪,无法处理买单成交,orderId={}", orderId);
return;
}
BigDecimal tpPrice = normalizePrice(gridPrice.add(currentGridGap));
GridPosition position = new GridPosition(clientPositionId, gridPrice, volume, tpPrice);
openPositions.add(position);
logger.info("买单成交:网格价={},成交价={},数量={},止盈价={},orderId={}",
gridPrice, fillPrice, volume, tpPrice, orderId);
submitAlarm("网格买入成交",
String.format("买入价 %s,数量 %s,挂卖单止盈 %s(间距 %s)",
formatPrice(gridPrice), volume.toPlainString(),
formatPrice(tpPrice), formatPrice(currentGridGap)),
AlarmLevel.INFO);
submitOrderSync(orderId, clientPositionId, gridPrice, volume, OrderSide.BUY, OrderStatus.FILLED);
submitPositionSync(clientPositionId, gridPrice, volume, true);
placeLimitSellOrder(position);
placeNextBuyBelow(gridPrice);
}
}
private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId, String clientOid) {
if (orderId != null && !processedSellFillOrderIds.add(orderId)) {
return;
}
synchronized (tradeLock) {
GridSellOrder pendingSell = removePendingSell(orderId, clientOid, null);
GridPosition matched = null;
if (pendingSell != null) {
for (GridPosition position : openPositions) {
if (position.clientPositionId.equals(pendingSell.clientPositionId)) {
matched = position;
break;
}
}
}
if (matched == null && clientOid != null) {
BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid);
if (entryPrice != null) {
String posId = "POS_" + entryPrice.toPlainString();
for (GridPosition position : openPositions) {
if (position.clientPositionId.equals(posId)) {
matched = position;
break;
}
}
}
}
if (matched != null) {
openPositions.remove(matched);
pendingSellOrders.remove(matched.clientPositionId);
submitPositionSync(matched.clientPositionId, price, matched.quantity, false);
}
logger.info("卖单成交(止盈):价格={},数量={},orderId={}", price, volume, orderId);
submitAlarm("网格止盈成交",
String.format("卖出价 %s,数量 %s", formatPrice(price),
volume != null ? volume.toPlainString() : "-"),
AlarmLevel.INFO);
submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume,
OrderSide.SELL, OrderStatus.FILLED);
replenishBuyGrids();
}
}
private String resolveSymbol(JSONObject payload) {
String symbol = payload.getString("symbol");
if (symbol == null || symbol.isEmpty()) {
symbol = payload.getString("instId");
}
return symbol;
}
private boolean isOpenBuy(String side, String tradeSide) {
if (!"buy".equals(side)) {
return false;
}
return tradeSide == null
|| "open".equals(tradeSide)
|| "buy_single".equals(tradeSide);
}
private boolean isCloseBuy(String side, String tradeSide) {
return "buy".equals(side) && "close".equals(tradeSide);
}
private BigDecimal firstPositive(BigDecimal... values) {
if (values == null) {
return null;
}
for (BigDecimal value : values) {
if (value != null && value.compareTo(BigDecimal.ZERO) > 0) {
return value;
}
}
return null;
}
/**
* 优先按 exchangeOrderId / clientOid 移除,避免成交价与挂单价不一致导致残留。
*/
private GridBuyOrder removePendingBuy(String orderId, String clientOid, BigDecimal price) {
if (orderId != null) {
for (Iterator<Map.Entry<String, GridBuyOrder>> it = pendingBuyOrders.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<String, GridBuyOrder> entry = it.next();
GridBuyOrder order = entry.getValue();
if (orderId.equals(order.exchangeOrderId)) {
it.remove();
return order;
}
}
}
if (clientOid != null) {
for (Iterator<Map.Entry<String, GridBuyOrder>> it = pendingBuyOrders.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<String, GridBuyOrder> entry = it.next();
GridBuyOrder order = entry.getValue();
if (clientOid.equals(order.clientOrderId)) {
it.remove();
return order;
}
}
}
if (price != null) {
return pendingBuyOrders.remove(priceKey(normalizePrice(price)));
}
return null;
}
private GridSellOrder removePendingSell(String orderId, String clientOid, BigDecimal price) {
if (orderId != null) {
for (Iterator<Map.Entry<String, GridSellOrder>> it = pendingSellOrders.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<String, GridSellOrder> entry = it.next();
GridSellOrder order = entry.getValue();
if (orderId.equals(order.exchangeOrderId)) {
it.remove();
return order;
}
}
}
if (clientOid != null) {
for (Iterator<Map.Entry<String, GridSellOrder>> it = pendingSellOrders.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<String, GridSellOrder> entry = it.next();
GridSellOrder order = entry.getValue();
if (clientOid.equals(order.clientOrderId)) {
it.remove();
return order;
}
}
}
if (price != null) {
BigDecimal normalized = normalizePrice(price);
for (Iterator<Map.Entry<String, GridSellOrder>> it = pendingSellOrders.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<String, GridSellOrder> entry = it.next();
GridSellOrder order = entry.getValue();
if (normalized.compareTo(order.sellPrice) == 0) {
it.remove();
return order;
}
}
}
return null;
}
/**
* 从 clientOid(SELL_{symbol}_{entryPrice}_{ts})解析入场价。
*/
private BigDecimal parseEntryPriceFromClientOid(String clientOid) {
if (clientOid == null || !clientOid.startsWith("SELL_")) {
return null;
}
String plainSymbol = getPlainSymbol();
String prefix = "SELL_" + plainSymbol + "_";
if (!clientOid.startsWith(prefix)) {
return null;
}
String remainder = clientOid.substring(prefix.length());
int lastUnderscore = remainder.lastIndexOf('_');
if (lastUnderscore <= 0) {
return null;
}
try {
return normalizePrice(new BigDecimal(remainder.substring(0, lastUnderscore)));
} catch (NumberFormatException e) {
return null;
}
}
// ======================== 交易所同步 ========================
private void syncOpenOrdersFromExchange() {
try {
JSONArray openOrders = bitGetClient.getOpenOrders(getSymbol());
if (openOrders == null) {
return;
}
int buyCount = 0;
int sellCount = 0;
for (int i = 0; i < openOrders.size(); i++) {
JSONObject order = openOrders.getJSONObject(i);
String side = order.getString("side");
String tradeSide = order.getString("tradeSide");
BigDecimal price = normalizePrice(order.getBigDecimal("price"));
String clientOid = order.getString("clientOid");
String exchangeOrderId = order.getString("orderId");
BigDecimal quantity = firstPositive(
order.getBigDecimal("size"),
order.getBigDecimal("baseVolume"));
if ("buy".equals(side) && "open".equals(tradeSide)) {
if (clientOid == null) {
clientOid = "SYNC_BUY_" + price.toPlainString();
}
GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price);
gridBuyOrder.exchangeOrderId = exchangeOrderId;
pendingBuyOrders.put(priceKey(price), gridBuyOrder);
buyCount++;
} else if ("buy".equals(side) && "close".equals(tradeSide)) {
if (clientOid == null) {
clientOid = "SYNC_SELL_" + price.toPlainString();
}
BigDecimal entryPrice = parseEntryPriceFromClientOid(clientOid);
String clientPositionId = entryPrice != null
? "POS_" + entryPrice.toPlainString()
: "SYNC_POS_" + price.toPlainString();
if (quantity == null) {
quantity = baseQuantity;
}
GridSellOrder sellOrder = new GridSellOrder(clientOid, clientPositionId, price, quantity);
sellOrder.exchangeOrderId = exchangeOrderId;
pendingSellOrders.put(clientPositionId, sellOrder);
sellCount++;
if (entryPrice != null && !hasOpenPosition(clientPositionId)) {
openPositions.add(new GridPosition(clientPositionId, entryPrice, quantity, price));
}
}
}
logger.info("同步挂单:买单 {} 笔,卖单 {} 笔", buyCount, sellCount);
} catch (Exception e) {
logger.warn("同步挂单失败:{}", e.getMessage());
}
}
private boolean hasOpenPosition(String clientPositionId) {
for (GridPosition position : openPositions) {
if (position.clientPositionId.equals(clientPositionId)) {
return true;
}
}
return false;
}
/**
* 若交易所上残留多笔买单,只保留最靠近现价(价格最高)的一笔,撤销其余。
*/
private void pruneExcessPendingBuys() {
if (pendingBuyOrders.size() <= 1) {
return;
}
synchronized (tradeLock) {
GridBuyOrder keep = pendingBuyOrders.values().stream()
.max(Comparator.comparing(o -> o.buyPrice))
.orElse(null);
if (keep == null) {
return;
}
List<GridBuyOrder> extras = new ArrayList<>();
for (GridBuyOrder order : pendingBuyOrders.values()) {
if (order.buyPrice.compareTo(keep.buyPrice) != 0) {
extras.add(order);
}
}
for (GridBuyOrder order : extras) {
try {
bitGetClient.cancelOrder(getSymbol(), order.exchangeOrderId, order.clientOrderId, null);
pendingBuyOrders.remove(priceKey(order.buyPrice));
logger.info("撤销多余买单:价格={},订单号={}", order.buyPrice, order.exchangeOrderId);
} catch (Exception e) {
logger.error("撤销多余买单失败,价格={}", order.buyPrice, e);
}
}
if (!extras.isEmpty()) {
submitAlarm("清理多余挂单",
String.format("仅保留最靠近现价的买单 %s,已撤销 %d 笔",
formatPrice(keep.buyPrice), extras.size()),
AlarmLevel.INFO);
}
}
}
private void fetchCurrentPriceFromRest() {
try {
JSONObject ticker = bitGetClient.getTicker(getSymbol());
if (ticker != null) {
BigDecimal last = ticker.getBigDecimal("lastPr");
if (last == null) {
last = ticker.getBigDecimal("last");
}
currentPrice = last;
}
} catch (Exception e) {
logger.warn("获取 ticker 失败:{}", e.getMessage());
}
}
private void syncAccount() {
try {
JSONArray balance = bitGetClient.getBalance();
for (int i = 0; i < balance.size(); i++) {
JSONObject item = balance.getJSONObject(i);
if ("USDT".equals(item.getString("marginCoin"))) {
updateAccountBalance(
item.getBigDecimal("accountEquity"),
item.getBigDecimal("available"),
item.getBigDecimal("locked"));
}
}
} catch (Exception e) {
logger.error("同步账户失败", e);
}
}
// ======================== 平台数据同步 ========================
private void submitOrderSync(String orderId, String clientOrderId, BigDecimal price,
BigDecimal qty, OrderSide side, OrderStatus status) {
if (robotId == null) {
return;
}
JSONObject orderInfo = new JSONObject();
orderInfo.put("robotId", robotId);
orderInfo.put("clientOrderId", clientOrderId);
orderInfo.put("orderId", orderId);
orderInfo.put("price", price);
orderInfo.put("origQty", qty);
orderInfo.put("symbol", getSymbol());
orderInfo.put("leverage", leverage);
orderInfo.put("side", side);
orderInfo.put("positionSide", PositionSide.LONG);
orderInfo.put("orderStatus", status);
submitOrder(orderInfo);
}
private void submitPositionSync(String clientPositionId, BigDecimal price,
BigDecimal qty, boolean open) {
if (robotId == null) {
return;
}
JSONObject positionInfo = new JSONObject();
positionInfo.put("robotId", robotId);
positionInfo.put("clientPositionId", clientPositionId);
positionInfo.put("symbol", getSymbol());
positionInfo.put("leverage", leverage);
positionInfo.put("positionSide", PositionSide.LONG);
positionInfo.put("marginType", MarginType.CROSSED);
positionInfo.put("openAvgPrice", price);
positionInfo.put("qty", qty);
positionInfo.put("qtyUnit", QtyUnitType.COIN);
positionInfo.put("positionStatus", open ? PositionStatus.POSITION : PositionStatus.CLOSED);
submitPosition(positionInfo);
}
// ======================== 仪表盘 ========================
private void updateDashboard() {
if (statusChart == null) {
return;
}
StringBuilder sb = new StringBuilder();
sb.append("<div>时间:").append(LocalDateTime.now()).append("</div>");
sb.append("<div>当前价:").append(formatPrice(currentPrice)).append("</div>");
sb.append("<div>ATR:").append(formatPrice(currentAtr)).append("</div>");
sb.append("<div>网格间距:").append(formatPrice(currentGridGap)).append("</div>");
sb.append("<div>待成交买单:").append(pendingBuyOrders.size()).append(" / 1 笔</div>");
sb.append("<div>待成交卖单:").append(pendingSellOrders.size()).append(" 笔</div>");
sb.append("<div>持仓待止盈:").append(openPositions.size())
.append(" / ").append(gridCount).append(" 层</div>");
sb.append("<div>上限暂停:").append(upperBoundPaused ? "是" : "否").append("</div>");
sb.append("<div>下限暂停:").append(lowerBoundPaused ? "是" : "否").append("</div>");
JSONObject style = new JSONObject();
style.put("fontSize", "14px");
updateText(statusChart, sb.toString(), style);
updateGridTable();
}
private void updateGridTable() {
if (gridTableChart == null || currentGridGap == null) {
return;
}
JSONArray rows = new JSONArray();
List<GridBuyOrder> pending = new ArrayList<>(pendingBuyOrders.values());
pending.sort(Comparator.comparing(o -> o.buyPrice, Comparator.reverseOrder()));
for (GridBuyOrder order : pending) {
JSONObject row = new JSONObject();
row.put("type", "待成交买单");
row.put("price", formatPrice(order.buyPrice));
row.put("quantity", baseQuantity.toPlainString());
row.put("takeProfit", "-");
rows.add(row);
}
List<GridSellOrder> pendingSells = new ArrayList<>(pendingSellOrders.values());
pendingSells.sort(Comparator.comparing(o -> o.sellPrice, Comparator.reverseOrder()));
for (GridSellOrder order : pendingSells) {
JSONObject row = new JSONObject();
row.put("type", "待成交卖单");
row.put("price", formatPrice(order.sellPrice));
row.put("quantity", order.quantity.toPlainString());
row.put("takeProfit", formatPrice(order.sellPrice));
rows.add(row);
}
List<GridPosition> positions = new ArrayList<>(openPositions);
positions.sort(Comparator.comparing(p -> p.entryPrice, Comparator.reverseOrder()));
for (GridPosition position : positions) {
JSONObject row = new JSONObject();
row.put("type", "持仓");
row.put("price", formatPrice(position.entryPrice));
row.put("quantity", position.quantity.toPlainString());
row.put("takeProfit", formatPrice(position.takeProfitPrice));
rows.add(row);
}
JSONObject option = new JSONObject();
option.put("height", 400);
JSONArray columns = new JSONArray();
columns.add(buildColumn("类型", "type", 120));
columns.add(buildColumn("价格", "price", 150));
columns.add(buildColumn("数量", "quantity", 150));
columns.add(buildColumn("止盈价", "takeProfit", 150));
option.put("column", columns);
ApiResult result = updateTable(gridTableChart, rows, option);
if (result.getCode() != 200) {
logger.debug("更新表格失败:{}", result.getMsg());
}
}
private JSONObject buildColumn(String label, String prop, int width) {
JSONObject col = new JSONObject();
col.put("label", label);
col.put("prop", prop);
col.put("width", width);
return col;
}
// ======================== 工具方法 ========================
private JSONObject buildOrderRequest(String side, String tradeSide, String orderType,
BigDecimal price, BigDecimal size) {
JSONObject orderInfo = new JSONObject();
orderInfo.put("symbol", getPlainSymbol());
orderInfo.put("marginMode", MARGIN_MODE);
orderInfo.put("size", normalizeQuantity(size).toPlainString());
orderInfo.put("side", side);
orderInfo.put("tradeSide", tradeSide);
orderInfo.put("orderType", orderType);
if (price != null) {
orderInfo.put("price", normalizePrice(price).toPlainString());
}
return orderInfo;
}
private boolean hasBuyOrderAt(BigDecimal price) {
return pendingBuyOrders.containsKey(priceKey(normalizePrice(price)));
}
private boolean hasOpenPositionAtPrice(BigDecimal price) {
if (price == null) {
return false;
}
String key = priceKey(price);
for (GridPosition position : openPositions) {
if (priceKey(position.entryPrice).equals(key)) {
return true;
}
}
return false;
}
/**
* 从 startPrice 起向下逐格查找:无持仓且无待成交买单的价位。
*/
private BigDecimal findNextAvailableBuyPrice(BigDecimal startPrice) {
if (startPrice == null || currentGridGap == null) {
return null;
}
BigDecimal candidate = normalizePrice(startPrice);
while (candidate.compareTo(lowerBound) >= 0) {
if (!hasOpenPositionAtPrice(candidate) && !hasBuyOrderAt(candidate)) {
return candidate;
}
candidate = normalizePrice(candidate.subtract(currentGridGap));
}
return null;
}
private BigDecimal alignPriceDown(BigDecimal price, BigDecimal gap) {
if (gap == null || gap.compareTo(BigDecimal.ZERO) <= 0) {
return normalizePrice(price);
}
BigDecimal steps = price.divide(gap, 0, RoundingMode.DOWN);
return normalizePrice(steps.multiply(gap));
}
private BigDecimal normalizePrice(BigDecimal price) {
return price.setScale(pricePrecision, RoundingMode.HALF_UP);
}
private BigDecimal normalizeQuantity(BigDecimal qty) {
return qty.setScale(quantityPrecision, RoundingMode.DOWN);
}
private String priceKey(BigDecimal price) {
return normalizePrice(price).toPlainString();
}
private String formatPrice(BigDecimal price) {
if (price == null) {
return "-";
}
return normalizePrice(price).toPlainString();
}
private String buildClientOrderId(String prefix, BigDecimal price) {
return prefix + "_" + getPlainSymbol() + "_" + price.toPlainString() + "_" + System.currentTimeMillis();
}
}