1368 lines
54 KiB
Java
1368 lines
54 KiB
Java
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();
|
||
}
|
||
}
|