From a9b3bbb0956f56dd34a418d2080579cb9e6f8015 Mon Sep 17 00:00:00 2001 From: dongzp_book <90fanhua@gmail.com> Date: Sun, 5 Jul 2026 23:43:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E9=80=82=E5=BA=94=E7=BD=91=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sample/trend/config/CommonConstant.java | 24 + .../trend/strategy/AutoGridStrategy.java | 1167 ++++++++++++----- .../trend/strategy/util/AtrCalculator.java | 59 + 3 files changed, 941 insertions(+), 309 deletions(-) create mode 100644 src/main/java/com/sample/trend/config/CommonConstant.java create mode 100644 src/main/java/com/sample/trend/strategy/util/AtrCalculator.java diff --git a/src/main/java/com/sample/trend/config/CommonConstant.java b/src/main/java/com/sample/trend/config/CommonConstant.java new file mode 100644 index 0000000..a54adfc --- /dev/null +++ b/src/main/java/com/sample/trend/config/CommonConstant.java @@ -0,0 +1,24 @@ +package com.sample.trend.config; + +import vip.uuquant.exhangeapi.core.ExchangeFactory; +import vip.uuquant.exhangeapi.core.HttpClient; +import vip.uuquant.exhangeapi.core.WebSocketManager; + +/** + * 交易所客户端全局单例 + */ +public class CommonConstant { + + public static final HttpClient httpClient = HttpClient.builder().build(); + + public static final WebSocketManager webSocketManager = new WebSocketManager(httpClient); + + public static final ExchangeFactory exchangeFactory = new ExchangeFactory(); + + static { + webSocketManager.setLogEnabled(false); + } + + private CommonConstant() { + } +} diff --git a/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java b/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java index c9ab60a..c7e7961 100644 --- a/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java +++ b/src/main/java/com/sample/trend/strategy/AutoGridStrategy.java @@ -3,226 +3,275 @@ 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 okhttp3.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import vip.uuquant.exhangeapi.enums.*; +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.MessagePushType; import vip.uuquant.tradesystem.runner.enums.RobotChartType; import vip.uuquant.tradesystem.runner.strategy.BaseStrategy; -import vip.uuquant.tradesystem.runner.vo.*; +import vip.uuquant.tradesystem.runner.vo.AccountVO; +import vip.uuquant.tradesystem.runner.vo.RobotVO; +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; /** - * @author dongzp - * @desc 策略示例,展示所有API调用方法 - **/ + * 基于 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 Integer textChart; - private Integer tableChart; - private Integer barChart; + // 运行参数 + 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; - // 定时任务ID - private String taskId; + // 精度 + 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("策略初始化开始"); + logger.info("ATR 动态网格策略初始化"); - try { - // 1. 检查运行参数 - boolean check = checkParams("a17640829211617765"); - logger.info("参数检查结果:{}", check); + tryParams("base_quantity", "atr_multiplier", "kline_interval", "kline_period", + "grid_adjust_interval", "min_gap", "max_gap", "upper_bound", "lower_bound", + "grid_count", "testMode"); - // 2. 检查运行参数,不存在则抛出异常 - tryParams("a17640829211617765"); - - // 3. 获取运行参数 - JSONObject runParams = getRunParams(); - logger.info("运行参数:{}", runParams); - - // 4. 获取交易对和币符号 - String symbol = getSymbol(); - String instrument = getInstrument(); - logger.info("交易对:{},币符号:{}", symbol, instrument); - - // 5. 获取账户信息 - AccountVO account = getAccount(); - if (account != null) { - logger.info("账户信息:{}", account.getName()); - } - - // 6. 获取策略信息 - StrategyVO strategy = getStrategy(); - if (strategy != null) { - logger.info("策略信息获取成功"); - } - - // 7. 获取托管服务器信息 - AgentVO agent = getAgent(); - if (agent != null) { - logger.info("Agent信息获取成功"); - } - - SymbolVO symbolVO = getSymbolInfo(); - if (symbolVO != null) { - logger.info("获取交易对信息成功"); - } - - // 8. 创建文字图表 - textChart = createChart(RobotChartType.TEXT, "显示时间", 1, 1, 24); - logger.info("文字图表创建成功,dataId:{}", textChart); - - // 9. 创建表格图表 - tableChart = createChart(RobotChartType.TABLE, "数据表格", 2, 2, 12); - logger.info("表格图表创建成功,dataId:{}", tableChart); - - // 10. 创建柱状图图表 - barChart = createChart(RobotChartType.CHART, "统计图表", 3, 3, 12); - logger.info("柱状图图表创建成功,dataId:{}", barChart); - - // 11. 保存共享数据 - JSONObject sharedData = new JSONObject(); - sharedData.put("key1", "value1"); - sharedData.put("key2", 123); - AgentResult result = putSharedData("testKey", sharedData); - logger.info("保存共享数据结果:code={}, msg={}", result.getCode(), result.getMsg()); - - // 12. 创建定时任务(每600秒执行一次,延迟5秒开始) - taskId = createTask(this::syncAccount, 5, 600); - logger.info("定时任务创建成功,taskId:{}", taskId); - - // 13. 提交告警信息 - AgentResult alarmResult = submitAlarm("策略初始化", "策略初始化完成", AlarmLevel.INFO); - logger.info("提交告警结果:code={}, msg={}", alarmResult.getCode(), alarmResult.getMsg()); - - // 提交订单信息 - submitOrderExample(); - - // 提交持仓信息 - submitPositionExample(); - - logger.info("策略初始化完成"); - } catch (Exception e) { - logger.error("策略初始化异常", e); + 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("testMode"); + 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(); + } + } + + RobotVO robotInfo = getRobotInfo(); + if (robotInfo != null) { + robotId = robotInfo.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 { - logger.info("策略运行中,当前时间:{}", LocalDateTime.now()); - - // 1. 更新文字图表 - JSONObject textStyle = new JSONObject(); - textStyle.put("color", "red"); - textStyle.put("fontSize", "16px"); - textStyle.put("fontWeight", "bold"); - String currentTime = LocalDateTime.now().toString(); - ApiResult result = updateText(textChart, "当前时间:" + currentTime, textStyle); - logger.info("更新文字图表结果:code={}, msg={}", result.getCode(), result.getMsg()); - - // 2. 更新表格图表 - JSONArray tableData = new JSONArray(); - for (int i = 0; i < 3; i++) { - JSONObject row = new JSONObject(); - row.put("name", "用户" + (i + 1)); - row.put("sex", i % 2 == 0 ? "男" : "女"); - row.put("datetime", LocalDateTime.now().toString()); - row.put("address", "地址" + (i + 1)); - tableData.add(row); - } - - JSONObject tableOption = new JSONObject(); - tableOption.put("height", 300); - JSONArray columns = new JSONArray(); - - JSONObject col1 = new JSONObject(); - col1.put("label", "姓名"); - col1.put("prop", "name"); - col1.put("width", 200); - col1.put("fixed", true); - columns.add(col1); - - JSONObject col2 = new JSONObject(); - col2.put("label", "性别"); - col2.put("prop", "sex"); - col2.put("width", 300); - columns.add(col2); - - JSONObject col3 = new JSONObject(); - col3.put("label", "日期"); - col3.put("prop", "datetime"); - col3.put("width", 300); - columns.add(col3); - - JSONObject col4 = new JSONObject(); - col4.put("label", "地址"); - col4.put("prop", "address"); - col4.put("width", 300); - columns.add(col4); - - tableOption.put("column", columns); - ApiResult apiResult = updateTable(tableChart, tableData, tableOption); - logger.info("更新表格图表结果:code={}, msg={}", apiResult.getCode(), apiResult.getMsg()); - - // 3. 更新柱状图图表 - JSONObject chartOptions = new JSONObject(); - JSONObject xAxis = new JSONObject(); - xAxis.put("type", "category"); - JSONArray xData = new JSONArray(); - xData.add("Mon"); - xData.add("Tue"); - xData.add("Wed"); - xData.add("Thu"); - xData.add("Fri"); - xData.add("Sat"); - xData.add("Sun"); - xAxis.put("data", xData); - chartOptions.put("xAxis", xAxis); - - JSONObject yAxis = new JSONObject(); - yAxis.put("type", "value"); - chartOptions.put("yAxis", yAxis); - - JSONArray series = new JSONArray(); - JSONObject seriesItem = new JSONObject(); - JSONArray seriesData = new JSONArray(); - seriesData.add(120); - seriesData.add(200); - seriesData.add(150); - seriesData.add(80); - seriesData.add(70); - seriesData.add(110); - seriesData.add(130); - seriesItem.put("data", seriesData); - seriesItem.put("type", "bar"); - series.add(seriesItem); - chartOptions.put("series", series); - - ApiResult apiResult1 = updateEChart(barChart, chartOptions); - logger.info("更新柱状图图表结果:code={}, msg={}", apiResult1.getCode(), apiResult1.getMsg()); - - // 4. 更新账户余额(示例) - updateAccountBalance(new BigDecimal("10000.00"), new BigDecimal("8000.00"), new BigDecimal("2000.00")); - - // 5. 发送通知到平台 - // AgentResult notifyResult = sendNotify(200, "策略运行正常", "当前时间:" + LocalDateTime.now()); - // logger.info("发送通知结果:code={}", notifyResult.getCode()); - - // 6. 获取共享数据 - JSONObject sharedData = getSharedData("testKey"); - if (sharedData != null) { - logger.info("获取共享数据:{}", sharedData); - } - + checkPriceBounds(); + refreshGridGap(false); + processTakeProfit(); + replenishBuyGrids(); + updateDashboard(); } catch (Exception e) { logger.error("策略运行异常", e); } @@ -230,142 +279,642 @@ public class AutoGridStrategy extends BaseStrategy { @Override public void notify(JSONObject data) { - logger.info("策略收到通知:{}", data); - - try { - // 1. 提交告警信息 - String title = "收到通知"; - String content = "通知内容:" + data.toJSONString(); - AgentResult alarmResult = submitAlarm(title, content, AlarmLevel.INFO); - logger.info("提交告警结果:code={}, msg={}", alarmResult.getCode(), alarmResult.getMsg()); - - // 2. 获取共享数据 - JSONObject sharedData = getSharedData("testKey"); - if (sharedData != null) { - logger.info("共享数据:{}", sharedData); - } - - // 3. 发送通知到平台 - AgentResult notifyResult = sendNotify(200, "已收到通知", data.toJSONString()); - logger.info("发送通知结果:code={}", notifyResult.getCode()); - - // 发送一个异常告警 - AgentResult alarmResult1 = submitAlarm("策略异常", "策略异常,请检查", AlarmLevel.ERROR); - logger.info("提交告警结果:code={}", alarmResult1.getCode()); - - } catch (Exception e) { - logger.error("处理通知异常", e); - } + logger.info("收到平台通知:{}", data); } @Override public void destroy() { - logger.info("策略销毁开始"); - + logger.info("策略销毁"); try { - // 1. 删除所有图表 removeChart(null); - - // 2. 删除共享数据 - AgentResult result = removeSharedData("testKey"); - logger.info("删除共享数据结果:code={}", result.getCode()); - - // 3. 删除定时任务 - if (taskId != null) { - removeTask(taskId); - logger.info("删除定时任务:{}", taskId); - } - - // 4. 删除所有定时任务 - // removeAllTask(); - - // 5. 提交告警信息 - AgentResult alarmResult = submitAlarm("策略销毁", "策略正在销毁", AlarmLevel.WARNING); - logger.info("提交告警结果:code={}", alarmResult.getCode()); - - // 发送邮件 - pushMessageExample(); - - logger.info("策略销毁完成"); + removeAllTask(); + CommonConstant.webSocketManager.closeAll(); + submitAlarm("策略销毁", "ATR 动态网格策略已停止", AlarmLevel.WARNING); } catch (Exception e) { logger.error("策略销毁异常", e); } } - /** - * 同步账户信息(定时任务示例) - */ - private void syncAccount() { - try { - logger.info("执行定时任务:同步账户信息"); - AccountVO account = getAccount(); - if (account != null) { - logger.info("账户名称:{}", account.getName()); - } + // ======================== 初始化 ======================== - // 更新账户余额示例 - updateAccountBalance(new BigDecimal("10000.00"), new BigDecimal("8000.00"), new BigDecimal("2000.00")); + 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.setLeverage(getSymbol(), leverage); } catch (Exception e) { - logger.error("同步账户信息异常", e); + logger.warn("设置杠杆失败:{}", e.getMessage()); + } + try { + bitGetClient.createPublicWebsocket(webSocketMessageHandler); + bitGetClient.createPrivateWebsocket(webSocketMessageHandler); + } catch (Exception e) { + throw new RuntimeException("BitGet WebSocket 连接失败", e); } } - /** - * 提交订单信息示例(可根据实际情况调用) - */ - private void submitOrderExample() { - JSONObject orderInfo = new JSONObject(); - orderInfo.put("robotId", 1993936305684348930L); - orderInfo.put("clientOrderId", "CLIENT_ORDER_" + System.currentTimeMillis()); - orderInfo.put("orderId", "EXCHANGE_ORDER_123"); - orderInfo.put("avgPrice", new BigDecimal("2000.50")); - orderInfo.put("cumQty", new BigDecimal("1.0")); - orderInfo.put("origQty", new BigDecimal("1.0")); - orderInfo.put("price", new BigDecimal("2000.00")); - orderInfo.put("symbol", "BTCUSDT"); - orderInfo.put("leverage", 10); - // 注意:以下字段需要根据实际的枚举类型设置 - orderInfo.put("side", OrderSide.BUY); - orderInfo.put("positionSide", PositionSide.LONG); - orderInfo.put("orderStatus", OrderStatus.FILLED); - - AgentResult result = submitOrder(orderInfo); - logger.info("提交订单结果:code={}, msg={}", result.getCode(), result.getMsg()); + private void initCharts() { + statusChart = createChart(RobotChartType.TEXT, "网格状态", 1, 1, 24); + gridTableChart = createChart(RobotChartType.TABLE, "网格明细", 2, 2, 24); } - /** - * 提交仓位信息示例(可根据实际情况调用) - */ - private void submitPositionExample() { - JSONObject positionInfo = new JSONObject(); - positionInfo.put("robotId", 1993936305684348930L); - positionInfo.put("clientPositionId", "POSITION_" + System.currentTimeMillis()); - positionInfo.put("profitValue", new BigDecimal("100.50")); - positionInfo.put("unrealizedProfitValue", new BigDecimal("50.25")); - positionInfo.put("symbol", "BTCUSDT"); - positionInfo.put("leverage", 10); - positionInfo.put("openAvgPrice", new BigDecimal("2000.00")); - positionInfo.put("qty", new BigDecimal("1.0")); - // 注意:以下字段需要根据实际的枚举类型设置 - positionInfo.put("positionSide", PositionSide.LONG); - positionInfo.put("marginType", MarginType.ISOLATED); - positionInfo.put("qtyUnit", QtyUnitType.COIN); - positionInfo.put("positionStatus", PositionStatus.POSITION); + // ======================== ATR 与网格间距 ======================== - AgentResult result = submitPosition(positionInfo); - logger.info("提交仓位结果:code={}, msg={}", result.getCode(), result.getMsg()); + 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 void pushMessageExample() { + private BigDecimal fetchAtr() throws Exception { JSONObject params = new JSONObject(); - params.put("email", "975303544@qq.com"); - ApiResult result = pushMessage(MessagePushType.EMAIL, "欢迎使用UU量化", - "您的订单已成交,成交均价:2000.87,请注意!", - params - ); - logger.info("消息推送结果:code={} msg={}", result.getCode(), result.getMsg()); + 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(); + } + if (currentPrice == null || currentGridGap == null) { + return; + } + + synchronized (tradeLock) { + int remainingSlots = gridCount - pendingBuyOrders.size(); + if (remainingSlots <= 0) { + return; + } + BigDecimal price = alignPriceDown(currentPrice, currentGridGap); + int placed = 0; + while (price.compareTo(lowerBound) >= 0 && placed < remainingSlots) { + if (!hasBuyOrderAt(price)) { + placeLimitBuyOrder(price); + placed++; + } + price = price.subtract(currentGridGap); + } + } + } + + 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(); } } diff --git a/src/main/java/com/sample/trend/strategy/util/AtrCalculator.java b/src/main/java/com/sample/trend/strategy/util/AtrCalculator.java new file mode 100644 index 0000000..25447f0 --- /dev/null +++ b/src/main/java/com/sample/trend/strategy/util/AtrCalculator.java @@ -0,0 +1,59 @@ +package com.sample.trend.strategy.util; + +import com.alibaba.fastjson2.JSONArray; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +/** + * ATR(Average True Range)计算工具 + */ +public final class AtrCalculator { + + private AtrCalculator() { + } + + /** + * 根据 K 线数据计算 ATR。 + * K 线格式:[timestamp, open, high, low, close, ...] + * + * @param klines K 线数组,按时间升序 + * @param period ATR 周期 + * @return ATR 值,数据不足时返回 null + */ + public static BigDecimal calculate(JSONArray klines, int period) { + if (klines == null || klines.size() < period + 1) { + return null; + } + + List trueRanges = new ArrayList<>(); + for (int i = 1; i < klines.size(); i++) { + JSONArray current = klines.getJSONArray(i); + JSONArray previous = klines.getJSONArray(i - 1); + + BigDecimal high = current.getBigDecimal(2); + BigDecimal low = current.getBigDecimal(3); + BigDecimal prevClose = previous.getBigDecimal(4); + + BigDecimal range1 = high.subtract(low); + BigDecimal range2 = high.subtract(prevClose).abs(); + BigDecimal range3 = low.subtract(prevClose).abs(); + + BigDecimal tr = range1.max(range2).max(range3); + trueRanges.add(tr); + } + + if (trueRanges.size() < period) { + return null; + } + + BigDecimal sum = BigDecimal.ZERO; + int start = trueRanges.size() - period; + for (int i = start; i < trueRanges.size(); i++) { + sum = sum.add(trueRanges.get(i)); + } + return sum.divide(BigDecimal.valueOf(period), 8, RoundingMode.HALF_UP); + } +}