jean vor 1 Monat
Ursprung
Commit
87f5d40868

+ 4 - 0
doge/contract-swap-api/src/main/java/com/bizzan/bitrade/controller/MarketController.java

@@ -254,6 +254,9 @@ public class MarketController {
         else if(resolution.endsWith("W") || resolution.endsWith("w")){
             period = resolution.substring(0,resolution.length()-1) + "week";
         }
+        else if(resolution.equalsIgnoreCase("1Y") || resolution.equalsIgnoreCase("Y")){
+            period = "1year";
+        }
         else if(resolution.endsWith("M") || resolution.endsWith("m")){
             period = resolution.substring(0,resolution.length()-1) + "mon";
         }
@@ -296,6 +299,7 @@ public class MarketController {
             case "1day":  return "1d";
             case "1week": return "1w";
             case "1mon":  return "1mon";
+            case "1year": return "1year";
             default:      return dogePeriod;
         }
     }

+ 1 - 1
doge/contract-swap-api/src/main/java/com/bizzan/bitrade/job/KLineSyncJob.java

@@ -25,7 +25,7 @@ public class KLineSyncJob {
     @Autowired
     private ContractMarketService contractMarketService;
 
-    public static String PERIOD[] ={ "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "1week" };
+    public static String PERIOD[] ={ "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "1week", "1year" };
 
     private List<CoinSyncItem> coinList = new ArrayList<CoinSyncItem>(); // 交易对列表
 

+ 28 - 15
doge/contract-swap-api/src/main/java/com/bizzan/bitrade/service/KLineService.java

@@ -24,7 +24,7 @@ public class KLineService {
     private ContractCoinMatchFactory matchFactory;
 	
 	//private Map<String, Integer> periodMap = new HashMap<String, Integer>();
-	public static String PERIOD[] = { "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "1week" };
+	public static String PERIOD[] = { "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "1week", "1year" };
     private Map<String, Map<String, KLine>> kLineMap = new HashMap<>();
     private Map<String, CoinThumb> thumbMap = new HashMap<>();
 	
@@ -72,7 +72,32 @@ public class KLineService {
 	    		KLine kLine = periodMap.get(period);
 	    		boolean save = false;
 	    		long newTime = 0;
-	    		if(!period.equals("1mon")) {
+	    		if (period.equals("1mon")) {
+	    			// 月线:按日历月份切换
+	    			Calendar calendar1 = Calendar.getInstance();
+	    			calendar1.setTimeInMillis(kLine.getTime()*1000);
+	    			Calendar calendar2 = Calendar.getInstance();
+	    			calendar2.setTimeInMillis(thumb.getTime()*1000);
+	    			if(calendar1.get(Calendar.MONTH) != calendar2.get(Calendar.MONTH)) {
+	    				save = true;
+	    				int months = calendar2.get(Calendar.MONTH) - calendar1.get(Calendar.MONTH) + (calendar2.get(Calendar.YEAR) - calendar1.get(Calendar.YEAR))*12;
+	    				calendar1.add(Calendar.MONTH, months);
+	    				newTime = calendar1.getTimeInMillis() / 1000;
+	    			}
+	    		} else if (period.equals("1year")) {
+	    			// 年线:按日历年份切换
+	    			Calendar calendar1 = Calendar.getInstance();
+	    			calendar1.setTimeInMillis(kLine.getTime()*1000);
+	    			Calendar calendar2 = Calendar.getInstance();
+	    			calendar2.setTimeInMillis(thumb.getTime()*1000);
+	    			if(calendar1.get(Calendar.YEAR) != calendar2.get(Calendar.YEAR)) {
+	    				save = true;
+	    				int years = calendar2.get(Calendar.YEAR) - calendar1.get(Calendar.YEAR);
+	    				calendar1.add(Calendar.YEAR, years);
+	    				newTime = calendar1.getTimeInMillis() / 1000;
+	    			}
+	    		} else {
+	    			// 固定秒数周期
 	    			int seconds = 1;
 		    		if(period.equals("1min")) {
 		    			seconds = 60;
@@ -91,24 +116,12 @@ public class KLineService {
 		    		} else if (period.equals("1week")) {
 		    			seconds = 7*24*3600;
 		    		}
-		    		
+
 		    		long count = (thumb.getTime() - kLine.getTime()) / seconds;
 		    		if(count>=1) {
 		    			save = true;
 		    			newTime = kLine.getTime() + count * seconds;
-		    			//contractMarketService.saveKLine(symbol, kLine);
 		    		}
-	    		} else {
-	    			Calendar calendar1 = Calendar.getInstance();
-	    			calendar1.setTimeInMillis(kLine.getTime()*1000);
-	    			Calendar calendar2 = Calendar.getInstance();
-	    			calendar2.setTimeInMillis(thumb.getTime()*1000);
-	    			if(calendar1.get(Calendar.MONTH) != calendar2.get(Calendar.MONTH)) {
-	    				save = true;
-	    				int months = calendar2.get(Calendar.MONTH) - calendar1.get(Calendar.MONTH) + (calendar2.get(Calendar.YEAR) - calendar1.get(Calendar.YEAR))*12;
-	    				calendar1.add(Calendar.MONTH, months);
-	    				newTime = calendar1.getTimeInMillis() / 1000;
-	    			}
 	    		}
 	    		
 	    		if(kLine.getHighestPrice().compareTo(thumb.getClose())<0)

+ 3 - 1
doge/contract-swap-core/src/main/java/com/bizzan/bitrade/entity/KLinePeriod.java

@@ -13,7 +13,9 @@ public enum KLinePeriod {
     H4("4h"),
     D1("1d"),
     W1("1w"),
-    MON1("1mon");
+    MON1("1mon"),
+    /** 年线 */
+    Y1("1year");
 
     private final String code;
 

+ 1 - 1
doge/contract-swap-engine/src/main/java/com/bizzan/bitrade/job/KLineSyncJob.java

@@ -25,7 +25,7 @@ public class KLineSyncJob {
     @Autowired
     private ContractMarketService contractMarketService;
 
-    public static String PERIOD[] ={ "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "1week" };
+    public static String PERIOD[] ={ "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "1week", "1year" };
 
     private List<CoinSyncItem> coinList = new ArrayList<CoinSyncItem>(); // 交易对列表
 

+ 94 - 1
market/src/main/java/com/market/service/SpotDataQueryService.java

@@ -119,7 +119,13 @@ public class SpotDataQueryService {
                 }
             }
 
-            List<Map<String, Object>> dataResult = queryKlineFromDatabase(symbol, dbPeriod, endIdx, pageSize);
+            // 年线不从币安获取,由月线聚合计算
+            List<Map<String, Object>> dataResult;
+            if ("1YEAR".equals(dbPeriod)) {
+                dataResult = queryYearlyKlineAggregated(symbol, endIdx, pageSize);
+            } else {
+                dataResult = queryKlineFromDatabase(symbol, dbPeriod, endIdx, pageSize);
+            }
             response.put(SpotConstant.RESPONSE_FIELD_DATA, dataResult);
         } catch (Exception e) {
             log.error("查询 K 线数据失败: channel={}, symbol={}", channel, symbol, e);
@@ -169,6 +175,93 @@ public class SpotDataQueryService {
         return result;
     }
 
+    // ------------------------------------------------------------------ 年线聚合(从月线计算)
+
+    /**
+     * 查询年线数据:从月线(1MONTH)按自然年聚合,按时间升序返回。
+     * <p>
+     * 币安不提供年线,year K-line 由月线聚合得到:
+     * open=首月开盘价,close=末月收盘价,high=最高,low=最低,volume/amount 累加。
+     *
+     * @param symbol   交易对(小写)
+     * @param endIdx   结束索引(秒,0 表示不限)
+     * @param pageSize 最大年数
+     * @return 年线数据列表(升序)
+     */
+    @DS("market")
+    private List<Map<String, Object>> queryYearlyKlineAggregated(String symbol, long endIdx, int pageSize) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        try {
+            String tableName = SpotConstant.TABLE_KLINE_PREFIX + symbol.toLowerCase();
+            // 年线最多 pageSize 年,月线最多需要 pageSize*12 条
+            int monthLimit = pageSize * 12;
+            Long endTime = endIdx > 0 ? endIdx * SpotConstant.MILLISECONDS_TO_SECONDS : null;
+
+            List<Kline> monthly = klineMapper.selectByPeriodAndTimeRange(
+                    tableName, "1MONTH", null, endTime, monthLimit);
+
+            if (monthly.isEmpty()) {
+                return result;
+            }
+
+            // 按自然年分组(UTC),倒序→升序:先按年倒序分组后,再逆序输出
+            Map<Integer, List<Kline>> yearGroups = new LinkedHashMap<>();
+            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+            // monthly 查询结果是倒序(最新在前),需要逆序遍历使年份从早到晚
+            for (int i = monthly.size() - 1; i >= 0; i--) {
+                Kline k = monthly.get(i);
+                cal.setTimeInMillis(k.getBeginTime());
+                int year = cal.get(Calendar.YEAR);
+                yearGroups.computeIfAbsent(year, y -> new ArrayList<>()).add(k);
+            }
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+            // 聚合并按升序添加,最多取 pageSize 条
+            List<Integer> years = new ArrayList<>(yearGroups.keySet());
+            int startIdx = Math.max(0, years.size() - pageSize);
+            for (int yi = startIdx; yi < years.size(); yi++) {
+                int year = years.get(yi);
+                List<Kline> months = yearGroups.get(year);
+
+                cal.set(year, Calendar.JANUARY, 1, 0, 0, 0);
+                cal.set(Calendar.MILLISECOND, 0);
+                long yearBeginMs = cal.getTimeInMillis();
+                long id = yearBeginMs / SpotConstant.MILLISECONDS_TO_SECONDS;
+
+                BigDecimal high = months.get(0).getHigh();
+                BigDecimal low  = months.get(0).getLow();
+                BigDecimal vol  = BigDecimal.ZERO;
+                BigDecimal turn = BigDecimal.ZERO;
+                for (Kline m : months) {
+                    if (m.getHigh().compareTo(high) > 0) {
+                        high = m.getHigh();
+                    }
+                    if (m.getLow().compareTo(low) < 0) {
+                        low = m.getLow();
+                    }
+                    vol  = vol.add(m.getVolume());
+                    turn = turn.add(m.getTurnover());
+                }
+
+                Map<String, Object> klineData = new HashMap<>();
+                klineData.put(SpotConstant.DATA_FIELD_ID,     id);
+                klineData.put(SpotConstant.DATA_FIELD_OPEN,   months.get(0).getOpen());
+                klineData.put(SpotConstant.DATA_FIELD_CLOSE,  months.get(months.size() - 1).getClose());
+                klineData.put(SpotConstant.DATA_FIELD_HIGH,   high);
+                klineData.put(SpotConstant.DATA_FIELD_LOW,    low);
+                klineData.put(SpotConstant.DATA_FIELD_VOL,    vol);
+                klineData.put(SpotConstant.DATA_FIELD_AMOUNT, turn);
+                klineData.put(SpotConstant.DATA_FIELD_DS,     sdf.format(new Date(yearBeginMs)));
+                result.add(klineData);
+            }
+        } catch (Exception e) {
+            log.error("聚合年线数据失败: symbol={}", symbol, e);
+        }
+        return result;
+    }
+
     // ------------------------------------------------------------------ 深度数据
 
     private Map<String, Object> queryDepthData(String channel, Map<String, Object> response, String symbol) {

+ 71 - 2
market/src/main/java/com/market/service/impl/KlineServiceImpl.java

@@ -15,8 +15,7 @@ import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -224,9 +223,79 @@ public class KlineServiceImpl implements KlineService {
 
     @Override
     public List<Kline> queryHistory(String symbol, String interval, Long from, Long to) {
+        // 年线不从币安获取,由月线聚合计算
+        if ("1year".equals(interval)) {
+            return aggregateYearlyFromMonthly(KlineUtils.tableName(symbol), from, to);
+        }
         return futuresKlineMapper.selectByTimeRange(KlineUtils.tableName(symbol), interval, from, to);
     }
 
+    /**
+     * 将月线数据聚合为年线
+     * <p>
+     * 币安不提供年线,通过查询月线(period=1mon)按自然年分组聚合:
+     * open=首月开盘价,close=末月收盘价,high=最高,low=最低,volume/turnover 累加。
+     *
+     * @param tableName 分表名
+     * @param from      开始时间(毫秒)
+     * @param to        结束时间(毫秒)
+     * @return 年线列表(按时间升序)
+     */
+    private List<Kline> aggregateYearlyFromMonthly(String tableName, Long from, Long to) {
+        List<Kline> monthly = futuresKlineMapper.selectByTimeRange(tableName, "1mon", from, to);
+        if (monthly.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 按自然年分组(UTC),保持插入顺序以确保升序输出
+        Map<Integer, List<Kline>> yearGroups = new LinkedHashMap<>();
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+        for (Kline k : monthly) {
+            cal.setTimeInMillis(k.getBeginTime());
+            int year = cal.get(Calendar.YEAR);
+            yearGroups.computeIfAbsent(year, y -> new ArrayList<>()).add(k);
+        }
+
+        // 聚合每年
+        List<Kline> result = new ArrayList<>();
+        for (Map.Entry<Integer, List<Kline>> entry : yearGroups.entrySet()) {
+            int year = entry.getKey();
+            List<Kline> months = entry.getValue();
+
+            // 计算当年 1 月 1 日 00:00:00 UTC 的毫秒时间戳作为 beginTime
+            cal.set(year, Calendar.JANUARY, 1, 0, 0, 0);
+            cal.set(Calendar.MILLISECOND, 0);
+            long yearBegin = cal.getTimeInMillis();
+
+            BigDecimal high = months.get(0).getHigh();
+            BigDecimal low  = months.get(0).getLow();
+            BigDecimal vol  = BigDecimal.ZERO;
+            BigDecimal turn = BigDecimal.ZERO;
+            for (Kline m : months) {
+                if (m.getHigh().compareTo(high) > 0) {
+                    high = m.getHigh();
+                }
+                if (m.getLow().compareTo(low) < 0) {
+                    low = m.getLow();
+                }
+                vol  = vol.add(m.getVolume());
+                turn = turn.add(m.getTurnover());
+            }
+
+            Kline yearly = new Kline();
+            yearly.setPeriod("1year");
+            yearly.setBeginTime(yearBegin);
+            yearly.setOpen(months.get(0).getOpen());
+            yearly.setClose(months.get(months.size() - 1).getClose());
+            yearly.setHigh(high);
+            yearly.setLow(low);
+            yearly.setVolume(vol);
+            yearly.setTurnover(turn);
+            result.add(yearly);
+        }
+        return result;
+    }
+
     // ------------------------------------------------------------------ 转换
 
     private Kline toEntity(String period, JsonNode k) {

+ 1 - 0
market/src/main/java/com/market/utils/SpotConstant.java

@@ -35,6 +35,7 @@ public final class SpotConstant {
         put("2h",     "2HOUR"); put("4h",    "4HOUR"); put("6h",    "6HOUR");
         put("8h",     "8HOUR"); put("12h",   "12HOUR"); put("1day", "1DAY");
         put("3d",     "3DAY");  put("1week", "1WEEK"); put("1month","1MONTH");
+        put("1year",  "1YEAR");
     }};
 
     /** 币安周期 → 数据库周期映射 */