From e4b5bb994a493f575d96a63ccb2f819276c66b81 Mon Sep 17 00:00:00 2001
From: zj <1772600164@qq.com>
Date: Mon, 15 Jun 2026 13:10:48 +0800
Subject: [PATCH] 1

---
 docs/db/V11__futures_para_bulk_from_item.sql                                                        |   69 +++++++++++
 trading-order-huobi/src/main/java/com/yami/trading/huobi/tradingview/TradingViewSymbolResolver.java |   98 ++++++++++++++++
 trading-order-huobi/src/main/java/com/yami/trading/huobi/data/NonCryptoKlineRemoteService.java      |  139 +++++++++++++++++++++++
 docs/db/V8_1__pre_market_config_base_entity_columns.sql                                             |    5 
 4 files changed, 311 insertions(+), 0 deletions(-)

diff --git a/docs/db/V11__futures_para_bulk_from_item.sql b/docs/db/V11__futures_para_bulk_from_item.sql
new file mode 100644
index 0000000..9047a46
--- /dev/null
+++ b/docs/db/V11__futures_para_bulk_from_item.sql
@@ -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);
diff --git a/docs/db/V8_1__pre_market_config_base_entity_columns.sql b/docs/db/V8_1__pre_market_config_base_entity_columns.sql
new file mode 100644
index 0000000..df5b9cc
--- /dev/null
+++ b/docs/db/V8_1__pre_market_config_base_entity_columns.sql
@@ -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;
diff --git a/trading-order-huobi/src/main/java/com/yami/trading/huobi/data/NonCryptoKlineRemoteService.java b/trading-order-huobi/src/main/java/com/yami/trading/huobi/data/NonCryptoKlineRemoteService.java
new file mode 100644
index 0000000..b01425f
--- /dev/null
+++ b/trading-order-huobi/src/main/java/com/yami/trading/huobi/data/NonCryptoKlineRemoteService.java
@@ -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);
+        }
+    }
+}
diff --git a/trading-order-huobi/src/main/java/com/yami/trading/huobi/tradingview/TradingViewSymbolResolver.java b/trading-order-huobi/src/main/java/com/yami/trading/huobi/tradingview/TradingViewSymbolResolver.java
new file mode 100644
index 0000000..9eda09f
--- /dev/null
+++ b/trading-order-huobi/src/main/java/com/yami/trading/huobi/tradingview/TradingViewSymbolResolver.java
@@ -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);
+        }
+    }
+}

--
Gitblit v1.9.3