1
zj
15 hours ago e4b5bb994a493f575d96a63ccb2f819276c66b81
1
4 files added
311 ■■■■■ changed files
docs/db/V11__futures_para_bulk_from_item.sql 69 ●●●●● patch | view | raw | blame | history
docs/db/V8_1__pre_market_config_base_entity_columns.sql 5 ●●●●● patch | view | raw | blame | history
trading-order-huobi/src/main/java/com/yami/trading/huobi/data/NonCryptoKlineRemoteService.java 139 ●●●●● patch | view | raw | blame | history
trading-order-huobi/src/main/java/com/yami/trading/huobi/tradingview/TradingViewSymbolResolver.java 98 ●●●●● patch | view | raw | blame | history
docs/db/V11__futures_para_bulk_from_item.sql
New file
@@ -0,0 +1,69 @@
-- 为 t_item 中每个品种批量插入交割合约参数(6 档秒级周期)
-- 周期与收益率:60秒10% | 120秒15% | 180秒20% | 300秒35% | 480秒50% | 720秒100%
-- profit_ratio / profit_ratio_max 存小数(0.10 = 10%)
-- 已存在相同 symbol + timenum + timeunit 的记录则跳过,可重复执行
-- USE trading_order_zh;
INSERT INTO `t_futures_para` (
    `uuid`,
    `symbol`,
    `timenum`,
    `timeunit`,
    `unit_amount`,
    `unit_max_amount`,
    `profit_ratio`,
    `profit_ratio_cardinality`,
    `unit_fee`,
    `profit_ratio_max`,
    `create_by`,
    `update_by`,
    `update_time`,
    `remarks`,
    `del_flag`,
    `create_time_ts`,
    `update_time_ts`,
    `create_time`
)
SELECT
    REPLACE(UUID(), '-', '') AS uuid,
    i.symbol,
    p.timenum,
    'second' AS timeunit,
    IFNULL(i.unit_amount, 100) AS unit_amount,
    0 AS unit_max_amount,
    p.profit_ratio,
    10000 AS profit_ratio_cardinality,
    0.01 AS unit_fee,
    p.profit_ratio_max,
    '2' AS create_by,
    '2' AS update_by,
    NOW() AS update_time,
    NULL AS remarks,
    '0' AS del_flag,
    UNIX_TIMESTAMP() AS create_time_ts,
    UNIX_TIMESTAMP() AS update_time_ts,
    NOW() AS create_time
FROM t_item i
CROSS JOIN (
    SELECT 60  AS timenum, 0.10 AS profit_ratio, 0.10 AS profit_ratio_max
    UNION ALL SELECT 120, 0.15, 0.15
    UNION ALL SELECT 180, 0.20, 0.20
    UNION ALL SELECT 300, 0.35, 0.35
    UNION ALL SELECT 480, 0.50, 0.50
    UNION ALL SELECT 720, 1.00, 1.00
) p
WHERE i.del_flag = '0'
  -- 仅股票类可取消下面注释;留空则 t_item 全部品种
  -- AND i.type IN ('US-stocks', 'HK-stocks', 'TW-stocks', 'A-stocks', 'JP-stocks', 'INDIA-stocks', 'UK-stocks')
  AND NOT EXISTS (
      SELECT 1
      FROM t_futures_para fp
      WHERE fp.symbol = i.symbol
        AND fp.timenum = p.timenum
        AND fp.timeunit = 'second'
        AND fp.del_flag = '0'
  );
-- 执行后核对条数(应为 品种数 × 6)
-- SELECT COUNT(*) FROM t_futures_para WHERE del_flag = '0' AND timeunit = 'second' AND timenum IN (60,120,180,300,480,720);
docs/db/V8_1__pre_market_config_base_entity_columns.sql
New file
@@ -0,0 +1,5 @@
-- 修复 t_item_pre_market_config 缺少 BaseEntity 字段(已执行旧版 V8 的库请执行本脚本)
ALTER TABLE t_item_pre_market_config
    ADD COLUMN create_by VARCHAR(64) NULL COMMENT '创建人' AFTER create_time_ts,
    ADD COLUMN update_by VARCHAR(64) NULL COMMENT '更新人' AFTER update_time_ts,
    ADD COLUMN del_flag INT NOT NULL DEFAULT 0 COMMENT '逻辑删除 0正常' AFTER update_by;
