feat(selection): 迁移知行B1选股策略
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Zhixing B1 fixed fixtures
|
||||
|
||||
These are small, deterministic artificial qfq OHLCV histories used to verify
|
||||
ordinary and wide-limit amplitude parameters without importing the legacy
|
||||
project at test time. The rows are calendar-spaced only to keep the fixture
|
||||
readable; the formula treats them as ascending trading observations.
|
||||
|
||||
`multi_signal.json` documents the independent-mask orchestration case. The
|
||||
unit test forces all seven masks on one prepared target row, which is
|
||||
intentional: the legacy result set did not contain a trustworthy historical
|
||||
same-day multi-hit sample, while the new contract requires retaining all hits.
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"ordinary": {
|
||||
"file": "ordinary.csv",
|
||||
"ts_code": "000001.SZ",
|
||||
"target_trade_date": "2023-04-25",
|
||||
"status": "no_signal",
|
||||
"categories": [],
|
||||
"note": "人工上升序列,验证普通代码使用 5% 振幅区间;不代表历史推荐结果。"
|
||||
},
|
||||
"wide_limit": {
|
||||
"file": "wide_limit.csv",
|
||||
"ts_code": "300001.SZ",
|
||||
"target_trade_date": "2023-04-25",
|
||||
"status": "no_signal",
|
||||
"categories": [],
|
||||
"note": "人工上升序列,验证 30 开头代码使用 8% 振幅区间;不代表历史推荐结果。"
|
||||
},
|
||||
"multi_signal": {
|
||||
"status": "selected",
|
||||
"categories": [
|
||||
"zhixing_b1_oversold_turn",
|
||||
"zhixing_b1_oversold_volume",
|
||||
"zhixing_b1_original_b1",
|
||||
"zhixing_b1_extreme_volume",
|
||||
"zhixing_b1_pullback_white",
|
||||
"zhixing_b1_pullback_super",
|
||||
"zhixing_b1_pullback_yellow"
|
||||
],
|
||||
"note": "人工构造的同日多命中契约;测试通过独立 mask 注入验证不 break。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
trade_date,open,high,low,close,volume
|
||||
2023-01-01,9.9600,10.1200,9.8800,10.0000,1000.00
|
||||
2023-01-02,9.9800,10.1400,9.9000,10.0200,1030.00
|
||||
2023-01-03,10.0000,10.1600,9.9200,10.0400,1060.00
|
||||
2023-01-04,10.0200,10.1800,9.9400,10.0600,1090.00
|
||||
2023-01-05,10.0400,10.2000,9.9600,10.0800,1120.00
|
||||
2023-01-06,10.0600,10.2200,9.9800,10.1000,1150.00
|
||||
2023-01-07,10.0800,10.2400,10.0000,10.1200,1180.00
|
||||
2023-01-08,10.1000,10.2600,10.0200,10.1400,1000.00
|
||||
2023-01-09,10.1200,10.2800,10.0400,10.1600,1030.00
|
||||
2023-01-10,10.1400,10.3000,10.0600,10.1800,1060.00
|
||||
2023-01-11,10.1600,10.3200,10.0800,10.2000,1090.00
|
||||
2023-01-12,10.1800,10.3400,10.1000,10.2200,1120.00
|
||||
2023-01-13,10.2000,10.3600,10.1200,10.2400,1150.00
|
||||
2023-01-14,10.2200,10.3800,10.1400,10.2600,1180.00
|
||||
2023-01-15,10.2400,10.4000,10.1600,10.2800,1000.00
|
||||
2023-01-16,10.2600,10.4200,10.1800,10.3000,1030.00
|
||||
2023-01-17,10.2800,10.4400,10.2000,10.3200,1060.00
|
||||
2023-01-18,10.3000,10.4600,10.2200,10.3400,1090.00
|
||||
2023-01-19,10.3200,10.4800,10.2400,10.3600,1120.00
|
||||
2023-01-20,10.3400,10.5000,10.2600,10.3800,1150.00
|
||||
2023-01-21,10.3600,10.5200,10.2800,10.4000,1180.00
|
||||
2023-01-22,10.3800,10.5400,10.3000,10.4200,1000.00
|
||||
2023-01-23,10.4000,10.5600,10.3200,10.4400,1030.00
|
||||
2023-01-24,10.4200,10.5800,10.3400,10.4600,1060.00
|
||||
2023-01-25,10.4400,10.6000,10.3600,10.4800,1090.00
|
||||
2023-01-26,10.4600,10.6200,10.3800,10.5000,1120.00
|
||||
2023-01-27,10.4800,10.6400,10.4000,10.5200,1150.00
|
||||
2023-01-28,10.5000,10.6600,10.4200,10.5400,1180.00
|
||||
2023-01-29,10.5200,10.6800,10.4400,10.5600,1000.00
|
||||
2023-01-30,10.5400,10.7000,10.4600,10.5800,1030.00
|
||||
2023-01-31,10.5600,10.7200,10.4800,10.6000,1060.00
|
||||
2023-02-01,10.5800,10.7400,10.5000,10.6200,1090.00
|
||||
2023-02-02,10.6000,10.7600,10.5200,10.6400,1120.00
|
||||
2023-02-03,10.6200,10.7800,10.5400,10.6600,1150.00
|
||||
2023-02-04,10.6400,10.8000,10.5600,10.6800,1180.00
|
||||
2023-02-05,10.6600,10.8200,10.5800,10.7000,1000.00
|
||||
2023-02-06,10.6800,10.8400,10.6000,10.7200,1030.00
|
||||
2023-02-07,10.7000,10.8600,10.6200,10.7400,1060.00
|
||||
2023-02-08,10.7200,10.8800,10.6400,10.7600,1090.00
|
||||
2023-02-09,10.7400,10.9000,10.6600,10.7800,1120.00
|
||||
2023-02-10,10.7600,10.9200,10.6800,10.8000,1150.00
|
||||
2023-02-11,10.7800,10.9400,10.7000,10.8200,1180.00
|
||||
2023-02-12,10.8000,10.9600,10.7200,10.8400,1000.00
|
||||
2023-02-13,10.8200,10.9800,10.7400,10.8600,1030.00
|
||||
2023-02-14,10.8400,11.0000,10.7600,10.8800,1060.00
|
||||
2023-02-15,10.8600,11.0200,10.7800,10.9000,1090.00
|
||||
2023-02-16,10.8800,11.0400,10.8000,10.9200,1120.00
|
||||
2023-02-17,10.9000,11.0600,10.8200,10.9400,1150.00
|
||||
2023-02-18,10.9200,11.0800,10.8400,10.9600,1180.00
|
||||
2023-02-19,10.9400,11.1000,10.8600,10.9800,1000.00
|
||||
2023-02-20,10.9600,11.1200,10.8800,11.0000,1030.00
|
||||
2023-02-21,10.9800,11.1400,10.9000,11.0200,1060.00
|
||||
2023-02-22,11.0000,11.1600,10.9200,11.0400,1090.00
|
||||
2023-02-23,11.0200,11.1800,10.9400,11.0600,1120.00
|
||||
2023-02-24,11.0400,11.2000,10.9600,11.0800,1150.00
|
||||
2023-02-25,11.0600,11.2200,10.9800,11.1000,1180.00
|
||||
2023-02-26,11.0800,11.2400,11.0000,11.1200,1000.00
|
||||
2023-02-27,11.1000,11.2600,11.0200,11.1400,1030.00
|
||||
2023-02-28,11.1200,11.2800,11.0400,11.1600,1060.00
|
||||
2023-03-01,11.1400,11.3000,11.0600,11.1800,1090.00
|
||||
2023-03-02,11.1600,11.3200,11.0800,11.2000,1120.00
|
||||
2023-03-03,11.1800,11.3400,11.1000,11.2200,1150.00
|
||||
2023-03-04,11.2000,11.3600,11.1200,11.2400,1180.00
|
||||
2023-03-05,11.2200,11.3800,11.1400,11.2600,1000.00
|
||||
2023-03-06,11.2400,11.4000,11.1600,11.2800,1030.00
|
||||
2023-03-07,11.2600,11.4200,11.1800,11.3000,1060.00
|
||||
2023-03-08,11.2800,11.4400,11.2000,11.3200,1090.00
|
||||
2023-03-09,11.3000,11.4600,11.2200,11.3400,1120.00
|
||||
2023-03-10,11.3200,11.4800,11.2400,11.3600,1150.00
|
||||
2023-03-11,11.3400,11.5000,11.2600,11.3800,1180.00
|
||||
2023-03-12,11.3600,11.5200,11.2800,11.4000,1000.00
|
||||
2023-03-13,11.3800,11.5400,11.3000,11.4200,1030.00
|
||||
2023-03-14,11.4000,11.5600,11.3200,11.4400,1060.00
|
||||
2023-03-15,11.4200,11.5800,11.3400,11.4600,1090.00
|
||||
2023-03-16,11.4400,11.6000,11.3600,11.4800,1120.00
|
||||
2023-03-17,11.4600,11.6200,11.3800,11.5000,1150.00
|
||||
2023-03-18,11.4800,11.6400,11.4000,11.5200,1180.00
|
||||
2023-03-19,11.5000,11.6600,11.4200,11.5400,1000.00
|
||||
2023-03-20,11.5200,11.6800,11.4400,11.5600,1030.00
|
||||
2023-03-21,11.5400,11.7000,11.4600,11.5800,1060.00
|
||||
2023-03-22,11.5600,11.7200,11.4800,11.6000,1090.00
|
||||
2023-03-23,11.5800,11.7400,11.5000,11.6200,1120.00
|
||||
2023-03-24,11.6000,11.7600,11.5200,11.6400,1150.00
|
||||
2023-03-25,11.6200,11.7800,11.5400,11.6600,1180.00
|
||||
2023-03-26,11.6400,11.8000,11.5600,11.6800,1000.00
|
||||
2023-03-27,11.6600,11.8200,11.5800,11.7000,1030.00
|
||||
2023-03-28,11.6800,11.8400,11.6000,11.7200,1060.00
|
||||
2023-03-29,11.7000,11.8600,11.6200,11.7400,1090.00
|
||||
2023-03-30,11.7200,11.8800,11.6400,11.7600,1120.00
|
||||
2023-03-31,11.7400,11.9000,11.6600,11.7800,1150.00
|
||||
2023-04-01,11.7600,11.9200,11.6800,11.8000,1180.00
|
||||
2023-04-02,11.7800,11.9400,11.7000,11.8200,1000.00
|
||||
2023-04-03,11.8000,11.9600,11.7200,11.8400,1030.00
|
||||
2023-04-04,11.8200,11.9800,11.7400,11.8600,1060.00
|
||||
2023-04-05,11.8400,12.0000,11.7600,11.8800,1090.00
|
||||
2023-04-06,11.8600,12.0200,11.7800,11.9000,1120.00
|
||||
2023-04-07,11.8800,12.0400,11.8000,11.9200,1150.00
|
||||
2023-04-08,11.9000,12.0600,11.8200,11.9400,1180.00
|
||||
2023-04-09,11.9200,12.0800,11.8400,11.9600,1000.00
|
||||
2023-04-10,11.9400,12.1000,11.8600,11.9800,1030.00
|
||||
2023-04-11,11.9600,12.1200,11.8800,12.0000,1060.00
|
||||
2023-04-12,11.9800,12.1400,11.9000,12.0200,1090.00
|
||||
2023-04-13,12.0000,12.1600,11.9200,12.0400,1120.00
|
||||
2023-04-14,12.0200,12.1800,11.9400,12.0600,1150.00
|
||||
2023-04-15,12.0400,12.2000,11.9600,12.0800,1180.00
|
||||
2023-04-16,12.0600,12.2200,11.9800,12.1000,1000.00
|
||||
2023-04-17,12.0800,12.2400,12.0000,12.1200,1030.00
|
||||
2023-04-18,12.1000,12.2600,12.0200,12.1400,1060.00
|
||||
2023-04-19,12.1200,12.2800,12.0400,12.1600,1090.00
|
||||
2023-04-20,12.1400,12.3000,12.0600,12.1800,1120.00
|
||||
2023-04-21,12.1600,12.3200,12.0800,12.2000,1150.00
|
||||
2023-04-22,12.1800,12.3400,12.1000,12.2200,1180.00
|
||||
2023-04-23,12.2000,12.3600,12.1200,12.2400,1000.00
|
||||
2023-04-24,12.2200,12.3800,12.1400,12.2600,1030.00
|
||||
2023-04-25,12.2400,12.4000,12.1600,12.2800,1060.00
|
||||
|
@@ -0,0 +1,116 @@
|
||||
trade_date,open,high,low,close,volume
|
||||
2023-01-01,19.9600,20.1200,19.8800,20.0000,1600.00
|
||||
2023-01-02,19.9900,20.1500,19.9100,20.0300,1630.00
|
||||
2023-01-03,20.0200,20.1800,19.9400,20.0600,1660.00
|
||||
2023-01-04,20.0500,20.2100,19.9700,20.0900,1690.00
|
||||
2023-01-05,20.0800,20.2400,20.0000,20.1200,1720.00
|
||||
2023-01-06,20.1100,20.2700,20.0300,20.1500,1750.00
|
||||
2023-01-07,20.1400,20.3000,20.0600,20.1800,1780.00
|
||||
2023-01-08,20.1700,20.3300,20.0900,20.2100,1600.00
|
||||
2023-01-09,20.2000,20.3600,20.1200,20.2400,1630.00
|
||||
2023-01-10,20.2300,20.3900,20.1500,20.2700,1660.00
|
||||
2023-01-11,20.2600,20.4200,20.1800,20.3000,1690.00
|
||||
2023-01-12,20.2900,20.4500,20.2100,20.3300,1720.00
|
||||
2023-01-13,20.3200,20.4800,20.2400,20.3600,1750.00
|
||||
2023-01-14,20.3500,20.5100,20.2700,20.3900,1780.00
|
||||
2023-01-15,20.3800,20.5400,20.3000,20.4200,1600.00
|
||||
2023-01-16,20.4100,20.5700,20.3300,20.4500,1630.00
|
||||
2023-01-17,20.4400,20.6000,20.3600,20.4800,1660.00
|
||||
2023-01-18,20.4700,20.6300,20.3900,20.5100,1690.00
|
||||
2023-01-19,20.5000,20.6600,20.4200,20.5400,1720.00
|
||||
2023-01-20,20.5300,20.6900,20.4500,20.5700,1750.00
|
||||
2023-01-21,20.5600,20.7200,20.4800,20.6000,1780.00
|
||||
2023-01-22,20.5900,20.7500,20.5100,20.6300,1600.00
|
||||
2023-01-23,20.6200,20.7800,20.5400,20.6600,1630.00
|
||||
2023-01-24,20.6500,20.8100,20.5700,20.6900,1660.00
|
||||
2023-01-25,20.6800,20.8400,20.6000,20.7200,1690.00
|
||||
2023-01-26,20.7100,20.8700,20.6300,20.7500,1720.00
|
||||
2023-01-27,20.7400,20.9000,20.6600,20.7800,1750.00
|
||||
2023-01-28,20.7700,20.9300,20.6900,20.8100,1780.00
|
||||
2023-01-29,20.8000,20.9600,20.7200,20.8400,1600.00
|
||||
2023-01-30,20.8300,20.9900,20.7500,20.8700,1630.00
|
||||
2023-01-31,20.8600,21.0200,20.7800,20.9000,1660.00
|
||||
2023-02-01,20.8900,21.0500,20.8100,20.9300,1690.00
|
||||
2023-02-02,20.9200,21.0800,20.8400,20.9600,1720.00
|
||||
2023-02-03,20.9500,21.1100,20.8700,20.9900,1750.00
|
||||
2023-02-04,20.9800,21.1400,20.9000,21.0200,1780.00
|
||||
2023-02-05,21.0100,21.1700,20.9300,21.0500,1600.00
|
||||
2023-02-06,21.0400,21.2000,20.9600,21.0800,1630.00
|
||||
2023-02-07,21.0700,21.2300,20.9900,21.1100,1660.00
|
||||
2023-02-08,21.1000,21.2600,21.0200,21.1400,1690.00
|
||||
2023-02-09,21.1300,21.2900,21.0500,21.1700,1720.00
|
||||
2023-02-10,21.1600,21.3200,21.0800,21.2000,1750.00
|
||||
2023-02-11,21.1900,21.3500,21.1100,21.2300,1780.00
|
||||
2023-02-12,21.2200,21.3800,21.1400,21.2600,1600.00
|
||||
2023-02-13,21.2500,21.4100,21.1700,21.2900,1630.00
|
||||
2023-02-14,21.2800,21.4400,21.2000,21.3200,1660.00
|
||||
2023-02-15,21.3100,21.4700,21.2300,21.3500,1690.00
|
||||
2023-02-16,21.3400,21.5000,21.2600,21.3800,1720.00
|
||||
2023-02-17,21.3700,21.5300,21.2900,21.4100,1750.00
|
||||
2023-02-18,21.4000,21.5600,21.3200,21.4400,1780.00
|
||||
2023-02-19,21.4300,21.5900,21.3500,21.4700,1600.00
|
||||
2023-02-20,21.4600,21.6200,21.3800,21.5000,1630.00
|
||||
2023-02-21,21.4900,21.6500,21.4100,21.5300,1660.00
|
||||
2023-02-22,21.5200,21.6800,21.4400,21.5600,1690.00
|
||||
2023-02-23,21.5500,21.7100,21.4700,21.5900,1720.00
|
||||
2023-02-24,21.5800,21.7400,21.5000,21.6200,1750.00
|
||||
2023-02-25,21.6100,21.7700,21.5300,21.6500,1780.00
|
||||
2023-02-26,21.6400,21.8000,21.5600,21.6800,1600.00
|
||||
2023-02-27,21.6700,21.8300,21.5900,21.7100,1630.00
|
||||
2023-02-28,21.7000,21.8600,21.6200,21.7400,1660.00
|
||||
2023-03-01,21.7300,21.8900,21.6500,21.7700,1690.00
|
||||
2023-03-02,21.7600,21.9200,21.6800,21.8000,1720.00
|
||||
2023-03-03,21.7900,21.9500,21.7100,21.8300,1750.00
|
||||
2023-03-04,21.8200,21.9800,21.7400,21.8600,1780.00
|
||||
2023-03-05,21.8500,22.0100,21.7700,21.8900,1600.00
|
||||
2023-03-06,21.8800,22.0400,21.8000,21.9200,1630.00
|
||||
2023-03-07,21.9100,22.0700,21.8300,21.9500,1660.00
|
||||
2023-03-08,21.9400,22.1000,21.8600,21.9800,1690.00
|
||||
2023-03-09,21.9700,22.1300,21.8900,22.0100,1720.00
|
||||
2023-03-10,22.0000,22.1600,21.9200,22.0400,1750.00
|
||||
2023-03-11,22.0300,22.1900,21.9500,22.0700,1780.00
|
||||
2023-03-12,22.0600,22.2200,21.9800,22.1000,1600.00
|
||||
2023-03-13,22.0900,22.2500,22.0100,22.1300,1630.00
|
||||
2023-03-14,22.1200,22.2800,22.0400,22.1600,1660.00
|
||||
2023-03-15,22.1500,22.3100,22.0700,22.1900,1690.00
|
||||
2023-03-16,22.1800,22.3400,22.1000,22.2200,1720.00
|
||||
2023-03-17,22.2100,22.3700,22.1300,22.2500,1750.00
|
||||
2023-03-18,22.2400,22.4000,22.1600,22.2800,1780.00
|
||||
2023-03-19,22.2700,22.4300,22.1900,22.3100,1600.00
|
||||
2023-03-20,22.3000,22.4600,22.2200,22.3400,1630.00
|
||||
2023-03-21,22.3300,22.4900,22.2500,22.3700,1660.00
|
||||
2023-03-22,22.3600,22.5200,22.2800,22.4000,1690.00
|
||||
2023-03-23,22.3900,22.5500,22.3100,22.4300,1720.00
|
||||
2023-03-24,22.4200,22.5800,22.3400,22.4600,1750.00
|
||||
2023-03-25,22.4500,22.6100,22.3700,22.4900,1780.00
|
||||
2023-03-26,22.4800,22.6400,22.4000,22.5200,1600.00
|
||||
2023-03-27,22.5100,22.6700,22.4300,22.5500,1630.00
|
||||
2023-03-28,22.5400,22.7000,22.4600,22.5800,1660.00
|
||||
2023-03-29,22.5700,22.7300,22.4900,22.6100,1690.00
|
||||
2023-03-30,22.6000,22.7600,22.5200,22.6400,1720.00
|
||||
2023-03-31,22.6300,22.7900,22.5500,22.6700,1750.00
|
||||
2023-04-01,22.6600,22.8200,22.5800,22.7000,1780.00
|
||||
2023-04-02,22.6900,22.8500,22.6100,22.7300,1600.00
|
||||
2023-04-03,22.7200,22.8800,22.6400,22.7600,1630.00
|
||||
2023-04-04,22.7500,22.9100,22.6700,22.7900,1660.00
|
||||
2023-04-05,22.7800,22.9400,22.7000,22.8200,1690.00
|
||||
2023-04-06,22.8100,22.9700,22.7300,22.8500,1720.00
|
||||
2023-04-07,22.8400,23.0000,22.7600,22.8800,1750.00
|
||||
2023-04-08,22.8700,23.0300,22.7900,22.9100,1780.00
|
||||
2023-04-09,22.9000,23.0600,22.8200,22.9400,1600.00
|
||||
2023-04-10,22.9300,23.0900,22.8500,22.9700,1630.00
|
||||
2023-04-11,22.9600,23.1200,22.8800,23.0000,1660.00
|
||||
2023-04-12,22.9900,23.1500,22.9100,23.0300,1690.00
|
||||
2023-04-13,23.0200,23.1800,22.9400,23.0600,1720.00
|
||||
2023-04-14,23.0500,23.2100,22.9700,23.0900,1750.00
|
||||
2023-04-15,23.0800,23.2400,23.0000,23.1200,1780.00
|
||||
2023-04-16,23.1100,23.2700,23.0300,23.1500,1600.00
|
||||
2023-04-17,23.1400,23.3000,23.0600,23.1800,1630.00
|
||||
2023-04-18,23.1700,23.3300,23.0900,23.2100,1660.00
|
||||
2023-04-19,23.2000,23.3600,23.1200,23.2400,1690.00
|
||||
2023-04-20,23.2300,23.3900,23.1500,23.2700,1720.00
|
||||
2023-04-21,23.2600,23.4200,23.1800,23.3000,1750.00
|
||||
2023-04-22,23.2900,23.4500,23.2100,23.3300,1780.00
|
||||
2023-04-23,23.3200,23.4800,23.2400,23.3600,1600.00
|
||||
2023-04-24,23.3500,23.5100,23.2700,23.3900,1630.00
|
||||
2023-04-25,23.3800,23.5400,23.3000,23.4200,1660.00
|
||||
|
@@ -0,0 +1,41 @@
|
||||
"""Offline golden checks for fixed, non-legacy selection fixtures."""
|
||||
|
||||
import csv
|
||||
import json
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from zhixing_server.modules.selection.domain.models import SelectionBar, StockHistory
|
||||
from zhixing_server.modules.selection.domain.zhixing_b1 import ZhixingB1Strategy
|
||||
|
||||
FIXTURE_ROOT = Path(__file__).parents[1] / "fixtures" / "selection" / "zhixing_b1"
|
||||
|
||||
|
||||
def _read_history(path: Path, ts_code: str) -> StockHistory:
|
||||
with path.open(newline="") as file:
|
||||
bars = tuple(
|
||||
SelectionBar(
|
||||
trade_date=date.fromisoformat(row["trade_date"]),
|
||||
open=float(row["open"]),
|
||||
high=float(row["high"]),
|
||||
low=float(row["low"]),
|
||||
close=float(row["close"]),
|
||||
volume=float(row["volume"]),
|
||||
)
|
||||
for row in csv.DictReader(file)
|
||||
)
|
||||
return StockHistory(ts_code=ts_code, name="fixture", bars=bars)
|
||||
|
||||
|
||||
def test_fixed_ordinary_and_wide_limit_goldens_are_reproducible() -> None:
|
||||
with (FIXTURE_ROOT / "golden.json").open() as file:
|
||||
golden = json.load(file)
|
||||
|
||||
strategy = ZhixingB1Strategy()
|
||||
for key in ("ordinary", "wide_limit"):
|
||||
expected = golden[key]
|
||||
history = _read_history(FIXTURE_ROOT / expected["file"], expected["ts_code"])
|
||||
target = date.fromisoformat(expected["target_trade_date"])
|
||||
result = strategy.evaluate(history, target)
|
||||
assert result.status == expected["status"]
|
||||
assert [signal.category.value for signal in result.signals] == expected["categories"]
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Application-level state and port mapping tests."""
|
||||
|
||||
from datetime import date
|
||||
|
||||
from zhixing_server.modules.selection.application.evaluate import EvaluateZhixingB1
|
||||
from zhixing_server.modules.selection.domain.models import StockHistory
|
||||
from zhixing_server.modules.selection.domain.ports import MarketDataReaderError
|
||||
|
||||
TARGET = date(2024, 1, 2)
|
||||
|
||||
|
||||
class EmptyReader:
|
||||
def load_history(self, ts_code: str, target_trade_date: date) -> StockHistory:
|
||||
return StockHistory(ts_code=ts_code, name="", bars=())
|
||||
|
||||
|
||||
class FailingReader:
|
||||
def load_history(self, ts_code: str, target_trade_date: date) -> StockHistory:
|
||||
raise MarketDataReaderError(f"database unavailable for {ts_code}")
|
||||
|
||||
|
||||
def test_evaluate_maps_reader_error_to_data_error() -> None:
|
||||
result = EvaluateZhixingB1(FailingReader()).execute("000001.SZ", TARGET)
|
||||
assert result.status == "data_error"
|
||||
assert result.signals == ()
|
||||
assert "000001.SZ" in (result.reason or "")
|
||||
|
||||
|
||||
def test_evaluate_distinguishes_missing_target_from_reader_error() -> None:
|
||||
result = EvaluateZhixingB1(EmptyReader()).execute("000001.SZ", TARGET)
|
||||
assert result.status == "missing_target_bar"
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Boundary tests for TDX-style selection indicators."""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from zhixing_server.modules.selection.domain.indicators import (
|
||||
BARSLAST,
|
||||
COUNT,
|
||||
CROSS,
|
||||
EVERY,
|
||||
HHVBARS,
|
||||
MA,
|
||||
REF,
|
||||
compute_amplitude_params,
|
||||
compute_kdj,
|
||||
compute_rsi,
|
||||
)
|
||||
|
||||
|
||||
def test_rolling_primitives_use_trading_rows_and_keep_every_warmup() -> None:
|
||||
values = pd.Series([1.0, 2.0, 3.0, 2.0])
|
||||
|
||||
assert MA(values, 3).tolist() == [1.0, 1.5, 2.0, 7 / 3]
|
||||
assert REF(values, 1).isna().iloc[0]
|
||||
assert EVERY(pd.Series([True, True, True]), 3).tolist() == [False, False, True]
|
||||
assert COUNT(pd.Series([True, False, True]), 2).tolist() == [1.0, 1.0, 1.0]
|
||||
|
||||
|
||||
def test_hhvbars_and_barslast_are_stable_for_ties_and_missing_prefix() -> None:
|
||||
values = pd.Series([1.0, 3.0, 3.0, 2.0])
|
||||
assert HHVBARS(values, 3).tolist() == [0.0, 0.0, 0.0, 1.0]
|
||||
bars_last = BARSLAST(pd.Series([False, True, False, True]))
|
||||
assert np.isnan(bars_last.iloc[0])
|
||||
assert bars_last.iloc[1:].tolist() == [0.0, 1.0, 0.0]
|
||||
|
||||
|
||||
def test_cross_does_not_match_without_a_previous_complete_row() -> None:
|
||||
assert CROSS(pd.Series([1.0, 3.0, 2.0]), pd.Series([2.0, 2.0, 2.0])).tolist() == [
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
]
|
||||
|
||||
|
||||
def test_zero_range_and_zero_rsi_denominator_do_not_create_finite_signals() -> None:
|
||||
frame = pd.DataFrame(
|
||||
{
|
||||
"low": [10.0, 10.0, 10.0],
|
||||
"high": [10.0, 10.0, 10.0],
|
||||
"close": [10.0, 10.0, 10.0],
|
||||
}
|
||||
)
|
||||
kdj = compute_kdj(frame, 3)
|
||||
rsi = compute_rsi(frame["close"], 3)
|
||||
assert kdj["J"].isna().all()
|
||||
assert rsi.isna().all()
|
||||
|
||||
|
||||
def test_amplitude_parameters_cover_wide_prefix_and_historical_wide_move() -> None:
|
||||
close = pd.Series([10.0, 10.0, 11.6, 11.0])
|
||||
assert compute_amplitude_params("688001", close) == (8.0, 0.9)
|
||||
assert compute_amplitude_params("000001", close) == (8.0, 0.9)
|
||||
assert compute_amplitude_params("000001", pd.Series([10.0, 10.1])) == (5.0, 1.0)
|
||||
|
||||
|
||||
def test_invalid_indicator_windows_fail_loudly() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
MA(pd.Series([1.0]), 0)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""PostgreSQL reader contract tests using a fake connection."""
|
||||
|
||||
from datetime import date
|
||||
from typing import cast
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
from zhixing_server.modules.selection.infrastructure.postgres_reader import (
|
||||
PostgresMarketDataReader,
|
||||
)
|
||||
|
||||
|
||||
class FakeConnection:
|
||||
def __init__(self, rows: list[tuple[object, ...]]) -> None:
|
||||
self.rows = rows
|
||||
self.query: str | None = None
|
||||
self.parameters: tuple[object, ...] | None = None
|
||||
|
||||
def __enter__(self) -> "FakeConnection":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
def execute(self, query: str, parameters: tuple[object, ...]) -> "FakeResult":
|
||||
self.query = query
|
||||
self.parameters = parameters
|
||||
return FakeResult(self.rows)
|
||||
|
||||
|
||||
class FakeResult:
|
||||
def __init__(self, rows: list[tuple[object, ...]]) -> None:
|
||||
self.rows = rows
|
||||
|
||||
def fetchall(self) -> list[tuple[object, ...]]:
|
||||
return self.rows
|
||||
|
||||
|
||||
def test_reader_parameterizes_target_and_maps_left_join(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
connection = FakeConnection(
|
||||
[
|
||||
(
|
||||
"000001.SZ",
|
||||
"平安银行",
|
||||
date(2024, 1, 2),
|
||||
"10",
|
||||
"11",
|
||||
"9",
|
||||
"10.5",
|
||||
"1000",
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
"000001.SZ",
|
||||
"平安银行",
|
||||
date(2024, 1, 3),
|
||||
"10.5",
|
||||
"11",
|
||||
"10",
|
||||
"10.8",
|
||||
"1200",
|
||||
"1.2",
|
||||
"100000",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def connect(database_url: str) -> FakeConnection:
|
||||
assert database_url == "postgresql://test"
|
||||
return connection
|
||||
|
||||
monkeypatch.setattr(psycopg, "connect", connect)
|
||||
history = PostgresMarketDataReader("postgresql://test").load_history(
|
||||
"000001.SZ", date(2024, 1, 3)
|
||||
)
|
||||
|
||||
assert [bar.trade_date for bar in history.bars] == [date(2024, 1, 2), date(2024, 1, 3)]
|
||||
assert history.daily_basic[date(2024, 1, 2)].turnover_rate is None
|
||||
assert history.daily_basic[date(2024, 1, 3)].total_mv == 100000.0
|
||||
assert connection.parameters == ("000001.SZ", date(2024, 1, 3))
|
||||
assert "source_adj = 'qfq'" in cast(str, connection.query)
|
||||
assert "trade_date <= %s" in cast(str, connection.query)
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Behavior tests for explicit-date Zhixing B1 evaluation."""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from zhixing_server.modules.selection.domain import zhixing_b1
|
||||
from zhixing_server.modules.selection.domain.models import SelectionBar, StockHistory
|
||||
from zhixing_server.modules.selection.domain.zhixing_b1 import (
|
||||
MINIMUM_HISTORY,
|
||||
ZHIXING_B1_SIGNAL_ORDER,
|
||||
ZhixingB1Strategy,
|
||||
compute_signal_masks,
|
||||
prepare_zhixing_b1_indicators,
|
||||
)
|
||||
|
||||
|
||||
def make_history(count: int = MINIMUM_HISTORY, code: str = "000001.SZ") -> StockHistory:
|
||||
bars = tuple(
|
||||
SelectionBar(
|
||||
trade_date=date(2020, 1, 1) + timedelta(days=index),
|
||||
open=10.0 + index * 0.02,
|
||||
high=10.2 + index * 0.02,
|
||||
low=9.9 + index * 0.02,
|
||||
close=10.1 + index * 0.02,
|
||||
volume=1000.0 + (index % 7) * 30,
|
||||
)
|
||||
for index in range(count)
|
||||
)
|
||||
return StockHistory(ts_code=code, name="测试股票", bars=bars)
|
||||
|
||||
|
||||
def test_strategy_has_seven_stable_categories_and_prepared_masks() -> None:
|
||||
history = make_history()
|
||||
frame = pd.DataFrame(
|
||||
{
|
||||
"open": [bar.open for bar in history.bars],
|
||||
"high": [bar.high for bar in history.bars],
|
||||
"low": [bar.low for bar in history.bars],
|
||||
"close": [bar.close for bar in history.bars],
|
||||
"volume": [bar.volume for bar in history.bars],
|
||||
}
|
||||
)
|
||||
prepared = prepare_zhixing_b1_indicators(frame, history.ts_code)
|
||||
masks = compute_signal_masks(prepared)
|
||||
|
||||
assert tuple(masks) == ZHIXING_B1_SIGNAL_ORDER
|
||||
assert all(mask.dtype == bool for mask in masks.values())
|
||||
assert all(len(mask) == len(history.bars) for mask in masks.values())
|
||||
|
||||
|
||||
def test_strategy_explicit_target_ignores_future_rows() -> None:
|
||||
history = make_history()
|
||||
target = history.bars[-1].trade_date
|
||||
future = SelectionBar(
|
||||
trade_date=target + timedelta(days=1),
|
||||
open=1.0,
|
||||
high=100.0,
|
||||
low=0.5,
|
||||
close=99.0,
|
||||
volume=1_000_000.0,
|
||||
)
|
||||
with_future = StockHistory(history.ts_code, history.name, history.bars + (future,))
|
||||
|
||||
strategy = ZhixingB1Strategy()
|
||||
assert strategy.evaluate(with_future, target) == strategy.evaluate(history, target)
|
||||
|
||||
|
||||
def test_strategy_returns_missing_and_warmup_states() -> None:
|
||||
strategy = ZhixingB1Strategy()
|
||||
history = make_history(MINIMUM_HISTORY - 1)
|
||||
target = history.bars[-1].trade_date
|
||||
assert strategy.evaluate(history, target).status == "insufficient_history"
|
||||
assert strategy.evaluate(history, target + timedelta(days=1)).status == "missing_target_bar"
|
||||
|
||||
|
||||
def test_strategy_keeps_all_same_day_subsignals_in_priority_order(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
history = make_history()
|
||||
target = history.bars[-1].trade_date
|
||||
|
||||
def all_masks(frame: pd.DataFrame) -> dict[zhixing_b1.ZhixingB1Category, pd.Series]:
|
||||
return {
|
||||
category: pd.Series(True, index=frame.index) for category in ZHIXING_B1_SIGNAL_ORDER
|
||||
}
|
||||
|
||||
monkeypatch.setattr(zhixing_b1, "compute_signal_masks", all_masks)
|
||||
result = ZhixingB1Strategy().evaluate(history, target)
|
||||
|
||||
assert result.status == "selected"
|
||||
assert tuple(signal.category for signal in result.signals) == ZHIXING_B1_SIGNAL_ORDER
|
||||
assert len({signal.identity for signal in result.signals}) == 7
|
||||
Reference in New Issue
Block a user