1. 历史分时KDJ到底解决什么问题:从回测到盘中异动
我第一次需要历史分时KDJ数据,是在复盘一套短线策略的时候。日线KDJ只能告诉我某一天收盘后的超买超卖状态,但策略真正的买卖点发生在分时级别,等日线指标给出信号,行情往往已经走完。所以当时的目标很明确:用Python、Java这类主流语言,通过正规股票数据接口拿到某一天的分时数据,再按分钟K线把KDJ指标完整重算一遍,形成“历史分时KDJ”序列。这套东西既能用来回测,也能做盘后复盘,把计算逻辑稍微改造还能搬到盘中做预警——同一套算法覆盖三个场景,非常值得做。
这篇文章不准备铺垫太多,直接讲清楚整条链路:历史分时数据从哪里拿、拿到之后怎么清洗、KDJ怎么按分钟窗口计算、Python和Java分别怎么落地,以及Go、Node.js、C#这些语言怎么快速移植同一套逻辑。代码都可以直接抄走,但有几个坑我会单拎出来说明白——数据连续性、复权、时间对齐、接口限流,这些比KDJ公式本身更容易让你翻车。
1.1 分时KDJ与日线KDJ:同一个公式,两种用法
KDJ的标准公式大家应该都不陌生。先算RSV(未成熟随机指标值),再用平滑公式得K、D、J三个值:
RSV = (当前收盘价 - 最近N个周期的最低最低价) / (最近N个周期的最高最高价 - 最近N个周期的最低最低价) * 100 K = 2/3 * 前一日K + 1/3 * RSV,K初始值取50 D = 2/3 * 前一日D + 1/3 * K,D初始值取50 J = 3 * K - 2 * D默认参数N=9、平滑因子M1=3、M2=3。日线KDJ的“周期”是交易日,分时KDJ的“周期”则换成K线根数。比如1分钟KDJ,用的就是最近9根1分钟K线的最高价、最低价以及当前1分钟K线的收盘价。日线KDJ适合看中线级别的超买超卖,分时KDJ则聚焦一天之内的短周期强弱转换,比如开盘后30分钟是否形成金叉、尾盘是否出现高位钝化。
很多人第一次上手时会把日线KDJ的计算结果直接拿来做分时判断,这就错了。日线KDJ里的“周期内最高最低价”来自最近9个交易日,跨度一天;分时KDJ里的“周期内最高最低价”来自最近9分钟,跨度几分钟。两者的RSV完全不同,平滑递推的K和D也会差出很多。做历史回测时尤其要注意,不能用日线级别指标替代分时级别指标,否则策略信号的时间粒度对不上,回测结果没有参考意义。
1.2 历史分时数据的“隐形门槛”:权限、连续性与口径
分时KDJ算法不复杂,真正的门槛在数据端。很多免费行情软件只展示当天的分时图,根本不提供历史分钟数据的批量导出;还有些接口看起来能拿数据,但只覆盖最近几个交易日。另一个常见菜鸟错误是不做数据连续性检查就直接计算:某一天因为停牌、默认缺一根K线,或者接口只返回了部分时段数据,算出来的KDJ从缺失位置开始全部偏移。
我经历过一次比较典型的翻车。当时我用一个接口拉某股票的历史分钟数据,明明选了早上9:30开始,返回的数据第一行却是10:00,导致RSV分母里的最低价和最高价窗口内少了几十条数据,K值从一开始就偏高,后面整条KDJ曲线都失真。那次之后,我把数据校验写成了常规步骤:先确认分钟K线数量是否符合当天交易时长,再检查时间是否连续,最后看一眼high、low、close是否满足高低关系。这一步做对了,后面不管切换到哪种语言,计算逻辑都可以直接复用。
2. 数据源选型:先解决合规问题,再谈接口速度
做股票数据接口对接,第一个要明确的不是代码,是合规边界。尽量选择有正式API文档、明确允许程序化访问的数据服务商,按照平台规则使用token和频率额度。个人学习和研究项目,完全可以跑起来;如果要上生产系统,务必先和官方确认数据使用权限。下面分享三类常见选型,以及我实际对接时的判断标准。
2.1 开放API、专业终端、自建库怎么选
开放API的首选特征是:文档清晰、注册后能拿到token、有明确的历史分钟数据支持。国内常见的Tushare Pro、AkShare都属于这一类,上手快,适合个人策略研究和快速验证。专业终端(如Wind、Choice、iFinD类产品)数据质量和稳定性更高,接口偏企业级,但需要授权和费用,适合团队化项目。自建库则适合量化团队自己维护行情数据,通过行情网关每天落库,可控性最强,但成本也最高。
我这几年做下来的体感是:如果只是自己研究历史分时KDJ,没必要一开始就上专业终端。先用开放平台把方法跑通、把指标验证好,等确实需要更稳定、更完整的数据时才考虑升级。有一点需要提醒:开源数据工具通常是从公开页面获取信息,具体使用时要遵守目标网站规则,不要高频请求,也不要用于商业分发。数据合规这一关守住了,后面才能安心做指标。
2.2 分钟线返回口径:时间、复权、成交量
同样是“1分钟K线”,不同数据源返回的字段、时间格式、K线根数定义可能都有区别。我在对接时通常固定检查四件事。
第一,时间字段。接口返回的trade_time要统一成北京时间,有些接口会给带时区偏移的时间字符串,直接parse会导致分钟错位。第二,K线起始时间和结束时间。A股全天交易240分钟,1分钟K线通常对应240根数据,很多数据源在开盘集合竞价处理上可能有差异,所以要以实际返回为准。第三,复权状态。分时KDJ计算本身用不到成交量,但价格必须经过复权处理,尤其除权除息日的跳空缺口会直接影响RSV的最高最低价,不做复权的话指标会突变。第四,成交量和成交额字段精度。虽然KDJ不依赖成交量,但如果你后续要做量能过滤,就要注意字段用整型还是浮点型,避免精度损失。
2.3 先做数据校验再做指标计算
我现在的习惯是,数据获取函数和指标计算函数完全分开。获取函数只负责拉数据,同时在函数内部做校验,校验不通过直接抛异常,绝不让脏数据流到KDJ计算层。校验逻辑包括:数据量是否大于0、时间是否严格递增、high是否大于等于low、close是否在low和high范围内。这几条写好之后,能过滤掉绝大多数数据源异常。
校验时还要注意一个细节:分钟数据并非一定按时间正序返回。某些接口默认倒序,有些则可能乱序。所以拿到数据后先按时间排序,再校验连续性,最后才开始递推KDJ。顺序错了,滚动窗口就会算错,而且这种错误不会报错,只会让指标悄悄偏离,特别难排查。
3. Python实例:从分钟K线到历史分时KDJ全流程
Python是做数据研究的首选语言,因为pandas处理时间序列太方便了。整个流程可以分成三步:拉取1分钟K线、清洗校验、滚动计算KDJ并落盘。以下代码以Tushare Pro为例,字段名和接口名以你实际使用的数据源为准;如果接入的是其他接口,只需要替换掉fetch函数。
3.1 环境准备与依赖
只需要两个核心库:requests负责请求接口,pandas负责数据清洗和计算。安装命令:
pip install requests pandasTushare Pro的token建议从环境变量读取,不要在代码里写死,避免代码传到公共仓库时泄露凭证。获取分钟的权限可能根据账户积分有限制,如果分钟接口暂时不可用,可以先申请数据权限再跑代码。以下示例股票我用的是000001.SZ(平安银行),你可以替换成任意股票代码。
3.2 拉取历史分钟K线:Tushare Pro示例
Tushare Pro的分钟接口是POST方式,body里包含api_name、token、params和fields四部分。params里指定股票代码、频率、起止时间。代码实现如下:
import os import requests import pandas as pd TOKEN = os.environ.get("TUSHARE_TOKEN") TS_CODE = "000001.SZ" START_TIME = "2024-03-01 09:30:00" END_TIME = "2024-03-01 15:00:00" def fetch_minute_kline(ts_code: str, start: str, end: str) -> pd.DataFrame: api = "http://api.tushare.pro" body = { "api_name": "stk_mins", "token": TOKEN, "params": { "ts_code": ts_code, "freq": "1min", "start_date": start, "end_date": end, }, "fields": "trade_time,open,high,low,close,volume", } resp = requests.post(api, json=body) data = resp.json().get("data") if data is None or "items" not in data: raise RuntimeError("接口返回为空,请检查token或分钟数据权限") df = pd.DataFrame(data["items"], columns=data["fields"]) df["trade_time"] = pd.to_datetime(df["trade_time"]) df = df.sort_values("trade_time").reset_index(drop=True) return df接口返回的items是二维数组,fields是列名。把这两者组合成DataFrame后,第一件事就是按时间排序。有的数据源会倒序返回,不排序的话后面全错。如果返回的data为None,优先去数据服务商的控制台确认分钟数据权限,而不是改代码。
3.3 KDJ滚动计算:不偷看未来数据
计算历史分时KDJ最核心的纪律是:每个时间点的K、D、J只能使用“当前时间点以及之前”的分钟K线,绝不能混入当天的未来数据。这也是回测和实盘信号不一致的常见原因。代码用循环实现滚动窗口,窗口大小为9根1分钟K线,前8个时间点窗口不足9根时,用截至当前的K线做计算,避免开盘阶段出现空值。
def calc_kdj(df: pd.DataFrame, n: int = 9) -> pd.DataFrame: close = df["close"].astype(float) high = df["high"].astype(float) low = df["low"].astype(float) pre_k, pre_d = 50.0, 50.0 rsv_list, k_list, d_list, j_list = [], [], [], [] for i in range(len(df)): start = max(0, i - n + 1) window_high = high.iloc[start:i + 1].max() window_low = low.iloc[start:i + 1].min() # 最高价等于最低价时RSV无意义,取50避免除零 if window_high == window_low: rsv = 50.0 else: rsv = (close.iloc[i] - window_low) / (window_high - window_low) * 100.0 cur_k = (2.0 / 3.0) * pre_k + (1.0 / 3.0) * rsv cur_d = (2.0 / 3.0) * pre_d + (1.0 / 3.0) * cur_k cur_j = 3.0 * cur_k - 2.0 * cur_d rsv_list.append(rsv) k_list.append(cur_k) d_list.append(cur_d) j_list.append(cur_j) pre_k, pre_d = cur_k, cur_d df["RSV"] = rsv_list df["K"] = k_list df["D"] = d_list df["J"] = j_list return df if __name__ == "__main__": minute_df = fetch_minute_kline(TS_CODE, START_TIME, END_TIME) kdj_df = calc_kdj(minute_df) print(kdj_df[["trade_time", "close", "K", "D", "J"]].tail())默认的K、D初值取50,这是行情软件最普遍的做法。如果你和某个特定软件对账,发现开盘前几分钟的数值不一样,先查它的初始值设定,有的软件可能把无数据阶段的K、D设为0或30,只影响前几根,后面会快速收敛。
3.4 输出CSV与简单应用
计算结果可以直接落盘CSV,方便复盘和二次开发。CSV列包括时间、OHLC、RSV、K、D、J。为了下一步做策略分析,我可以按常见口径把J大于100标记为超买区域、J小于0标记为超卖区域,供回测时统计信号胜率。
kdj_df.to_csv("000001_20240301_kdj.csv", index=False) def mark_zone(row): if row["J"] > 100: return "overbought" if row["J"] < 0: return "oversold" return "neutral" kdj_df["zone"] = kdj_df.apply(mark_zone, axis=1)需要提醒的是,J值超过100后可能进入高位钝化阶段,此时不一定是反转信号,反而可能是强势延续。所以我不建议直接把“J>100就卖出”写成策略条件,更稳妥的做法是叠加时间过滤和成交量确认,比如超买信号持续超过多少分钟才触发。这个和纯KDJ公式没有关系,属于策略层面的经验。
4. Java实例:工程化环境下的完整落地写法
Java在企业级开发里依然是很常见的语言,尤其当KDJ计算要嵌入交易系统、定时任务或后端服务时,用Java写会比Python更容易和现有系统集成。这一章给出从接口调用到KDJ计算的完整落地写法。
4.1 Maven依赖和工程结构
Java 11及以上可以直接用原生HttpClient,不需要额外引入第三方HTTP库。JSON解析我用Jackson,强烈不建议手写字符串解析,因为分钟接口返回的嵌套结构很容易出错。Maven依赖只需要加一个jackson-databind:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency>工程结构保持简单:一个主类负责调用接口、解析数据;一个工具类负责KDJ计算。如果后续要接入数据库,再加DAO层即可。
4.2 用HttpClient调接口并解析JSON
Tushare Pro接口的请求体是JSON格式,Java用HttpClient发送POST请求,注意设置Content-Type为application/json。下面给出一段可直接运行的代码骨架,token同样从环境变量读取:
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MinuteDataFetcher { public static String fetch(String token) throws Exception { String body = """ { "api_name": "stk_mins", "token": "%s", "params": { "ts_code": "000001.SZ", "freq": "1min", "start_date": "2024-03-01 09:30:00", "end_date": "2024-03-01 15:00:00" }, "fields": "trade_time,open,high,low,close,volume" } """.formatted(token); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://api.tushare.pro")) .header("Content-Type", "application/json") .timeout(Duration.ofSeconds(30)) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IllegalStateException("HTTP error: " + response.statusCode()); } return response.body(); } }解析响应时,用Jackson把data.fields和data.items提取出来,把二维数组按列名转成Java对象列表。常用的做法是先读fields得到列名顺序,再逐行组装记录:
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class KLineParser { public static double[][] parseHighLowClose(String json) throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(json); JsonNode data = root.get("data"); if (data == null || data.get("items") == null) { throw new IllegalStateException("接口返回为空,请检查权限"); } JsonNode items = data.get("items"); int rowCount = items.size(); double[] high = new double[rowCount]; double[] low = new double[rowCount]; double[] close = new double[rowCount]; for (int i = 0; i < rowCount; i++) { JsonNode row = items.get(i); close[i] = row.get(3).asDouble(); high[i] = row.get(2).asDouble(); low[i] = row.get(1).asDouble(); } return new double[][]{close, high, low}; } }这里我直接按Tushare fields字段顺序取数:trade_time在第0位,open第1位,high第2位,low第3位,close第4位,volume第5位。实际使用时建议把列名和下标做成常量或者枚举,避免字段顺序变化时定位困难。
4.3 KDJ计算的Java实现与边界处理
Java版KDJ计算逻辑和Python版完全一致。为了工程化使用,我把它写成静态方法,输入是high、low、close三个价格数组,输出是K、D、J三个数组。窗口不足9根时同样用start=max(0, i-n+1)来处理,最高价等于最低价时RSV取50,防止除零异常:
public class KDJCalculator { public static double[][] calc(double[] high, double[] low, double[] close, int n) { int len = close.length; double[] k = new double[len]; double[] d = new double[len]; double[] j = new double[len]; double preK = 50.0; double preD = 50.0; for (int i = 0; i < len; i++) { int start = Math.max(0, i - n + 1); double maxHigh = high[start]; double minLow = low[start]; for (int m = start; m <= i; m++) { maxHigh = Math.max(maxHigh, high[m]); minLow = Math.min(minLow, low[m]); } double rsv; if (maxHigh == minLow) { rsv = 50.0; } else { rsv = (close[i] - minLow) / (maxHigh - minLow) * 100.0; } double curK = (2.0 / 3.0) * preK + (1.0 / 3.0) * rsv; double curD = (2.0 / 3.0) * preD + (1.0 / 3.0) * curK; k[i] = curK; d[i] = curD; j[i] = 3.0 * curK - 2.0 * curD; preK = curK; preD = curD; } return new double[][]{k, d, j}; } }特别注意Java的整数除法:2/3在Java里结果是0,必须写成2.0/3.0或2/3.0。这个坑非常隐蔽,代码看起来没问题,但算出来的K值会一路衰减到接近0,不信可以自己试。
4.4 批量扫描时的频率控制与线程池
Java版最常见的生产场景是批量扫描多只股票的历史分时KDJ。一个容易踩的坑是接口频率限制。数据服务商一般有每秒调用次数和每日调用上限,无脑并发调用很容易被限流。我通常用一个固定线程池,同时加信号量控制并发数,保证请求间隔不低于接口要求的频率。
ExecutorService executor = Executors.newFixedThreadPool(4); Semaphore semaphore = new Semaphore(2); // 同一时间最多2个请求 for (String tsCode : tsCodes) { executor.submit(() -> { try { semaphore.acquire(); String json = MinuteDataFetcher.fetchWithToken(token, tsCode); // parse and calc KDJ } catch (Exception e) { // 记录日志,单独重试 } finally { semaphore.release(); } }); } executor.shutdown();另外,批量任务一定要做失败重试和日志记录。接口偶发超时是常态,单个股票失败不要影响整个队列,失败任务单独放进重试队列,等本轮跑完再补拉。历史分时KDJ这类离线计算任务,本来就不需要实时性,重试机制比盲目增加线程数有效得多。
5. 用Go、Node.js、C#快速移植同一套逻辑
很多人以为换语言就要重新设计算法,其实KDJ计算在不同语言之间几乎可以逐行翻译。核心模式固定:拉取分钟K线数组,按窗口扫描,滚动更新K、D、J。下面给三个常见场景的移植片段,方便各自按项目语言取用。
5.1 Go版:单文件搞定计算,适合服务化
Go的优势是编译成单二进制,部署简单,并发能力强,很适合把历史分时KDJ计算做成内部服务。计算部分可以直接抄这段核心函数:
func calcKDJ(high, low, close []float64, n int) (k, d, j []float64) { k = make([]float64, len(close)) d = make([]float64, len(close)) j = make([]float64, len(close)) preK, preD := 50.0, 50.0 for i := 0; i < len(close); i++ { start := i - n + 1 if start < 0 { start = 0 } maxH, minL := high[start], low[start] for m := start; m <= i; m++ { if high[m] > maxH { maxH = high[m] } if low[m] < minL { minL = low[m] } } rsv := 50.0 if maxH != minL { rsv = (close[i] - minL) / (maxH - minL) * 100.0 } curK := preK*2.0/3.0 + rsv/3.0 curD := preD*2.0/3.0 + curK/3.0 k[i], d[i], j[i] = curK, curD, 3.0*curK-2.0*curD preK, preD = curK, curD } return k, d, j }Go接口请求直接用标准库net/http和encoding/json就够了,这里不展开,主流程和Java版类似,只是把JSON字段解析成struct时注意一下字段名大小写映射。
5.2 Node.js版:给行情看板用很顺手
如果团队前端是TypeScript/JavaScript,Node.js版可以直接接入行情看板或者内部管理后台,不需要额外起服务翻译数据。核心计算如下:
function calcKDJ(bars, n = 9) { let k = 50; let d = 50; const result = []; for (let i = 0; i < bars.length; i++) { const start = Math.max(0, i - n + 1); let maxHigh = bars[start].high; let minLow = bars[start].low; for (let m = start; m <= i; m++) { maxHigh = Math.max(maxHigh, bars[m].high); minLow = Math.min(minLow, bars[m].low); } const rsv = maxHigh === minLow ? 50 : ((bars[i].close - minLow) / (maxHigh - minLow)) * 100; k = (2 / 3) * k + (1 / 3) * rsv; d = (2 / 3) * d + (1 / 3) * k; result.push({ time: bars[i].time, k: Number(k.toFixed(4)), d: Number(d.toFixed(4)), j: Number((3 * k - 2 * d).toFixed(4)), }); } return result; }JavaScript里没有浮点精度问题,但1/3这类运算会得到很长的浮点数,输出前建议用toFixed做一下格式化。Node.js版本做实时更新的话,要注意把分钟K线按时间追加到队列尾部,不要每次全量重算,否则数据量一上来CPU占用会比较难看。
5.3 C#版:Windows桌面工具直接用
C#在Windows量化客户端和本地分析工具里依然很有存在感。核心计算逻辑可以写成下面的方法,返回K、D、J三个数组:
public static (double[] K, double[] D, double[] J) CalcKDJ( double[] high, double[] low, double[] close, int n) { int len = close.Length; var k = new double[len]; var d = new double[len]; var j = new double[len]; double preK = 50, preD = 50; for (int i = 0; i < len; i++) { int start = Math.Max(0, i - n + 1); double maxHigh = high[start]; double minLow = low[start]; for (int m = start; m <= i; m++) { maxHigh = Math.Max(maxHigh, high[m]); minLow = Math.Min(minLow, low[m]); } double rsv = maxHigh == minLow ? 50 : (close[i] - minLow) / (maxHigh - minLow) * 100.0; double curK = preK * 2 / 3.0 + rsv / 3.0; double curD = preD * 2 / 3.0 + curK / 3.0; k[i] = curK; d[i] = curD; j[i] = 3 * curK - 2 * curD; preK = curK; preD = curD; } return (k, d, j); }C#里的注意点和Java类似,2/3这种整数除法也要写成2/3.0,否则结果完全不同。如果要用LINQ或ArraySegment优化循环性能,可以后面慢慢做,初期先把这段跑通最实在。
6. 多语言实现对比与分时数据处理的常见坑
五门语言都实现过以后,我的体感很明确:语言不是瓶颈,数据才是。KDJ计算本身不到100行,数据获取和校验反而占了整个项目大部分工作量。下面把几种语言的差异和落地过程中的高频坑总结一下。
6.1 五门语言实现同一KDJ的真实体感
对比维度可以从开发效率、运行性能、部署难度和适用场景四个角度看:
| 语言 | 开发效率 | 性能 | 部署 | 最适合的场景 |
|---|---|---|---|---|
| Python | 最高 | 中低 | 依赖库较多 | 快速研究、回测、一次性计算 |
| Java | 中 | 高 | 需要JVM | 企业后端、定时任务、交易系统 |
| Go | 中 | 高 | 单二进制,最简单 | 批量计算服务、内部微服务 |
| Node.js | 中高 | 中 | 依赖Node运行时 | 行情看板、前端团队维护 |
| C# | 中 | 高 | 需要.NET运行库 | Windows桌面工具、本地分析 |
我的建议是:研究阶段优先Python,因为pandas处理时间序列实在太顺手;生产阶段选Java或Go,稳定性更好且更容易做并发控制;如果只是搭个内部看板,Node.js完全够用。不要为了“统一技术栈”把研究工具卡死在一种语言上,计算逻辑跨语言复制的成本真的不高。
6.2 时间错乱、复权跳空、成交量溢出的实际案例
分时数据处理有三个坑我几乎每次都要提醒自己注意。
第一个是时间错乱。有些接口返回的时间是UTC字符串,直接转成LocalDateTime会丢掉8小时时差,导致分钟K线和真实交易时间对不上。解决方式是在数据接入层统一做一个时区转换,并且以11:30和15:00两个收盘节点为锚点检查时间序列。第二个是复权跳空。除权除息日当天,股价会有一个理论缺口,如果拿前复权数据计算,历史和当天的价格基准不同,RSV会突变。历史分时KDJ研究建议使用明确的复权口径,并在计算前确认数据源是否已经复权。第三个是成交量精度。KDJ用不到成交量,但后续做量能确认时会用到。成交量超过int范围在某些语言里会溢出,建议直接用long或double读取,别为了省内存用short。
6.3 从“能算出来”到“敢用结果”的几道检查
计算逻辑没报错不代表结果可信。我第一次做出历史分时KDJ之后,和行情软件的分时KDJ曲线对了一下,发现K值相差很大。排查了半天才发现,问题不是公式,而是数据源返回的分钟K线里,有一根high、low反了,导致某个窗口的RSV算成了负数。从那次以后,我每次都会做下面几道检查。
第一,抽一天行情波动大的股票做回放,把KDJ曲线叠在1分钟收盘价曲线上,看金叉死叉是否出现在行情拐点附近。第二,找个自己能信任的行情软件,手动对照同一时段的K、D、J值,误差在0.5以内就说明公式和数据处理过程基本一致;如果差很多,优先检查初值和复权口径。第三,做策略回测时,不要用KDJ单一信号直接下单,分钟级指标噪声很大,最好加一个持续时间过滤,比如J值进入超买区域后至少持续3分钟再触发卖出逻辑。
按我自己的习惯,凡是拿到历史分钟数据,第一件事永远是打印前20行和最后20行,看时间是否连续、收盘价是否合理。KDJ公式本身不会错,错的大部分是数据本身。数据校验过关了,Python还是Java,只是工具选择问题。这个链路跑通之后,后续不管接入实时行情还是扩展其他指标,都会顺很多。