trading-order-huobi/src/main/java/com/yami/trading/huobi/data/NonCryptoKlineRemoteService.java
New file
@@ -0,0 +1,139 @@
package com.yami.trading.huobi.data;
import cn.hutool.core.collection.CollectionUtil;
import com.yami.trading.bean.data.domain.Kline;
import com.yami.trading.bean.item.domain.Item;
import com.yami.trading.huobi.hobi.HobiDataService;
import com.yami.trading.huobi.tradingview.TradingViewSymbolResolver;
import com.yami.trading.huobi.tradingview.service.TradingViewService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
 * 非加密货币 K 线远程拉取:TradingView 多 symbol 重试 → Hobi 数据源兜底。
 */
@Slf4j
@Service
public class NonCryptoKlineRemoteService {
    private static final int TV_PER_SYMBOL_TIMEOUT_SECONDS = 5;
    @Autowired
    private TradingViewService tradingViewService;
    @Autowired
    private HobiDataService hobiDataService;
    public List<Kline> fetch(Item item, String requestLine, String twLine) {
        List<Kline> tvData = fetchFromTradingView(item, twLine, requestLine);
        if (CollectionUtil.isNotEmpty(tvData)) {
            return tvData;
        }
        return fetchFromHobi(item, requestLine);
    }
    private List<Kline> fetchFromTradingView(Item item, String twLine, String requestLine) {
        List<String> candidates = TradingViewSymbolResolver.resolveCandidates(item);
        for (String tvSymbol : candidates) {
            try {
                Map<String, List<com.yami.trading.huobi.tradingview.api.model.Kline>> klineData =
                        tradingViewService.getKlineData(tvSymbol, twLine)
                                .get(TV_PER_SYMBOL_TIMEOUT_SECONDS, TimeUnit.SECONDS);
                List<com.yami.trading.huobi.tradingview.api.model.Kline> klines = klineData.get(tvSymbol);
                if ((klines == null || klines.isEmpty()) && klineData != null && !klineData.isEmpty()) {
                    klines = klineData.values().stream()
                            .filter(list -> list != null && !list.isEmpty())
                            .findFirst()
                            .orElse(null);
                }
                if (klines == null || klines.isEmpty()) {
                    continue;
                }
                log.info("TradingView kline hit, symbol={}, tvSymbol={}, bars={}", item.getSymbol(), tvSymbol, klines.size());
                return convertTvKlines(klines, item, requestLine);
            } catch (TimeoutException e) {
                log.warn("TradingView kline timeout, symbol={}, tvSymbol={}, twLine={}", item.getSymbol(), tvSymbol, twLine);
            } catch (Exception e) {
                log.warn("TradingView kline error, symbol={}, tvSymbol={}, twLine={}", item.getSymbol(), tvSymbol, twLine, e);
            }
        }
        return new ArrayList<>();
    }
    private List<Kline> convertTvKlines(List<com.yami.trading.huobi.tradingview.api.model.Kline> klines,
                                        Item item, String requestLine) {
        List<Kline> data = new ArrayList<>();
        String period = requestLine != null ? requestLine : Kline.PERIOD_1MIN;
        for (com.yami.trading.huobi.tradingview.api.model.Kline k : klines) {
            Kline kline = new Kline();
            kline.setSymbol(item.getSymbolData());
            kline.setTs(k.getTimestamp());
            kline.setOpen(k.getOpen());
            kline.setHigh(k.getHigh());
            kline.setLow(k.getLow());
            kline.setClose(k.getClose());
            kline.setPeriod(period);
            kline.setAmount(0D);
            kline.setVolume(k.getVolume());
            data.add(kline);
        }
        return data;
    }
    private List<Kline> fetchFromHobi(Item item, String line) {
        String symbol = item.getSymbol();
        try {
            List<Kline> klines = fetchPeriodFromHobi(symbol, line);
            if (CollectionUtil.isNotEmpty(klines)) {
                log.info("Hobi kline fallback hit, symbol={}, line={}, bars={}", symbol, line, klines.size());
            }
            return klines == null ? new ArrayList<>() : klines;
        } catch (Exception e) {
            log.warn("Hobi kline fallback failed, symbol={}, line={}", symbol, line, e);
            return new ArrayList<>();
        }
    }
    private List<Kline> fetchPeriodFromHobi(String symbol, String line) {
        if (line == null) {
            return new ArrayList<>();
        }
        switch (line.toLowerCase()) {
            case "1min":
                return hobiDataService.getTimeseriesOneMinute(symbol);
            case "5min":
                return hobiDataService.getTimeseriesFiveMinute(symbol);
            case "15min":
                return hobiDataService.getTimeseriesFifteenMinute(symbol);
            case "30min":
                return hobiDataService.getTimeseriesThirtyMinute(symbol);
            case "60min":
                return hobiDataService.getTimeseriesForOneHourly(symbol);
            case "120min":
            case "2hour":
                return hobiDataService.getTimeseriesForTwoHourly(symbol);
            case "4hour":
                return hobiDataService.getTimeseriesForFourHourly(symbol);
            case "1day":
            case "5day":
            case "1week":
            case "1mon":
            case "quarter":
            case "year":
                Map<String, List<Kline>> dailyMap = hobiDataService.getDailyWeekMonthHistory(symbol);
                if (dailyMap == null) {
                    return new ArrayList<>();
                }
                List<Kline> periodData = dailyMap.get(line);
                return periodData == null ? new ArrayList<>() : periodData;
            default:
                return hobiDataService.getTimeseriesOneMinute(symbol);
        }
    }
}
trading-order-huobi/src/main/java/com/yami/trading/huobi/tradingview/TradingViewSymbolResolver.java
New file
@@ -0,0 +1,98 @@
package com.yami.trading.huobi.tradingview;
import com.yami.trading.bean.item.domain.Item;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
 * 将品种配置解析为 TradingView 可识别的 symbol(含交易所前缀)。
 */
public final class TradingViewSymbolResolver {
    private static final String[] US_EXCHANGES = {"NYSE", "NASDAQ", "AMEX", "OTC", "CBOE"};
    private TradingViewSymbolResolver() {
    }
    public static List<String> resolveCandidates(Item item) {
        Set<String> candidates = new LinkedHashSet<>();
        if (item == null) {
            return new ArrayList<>();
        }
        String symbolData = StringUtils.trimToEmpty(item.getSymbolData());
        String symbol = StringUtils.trimToEmpty(item.getSymbol());
        String code = StringUtils.isNotBlank(symbolData) ? symbolData : symbol;
        if (StringUtils.isBlank(code)) {
            return new ArrayList<>();
        }
        if (code.contains(":")) {
            candidates.add(code);
            return new ArrayList<>(candidates);
        }
        String type = StringUtils.defaultString(item.getType());
        String openCloseType = StringUtils.defaultString(item.getOpenCloseType());
        if (Item.HK_STOCKS.equalsIgnoreCase(openCloseType) || Item.HK_STOCKS.equalsIgnoreCase(type)) {
            String numeric = code.replaceAll("\\D", "");
            if (StringUtils.isNotBlank(numeric)) {
                candidates.add("HKEX:" + numeric);
            }
            candidates.add("HKEX:" + code);
            return new ArrayList<>(candidates);
        }
        if (Item.forex.equalsIgnoreCase(type)) {
            candidates.add("FX_IDC:" + code);
            candidates.add("OANDA:" + code);
            candidates.add("FX:" + code);
            return new ArrayList<>(candidates);
        }
        if (isUsLike(item, type, openCloseType)) {
            addUsExchangeCandidates(candidates, code, item.getMarket());
        }
        if (Item.TW_STOCKS.equalsIgnoreCase(type)) {
            candidates.add("TWSE:" + code);
        }
        candidates.add(code);
        return new ArrayList<>(candidates);
    }
    private static boolean isUsLike(Item item, String type, String openCloseType) {
        if (Item.US_STOCKS.equalsIgnoreCase(type) || Item.US_ETF.equalsIgnoreCase(type)
                || Item.US_STOCKS.equalsIgnoreCase(openCloseType) || Item.US_ETF.equalsIgnoreCase(item.getMarket())) {
            return true;
        }
        return type.contains("stock") || type.contains("ETF")
                || Item.indices.equalsIgnoreCase(type);
    }
    private static void addUsExchangeCandidates(Set<String> candidates, String code, String market) {
        if (StringUtils.isNotBlank(market) && market.contains(":")) {
            candidates.add(market.toUpperCase(Locale.ROOT));
            return;
        }
        if (StringUtils.isNotBlank(market)) {
            String upperMarket = market.toUpperCase(Locale.ROOT);
            for (String exchange : US_EXCHANGES) {
                if (upperMarket.contains(exchange)) {
                    candidates.add(exchange + ":" + code);
                    return;
                }
            }
        }
        for (String exchange : US_EXCHANGES) {
            candidates.add(exchange + ":" + code);
        }
    }
}