896 lines
34 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.util.AtrCalculator;
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.ExchangeType;
import vip.uuquant.exhangeapi.enums.MarginType;
import vip.uuquant.exhangeapi.enums.OrderSide;
import vip.uuquant.exhangeapi.enums.OrderStatus;
import vip.uuquant.exhangeapi.enums.PositionSide;
import vip.uuquant.exhangeapi.enums.PositionStatus;
import vip.uuquant.exhangeapi.enums.QtyUnitType;
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.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 基于 ATR 的动态网格策略(只做多)。
* <p>
* 网格间距 = atr_multiplier × ATR,并限制在 [min_gap, max_gap] 区间。
* 采用限价挂单买入,市价止盈卖出。
* 网格间距变化时不撤销已有挂单,已成交仓位按最新间距计算止盈价。
*/
public class AutoGridStrategy extends BaseStrategy {
private static final String MARGIN_MODE = "crossed";
private final Logger logger = LoggerFactory.getLogger(AutoGridStrategy.class);
// 运行参数
private BigDecimal baseQuantity;
private BigDecimal atrMultiplier;
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;
/** 最大待成交买单数量 */
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<>();
/** 已成交待止盈的多头仓位 */
private final List<GridPosition> openPositions = new CopyOnWriteArrayList<>();
/** 正在平仓中的仓位,防止重复触发 */
private final Map<String, Boolean> closingPositions = new ConcurrentHashMap<>();
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 GridPosition {
private final String clientPositionId;
private final BigDecimal entryPrice;
private final BigDecimal quantity;
GridPosition(String clientPositionId, BigDecimal entryPrice, BigDecimal quantity) {
this.clientPositionId = clientPositionId;
this.entryPrice = entryPrice;
this.quantity = quantity;
}
BigDecimal takeProfitPrice(BigDecimal gridGap) {
return entryPrice.add(gridGap);
}
}
// ======================== 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 {
bitGetClient.login();
bitGetClient.subscribeFill();
bitGetClient.subscribeOrders();
}
}
@Override
public void onMessage(String connectionId, String message) {
if ("pong".equals(message)) {
return;
}
try {
JSONObject json = JSONObject.parseObject(message);
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))) {
handleFill(data.getJSONObject(0));
} else if ("orders".equals(channel)
&& ("snapshot".equals(action) || "update".equals(action))) {
handleOrderUpdate(data.getJSONObject(0));
}
} 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", "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");
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();
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);
processTakeProfit();
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(),
null, 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;
}
BigDecimal 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 BigDecimal fetchAtr() throws Exception {
JSONObject params = new JSONObject();
params.put("symbol", getPlainSymbol());
params.put("granularity", normalizeKlineInterval(klineInterval));
params.put("limit", String.valueOf(klinePeriod + 2));
JSONArray klines = bitGetClient.getKLines(params);
if (klines == null || klines.isEmpty()) {
return null;
}
// BitGet 返回倒序,转为升序
JSONArray sorted = new JSONArray();
for (int i = klines.size() - 1; i >= 0; i--) {
sorted.add(klines.getJSONArray(i));
}
return AtrCalculator.calculate(sorted, klinePeriod);
}
private String normalizeKlineInterval(String interval) {
if (interval == null) {
return "5m";
}
String normalized = interval.trim();
if (normalized.endsWith("m") || normalized.endsWith("H") || normalized.endsWith("D")) {
return normalized;
}
return normalized + "m";
}
// ======================== 价格边界 ========================
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();
}
private void replenishBuyGrids() {
if (!canPlaceBuyOrders() || currentGridGap == null) {
return;
}
synchronized (tradeLock) {
if (pendingBuyOrders.size() >= gridCount) {
return;
}
BigDecimal lowestPending = findLowestPendingBuyPrice();
BigDecimal nextBuyPrice;
if (lowestPending != null) {
nextBuyPrice = lowestPending.subtract(currentGridGap);
} else if (currentPrice != null) {
nextBuyPrice = alignPriceDown(currentPrice, currentGridGap);
} else {
return;
}
if (nextBuyPrice.compareTo(lowerBound) >= 0 && !hasBuyOrderAt(nextBuyPrice)) {
placeLimitBuyOrder(nextBuyPrice);
}
}
}
private void placeLimitBuyOrder(BigDecimal price) {
if (pendingBuyOrders.size() >= gridCount) {
return;
}
BigDecimal normalizedPrice = normalizePrice(price);
if (normalizedPrice.compareTo(lowerBound) < 0 || normalizedPrice.compareTo(upperBound) > 0) {
return;
}
if (hasBuyOrderAt(normalizedPrice)) {
return;
}
String clientOrderId = buildClientOrderId("BUY", normalizedPrice);
try {
JSONObject orderInfo = buildOrderRequest("buy", "open", "limit", normalizedPrice, baseQuantity, false);
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 processTakeProfit() {
if (currentGridGap == null || currentPrice == null) {
return;
}
for (GridPosition position : openPositions) {
BigDecimal tpPrice = position.takeProfitPrice(currentGridGap);
if (currentPrice.compareTo(tpPrice) < 0) {
continue;
}
if (closingPositions.putIfAbsent(position.clientPositionId, Boolean.TRUE) != null) {
continue;
}
marketClosePosition(position, tpPrice);
}
}
private void marketClosePosition(GridPosition position, BigDecimal tpPrice) {
synchronized (tradeLock) {
try {
JSONObject orderInfo = buildOrderRequest("sell", "close", "market", null,
position.quantity, true);
orderInfo.put("clientOid", buildClientOrderId("SELL", position.entryPrice));
JSONObject result = bitGetClient.placeOrder(orderInfo);
String exchangeOrderId = result.getString("orderId");
logger.info("市价止盈:入场={},止盈价={},当前价={},订单号={}",
position.entryPrice, tpPrice, currentPrice, exchangeOrderId);
submitAlarm("网格止盈",
String.format("入场 %s,止盈目标 %s,当前价 %s,市价平仓已提交",
formatPrice(position.entryPrice), formatPrice(tpPrice), formatPrice(currentPrice)),
AlarmLevel.INFO);
submitOrderSync(exchangeOrderId, position.clientPositionId, currentPrice, position.quantity,
OrderSide.SELL, OrderStatus.NEW);
} catch (Exception e) {
closingPositions.remove(position.clientPositionId);
logger.error("市价止盈失败,入场价={}", position.entryPrice, e);
submitAlarm("止盈失败",
String.format("入场 %s:%s", formatPrice(position.entryPrice), e.getMessage()),
AlarmLevel.ERROR);
}
}
}
// ======================== WebSocket 事件处理 ========================
private void handleTicker(JSONObject ticker) {
String symbol = ticker.getString("symbol");
if (!getPlainSymbol().equals(symbol)) {
return;
}
BigDecimal lastPrice = ticker.getBigDecimal("lastPr");
if (lastPrice == null) {
lastPrice = ticker.getBigDecimal("last");
}
if (lastPrice != null) {
currentPrice = lastPrice;
}
}
private void handleFill(JSONObject fill) {
String symbol = fill.getString("symbol");
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");
if ("buy".equals(side) && "open".equals(tradeSide)) {
onBuyFilled(price, volume, orderId);
} else if ("sell".equals(side) && "close".equals(tradeSide)) {
onSellFilled(price, volume, orderId);
}
}
private void handleOrderUpdate(JSONObject order) {
String symbol = order.getString("symbol");
if (!getPlainSymbol().equals(symbol)) {
return;
}
String status = order.getString("status");
if (!"cancelled".equals(status) && !"canceled".equals(status)) {
return;
}
String side = order.getString("side");
if (!"buy".equals(side)) {
return;
}
BigDecimal price = order.getBigDecimal("price");
if (price != null) {
pendingBuyOrders.remove(priceKey(normalizePrice(price)));
}
}
private void onBuyFilled(BigDecimal price, BigDecimal volume, String orderId) {
BigDecimal normalizedPrice = normalizePrice(price);
pendingBuyOrders.remove(priceKey(normalizedPrice));
String clientPositionId = "POS_" + normalizedPrice.toPlainString();
GridPosition position = new GridPosition(clientPositionId, normalizedPrice, volume);
openPositions.add(position);
BigDecimal tpPrice = position.takeProfitPrice(currentGridGap);
logger.info("买单成交:价格={},数量={},止盈目标={}", normalizedPrice, volume, tpPrice);
submitAlarm("网格买入成交",
String.format("买入价 %s,数量 %s,止盈目标 %s(间距 %s)",
formatPrice(normalizedPrice), volume.toPlainString(),
formatPrice(tpPrice), formatPrice(currentGridGap)),
AlarmLevel.INFO);
submitOrderSync(orderId, clientPositionId, normalizedPrice, volume, OrderSide.BUY, OrderStatus.FILLED);
submitPositionSync(clientPositionId, normalizedPrice, volume, true);
}
private void onSellFilled(BigDecimal price, BigDecimal volume, String orderId) {
GridPosition matched = null;
for (String closingId : closingPositions.keySet()) {
for (GridPosition position : openPositions) {
if (position.clientPositionId.equals(closingId)) {
matched = position;
break;
}
}
if (matched != null) {
break;
}
}
if (matched == null) {
for (GridPosition position : openPositions) {
if (position.quantity.compareTo(volume) == 0) {
matched = position;
break;
}
}
}
if (matched == null && !openPositions.isEmpty()) {
matched = openPositions.get(0);
}
if (matched != null) {
openPositions.remove(matched);
closingPositions.remove(matched.clientPositionId);
submitPositionSync(matched.clientPositionId, price, matched.quantity, false);
}
logger.info("卖单成交(止盈):价格={},数量={}", price, volume);
submitAlarm("网格止盈成交",
String.format("卖出价 %s,数量 %s", formatPrice(price), volume.toPlainString()),
AlarmLevel.INFO);
submitOrderSync(orderId, matched != null ? matched.clientPositionId : "CLOSE", price, volume,
OrderSide.SELL, OrderStatus.FILLED);
// 成交后补充下一格买单
replenishBuyGrids();
}
// ======================== 交易所同步 ========================
private void syncOpenOrdersFromExchange() {
try {
JSONArray openOrders = bitGetClient.getOpenOrders(getSymbol());
if (openOrders == null) {
return;
}
for (int i = 0; i < openOrders.size(); i++) {
JSONObject order = openOrders.getJSONObject(i);
if (!"buy".equals(order.getString("side"))) {
continue;
}
if (!"open".equals(order.getString("tradeSide"))) {
continue;
}
BigDecimal price = normalizePrice(order.getBigDecimal("price"));
String clientOid = order.getString("clientOid");
if (clientOid == null) {
clientOid = "SYNC_" + price.toPlainString();
}
GridBuyOrder gridBuyOrder = new GridBuyOrder(clientOid, price);
gridBuyOrder.exchangeOrderId = order.getString("orderId");
pendingBuyOrders.put(priceKey(price), gridBuyOrder);
}
logger.info("同步挂单 {} 笔", pendingBuyOrders.size());
} catch (Exception e) {
logger.warn("同步挂单失败:{}", e.getMessage());
}
}
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(" / ").append(gridCount).append(" 笔</div>");
sb.append("<div>持仓待止盈:").append(openPositions.size()).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<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(currentGridGap)));
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, boolean reduceOnly) {
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());
}
if (reduceOnly) {
orderInfo.put("reduceOnly", "YES");
}
return orderInfo;
}
private BigDecimal findLowestPendingBuyPrice() {
return pendingBuyOrders.values().stream()
.map(o -> o.buyPrice)
.min(BigDecimal::compareTo)
.orElse(null);
}
private boolean hasBuyOrderAt(BigDecimal price) {
return pendingBuyOrders.containsKey(priceKey(normalizePrice(price)));
}
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();
}
}