| New file |
| | |
| | | -- 为 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); |
| New file |
| | |
| | | -- 修复 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; |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |