jean il y a 1 mois
Parent
commit
fc053dcc82

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

@@ -299,6 +299,7 @@ public class MarketController {
             case "1day":  return "1d";
             case "1week": return "1w";
             case "1mon":  return "1mon";
+            case "3mon":  return "3mon";
             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", "1year" };
+    public static String PERIOD[] ={ "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "3mon", "1week", "1year" };
 
     private List<CoinSyncItem> coinList = new ArrayList<CoinSyncItem>(); // 交易对列表
 

+ 15 - 1
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", "1year" };
+	public static String PERIOD[] = { "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "3mon", "1week", "1year" };
     private Map<String, Map<String, KLine>> kLineMap = new HashMap<>();
     private Map<String, CoinThumb> thumbMap = new HashMap<>();
 	
@@ -84,6 +84,20 @@ public class KLineService {
 	    				calendar1.add(Calendar.MONTH, months);
 	    				newTime = calendar1.getTimeInMillis() / 1000;
 	    			}
+	    		} else if (period.equals("3mon")) {
+	    			// 季线(3个月):按自然季度切换(Q1=1-3月,Q2=4-6月,Q3=7-9月,Q4=10-12月)
+	    			Calendar calendar1 = Calendar.getInstance();
+	    			calendar1.setTimeInMillis(kLine.getTime()*1000);
+	    			Calendar calendar2 = Calendar.getInstance();
+	    			calendar2.setTimeInMillis(thumb.getTime()*1000);
+	    			int quarter1 = calendar1.get(Calendar.YEAR) * 4 + calendar1.get(Calendar.MONTH) / 3;
+	    			int quarter2 = calendar2.get(Calendar.YEAR) * 4 + calendar2.get(Calendar.MONTH) / 3;
+	    			if(quarter1 != quarter2) {
+	    				save = true;
+	    				int quarters = quarter2 - quarter1;
+	    				calendar1.add(Calendar.MONTH, quarters * 3);
+	    				newTime = calendar1.getTimeInMillis() / 1000;
+	    			}
 	    		} else if (period.equals("1year")) {
 	    			// 年线:按日历年份切换
 	    			Calendar calendar1 = Calendar.getInstance();

+ 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", "1year" };
+    public static String PERIOD[] ={ "1min", "5min", "15min", "30min", "60min","4hour", "1day", "1mon", "3mon", "1week", "1year" };
 
     private List<CoinSyncItem> coinList = new ArrayList<CoinSyncItem>(); // 交易对列表
 

+ 5 - 5
doge/core/src/main/java/com/bizzan/bitrade/service/StakingService.java

@@ -78,8 +78,11 @@ public class StakingService extends BaseService {
     @Autowired
     private RedisManager redisManager;
 
+    /** IDO 预售固定认购价(USDT / 质押币) */
+    private static final BigDecimal IDO_SUBSCRIPTION_USDT_PRICE = new BigDecimal("0.6");
+
     /**
-     * IDO 预售:USDT 按现货价闪兑为质押币后走质押流程
+     * IDO 预售:USDT 按固定价 0.6 闪兑为质押币后走质押流程
      */
     @Transactional(rollbackFor = Exception.class)
     public MessageResult stakeWithUsdt(Long memberId, Long configId, BigDecimal usdtAmount) {
@@ -91,10 +94,7 @@ public class StakingService extends BaseService {
             return MessageResult.error(msService.getMessage("STAKING_AMOUNT_INVALID"));
         }
 
-        BigDecimal price = resolveSpotPriceUsdt(config.getCoinUnit());
-        if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
-            return MessageResult.error(msService.getMessage("STAKING_PRICE_UNAVAILABLE"));
-        }
+        BigDecimal price = IDO_SUBSCRIPTION_USDT_PRICE;
 
         Coin coin = resolveCoinByUnit(config.getCoinUnit());
         if (coin == null) {

+ 1 - 1
market/src/main/java/com/market/controller/MarketRankController.java

@@ -27,7 +27,7 @@ public class MarketRankController {
 
     private static final String TICKET_PREFIX = KlineUtils.TOPIC_PREFIX;
     private static final String TICKET_SUFFIX = "." + KlineUtils.TOPIC_TICKET;
-    private static final int TOP_N = 5;
+    private static final int TOP_N = 30;
 
     private final SubscriptionManager subscriptionManager;
     private final KlineUtils klineUtils;

+ 90 - 2
market/src/main/java/com/market/service/SpotDataQueryService.java

@@ -119,9 +119,11 @@ public class SpotDataQueryService {
                 }
             }
 
-            // 年线不从币安获取,由月线聚合计算
+            // 季线(3MONTH)和年线(1YEAR)不从币安获取,由月线聚合计算
             List<Map<String, Object>> dataResult;
-            if ("1YEAR".equals(dbPeriod)) {
+            if ("3MONTH".equals(dbPeriod)) {
+                dataResult = queryQuarterlyKlineAggregated(symbol, endIdx, pageSize);
+            } else if ("1YEAR".equals(dbPeriod)) {
                 dataResult = queryYearlyKlineAggregated(symbol, endIdx, pageSize);
             } else {
                 dataResult = queryKlineFromDatabase(symbol, dbPeriod, endIdx, pageSize);
@@ -175,6 +177,92 @@ public class SpotDataQueryService {
         return result;
     }
 
+    // ------------------------------------------------------------------ 季线聚合(从月线计算)
+
+    /**
+     * 查询季线数据:从月线(1MONTH)按自然季度聚合,按时间升序返回。
+     * <p>
+     * 季度定义:Q1=1-3月,Q2=4-6月,Q3=7-9月,Q4=10-12月。
+     * open=首月开盘价,close=末月收盘价,high/low 取极值,volume/amount 累加。
+     *
+     * @param symbol   交易对(小写)
+     * @param endIdx   结束索引(秒,0 表示不限)
+     * @param pageSize 最大季度数
+     * @return 季线数据列表(升序)
+     */
+    @DS("market")
+    private List<Map<String, Object>> queryQuarterlyKlineAggregated(String symbol, long endIdx, int pageSize) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        try {
+            String tableName = SpotConstant.TABLE_KLINE_PREFIX + symbol.toLowerCase();
+            int monthLimit = pageSize * 3;
+            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>> quarterGroups = new LinkedHashMap<>();
+            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+            for (int i = monthly.size() - 1; i >= 0; i--) {
+                Kline k = monthly.get(i);
+                cal.setTimeInMillis(k.getBeginTime());
+                int quarterKey = cal.get(Calendar.YEAR) * 4 + cal.get(Calendar.MONTH) / 3;
+                quarterGroups.computeIfAbsent(quarterKey, q -> new ArrayList<>()).add(k);
+            }
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+            List<Integer> keys = new ArrayList<>(quarterGroups.keySet());
+            int startIdx = Math.max(0, keys.size() - pageSize);
+            for (int ki = startIdx; ki < keys.size(); ki++) {
+                int quarterKey = keys.get(ki);
+                List<Kline> months = quarterGroups.get(quarterKey);
+
+                int year = quarterKey / 4;
+                int startMonth = (quarterKey % 4) * 3;
+                cal.set(year, startMonth, 1, 0, 0, 0);
+                cal.set(Calendar.MILLISECOND, 0);
+                long quarterBeginMs = cal.getTimeInMillis();
+                long id = quarterBeginMs / 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(quarterBeginMs)));
+                result.add(klineData);
+            }
+        } catch (Exception e) {
+            log.error("聚合季线数据失败: symbol={}", symbol, e);
+        }
+        return result;
+    }
+
     // ------------------------------------------------------------------ 年线聚合(从月线计算)
 
     /**

+ 73 - 1
market/src/main/java/com/market/service/impl/KlineServiceImpl.java

@@ -223,13 +223,85 @@ public class KlineServiceImpl implements KlineService {
 
     @Override
     public List<Kline> queryHistory(String symbol, String interval, Long from, Long to) {
-        // 年线不从币安获取,由月线聚合计算
+        // 季线(3mon)和年线(1year)不从币安获取,由月线聚合计算
+        if ("3mon".equals(interval)) {
+            return aggregateQuarterlyFromMonthly(KlineUtils.tableName(symbol), from, to);
+        }
         if ("1year".equals(interval)) {
             return aggregateYearlyFromMonthly(KlineUtils.tableName(symbol), from, to);
         }
         return futuresKlineMapper.selectByTimeRange(KlineUtils.tableName(symbol), interval, from, to);
     }
 
+    /**
+     * 将月线数据聚合为季线(3个月一根)
+     * <p>
+     * 币安不提供季线,通过查询月线(period=1mon)按自然季度分组聚合:
+     * open=首月开盘价,close=末月收盘价,high/low 取极值,volume/turnover 累加。
+     * 季度定义:Q1=1-3月,Q2=4-6月,Q3=7-9月,Q4=10-12月。
+     *
+     * @param tableName 分表名
+     * @param from      开始时间(毫秒)
+     * @param to        结束时间(毫秒)
+     * @return 季线列表(按时间升序)
+     */
+    private List<Kline> aggregateQuarterlyFromMonthly(String tableName, Long from, Long to) {
+        List<Kline> monthly = futuresKlineMapper.selectByTimeRange(tableName, "1mon", from, to);
+        if (monthly.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 按自然季度分组(UTC),key = year*4 + quarter(0-3),保持升序
+        Map<Integer, List<Kline>> quarterGroups = new LinkedHashMap<>();
+        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+        for (Kline k : monthly) {
+            cal.setTimeInMillis(k.getBeginTime());
+            int quarterKey = cal.get(Calendar.YEAR) * 4 + cal.get(Calendar.MONTH) / 3;
+            quarterGroups.computeIfAbsent(quarterKey, q -> new ArrayList<>()).add(k);
+        }
+
+        // 聚合每季度
+        List<Kline> result = new ArrayList<>();
+        for (Map.Entry<Integer, List<Kline>> entry : quarterGroups.entrySet()) {
+            int quarterKey = entry.getKey();
+            List<Kline> months = entry.getValue();
+
+            // 当季第一个月为季度第1月(季度起始月 = quarterKey%4 * 3)
+            int year = quarterKey / 4;
+            int startMonth = (quarterKey % 4) * 3; // 0,3,6,9
+            cal.set(year, startMonth, 1, 0, 0, 0);
+            cal.set(Calendar.MILLISECOND, 0);
+            long quarterBegin = 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 quarterly = new Kline();
+            quarterly.setPeriod("3mon");
+            quarterly.setBeginTime(quarterBegin);
+            quarterly.setOpen(months.get(0).getOpen());
+            quarterly.setClose(months.get(months.size() - 1).getClose());
+            quarterly.setHigh(high);
+            quarterly.setLow(low);
+            quarterly.setVolume(vol);
+            quarterly.setTurnover(turn);
+            result.add(quarterly);
+        }
+        return result;
+    }
+
     /**
      * 将月线数据聚合为年线
      * <p>

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

@@ -35,7 +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");
+        put("3month", "3MONTH"); put("1year", "1YEAR");
     }};
 
     /** 币安周期 → 数据库周期映射 */