feat(selection): 集成 B1 FastDTW 图形评分

This commit is contained in:
yuxuanhui
2026-08-31 16:14:16 +08:00
parent 86762c0d9a
commit 6ce291e242
51 changed files with 2917 additions and 41 deletions
@@ -0,0 +1,5 @@
# FastDTW v1 离线基线
十个 CSV 仅保留原项目固定案例突破日前最后 25 个升序交易日,测试运行不读取原项目、网络或数据库。`golden.json` 使用修正后可工作的 FastDTW、标量欧氏距离与 `radius=1` 离线生成;它有意不兼容原项目实际执行的 simple-DTW fallback。
25 行窗口不足以产生 114 日多空线。领域 extractor 将这些旧公式产生的非有限中间值显式转换为 `None`,matcher 按旧比较的最终效果记为零相似度,保证 dataclass、JSONB 和 HTTP 不包含 `NaN`/`Infinity`。案例库现在要求十例各 25 行完整 OHLCV,不再静默接受部分案例。
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-04-01,27.93,29.03,27.8,28.94,27218.84,5612754000
2025-04-02,28.9,29.18,28.69,28.94,12933.01,5612754000
2025-04-03,28.71,29.07,28.54,28.73,11232.75,5612754000
2025-04-07,27.93,27.93,23.19,24.02,37675.08,5612754000
2025-04-08,24.03,25.03,24.03,24.86,16676.65,5612754000
2025-04-09,24.49,24.87,23.12,24.73,14468.45,5612754000
2025-04-10,24.96,25.53,24.89,25.1,11065.87,5612754000
2025-04-11,25.02,25.91,24.7,25.67,11201.14,5612754000
2025-04-14,25.75,26.88,25.75,26.29,14566.38,5612754000
2025-04-15,26.41,27.19,26.09,26.17,10132.02,5612754000
2025-04-16,26.04,26.71,25.88,26.38,13525.38,5612754000
2025-04-17,26.11,28.69,26.04,28.39,42729.26,5612754000
2025-04-18,28.83,29.54,27.91,28.51,48214.96,5612754000
2025-04-21,28.83,31.25,27.94,30.52,96121.97,5612754000
2025-04-22,30.52,35.02,30.52,32.77,148408.79,5612754000
2025-04-23,32.43,33.71,30.92,32.34,56626.78,5612754000
2025-04-24,32.44,34.67,32.44,34.18,48618.11,5612754000
2025-04-25,34.18,34.55,30.24,30.67,75477.69,5612754000
2025-04-28,30.93,32.63,30.24,31.15,62031.09,5612754000
2025-04-29,31.8,32.54,30.97,31.39,34211.51,5612754000
2025-04-30,32.02,32.02,29.92,30.19,48359.44,5612754000
2025-05-06,30.24,30.62,29.2,29.5,36216.67,5612754000
2025-05-07,29.67,30.34,29.18,29.54,26316.22,5612754000
2025-05-08,29.54,29.94,29.26,29.82,20883.91,5612754000
2025-05-09,29.82,30.44,29.32,29.44,16659.71,5612754000
1 date open high low close volume market_cap
2 2025-04-01 27.93 29.03 27.8 28.94 27218.84 5612754000
3 2025-04-02 28.9 29.18 28.69 28.94 12933.01 5612754000
4 2025-04-03 28.71 29.07 28.54 28.73 11232.75 5612754000
5 2025-04-07 27.93 27.93 23.19 24.02 37675.08 5612754000
6 2025-04-08 24.03 25.03 24.03 24.86 16676.65 5612754000
7 2025-04-09 24.49 24.87 23.12 24.73 14468.45 5612754000
8 2025-04-10 24.96 25.53 24.89 25.1 11065.87 5612754000
9 2025-04-11 25.02 25.91 24.7 25.67 11201.14 5612754000
10 2025-04-14 25.75 26.88 25.75 26.29 14566.38 5612754000
11 2025-04-15 26.41 27.19 26.09 26.17 10132.02 5612754000
12 2025-04-16 26.04 26.71 25.88 26.38 13525.38 5612754000
13 2025-04-17 26.11 28.69 26.04 28.39 42729.26 5612754000
14 2025-04-18 28.83 29.54 27.91 28.51 48214.96 5612754000
15 2025-04-21 28.83 31.25 27.94 30.52 96121.97 5612754000
16 2025-04-22 30.52 35.02 30.52 32.77 148408.79 5612754000
17 2025-04-23 32.43 33.71 30.92 32.34 56626.78 5612754000
18 2025-04-24 32.44 34.67 32.44 34.18 48618.11 5612754000
19 2025-04-25 34.18 34.55 30.24 30.67 75477.69 5612754000
20 2025-04-28 30.93 32.63 30.24 31.15 62031.09 5612754000
21 2025-04-29 31.8 32.54 30.97 31.39 34211.51 5612754000
22 2025-04-30 32.02 32.02 29.92 30.19 48359.44 5612754000
23 2025-05-06 30.24 30.62 29.2 29.5 36216.67 5612754000
24 2025-05-07 29.67 30.34 29.18 29.54 26316.22 5612754000
25 2025-05-08 29.54 29.94 29.26 29.82 20883.91 5612754000
26 2025-05-09 29.82 30.44 29.32 29.44 16659.71 5612754000
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-07-02,10.88,11.1,10.57,10.67,1287084.86,12045490456
2025-07-03,10.62,10.9,10.54,10.82,991702.0,12045490456
2025-07-04,10.83,10.88,10.4,10.45,855100.82,12045490456
2025-07-07,10.28,11.26,10.28,10.98,1216456.82,12045490456
2025-07-08,10.89,11.52,10.81,11.15,1558398.35,12045490456
2025-07-09,11.18,11.23,10.79,10.84,1033990.38,12045490456
2025-07-10,11.27,11.86,10.93,11.64,2056513.24,12045490456
2025-07-11,11.87,12.46,11.53,12.13,2320402.62,12045490456
2025-07-14,12.19,12.46,11.57,11.62,1491417.84,12045490456
2025-07-15,11.58,12.78,11.58,12.2,2460847.71,12045490456
2025-07-16,11.98,12.04,11.27,11.32,1938934.42,12045490456
2025-07-17,11.08,11.49,10.97,11.4,1018625.42,12045490456
2025-07-18,11.34,12.14,11.32,11.68,1574602.28,12045490456
2025-07-21,11.6,11.98,11.57,11.8,1226347.09,12045490456
2025-07-22,11.68,12.0,11.47,11.56,985223.02,12045490456
2025-07-23,11.46,11.73,11.2,11.5,751845.98,12045490456
2025-07-24,11.42,12.4,11.39,12.27,1884541.46,12045490456
2025-07-25,12.22,13.06,12.12,12.61,1848357.03,12045490456
2025-07-28,12.91,12.97,12.61,12.69,1106575.15,12045490456
2025-07-29,12.41,12.64,12.28,12.4,794365.97,12045490456
2025-07-30,12.38,12.44,11.83,12.09,880349.27,12045490456
2025-07-31,11.97,12.13,11.75,11.81,547576.88,12045490456
2025-08-01,11.8,11.8,11.54,11.58,448552.57,12045490456
2025-08-04,11.6,11.68,11.51,11.63,404376.06,12045490456
2025-08-05,11.8,11.89,11.63,11.68,518346.76,12045490456
1 date open high low close volume market_cap
2 2025-07-02 10.88 11.1 10.57 10.67 1287084.86 12045490456
3 2025-07-03 10.62 10.9 10.54 10.82 991702.0 12045490456
4 2025-07-04 10.83 10.88 10.4 10.45 855100.82 12045490456
5 2025-07-07 10.28 11.26 10.28 10.98 1216456.82 12045490456
6 2025-07-08 10.89 11.52 10.81 11.15 1558398.35 12045490456
7 2025-07-09 11.18 11.23 10.79 10.84 1033990.38 12045490456
8 2025-07-10 11.27 11.86 10.93 11.64 2056513.24 12045490456
9 2025-07-11 11.87 12.46 11.53 12.13 2320402.62 12045490456
10 2025-07-14 12.19 12.46 11.57 11.62 1491417.84 12045490456
11 2025-07-15 11.58 12.78 11.58 12.2 2460847.71 12045490456
12 2025-07-16 11.98 12.04 11.27 11.32 1938934.42 12045490456
13 2025-07-17 11.08 11.49 10.97 11.4 1018625.42 12045490456
14 2025-07-18 11.34 12.14 11.32 11.68 1574602.28 12045490456
15 2025-07-21 11.6 11.98 11.57 11.8 1226347.09 12045490456
16 2025-07-22 11.68 12.0 11.47 11.56 985223.02 12045490456
17 2025-07-23 11.46 11.73 11.2 11.5 751845.98 12045490456
18 2025-07-24 11.42 12.4 11.39 12.27 1884541.46 12045490456
19 2025-07-25 12.22 13.06 12.12 12.61 1848357.03 12045490456
20 2025-07-28 12.91 12.97 12.61 12.69 1106575.15 12045490456
21 2025-07-29 12.41 12.64 12.28 12.4 794365.97 12045490456
22 2025-07-30 12.38 12.44 11.83 12.09 880349.27 12045490456
23 2025-07-31 11.97 12.13 11.75 11.81 547576.88 12045490456
24 2025-08-01 11.8 11.8 11.54 11.58 448552.57 12045490456
25 2025-08-04 11.6 11.68 11.51 11.63 404376.06 12045490456
26 2025-08-05 11.8 11.89 11.63 11.68 518346.76 12045490456
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-05-15,17.33,17.42,17.1,17.25,20642.05,11720226798
2025-05-16,17.18,17.73,17.17,17.43,35369.48,11720226798
2025-05-19,17.48,17.48,17.08,17.25,25489.78,11720226798
2025-05-20,17.36,17.74,17.32,17.53,36708.2,11720226798
2025-05-21,17.72,18.22,17.47,17.72,41463.24,11720226798
2025-05-22,17.62,17.81,17.37,17.58,40314.58,11720226798
2025-05-23,17.52,17.97,17.47,17.51,46281.21,11720226798
2025-05-26,17.63,17.63,17.05,17.09,38830.29,11720226798
2025-05-27,17.17,17.32,17.0,17.16,42731.45,11720226798
2025-05-28,17.17,18.4,17.08,18.21,123423.01,11720226798
2025-05-29,18.44,20.16,18.36,19.65,194317.79,11720226798
2025-05-30,19.74,19.96,19.39,19.76,132173.99,11720226798
2025-06-03,19.86,22.94,19.85,22.36,290301.1,11720226798
2025-06-04,22.17,22.76,21.58,22.54,199596.74,11720226798
2025-06-05,22.54,23.42,21.96,23.31,231289.99,11720226798
2025-06-06,23.01,23.11,21.66,22.86,233436.91,11720226798
2025-06-09,22.76,24.44,22.76,23.71,261851.09,11720226798
2025-06-10,23.69,23.82,22.66,22.81,190046.45,11720226798
2025-06-11,22.89,23.06,22.32,22.37,116651.04,11720226798
2025-06-12,22.64,24.05,22.18,23.28,190460.15,11720226798
2025-06-13,23.16,23.64,22.72,22.88,106830.32,11720226798
2025-06-16,22.88,23.32,22.51,22.75,70989.1,11720226798
2025-06-17,23.23,23.41,21.97,22.18,139623.83,11720226798
2025-06-18,21.85,22.29,21.61,22.22,100081.97,11720226798
2025-06-19,22.22,22.49,21.31,21.44,76485.29,11720226798
1 date open high low close volume market_cap
2 2025-05-15 17.33 17.42 17.1 17.25 20642.05 11720226798
3 2025-05-16 17.18 17.73 17.17 17.43 35369.48 11720226798
4 2025-05-19 17.48 17.48 17.08 17.25 25489.78 11720226798
5 2025-05-20 17.36 17.74 17.32 17.53 36708.2 11720226798
6 2025-05-21 17.72 18.22 17.47 17.72 41463.24 11720226798
7 2025-05-22 17.62 17.81 17.37 17.58 40314.58 11720226798
8 2025-05-23 17.52 17.97 17.47 17.51 46281.21 11720226798
9 2025-05-26 17.63 17.63 17.05 17.09 38830.29 11720226798
10 2025-05-27 17.17 17.32 17.0 17.16 42731.45 11720226798
11 2025-05-28 17.17 18.4 17.08 18.21 123423.01 11720226798
12 2025-05-29 18.44 20.16 18.36 19.65 194317.79 11720226798
13 2025-05-30 19.74 19.96 19.39 19.76 132173.99 11720226798
14 2025-06-03 19.86 22.94 19.85 22.36 290301.1 11720226798
15 2025-06-04 22.17 22.76 21.58 22.54 199596.74 11720226798
16 2025-06-05 22.54 23.42 21.96 23.31 231289.99 11720226798
17 2025-06-06 23.01 23.11 21.66 22.86 233436.91 11720226798
18 2025-06-09 22.76 24.44 22.76 23.71 261851.09 11720226798
19 2025-06-10 23.69 23.82 22.66 22.81 190046.45 11720226798
20 2025-06-11 22.89 23.06 22.32 22.37 116651.04 11720226798
21 2025-06-12 22.64 24.05 22.18 23.28 190460.15 11720226798
22 2025-06-13 23.16 23.64 22.72 22.88 106830.32 11720226798
23 2025-06-16 22.88 23.32 22.51 22.75 70989.1 11720226798
24 2025-06-17 23.23 23.41 21.97 22.18 139623.83 11720226798
25 2025-06-18 21.85 22.29 21.61 22.22 100081.97 11720226798
26 2025-06-19 22.22 22.49 21.31 21.44 76485.29 11720226798
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-06-18,4.65,4.86,4.6,4.83,2051281.76,39275697251
2025-06-19,4.79,4.98,4.75,4.78,1715941.43,39275697251
2025-06-20,4.77,4.81,4.63,4.65,1051952.17,39275697251
2025-06-23,4.6,4.75,4.57,4.7,934722.87,39275697251
2025-06-24,4.72,4.81,4.7,4.78,945714.0,39275697251
2025-06-25,4.8,4.85,4.73,4.81,1124786.7,39275697251
2025-06-26,4.86,5.05,4.83,4.94,2459293.2,39275697251
2025-06-27,4.94,5.42,4.85,5.27,4029657.48,39275697251
2025-06-30,5.25,5.43,5.25,5.34,2441261.2,39275697251
2025-07-01,5.31,5.38,5.24,5.3,1702111.13,39275697251
2025-07-02,5.26,5.28,5.05,5.08,1565861.38,39275697251
2025-07-03,5.08,5.59,5.08,5.59,4250014.47,39275697251
2025-07-04,5.6,5.74,5.52,5.6,4529145.33,39275697251
2025-07-07,5.5,5.79,5.49,5.58,2463078.1,39275697251
2025-07-08,5.55,5.95,5.53,5.78,3665165.9,39275697251
2025-07-09,5.75,5.82,5.65,5.69,2274246.96,39275697251
2025-07-10,5.67,5.76,5.51,5.58,2005171.32,39275697251
2025-07-11,5.57,5.58,5.39,5.5,1839462.11,39275697251
2025-07-14,5.51,5.55,5.42,5.44,1238426.57,39275697251
2025-07-15,5.45,5.6,5.4,5.47,2322143.38,39275697251
2025-07-16,5.29,5.47,5.29,5.36,1945350.4,39275697251
2025-07-17,5.33,5.57,5.3,5.48,2190584.97,39275697251
2025-07-18,5.47,5.65,5.45,5.5,2020531.6,39275697251
2025-07-21,5.52,5.66,5.43,5.48,1384268.25,39275697251
2025-07-22,5.45,5.55,5.35,5.37,1735870.73,39275697251
1 date open high low close volume market_cap
2 2025-06-18 4.65 4.86 4.6 4.83 2051281.76 39275697251
3 2025-06-19 4.79 4.98 4.75 4.78 1715941.43 39275697251
4 2025-06-20 4.77 4.81 4.63 4.65 1051952.17 39275697251
5 2025-06-23 4.6 4.75 4.57 4.7 934722.87 39275697251
6 2025-06-24 4.72 4.81 4.7 4.78 945714.0 39275697251
7 2025-06-25 4.8 4.85 4.73 4.81 1124786.7 39275697251
8 2025-06-26 4.86 5.05 4.83 4.94 2459293.2 39275697251
9 2025-06-27 4.94 5.42 4.85 5.27 4029657.48 39275697251
10 2025-06-30 5.25 5.43 5.25 5.34 2441261.2 39275697251
11 2025-07-01 5.31 5.38 5.24 5.3 1702111.13 39275697251
12 2025-07-02 5.26 5.28 5.05 5.08 1565861.38 39275697251
13 2025-07-03 5.08 5.59 5.08 5.59 4250014.47 39275697251
14 2025-07-04 5.6 5.74 5.52 5.6 4529145.33 39275697251
15 2025-07-07 5.5 5.79 5.49 5.58 2463078.1 39275697251
16 2025-07-08 5.55 5.95 5.53 5.78 3665165.9 39275697251
17 2025-07-09 5.75 5.82 5.65 5.69 2274246.96 39275697251
18 2025-07-10 5.67 5.76 5.51 5.58 2005171.32 39275697251
19 2025-07-11 5.57 5.58 5.39 5.5 1839462.11 39275697251
20 2025-07-14 5.51 5.55 5.42 5.44 1238426.57 39275697251
21 2025-07-15 5.45 5.6 5.4 5.47 2322143.38 39275697251
22 2025-07-16 5.29 5.47 5.29 5.36 1945350.4 39275697251
23 2025-07-17 5.33 5.57 5.3 5.48 2190584.97 39275697251
24 2025-07-18 5.47 5.65 5.45 5.5 2020531.6 39275697251
25 2025-07-21 5.52 5.66 5.43 5.48 1384268.25 39275697251
26 2025-07-22 5.45 5.55 5.35 5.37 1735870.73 39275697251
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-06-30,32.11,33.05,31.61,32.23,1421653.88,51810407315
2025-07-01,31.82,32.18,30.63,31.51,1313625.56,51810407315
2025-07-02,31.51,31.57,30.79,30.9,615208.98,51810407315
2025-07-03,31.16,31.21,30.49,30.96,810657.45,51810407315
2025-07-04,30.65,30.93,29.93,30.43,799767.87,51810407315
2025-07-07,30.43,30.67,30.09,30.22,482072.12,51810407315
2025-07-08,30.11,30.34,29.97,30.12,621125.66,51810407315
2025-07-09,30.19,30.86,29.73,29.83,1103713.14,51810407315
2025-07-10,29.58,30.08,29.46,29.7,591122.59,51810407315
2025-07-11,29.61,30.49,29.52,30.06,833099.95,51810407315
2025-07-14,30.07,30.42,29.69,29.86,504302.93,51810407315
2025-07-15,29.75,30.23,29.0,29.19,737141.8,51810407315
2025-07-16,29.18,29.45,29.01,29.21,367657.7,51810407315
2025-07-17,29.2,29.94,28.85,29.79,688212.84,51810407315
2025-07-18,30.09,31.56,29.9,31.0,1211206.13,51810407315
2025-07-21,30.98,31.37,30.36,31.07,772026.09,51810407315
2025-07-22,30.78,31.45,30.43,30.8,785708.74,51810407315
2025-07-23,30.56,30.57,29.89,29.92,703169.55,51810407315
2025-07-24,29.84,30.46,29.77,30.31,543627.27,51810407315
2025-07-25,30.36,31.13,30.36,30.45,619316.45,51810407315
2025-07-28,30.43,31.24,30.18,31.04,702169.14,51810407315
2025-07-29,30.79,31.15,30.33,30.69,542088.91,51810407315
2025-07-30,30.84,30.85,29.24,29.48,761650.21,51810407315
2025-07-31,29.34,30.01,28.94,29.12,470283.8,51810407315
2025-08-01,28.99,29.22,28.64,28.67,386407.75,51810407315
1 date open high low close volume market_cap
2 2025-06-30 32.11 33.05 31.61 32.23 1421653.88 51810407315
3 2025-07-01 31.82 32.18 30.63 31.51 1313625.56 51810407315
4 2025-07-02 31.51 31.57 30.79 30.9 615208.98 51810407315
5 2025-07-03 31.16 31.21 30.49 30.96 810657.45 51810407315
6 2025-07-04 30.65 30.93 29.93 30.43 799767.87 51810407315
7 2025-07-07 30.43 30.67 30.09 30.22 482072.12 51810407315
8 2025-07-08 30.11 30.34 29.97 30.12 621125.66 51810407315
9 2025-07-09 30.19 30.86 29.73 29.83 1103713.14 51810407315
10 2025-07-10 29.58 30.08 29.46 29.7 591122.59 51810407315
11 2025-07-11 29.61 30.49 29.52 30.06 833099.95 51810407315
12 2025-07-14 30.07 30.42 29.69 29.86 504302.93 51810407315
13 2025-07-15 29.75 30.23 29.0 29.19 737141.8 51810407315
14 2025-07-16 29.18 29.45 29.01 29.21 367657.7 51810407315
15 2025-07-17 29.2 29.94 28.85 29.79 688212.84 51810407315
16 2025-07-18 30.09 31.56 29.9 31.0 1211206.13 51810407315
17 2025-07-21 30.98 31.37 30.36 31.07 772026.09 51810407315
18 2025-07-22 30.78 31.45 30.43 30.8 785708.74 51810407315
19 2025-07-23 30.56 30.57 29.89 29.92 703169.55 51810407315
20 2025-07-24 29.84 30.46 29.77 30.31 543627.27 51810407315
21 2025-07-25 30.36 31.13 30.36 30.45 619316.45 51810407315
22 2025-07-28 30.43 31.24 30.18 31.04 702169.14 51810407315
23 2025-07-29 30.79 31.15 30.33 30.69 542088.91 51810407315
24 2025-07-30 30.84 30.85 29.24 29.48 761650.21 51810407315
25 2025-07-31 29.34 30.01 28.94 29.12 470283.8 51810407315
26 2025-08-01 28.99 29.22 28.64 28.67 386407.75 51810407315
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-06-27,19.98,22.09,19.61,22.09,276542.2,3567378360
2025-06-30,21.86,24.19,21.46,23.37,328614.33,3567378360
2025-07-01,22.39,22.77,21.46,21.46,246739.02,3567378360
2025-07-02,20.91,21.41,20.4,20.88,152920.45,3567378360
2025-07-03,20.81,22.49,20.72,22.05,226316.34,3567378360
2025-07-04,21.4,21.76,20.75,20.76,157410.92,3567378360
2025-07-07,20.47,21.17,20.32,21.0,89066.04,3567378360
2025-07-08,21.01,21.11,20.67,20.9,81440.83,3567378360
2025-07-09,20.91,21.39,20.42,20.51,94518.29,3567378360
2025-07-10,20.51,20.51,19.94,20.32,82854.2,3567378360
2025-07-11,20.42,20.6,20.12,20.41,63631.7,3567378360
2025-07-14,20.57,20.93,20.51,20.6,71670.49,3567378360
2025-07-15,20.45,20.78,20.13,20.47,70849.72,3567378360
2025-07-16,20.6,20.86,20.33,20.47,68310.79,3567378360
2025-07-17,20.26,20.6,19.92,20.51,62354.29,3567378360
2025-07-18,20.48,20.79,20.36,20.49,62896.87,3567378360
2025-07-21,20.36,20.97,20.12,20.52,68576.12,3567378360
2025-07-22,20.4,21.25,20.34,20.96,129095.1,3567378360
2025-07-23,20.84,20.88,20.09,20.17,96276.56,3567378360
2025-07-24,20.16,20.35,20.08,20.19,45888.62,3567378360
2025-07-25,20.21,20.21,19.97,20.08,38465.12,3567378360
2025-07-28,20.09,20.55,20.06,20.4,51218.04,3567378360
2025-07-29,20.34,20.54,19.79,19.93,61055.53,3567378360
2025-07-30,19.81,20.21,19.2,19.86,79996.39,3567378360
2025-07-31,19.66,19.99,19.37,19.48,43501.6,3567378360
1 date open high low close volume market_cap
2 2025-06-27 19.98 22.09 19.61 22.09 276542.2 3567378360
3 2025-06-30 21.86 24.19 21.46 23.37 328614.33 3567378360
4 2025-07-01 22.39 22.77 21.46 21.46 246739.02 3567378360
5 2025-07-02 20.91 21.41 20.4 20.88 152920.45 3567378360
6 2025-07-03 20.81 22.49 20.72 22.05 226316.34 3567378360
7 2025-07-04 21.4 21.76 20.75 20.76 157410.92 3567378360
8 2025-07-07 20.47 21.17 20.32 21.0 89066.04 3567378360
9 2025-07-08 21.01 21.11 20.67 20.9 81440.83 3567378360
10 2025-07-09 20.91 21.39 20.42 20.51 94518.29 3567378360
11 2025-07-10 20.51 20.51 19.94 20.32 82854.2 3567378360
12 2025-07-11 20.42 20.6 20.12 20.41 63631.7 3567378360
13 2025-07-14 20.57 20.93 20.51 20.6 71670.49 3567378360
14 2025-07-15 20.45 20.78 20.13 20.47 70849.72 3567378360
15 2025-07-16 20.6 20.86 20.33 20.47 68310.79 3567378360
16 2025-07-17 20.26 20.6 19.92 20.51 62354.29 3567378360
17 2025-07-18 20.48 20.79 20.36 20.49 62896.87 3567378360
18 2025-07-21 20.36 20.97 20.12 20.52 68576.12 3567378360
19 2025-07-22 20.4 21.25 20.34 20.96 129095.1 3567378360
20 2025-07-23 20.84 20.88 20.09 20.17 96276.56 3567378360
21 2025-07-24 20.16 20.35 20.08 20.19 45888.62 3567378360
22 2025-07-25 20.21 20.21 19.97 20.08 38465.12 3567378360
23 2025-07-28 20.09 20.55 20.06 20.4 51218.04 3567378360
24 2025-07-29 20.34 20.54 19.79 19.93 61055.53 3567378360
25 2025-07-30 19.81 20.21 19.2 19.86 79996.39 3567378360
26 2025-07-31 19.66 19.99 19.37 19.48 43501.6 3567378360
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-06-05,13.86,13.93,13.61,13.82,76839.0,9399394575
2025-06-06,13.83,14.01,13.66,13.7,69401.0,9399394575
2025-06-09,13.69,13.87,13.64,13.76,75300.24,9399394575
2025-06-10,13.7,13.74,12.99,13.18,174574.3,9399394575
2025-06-11,13.2,13.34,13.11,13.3,61388.02,9399394575
2025-06-12,13.26,13.35,13.14,13.21,46718.0,9399394575
2025-06-13,13.17,13.52,13.17,13.38,164443.0,9399394575
2025-06-16,13.48,13.75,13.17,13.68,140522.0,9399394575
2025-06-17,13.63,14.09,13.62,13.97,143405.8,9399394575
2025-06-18,13.98,14.72,13.89,14.72,275552.83,9399394575
2025-06-19,14.48,14.48,13.72,14.15,252934.0,9399394575
2025-06-20,14.15,14.16,13.75,13.8,127924.0,9399394575
2025-06-23,14.01,14.33,13.9,14.33,160493.0,9399394575
2025-06-24,14.19,14.87,13.84,14.54,252237.43,9399394575
2025-06-25,14.78,16.0,14.71,16.0,600588.02,9399394575
2025-06-26,16.0,17.6,15.98,16.63,846170.51,9399394575
2025-06-27,16.56,17.27,16.3,16.42,651687.06,9399394575
2025-06-30,16.58,17.57,16.58,17.54,612607.43,9399394575
2025-07-01,17.28,17.9,16.88,17.24,468426.25,9399394575
2025-07-02,17.18,17.18,16.42,16.61,337259.72,9399394575
2025-07-03,16.62,16.84,16.37,16.46,199869.31,9399394575
2025-07-04,16.37,16.45,16.04,16.1,180557.04,9399394575
2025-07-07,16.03,16.32,15.86,16.12,142471.31,9399394575
2025-07-08,15.98,16.16,15.91,16.07,122700.83,9399394575
2025-07-09,16.08,16.45,15.94,15.99,230184.09,9399394575
1 date open high low close volume market_cap
2 2025-06-05 13.86 13.93 13.61 13.82 76839.0 9399394575
3 2025-06-06 13.83 14.01 13.66 13.7 69401.0 9399394575
4 2025-06-09 13.69 13.87 13.64 13.76 75300.24 9399394575
5 2025-06-10 13.7 13.74 12.99 13.18 174574.3 9399394575
6 2025-06-11 13.2 13.34 13.11 13.3 61388.02 9399394575
7 2025-06-12 13.26 13.35 13.14 13.21 46718.0 9399394575
8 2025-06-13 13.17 13.52 13.17 13.38 164443.0 9399394575
9 2025-06-16 13.48 13.75 13.17 13.68 140522.0 9399394575
10 2025-06-17 13.63 14.09 13.62 13.97 143405.8 9399394575
11 2025-06-18 13.98 14.72 13.89 14.72 275552.83 9399394575
12 2025-06-19 14.48 14.48 13.72 14.15 252934.0 9399394575
13 2025-06-20 14.15 14.16 13.75 13.8 127924.0 9399394575
14 2025-06-23 14.01 14.33 13.9 14.33 160493.0 9399394575
15 2025-06-24 14.19 14.87 13.84 14.54 252237.43 9399394575
16 2025-06-25 14.78 16.0 14.71 16.0 600588.02 9399394575
17 2025-06-26 16.0 17.6 15.98 16.63 846170.51 9399394575
18 2025-06-27 16.56 17.27 16.3 16.42 651687.06 9399394575
19 2025-06-30 16.58 17.57 16.58 17.54 612607.43 9399394575
20 2025-07-01 17.28 17.9 16.88 17.24 468426.25 9399394575
21 2025-07-02 17.18 17.18 16.42 16.61 337259.72 9399394575
22 2025-07-03 16.62 16.84 16.37 16.46 199869.31 9399394575
23 2025-07-04 16.37 16.45 16.04 16.1 180557.04 9399394575
24 2025-07-07 16.03 16.32 15.86 16.12 142471.31 9399394575
25 2025-07-08 15.98 16.16 15.91 16.07 122700.83 9399394575
26 2025-07-09 16.08 16.45 15.94 15.99 230184.09 9399394575
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-06-27,20.43,20.56,20.04,20.27,54029.3,4707136785
2025-06-30,20.22,20.5,20.18,20.44,45101.98,4707136785
2025-07-01,20.43,20.56,20.05,20.3,46657.31,4707136785
2025-07-02,20.28,20.28,19.76,20.03,40227.67,4707136785
2025-07-03,20.1,20.18,19.73,19.89,29471.22,4707136785
2025-07-04,20.02,20.02,19.43,19.62,32150.52,4707136785
2025-07-07,19.61,19.85,19.4,19.78,24560.45,4707136785
2025-07-08,19.71,21.16,19.71,20.73,110928.39,4707136785
2025-07-09,21.04,21.22,20.32,20.47,80045.04,4707136785
2025-07-10,20.25,20.51,19.88,20.01,62948.74,4707136785
2025-07-11,19.93,21.34,19.73,21.0,126620.87,4707136785
2025-07-14,21.59,24.48,21.11,23.66,270019.12,4707136785
2025-07-15,23.57,24.31,23.06,23.93,231598.13,4707136785
2025-07-16,23.56,24.31,23.23,23.5,185860.26,4707136785
2025-07-17,23.37,24.42,23.03,23.5,162749.05,4707136785
2025-07-18,23.37,23.72,22.91,23.16,111343.75,4707136785
2025-07-21,23.44,24.61,23.14,24.02,176471.75,4707136785
2025-07-22,23.79,23.95,22.83,23.07,132565.5,4707136785
2025-07-23,22.9,23.07,22.38,22.71,71180.1,4707136785
2025-07-24,22.55,23.17,22.52,22.71,55999.01,4707136785
2025-07-25,22.63,22.87,22.42,22.61,54779.45,4707136785
2025-07-28,22.97,25.66,22.97,24.61,247298.15,4707136785
2025-07-29,24.08,24.45,23.8,24.14,144127.75,4707136785
2025-07-30,23.96,24.23,23.15,23.27,110016.38,4707136785
2025-07-31,23.07,23.58,22.74,22.87,84262.28,4707136785
1 date open high low close volume market_cap
2 2025-06-27 20.43 20.56 20.04 20.27 54029.3 4707136785
3 2025-06-30 20.22 20.5 20.18 20.44 45101.98 4707136785
4 2025-07-01 20.43 20.56 20.05 20.3 46657.31 4707136785
5 2025-07-02 20.28 20.28 19.76 20.03 40227.67 4707136785
6 2025-07-03 20.1 20.18 19.73 19.89 29471.22 4707136785
7 2025-07-04 20.02 20.02 19.43 19.62 32150.52 4707136785
8 2025-07-07 19.61 19.85 19.4 19.78 24560.45 4707136785
9 2025-07-08 19.71 21.16 19.71 20.73 110928.39 4707136785
10 2025-07-09 21.04 21.22 20.32 20.47 80045.04 4707136785
11 2025-07-10 20.25 20.51 19.88 20.01 62948.74 4707136785
12 2025-07-11 19.93 21.34 19.73 21.0 126620.87 4707136785
13 2025-07-14 21.59 24.48 21.11 23.66 270019.12 4707136785
14 2025-07-15 23.57 24.31 23.06 23.93 231598.13 4707136785
15 2025-07-16 23.56 24.31 23.23 23.5 185860.26 4707136785
16 2025-07-17 23.37 24.42 23.03 23.5 162749.05 4707136785
17 2025-07-18 23.37 23.72 22.91 23.16 111343.75 4707136785
18 2025-07-21 23.44 24.61 23.14 24.02 176471.75 4707136785
19 2025-07-22 23.79 23.95 22.83 23.07 132565.5 4707136785
20 2025-07-23 22.9 23.07 22.38 22.71 71180.1 4707136785
21 2025-07-24 22.55 23.17 22.52 22.71 55999.01 4707136785
22 2025-07-25 22.63 22.87 22.42 22.61 54779.45 4707136785
23 2025-07-28 22.97 25.66 22.97 24.61 247298.15 4707136785
24 2025-07-29 24.08 24.45 23.8 24.14 144127.75 4707136785
25 2025-07-30 23.96 24.23 23.15 23.27 110016.38 4707136785
26 2025-07-31 23.07 23.58 22.74 22.87 84262.28 4707136785
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-06-06,17.64,19.41,17.16,19.41,295488.65,4910064388
2025-06-09,20.53,21.35,20.53,21.35,162351.45,4910064388
2025-06-10,23.48,23.48,22.95,23.48,96161.81,4910064388
2025-06-11,25.5,25.83,24.22,25.83,538547.27,4910064388
2025-06-12,27.48,28.07,25.7,26.81,487592.83,4910064388
2025-06-13,25.98,26.92,25.23,26.07,302545.06,4910064388
2025-06-16,25.81,28.68,25.41,28.68,285190.35,4910064388
2025-06-17,30.1,31.49,28.19,28.7,386383.92,4910064388
2025-06-18,28.11,28.57,26.72,27.7,314113.19,4910064388
2025-06-19,28.59,30.13,27.97,28.73,229020.92,4910064388
2025-06-20,26.78,31.6,26.78,31.6,174055.39,4910064388
2025-06-23,31.05,31.95,28.44,31.67,234024.79,4910064388
2025-06-24,31.07,34.13,31.07,33.05,233411.76,4910064388
2025-06-25,31.9,34.7,31.85,32.5,242343.26,4910064388
2025-06-26,30.83,32.1,29.28,30.55,193713.48,4910064388
2025-06-27,30.57,33.6,30.26,33.6,131012.98,4910064388
2025-06-30,33.6,36.97,33.6,36.71,195320.19,4910064388
2025-07-01,35.84,40.38,35.83,40.38,161306.24,4910064388
2025-07-02,40.35,44.42,39.75,44.42,212138.21,4910064388
2025-07-03,40.56,48.08,40.56,44.44,176844.52,4910064388
2025-07-04,43.53,44.12,40.49,40.96,138196.86,4910064388
2025-07-07,42.76,42.76,39.49,41.12,105590.86,4910064388
2025-07-08,41.36,41.5,38.07,39.52,105213.28,4910064388
2025-07-09,39.31,40.63,37.92,39.33,97899.1,4910064388
2025-07-10,39.46,39.56,36.96,37.43,79650.61,4910064388
1 date open high low close volume market_cap
2 2025-06-06 17.64 19.41 17.16 19.41 295488.65 4910064388
3 2025-06-09 20.53 21.35 20.53 21.35 162351.45 4910064388
4 2025-06-10 23.48 23.48 22.95 23.48 96161.81 4910064388
5 2025-06-11 25.5 25.83 24.22 25.83 538547.27 4910064388
6 2025-06-12 27.48 28.07 25.7 26.81 487592.83 4910064388
7 2025-06-13 25.98 26.92 25.23 26.07 302545.06 4910064388
8 2025-06-16 25.81 28.68 25.41 28.68 285190.35 4910064388
9 2025-06-17 30.1 31.49 28.19 28.7 386383.92 4910064388
10 2025-06-18 28.11 28.57 26.72 27.7 314113.19 4910064388
11 2025-06-19 28.59 30.13 27.97 28.73 229020.92 4910064388
12 2025-06-20 26.78 31.6 26.78 31.6 174055.39 4910064388
13 2025-06-23 31.05 31.95 28.44 31.67 234024.79 4910064388
14 2025-06-24 31.07 34.13 31.07 33.05 233411.76 4910064388
15 2025-06-25 31.9 34.7 31.85 32.5 242343.26 4910064388
16 2025-06-26 30.83 32.1 29.28 30.55 193713.48 4910064388
17 2025-06-27 30.57 33.6 30.26 33.6 131012.98 4910064388
18 2025-06-30 33.6 36.97 33.6 36.71 195320.19 4910064388
19 2025-07-01 35.84 40.38 35.83 40.38 161306.24 4910064388
20 2025-07-02 40.35 44.42 39.75 44.42 212138.21 4910064388
21 2025-07-03 40.56 48.08 40.56 44.44 176844.52 4910064388
22 2025-07-04 43.53 44.12 40.49 40.96 138196.86 4910064388
23 2025-07-07 42.76 42.76 39.49 41.12 105590.86 4910064388
24 2025-07-08 41.36 41.5 38.07 39.52 105213.28 4910064388
25 2025-07-09 39.31 40.63 37.92 39.33 97899.1 4910064388
26 2025-07-10 39.46 39.56 36.96 37.43 79650.61 4910064388
@@ -0,0 +1,26 @@
date,open,high,low,close,volume,market_cap
2025-09-30,7.57,7.76,7.57,7.74,204915.56,22650295811
2025-10-09,7.75,7.8,7.68,7.8,196793.96,22650295811
2025-10-10,7.79,7.82,7.73,7.75,163527.18,22650295811
2025-10-13,7.6,7.8,7.47,7.8,208009.58,22650295811
2025-10-14,7.82,7.9,7.73,7.78,203765.38,22650295811
2025-10-15,7.77,7.78,7.67,7.75,158196.56,22650295811
2025-10-16,7.74,7.76,7.61,7.63,151268.43,22650295811
2025-10-17,7.62,7.72,7.47,7.48,162246.05,22650295811
2025-10-20,7.55,7.61,7.51,7.58,122212.11,22650295811
2025-10-21,7.58,7.67,7.56,7.64,121825.06,22650295811
2025-10-22,7.64,7.86,7.58,7.82,322717.34,22650295811
2025-10-23,7.8,7.82,7.67,7.81,170156.0,22650295811
2025-10-24,8.1,8.3,7.95,7.98,615299.42,22650295811
2025-10-27,8.0,8.18,7.94,8.04,433223.34,22650295811
2025-10-28,7.99,8.84,7.97,8.84,1610159.98,22650295811
2025-10-29,8.6,9.0,8.41,8.75,1722050.97,22650295811
2025-10-30,8.7,8.82,8.51,8.6,1035934.51,22650295811
2025-10-31,8.57,8.62,8.34,8.37,686044.09,22650295811
2025-11-03,8.37,8.61,8.33,8.6,748009.31,22650295811
2025-11-04,8.52,9.26,8.5,8.98,1365750.29,22650295811
2025-11-05,8.76,8.91,8.67,8.81,823459.67,22650295811
2025-11-06,8.77,8.8,8.6,8.65,553188.01,22650295811
2025-11-07,8.67,8.75,8.56,8.67,592496.69,22650295811
2025-11-10,8.74,8.79,8.51,8.53,513705.42,22650295811
2025-11-11,8.48,8.54,8.39,8.47,440314.81,22650295811
1 date open high low close volume market_cap
2 2025-09-30 7.57 7.76 7.57 7.74 204915.56 22650295811
3 2025-10-09 7.75 7.8 7.68 7.8 196793.96 22650295811
4 2025-10-10 7.79 7.82 7.73 7.75 163527.18 22650295811
5 2025-10-13 7.6 7.8 7.47 7.8 208009.58 22650295811
6 2025-10-14 7.82 7.9 7.73 7.78 203765.38 22650295811
7 2025-10-15 7.77 7.78 7.67 7.75 158196.56 22650295811
8 2025-10-16 7.74 7.76 7.61 7.63 151268.43 22650295811
9 2025-10-17 7.62 7.72 7.47 7.48 162246.05 22650295811
10 2025-10-20 7.55 7.61 7.51 7.58 122212.11 22650295811
11 2025-10-21 7.58 7.67 7.56 7.64 121825.06 22650295811
12 2025-10-22 7.64 7.86 7.58 7.82 322717.34 22650295811
13 2025-10-23 7.8 7.82 7.67 7.81 170156.0 22650295811
14 2025-10-24 8.1 8.3 7.95 7.98 615299.42 22650295811
15 2025-10-27 8.0 8.18 7.94 8.04 433223.34 22650295811
16 2025-10-28 7.99 8.84 7.97 8.84 1610159.98 22650295811
17 2025-10-29 8.6 9.0 8.41 8.75 1722050.97 22650295811
18 2025-10-30 8.7 8.82 8.51 8.6 1035934.51 22650295811
19 2025-10-31 8.57 8.62 8.34 8.37 686044.09 22650295811
20 2025-11-03 8.37 8.61 8.33 8.6 748009.31 22650295811
21 2025-11-04 8.52 9.26 8.5 8.98 1365750.29 22650295811
22 2025-11-05 8.76 8.91 8.67 8.81 823459.67 22650295811
23 2025-11-06 8.77 8.8 8.6 8.65 553188.01 22650295811
24 2025-11-07 8.67 8.75 8.56 8.67 592496.69 22650295811
25 2025-11-10 8.74 8.79 8.51 8.53 513705.42 22650295811
26 2025-11-11 8.48 8.54 8.39 8.47 440314.81 22650295811
@@ -0,0 +1,43 @@
{
"algorithm": {
"version": "zhixing_b1_pattern_fastdtw_v1",
"radius": 1,
"distance": "scalar_euclidean",
"lookback_days": 25,
"threshold": 60.0,
"weights": [0.10, 0.20, 0.25, 0.45]
},
"self_match": {
"status": "matched",
"value": 95.0,
"case_id": "case_001",
"breakdown": {
"trend_structure": 50.0,
"kdj_state": 100.0,
"volume_pattern": 100.0,
"price_shape": 100.0
}
},
"time_warped": {
"status": "matched",
"value": 78.38,
"case_id": "case_001",
"breakdown": {
"trend_structure": 44.51,
"kdj_state": 77.15,
"volume_pattern": 65.0,
"price_shape": 93.89
}
},
"below_threshold": {
"status": "below_threshold",
"value": 46.27,
"case_id": "case_010",
"breakdown": {
"trend_structure": 28.33,
"kdj_state": 91.79,
"volume_pattern": 27.5,
"price_shape": 40.45
}
}
}
@@ -26,7 +26,8 @@ def test_postgres_migration_creates_market_data_contract(
engine: Engine = create_engine(sqlalchemy_url)
command.upgrade(config, "head")
try:
tables = set(inspect(engine).get_table_names())
inspector = inspect(engine)
tables = set(inspector.get_table_names())
assert {
"market_stock",
"market_daily_bar",
@@ -39,6 +40,25 @@ def test_postgres_migration_creates_market_data_contract(
"selection_run_item",
"selection_signal",
} <= tables
item_columns = {column["name"] for column in inspector.get_columns("selection_run_item")}
assert {
"score_status",
"score_value",
"score_threshold",
"score_version",
"match_case_id",
"match_case_name",
"match_case_breakout_date",
"match_breakdown",
"score_reason",
} <= item_columns
constraint_names = {
constraint["name"]
for constraint in inspector.get_check_constraints("selection_run_item")
}
assert "ck_selection_run_item_breakdown_range" in constraint_names
index_names = {index["name"] for index in inspector.get_indexes("selection_run_item")}
assert "ix_selection_run_item_score" in index_names
finally:
engine.dispose()
get_settings.cache_clear()
+100
View File
@@ -1,5 +1,6 @@
"""HTTP contracts for triggering and querying persisted selection runs."""
from dataclasses import replace
from datetime import date
from decimal import Decimal
@@ -11,6 +12,12 @@ from zhixing_server.bootstrap.app import create_app
from zhixing_server.bootstrap.config import Settings
from zhixing_server.modules.selection.application.run import PreparedSelectionRun
from zhixing_server.modules.selection.domain.models import SelectionSignal, ZhixingB1Category
from zhixing_server.modules.selection.domain.pattern_scoring import (
PATTERN_SCORING_VERSION,
ZHIXING_B1_PATTERN_CASES,
PatternScore,
PatternScoreBreakdown,
)
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionRerunRequired,
@@ -130,6 +137,14 @@ def _run(run_id: str, status: str) -> SelectionRun:
name="平安银行",
status="selected",
signal_count=2,
pattern_score=PatternScore(
status="matched",
value=86.4,
threshold=60.0,
version=PATTERN_SCORING_VERSION,
case=ZHIXING_B1_PATTERN_CASES[0],
breakdown=PatternScoreBreakdown(71.2, 83.0, 88.0, 90.1),
),
signals=(original_signal, pullback_signal),
),
),
@@ -238,6 +253,24 @@ def test_query_returns_persisted_signal_details() -> None:
assert "signals" not in body
assert len(body["stocks"]) == 1
assert body["stocks"][0]["ts_code"] == "000001.SZ"
assert body["stocks"][0]["score"] == {
"status": "matched",
"value": 86.4,
"threshold": 60.0,
"version": PATTERN_SCORING_VERSION,
"case": {
"id": "case_001",
"name": "华纳药厂",
"breakout_date": "2025-05-12",
},
"breakdown": {
"trend_structure": 71.2,
"kdj_state": 83.0,
"volume_pattern": 88.0,
"price_shape": 90.1,
},
"reason": None,
}
assert [signal["category"] for signal in body["stocks"][0]["signals"]] == [
"zhixing_b1_original_b1",
"zhixing_b1_pullback_white",
@@ -248,6 +281,59 @@ def test_query_returns_persisted_signal_details() -> None:
]
@pytest.mark.parametrize(
("pattern_score", "expected_score"),
[
(
PatternScore(
status="below_threshold",
value=42.5,
threshold=60.0,
version=PATTERN_SCORING_VERSION,
case=ZHIXING_B1_PATTERN_CASES[0],
breakdown=PatternScoreBreakdown(40.0, 42.0, 43.0, 44.0),
),
{
"status": "below_threshold",
"value": None,
"threshold": 60.0,
"version": PATTERN_SCORING_VERSION,
"case": None,
"breakdown": None,
"reason": "未匹配到评分阈值以上案例",
},
),
(
PatternScore.failed("FastDTW unavailable"),
{
"status": "failed",
"value": None,
"threshold": None,
"version": None,
"case": None,
"breakdown": None,
"reason": "FastDTW unavailable",
},
),
(PatternScore(), None),
],
)
def test_query_preserves_signals_for_every_pattern_score_state(
pattern_score: PatternScore,
expected_score: dict[str, object] | None,
) -> None:
run = _run("run-http", "success")
run = replace(run, items=(replace(run.items[0], pattern_score=pattern_score),))
response = _client(FakeSelectionService(run)).get("/api/v1/selection/results")
assert response.status_code == 200
stock = response.json()["stocks"][0]
assert stock["score"] == expected_score
assert len(stock["signals"]) == 2
assert response.json()["failures"] == []
def test_query_forwards_pagination_and_filters() -> None:
service = FakeSelectionService(_run("run-http", "success"))
@@ -259,6 +345,7 @@ def test_query_forwards_pagination_and_filters() -> None:
"page_size": 5,
"search": " 平安银行 ",
"category": "original",
"sort": "score_desc",
},
)
@@ -268,6 +355,7 @@ def test_query_forwards_pagination_and_filters() -> None:
page_size=5,
search="平安银行",
category="original",
sort="score_desc",
)
assert response.json()["page"] == 2
assert response.json()["page_size"] == 5
@@ -282,6 +370,18 @@ def test_query_rejects_invalid_page_size() -> None:
assert response.status_code == 422
def test_query_forwards_score_ascending_sort() -> None:
service = FakeSelectionService(_run("run-http", "success"))
response = _client(service).get(
"/api/v1/selection/results",
params={"strategy": "zhixing_b1", "sort": "score_asc"},
)
assert response.status_code == 200
assert service.last_query == SelectionResultQuery(sort="score_asc")
def test_run_polling_returns_the_persisted_terminal_result() -> None:
response = _client(FakeSelectionService(_run("run-http", "success"))).get(
"/api/v1/selection/runs/run-http"
@@ -0,0 +1,182 @@
"""Golden and invariant tests for versioned B1 FastDTW scoring."""
from __future__ import annotations
import json
from datetime import date, timedelta
from pathlib import Path
from typing import cast
import pandas as pd
import pytest
from zhixing_server.modules.selection.domain.models import SelectionBar, StockHistory
from zhixing_server.modules.selection.domain.pattern_scoring import (
PATTERN_FASTDTW_RADIUS,
PATTERN_SCORING_VERSION,
ZHIXING_B1_PATTERN_CASES,
PatternCase,
PatternCaseLibraryError,
PatternFeatures,
PatternScore,
PatternScoreBreakdown,
PatternScoringError,
ZhixingB1PatternScorer,
build_pattern_case,
)
FIXTURES = Path(__file__).parents[2] / "fixtures" / "selection" / "zhixing_b1" / "pattern_scoring"
def _history(case_id: str, ts_code: str, name: str) -> StockHistory:
frame = pd.read_csv(FIXTURES / f"{case_id}.csv")
bars = tuple(
SelectionBar(
trade_date=date.fromisoformat(str(row.date)),
open=float(str(row.open)),
high=float(str(row.high)),
low=float(str(row.low)),
close=float(str(row.close)),
volume=float(str(row.volume)),
)
for row in frame.itertuples(index=False)
)
return StockHistory(ts_code=ts_code, name=name, bars=bars)
def _cases() -> tuple[PatternCase, ...]:
return tuple(
build_pattern_case(
definition,
_history(definition.id, definition.ts_code, definition.name),
)
for definition in ZHIXING_B1_PATTERN_CASES
)
def _golden(name: str) -> dict[str, object]:
payload = cast(dict[str, object], json.loads((FIXTURES / "golden.json").read_text()))
return cast(dict[str, object], payload[name])
def _assert_golden(score: PatternScore, expected: dict[str, object]) -> None:
assert score.status == expected["status"]
assert score.value == expected["value"]
assert score.case is not None
assert score.case.id == expected["case_id"]
assert score.breakdown is not None
assert score.breakdown.as_dict() == expected["breakdown"]
def test_fastdtw_v1_self_match_golden_is_finite_and_deterministic() -> None:
cases = _cases()
scorer = ZhixingB1PatternScorer()
first = scorer.score(cases[0].history, cases)
second = scorer.score(cases[0].history, cases)
assert PATTERN_SCORING_VERSION == "zhixing_b1_pattern_fastdtw_v1"
assert PATTERN_FASTDTW_RADIUS == 1
assert first == second
_assert_golden(first, _golden("self_match"))
assert cases[0].features.trend_structure["short_vs_bullbear"] is None
def test_fastdtw_v1_time_warped_curve_golden() -> None:
cases = _cases()
base = cases[0].history.bars
delayed = base[:1] * 3 + base[:-3]
bars = tuple(
SelectionBar(
trade_date=base[index].trade_date,
open=delayed[index].open,
high=delayed[index].high,
low=delayed[index].low,
close=delayed[index].close,
volume=delayed[index].volume,
)
for index in range(25)
)
result = ZhixingB1PatternScorer().score(
StockHistory(ts_code="TEST.SZ", name="time warped", bars=bars),
cases,
)
_assert_golden(result, _golden("time_warped"))
def test_below_threshold_golden_remains_a_successful_computation() -> None:
bars = tuple(
SelectionBar(
trade_date=date(2026, 1, 1) + timedelta(days=index),
open=100.0 if index % 2 == 0 else 1.0,
high=110.0,
low=0.9,
close=1.0 if index % 2 == 0 else 100.0,
volume=1.0 if index < 13 else 1_000_000.0,
)
for index in range(25)
)
result = ZhixingB1PatternScorer().score(
StockHistory(ts_code="TEST.SZ", name="below", bars=bars),
_cases(),
)
_assert_golden(result, _golden("below_threshold"))
def test_case_library_rejects_partial_or_short_input() -> None:
cases = _cases()
with pytest.raises(PatternScoringError, match="incomplete or out of order"):
ZhixingB1PatternScorer().score(cases[0].history, cases[:-1])
definition = ZHIXING_B1_PATTERN_CASES[0]
short = _history(definition.id, definition.ts_code, definition.name)
with pytest.raises(PatternCaseLibraryError, match="requires 25 complete rows"):
build_pattern_case(
definition,
StockHistory(short.ts_code, short.name, short.bars[:-1]),
)
def test_fastdtw_failure_is_not_replaced_by_simple_dtw(monkeypatch: pytest.MonkeyPatch) -> None:
import zhixing_server.modules.selection.domain.pattern_scoring as scoring
def fail(*_args: object, **_kwargs: object) -> tuple[float, list[tuple[int, int]]]:
raise RuntimeError("fastdtw unavailable")
monkeypatch.setattr(scoring, "_fastdtw", lambda: fail)
with pytest.raises(RuntimeError, match="fastdtw unavailable"):
ZhixingB1PatternScorer().score(_cases()[0].history, _cases())
def test_failed_score_requires_a_safe_reason() -> None:
with pytest.raises(ValueError, match="requires a safe reason"):
PatternScore(status="failed")
assert PatternScore.failed(" ").reason == "pattern scoring failed"
def test_threshold_is_inclusive_and_equal_scores_keep_first_case(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import zhixing_server.modules.selection.domain.pattern_scoring as scoring
tied = PatternScoreBreakdown(60.0, 60.0, 60.0, 60.0)
def tied_match(
_candidate: PatternFeatures,
_case: PatternFeatures,
) -> PatternScoreBreakdown:
return tied
monkeypatch.setattr(scoring, "_match", tied_match)
result = ZhixingB1PatternScorer().score(_cases()[0].history, _cases())
assert result.status == "matched"
assert result.value == 60.0
assert result.case == ZHIXING_B1_PATTERN_CASES[0]
@@ -2,17 +2,19 @@
from collections.abc import Generator
from contextlib import contextmanager
from datetime import date
from datetime import date, timedelta
from decimal import Decimal
from typing import cast
import psycopg
import pytest
from zhixing_server.modules.selection.domain.pattern_scoring import ZHIXING_B1_PATTERN_CASES
from zhixing_server.modules.selection.domain.runs import SelectionStock
from zhixing_server.modules.selection.infrastructure.postgres_pool import SelectionPostgresPool
from zhixing_server.modules.selection.infrastructure.postgres_reader import (
PostgresMarketDataReader,
PostgresPatternCaseLibraryLoader,
SelectionMarketDataNotReady,
)
@@ -237,3 +239,34 @@ def test_reader_rejects_date_without_eligible_market_batch(monkeypatch: pytest.M
"zhixing_b1",
date(2026, 8, 8),
)
def test_pattern_case_loader_reads_one_complete_exclusive_qfq_library() -> None:
rows: list[tuple[object, ...]] = []
for definition in ZHIXING_B1_PATTERN_CASES:
for offset in range(definition.lookback_days, 0, -1):
rows.append(
(
definition.id,
definition.ts_code,
definition.breakout_date - timedelta(days=offset),
"10",
"11",
"9",
str(10 + offset / 100),
str(1000 + offset),
)
)
connection = FakeConnection(rows)
pool = Pool(connection)
owner = SelectionPostgresPool("postgresql://test", max_connections=2, pool=pool)
cases = PostgresPatternCaseLibraryLoader("postgresql://test", pool=owner).load()
assert tuple(case.definition for case in cases) == ZHIXING_B1_PATTERN_CASES
assert all(len(case.history.bars) == 25 for case in cases)
assert all(case.history.bars[-1].trade_date < case.definition.breakout_date for case in cases)
assert "bar.trade_date < definition.breakout_date" in cast(str, connection.query)
assert "bar.source_adj = 'qfq'" in cast(str, connection.query)
assert connection.parameters is not None
assert connection.parameters[0] == [definition.id for definition in ZHIXING_B1_PATTERN_CASES]
@@ -8,6 +8,12 @@ import pytest
from psycopg.types.json import Jsonb
from zhixing_server.modules.selection.domain.models import SelectionSignal, ZhixingB1Category
from zhixing_server.modules.selection.domain.pattern_scoring import (
PATTERN_SCORING_VERSION,
ZHIXING_B1_PATTERN_CASES,
PatternScore,
PatternScoreBreakdown,
)
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionRerunRequired,
@@ -222,6 +228,14 @@ def test_record_items_uses_one_delete_and_two_batch_upserts(
name="平安银行",
status="selected",
signal_count=2,
pattern_score=PatternScore(
status="matched",
value=86.4,
threshold=60.0,
version=PATTERN_SCORING_VERSION,
case=ZHIXING_B1_PATTERN_CASES[0],
breakdown=PatternScoreBreakdown(71.2, 83.0, 88.0, 90.1),
),
signals=(first, second),
),
SelectionRunItem(
@@ -238,6 +252,28 @@ def test_record_items_uses_one_delete_and_two_batch_upserts(
assert delete_parameters == ("run-1", ["000001.SZ", "600000.SH"])
assert len(connection.executemany_calls) == 2
assert "INSERT INTO selection_run_item" in connection.executemany_calls[0][0]
item_parameters = connection.executemany_calls[0][1]
assert item_parameters[0][6:13] == (
"matched",
86.4,
60.0,
PATTERN_SCORING_VERSION,
"case_001",
"华纳药厂",
date(2025, 5, 12),
)
assert isinstance(item_parameters[0][13], Jsonb)
assert item_parameters[1][6:] == (
"not_executed",
None,
None,
None,
None,
None,
None,
None,
None,
)
assert "INSERT INTO selection_signal" in connection.executemany_calls[1][0]
signal_parameters = connection.executemany_calls[1][1]
assert len(signal_parameters) == 2
@@ -281,11 +317,35 @@ class LoadConnection:
None,
)
)
if "FROM selection_run_item" in query:
return LoadResult(rows=[("000001.SZ", "平安银行", "selected", 2, None)])
if "COUNT(DISTINCT ts_code) FROM selection_signal" in query:
if "FROM selection_run_item\n" in query:
return LoadResult(
rows=[
(
"000001.SZ",
"平安银行",
"selected",
2,
None,
"matched",
Decimal("86.40"),
Decimal("60.00"),
PATTERN_SCORING_VERSION,
"case_001",
"华纳药厂",
date(2025, 5, 12),
{
"trend_structure": 71.2,
"kdj_state": 83.0,
"volume_pattern": 88.0,
"price_shape": 90.1,
},
None,
)
]
)
if "SELECT COUNT(*) FROM selection_run_item AS item" in query:
return LoadResult(row=(2,))
if "SELECT DISTINCT ts_code" in query:
if "SELECT item.ts_code" in query:
return LoadResult(rows=[("000001.SZ",)])
return LoadResult(
rows=[
@@ -315,7 +375,7 @@ class EmptyStockPageConnection(LoadConnection):
"""Return a non-zero filtered total with no stocks on the requested page."""
def execute(self, query: str, parameters: tuple[object, ...]) -> "LoadResult":
if "SELECT DISTINCT ts_code" in query:
if "SELECT item.ts_code" in query:
self.statements.append((query, parameters))
return LoadResult(rows=[])
return super().execute(query, parameters)
@@ -355,6 +415,7 @@ def test_get_run_pages_stocks_and_loads_all_signals_for_category_matches(
page_size=1,
search="100%",
category="pullback",
sort="score_desc",
),
)
@@ -364,19 +425,21 @@ def test_get_run_pages_stocks_and_loads_all_signals_for_category_matches(
ZHIXING_B1_SIGNAL_ORDER[-1],
]
assert run.stocks_total == 2
assert run.items[0].pattern_score.status == "matched"
assert run.items[0].pattern_score.value == 86.4
count_query, count_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "COUNT(DISTINCT ts_code) FROM selection_signal" in query
if "SELECT COUNT(*) FROM selection_run_item AS item" in query
)
assert "name ILIKE %s ESCAPE" in count_query
assert count_parameters == ("run-1", "%100\\%%", "%100\\%%", "zhixing_b1_pullback_%")
stock_page_query, page_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "SELECT DISTINCT ts_code" in query
if "SELECT item.ts_code" in query
)
assert "ORDER BY ts_code" in stock_page_query
assert "ORDER BY item.score_value DESC NULLS LAST, item.ts_code ASC" in stock_page_query
assert page_parameters[-2:] == (1, 0)
signal_query, signal_parameters = next(
(query, parameters)
@@ -409,8 +472,30 @@ def test_get_run_does_not_load_signals_for_an_empty_stock_page(
stock_page_query, stock_page_parameters = next(
(query, parameters)
for query, parameters in connection.statements
if "SELECT DISTINCT ts_code" in query
if "SELECT item.ts_code" in query
)
assert "ORDER BY ts_code" in stock_page_query
assert "ORDER BY item.ts_code ASC" in stock_page_query
assert stock_page_parameters[-2:] == (1, 2)
assert not any("ts_code = ANY(%s)" in query for query, _ in connection.statements)
def test_get_run_sorts_scores_ascending_with_nulls_last_and_code_tiebreak(
monkeypatch: pytest.MonkeyPatch,
) -> None:
connection = LoadConnection()
def connect(database_url: str) -> LoadConnection:
assert database_url == "postgresql://test"
return connection
monkeypatch.setattr(psycopg, "connect", connect)
run = PostgresSelectionRunRepository("postgresql://test").get_run(
"run-1",
query=SelectionResultQuery(sort="score_asc"),
)
assert run is not None
stock_page_query = next(
query for query, _ in connection.statements if "SELECT item.ts_code" in query
)
assert "ORDER BY item.score_value ASC NULLS LAST, item.ts_code ASC" in stock_page_query
@@ -2,6 +2,7 @@
import threading
import time
from collections.abc import Sequence
from datetime import date
from decimal import Decimal
from typing import Literal
@@ -15,6 +16,14 @@ from zhixing_server.modules.selection.domain.models import (
SelectionSignal,
StockHistory,
)
from zhixing_server.modules.selection.domain.pattern_scoring import (
PATTERN_SCORE_THRESHOLD,
PATTERN_SCORING_VERSION,
ZHIXING_B1_PATTERN_CASES,
PatternCase,
PatternScore,
PatternScoreBreakdown,
)
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionResultQuery,
@@ -198,6 +207,37 @@ class ConcurrentHistoryEvaluator:
return SelectionEvaluation(history.ts_code, target_trade_date, "no_signal")
class FakePatternCaseLoader:
def __init__(self, *, error: Exception | None = None) -> None:
self.calls = 0
self.error = error
def load(self) -> tuple[PatternCase, ...]:
self.calls += 1
if self.error is not None:
raise self.error
return ()
class FakePatternScorer:
def __init__(self, *, error: Exception | None = None) -> None:
self.calls: list[str] = []
self.error = error
def score(self, history: StockHistory, cases: Sequence[PatternCase]) -> PatternScore:
self.calls.append(history.ts_code)
if self.error is not None:
raise self.error
return PatternScore(
status="matched",
value=88.0,
threshold=PATTERN_SCORE_THRESHOLD,
version=PATTERN_SCORING_VERSION,
case=ZHIXING_B1_PATTERN_CASES[0],
breakdown=PatternScoreBreakdown(80.0, 85.0, 90.0, 88.0),
)
def _source() -> SelectionExecutionSource:
return SelectionExecutionSource(
market_sync_batch_id="market-run-1",
@@ -364,3 +404,149 @@ def test_execute_marks_batch_write_failure_as_failed() -> None:
assert store.finished[0:2] == ("run-1", "failed")
assert store.finished[2]["error_type"] == "batch_error"
assert store.finished[2]["failed_count"] == 1
def test_execute_loads_cases_once_and_scores_only_selected_stocks() -> None:
source = _source()
reader = BatchReader(source)
store = FakeStore()
loader = FakePatternCaseLoader()
scorer = FakePatternScorer()
evaluator = FakeEvaluator(
{
"000001.SZ": SelectionEvaluation(
"000001.SZ",
TARGET,
"selected",
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
),
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
}
)
service = RunZhixingB1(
reader,
store,
evaluator,
loader,
scorer,
pattern_scoring_enabled=True,
batch_size=1,
)
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
assert loader.calls == 1
assert scorer.calls == ["000001.SZ"]
assert [item.pattern_score.status for item in store.items] == ["matched", "not_executed"]
assert store.finished is not None
assert store.finished[0:2] == ("run-1", "success")
assert store.finished[2]["failed_count"] == 0
def test_execute_isolates_pattern_scoring_failure_from_selection_status() -> None:
source = _source()
store = FakeStore()
loader = FakePatternCaseLoader()
scorer = FakePatternScorer(error=RuntimeError("FastDTW unavailable"))
evaluator = FakeEvaluator(
{
"000001.SZ": SelectionEvaluation(
"000001.SZ",
TARGET,
"selected",
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
),
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
}
)
service = RunZhixingB1(
BatchReader(source),
store,
evaluator,
loader,
scorer,
pattern_scoring_enabled=True,
)
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
assert store.items[0].status == "selected"
assert store.items[0].pattern_score == PatternScore.failed("FastDTW unavailable")
assert store.finished is not None
assert store.finished[0:2] == ("run-1", "success")
assert store.finished[2]["failed_count"] == 0
def test_execute_skips_pattern_dependencies_when_feature_flag_is_disabled() -> None:
source = _source()
store = FakeStore()
loader = FakePatternCaseLoader(error=AssertionError("loader must not run"))
scorer = FakePatternScorer(error=AssertionError("scorer must not run"))
evaluator = FakeEvaluator(
{
"000001.SZ": SelectionEvaluation(
"000001.SZ",
TARGET,
"selected",
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
),
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
}
)
service = RunZhixingB1(
BatchReader(source),
store,
evaluator,
loader,
scorer,
pattern_scoring_enabled=False,
)
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
assert loader.calls == 0
assert scorer.calls == []
assert [item.pattern_score.status for item in store.items] == [
"not_executed",
"not_executed",
]
assert store.items[0].signal_count == 1
assert store.finished is not None
assert store.finished[2]["failed_count"] == 0
def test_execute_marks_scores_failed_when_case_library_is_unavailable() -> None:
source = _source()
store = FakeStore()
loader = FakePatternCaseLoader(error=RuntimeError("case_011 requires 25 qfq rows"))
scorer = FakePatternScorer()
evaluator = FakeEvaluator(
{
"000001.SZ": SelectionEvaluation(
"000001.SZ",
TARGET,
"selected",
signals=(_signal("000001.SZ", "zhixing_b1_original_b1"),),
),
"600000.SH": SelectionEvaluation("600000.SH", TARGET, "no_signal"),
}
)
service = RunZhixingB1(
BatchReader(source),
store,
evaluator,
loader,
scorer,
pattern_scoring_enabled=True,
)
service.execute(service.prepare("zhixing_b1", TARGET, rerun=False))
assert loader.calls == 1
assert scorer.calls == []
assert store.items[0].status == "selected"
assert store.items[0].pattern_score.status == "failed"
assert store.items[0].signals[0].category.value == "zhixing_b1_original_b1"
assert store.finished is not None
assert store.finished[0:2] == ("run-1", "success")
assert store.finished[2]["failed_count"] == 0