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