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);
+ }
+}