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 的动态网格策略(只做多)。
*
* 网格间距 = 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 pendingBuyOrders = new ConcurrentHashMap<>();
/** 已成交待止盈的多头仓位 */
private final List openPositions = new CopyOnWriteArrayList<>();
/** 正在平仓中的仓位,防止重复触发 */
private final Map 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("时间:").append(LocalDateTime.now()).append("
");
sb.append("当前价:").append(formatPrice(currentPrice)).append("
");
sb.append("ATR:").append(formatPrice(currentAtr)).append("
");
sb.append("网格间距:").append(formatPrice(currentGridGap)).append("
");
sb.append("待成交买单:").append(pendingBuyOrders.size())
.append(" / ").append(gridCount).append(" 笔
");
sb.append("持仓待止盈:").append(openPositions.size()).append(" 笔
");
sb.append("上限暂停:").append(upperBoundPaused ? "是" : "否").append("
");
sb.append("下限暂停:").append(lowerBoundPaused ? "是" : "否").append("
");
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 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 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();
}
}