@zhin.js/plugin-lottery 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +114 -0
  2. package/agent/agent.ts +6 -0
  3. package/agent/instructions.md +2 -0
  4. package/agent/schedules/daily_pipeline.ts +34 -0
  5. package/agent/skills/lottery.md +36 -0
  6. package/agent/tools/compute_recommend.ts +11 -0
  7. package/agent/tools/get_model_state.ts +11 -0
  8. package/agent/tools/history.ts +22 -0
  9. package/agent/tools/list_pending.ts +11 -0
  10. package/agent/tools/save_prediction.ts +14 -0
  11. package/agent/tools/stats_snapshot.ts +11 -0
  12. package/agent/tools/sync.ts +18 -0
  13. package/evals/pipeline_smoke.eval.ts +8 -0
  14. package/lib/agent/agent.d.ts +3 -0
  15. package/lib/agent/agent.d.ts.map +1 -0
  16. package/lib/agent/agent.js +6 -0
  17. package/lib/agent/agent.js.map +1 -0
  18. package/lib/agent/prompts.d.ts +11 -0
  19. package/lib/agent/prompts.d.ts.map +1 -0
  20. package/lib/agent/prompts.js +11 -0
  21. package/lib/agent/prompts.js.map +1 -0
  22. package/lib/agent/read-skills.d.ts +3 -0
  23. package/lib/agent/read-skills.d.ts.map +1 -0
  24. package/lib/agent/read-skills.js +25 -0
  25. package/lib/agent/read-skills.js.map +1 -0
  26. package/lib/agent/run-turn.d.ts +14 -0
  27. package/lib/agent/run-turn.d.ts.map +1 -0
  28. package/lib/agent/run-turn.js +47 -0
  29. package/lib/agent/run-turn.js.map +1 -0
  30. package/lib/agent/schedules/daily_pipeline.d.ts +3 -0
  31. package/lib/agent/schedules/daily_pipeline.d.ts.map +1 -0
  32. package/lib/agent/schedules/daily_pipeline.js +36 -0
  33. package/lib/agent/schedules/daily_pipeline.js.map +1 -0
  34. package/lib/agent/tools/compute_recommend.d.ts +5 -0
  35. package/lib/agent/tools/compute_recommend.d.ts.map +1 -0
  36. package/lib/agent/tools/compute_recommend.js +11 -0
  37. package/lib/agent/tools/compute_recommend.js.map +1 -0
  38. package/lib/agent/tools/get_model_state.d.ts +5 -0
  39. package/lib/agent/tools/get_model_state.d.ts.map +1 -0
  40. package/lib/agent/tools/get_model_state.js +11 -0
  41. package/lib/agent/tools/get_model_state.js.map +1 -0
  42. package/lib/agent/tools/history.d.ts +6 -0
  43. package/lib/agent/tools/history.d.ts.map +1 -0
  44. package/lib/agent/tools/history.js +24 -0
  45. package/lib/agent/tools/history.js.map +1 -0
  46. package/lib/agent/tools/list_pending.d.ts +5 -0
  47. package/lib/agent/tools/list_pending.d.ts.map +1 -0
  48. package/lib/agent/tools/list_pending.js +11 -0
  49. package/lib/agent/tools/list_pending.js.map +1 -0
  50. package/lib/agent/tools/save_prediction.d.ts +6 -0
  51. package/lib/agent/tools/save_prediction.d.ts.map +1 -0
  52. package/lib/agent/tools/save_prediction.js +14 -0
  53. package/lib/agent/tools/save_prediction.js.map +1 -0
  54. package/lib/agent/tools/stats_snapshot.d.ts +5 -0
  55. package/lib/agent/tools/stats_snapshot.d.ts.map +1 -0
  56. package/lib/agent/tools/stats_snapshot.js +11 -0
  57. package/lib/agent/tools/stats_snapshot.js.map +1 -0
  58. package/lib/agent/tools/sync.d.ts +5 -0
  59. package/lib/agent/tools/sync.d.ts.map +1 -0
  60. package/lib/agent/tools/sync.js +19 -0
  61. package/lib/agent/tools/sync.js.map +1 -0
  62. package/lib/db.d.ts +33 -0
  63. package/lib/db.d.ts.map +1 -0
  64. package/lib/db.js +108 -0
  65. package/lib/db.js.map +1 -0
  66. package/lib/evals/pipeline_smoke.eval.d.ts +3 -0
  67. package/lib/evals/pipeline_smoke.eval.d.ts.map +1 -0
  68. package/lib/evals/pipeline_smoke.eval.js +8 -0
  69. package/lib/evals/pipeline_smoke.eval.js.map +1 -0
  70. package/lib/evaluate/adaptive-sim.d.ts +31 -0
  71. package/lib/evaluate/adaptive-sim.d.ts.map +1 -0
  72. package/lib/evaluate/adaptive-sim.js +155 -0
  73. package/lib/evaluate/adaptive-sim.js.map +1 -0
  74. package/lib/evaluate/backtest.d.ts +29 -0
  75. package/lib/evaluate/backtest.d.ts.map +1 -0
  76. package/lib/evaluate/backtest.js +87 -0
  77. package/lib/evaluate/backtest.js.map +1 -0
  78. package/lib/evaluate/hit-rate.d.ts +8 -0
  79. package/lib/evaluate/hit-rate.d.ts.map +1 -0
  80. package/lib/evaluate/hit-rate.js +120 -0
  81. package/lib/evaluate/hit-rate.js.map +1 -0
  82. package/lib/evaluate/review-pending.d.ts +13 -0
  83. package/lib/evaluate/review-pending.d.ts.map +1 -0
  84. package/lib/evaluate/review-pending.js +89 -0
  85. package/lib/evaluate/review-pending.js.map +1 -0
  86. package/lib/evaluate/tracker.d.ts +28 -0
  87. package/lib/evaluate/tracker.d.ts.map +1 -0
  88. package/lib/evaluate/tracker.js +265 -0
  89. package/lib/evaluate/tracker.js.map +1 -0
  90. package/lib/evaluate/train-weights.d.ts +28 -0
  91. package/lib/evaluate/train-weights.d.ts.map +1 -0
  92. package/lib/evaluate/train-weights.js +101 -0
  93. package/lib/evaluate/train-weights.js.map +1 -0
  94. package/lib/evaluate/weight-guard.d.ts +15 -0
  95. package/lib/evaluate/weight-guard.d.ts.map +1 -0
  96. package/lib/evaluate/weight-guard.js +46 -0
  97. package/lib/evaluate/weight-guard.js.map +1 -0
  98. package/lib/games/kl8-groups.d.ts +31 -0
  99. package/lib/games/kl8-groups.d.ts.map +1 -0
  100. package/lib/games/kl8-groups.js +62 -0
  101. package/lib/games/kl8-groups.js.map +1 -0
  102. package/lib/games/registry.d.ts +13 -0
  103. package/lib/games/registry.d.ts.map +1 -0
  104. package/lib/games/registry.js +45 -0
  105. package/lib/games/registry.js.map +1 -0
  106. package/lib/index.d.ts +3 -0
  107. package/lib/index.d.ts.map +1 -0
  108. package/lib/index.js +215 -0
  109. package/lib/index.js.map +1 -0
  110. package/lib/pipeline.d.ts +36 -0
  111. package/lib/pipeline.d.ts.map +1 -0
  112. package/lib/pipeline.js +130 -0
  113. package/lib/pipeline.js.map +1 -0
  114. package/lib/push.d.ts +3 -0
  115. package/lib/push.d.ts.map +1 -0
  116. package/lib/push.js +31 -0
  117. package/lib/push.js.map +1 -0
  118. package/lib/recommend/game-pick.d.ts +14 -0
  119. package/lib/recommend/game-pick.d.ts.map +1 -0
  120. package/lib/recommend/game-pick.js +169 -0
  121. package/lib/recommend/game-pick.js.map +1 -0
  122. package/lib/recommend/report.d.ts +22 -0
  123. package/lib/recommend/report.d.ts.map +1 -0
  124. package/lib/recommend/report.js +173 -0
  125. package/lib/recommend/report.js.map +1 -0
  126. package/lib/src/agent/prompts.d.ts +11 -0
  127. package/lib/src/agent/prompts.d.ts.map +1 -0
  128. package/lib/src/agent/prompts.js +11 -0
  129. package/lib/src/agent/prompts.js.map +1 -0
  130. package/lib/src/agent/read-skills.d.ts +3 -0
  131. package/lib/src/agent/read-skills.d.ts.map +1 -0
  132. package/lib/src/agent/read-skills.js +25 -0
  133. package/lib/src/agent/read-skills.js.map +1 -0
  134. package/lib/src/agent/run-turn.d.ts +14 -0
  135. package/lib/src/agent/run-turn.d.ts.map +1 -0
  136. package/lib/src/agent/run-turn.js +47 -0
  137. package/lib/src/agent/run-turn.js.map +1 -0
  138. package/lib/src/db.d.ts +33 -0
  139. package/lib/src/db.d.ts.map +1 -0
  140. package/lib/src/db.js +108 -0
  141. package/lib/src/db.js.map +1 -0
  142. package/lib/src/evaluate/adaptive-sim.d.ts +31 -0
  143. package/lib/src/evaluate/adaptive-sim.d.ts.map +1 -0
  144. package/lib/src/evaluate/adaptive-sim.js +159 -0
  145. package/lib/src/evaluate/adaptive-sim.js.map +1 -0
  146. package/lib/src/evaluate/backtest.d.ts +29 -0
  147. package/lib/src/evaluate/backtest.d.ts.map +1 -0
  148. package/lib/src/evaluate/backtest.js +87 -0
  149. package/lib/src/evaluate/backtest.js.map +1 -0
  150. package/lib/src/evaluate/hit-rate.d.ts +8 -0
  151. package/lib/src/evaluate/hit-rate.d.ts.map +1 -0
  152. package/lib/src/evaluate/hit-rate.js +118 -0
  153. package/lib/src/evaluate/hit-rate.js.map +1 -0
  154. package/lib/src/evaluate/review-pending.d.ts +13 -0
  155. package/lib/src/evaluate/review-pending.d.ts.map +1 -0
  156. package/lib/src/evaluate/review-pending.js +89 -0
  157. package/lib/src/evaluate/review-pending.js.map +1 -0
  158. package/lib/src/evaluate/tracker.d.ts +28 -0
  159. package/lib/src/evaluate/tracker.d.ts.map +1 -0
  160. package/lib/src/evaluate/tracker.js +269 -0
  161. package/lib/src/evaluate/tracker.js.map +1 -0
  162. package/lib/src/evaluate/train-weights.d.ts +28 -0
  163. package/lib/src/evaluate/train-weights.d.ts.map +1 -0
  164. package/lib/src/evaluate/train-weights.js +101 -0
  165. package/lib/src/evaluate/train-weights.js.map +1 -0
  166. package/lib/src/evaluate/weight-guard.d.ts +15 -0
  167. package/lib/src/evaluate/weight-guard.d.ts.map +1 -0
  168. package/lib/src/evaluate/weight-guard.js +46 -0
  169. package/lib/src/evaluate/weight-guard.js.map +1 -0
  170. package/lib/src/games/kl8-groups.d.ts +31 -0
  171. package/lib/src/games/kl8-groups.d.ts.map +1 -0
  172. package/lib/src/games/kl8-groups.js +62 -0
  173. package/lib/src/games/kl8-groups.js.map +1 -0
  174. package/lib/src/games/registry.d.ts +13 -0
  175. package/lib/src/games/registry.d.ts.map +1 -0
  176. package/lib/src/games/registry.js +45 -0
  177. package/lib/src/games/registry.js.map +1 -0
  178. package/lib/src/index.d.ts +3 -0
  179. package/lib/src/index.d.ts.map +1 -0
  180. package/lib/src/index.js +183 -0
  181. package/lib/src/index.js.map +1 -0
  182. package/lib/src/lottery-agent-deps.d.ts +24 -0
  183. package/lib/src/lottery-agent-deps.d.ts.map +1 -0
  184. package/lib/src/lottery-agent-deps.js +10 -0
  185. package/lib/src/lottery-agent-deps.js.map +1 -0
  186. package/lib/src/lottery-tool-handlers.d.ts +17 -0
  187. package/lib/src/lottery-tool-handlers.d.ts.map +1 -0
  188. package/lib/src/lottery-tool-handlers.js +155 -0
  189. package/lib/src/lottery-tool-handlers.js.map +1 -0
  190. package/lib/src/pipeline.d.ts +36 -0
  191. package/lib/src/pipeline.d.ts.map +1 -0
  192. package/lib/src/pipeline.js +130 -0
  193. package/lib/src/pipeline.js.map +1 -0
  194. package/lib/src/push.d.ts +3 -0
  195. package/lib/src/push.d.ts.map +1 -0
  196. package/lib/src/push.js +31 -0
  197. package/lib/src/push.js.map +1 -0
  198. package/lib/src/recommend/game-pick.d.ts +14 -0
  199. package/lib/src/recommend/game-pick.d.ts.map +1 -0
  200. package/lib/src/recommend/game-pick.js +169 -0
  201. package/lib/src/recommend/game-pick.js.map +1 -0
  202. package/lib/src/recommend/report.d.ts +22 -0
  203. package/lib/src/recommend/report.d.ts.map +1 -0
  204. package/lib/src/recommend/report.js +176 -0
  205. package/lib/src/recommend/report.js.map +1 -0
  206. package/lib/src/stats/engine.d.ts +18 -0
  207. package/lib/src/stats/engine.d.ts.map +1 -0
  208. package/lib/src/stats/engine.js +122 -0
  209. package/lib/src/stats/engine.js.map +1 -0
  210. package/lib/src/stats/pro-analysis.d.ts +7 -0
  211. package/lib/src/stats/pro-analysis.d.ts.map +1 -0
  212. package/lib/src/stats/pro-analysis.js +121 -0
  213. package/lib/src/stats/pro-analysis.js.map +1 -0
  214. package/lib/src/stats/weights.d.ts +8 -0
  215. package/lib/src/stats/weights.d.ts.map +1 -0
  216. package/lib/src/stats/weights.js +25 -0
  217. package/lib/src/stats/weights.js.map +1 -0
  218. package/lib/src/sync/fetch-official.d.ts +4 -0
  219. package/lib/src/sync/fetch-official.d.ts.map +1 -0
  220. package/lib/src/sync/fetch-official.js +160 -0
  221. package/lib/src/sync/fetch-official.js.map +1 -0
  222. package/lib/src/sync/run-sync.d.ts +8 -0
  223. package/lib/src/sync/run-sync.d.ts.map +1 -0
  224. package/lib/src/sync/run-sync.js +29 -0
  225. package/lib/src/sync/run-sync.js.map +1 -0
  226. package/lib/src/tools/agent-tools.d.ts +15 -0
  227. package/lib/src/tools/agent-tools.d.ts.map +1 -0
  228. package/lib/src/tools/agent-tools.js +211 -0
  229. package/lib/src/tools/agent-tools.js.map +1 -0
  230. package/lib/src/types.d.ts +84 -0
  231. package/lib/src/types.d.ts.map +1 -0
  232. package/lib/src/types.js +3 -0
  233. package/lib/src/types.js.map +1 -0
  234. package/lib/stats/engine.d.ts +18 -0
  235. package/lib/stats/engine.d.ts.map +1 -0
  236. package/lib/stats/engine.js +122 -0
  237. package/lib/stats/engine.js.map +1 -0
  238. package/lib/stats/pro-analysis.d.ts +7 -0
  239. package/lib/stats/pro-analysis.d.ts.map +1 -0
  240. package/lib/stats/pro-analysis.js +121 -0
  241. package/lib/stats/pro-analysis.js.map +1 -0
  242. package/lib/stats/weights.d.ts +8 -0
  243. package/lib/stats/weights.d.ts.map +1 -0
  244. package/lib/stats/weights.js +25 -0
  245. package/lib/stats/weights.js.map +1 -0
  246. package/lib/sync/fetch-official.d.ts +4 -0
  247. package/lib/sync/fetch-official.d.ts.map +1 -0
  248. package/lib/sync/fetch-official.js +160 -0
  249. package/lib/sync/fetch-official.js.map +1 -0
  250. package/lib/sync/run-sync.d.ts +8 -0
  251. package/lib/sync/run-sync.d.ts.map +1 -0
  252. package/lib/sync/run-sync.js +29 -0
  253. package/lib/sync/run-sync.js.map +1 -0
  254. package/lib/tools/agent-tools.d.ts +15 -0
  255. package/lib/tools/agent-tools.d.ts.map +1 -0
  256. package/lib/tools/agent-tools.js +211 -0
  257. package/lib/tools/agent-tools.js.map +1 -0
  258. package/lib/tsconfig.tsbuildinfo +1 -0
  259. package/lib/types.d.ts +84 -0
  260. package/lib/types.d.ts.map +1 -0
  261. package/lib/types.js +3 -0
  262. package/lib/types.js.map +1 -0
  263. package/package.json +73 -0
  264. package/src/agent/prompts.ts +21 -0
  265. package/src/agent/read-skills.ts +27 -0
  266. package/src/agent/run-turn.ts +74 -0
  267. package/src/db.ts +132 -0
  268. package/src/evaluate/adaptive-sim.ts +210 -0
  269. package/src/evaluate/backtest.ts +128 -0
  270. package/src/evaluate/hit-rate.ts +132 -0
  271. package/src/evaluate/review-pending.ts +120 -0
  272. package/src/evaluate/tracker.ts +311 -0
  273. package/src/evaluate/train-weights.ts +134 -0
  274. package/src/evaluate/weight-guard.ts +68 -0
  275. package/src/games/kl8-groups.ts +98 -0
  276. package/src/games/registry.ts +55 -0
  277. package/src/index.ts +216 -0
  278. package/src/lottery-agent-deps.ts +29 -0
  279. package/src/lottery-tool-handlers.ts +166 -0
  280. package/src/pipeline.ts +206 -0
  281. package/src/push.ts +34 -0
  282. package/src/recommend/game-pick.ts +248 -0
  283. package/src/recommend/report.ts +211 -0
  284. package/src/stats/engine.ts +143 -0
  285. package/src/stats/pro-analysis.ts +136 -0
  286. package/src/stats/weights.ts +29 -0
  287. package/src/sync/fetch-official.ts +185 -0
  288. package/src/sync/run-sync.ts +33 -0
  289. package/src/types.ts +98 -0
@@ -0,0 +1,210 @@
1
+ import { DEFAULT_WEIGHTS, type DrawNumbers, type GameId, type GamePick, type NormalizedDraw, type ScoreWeights } from '../types.js';
2
+
3
+ import { comparePickToDraw, tuneWeightsFromDraw } from './hit-rate.js';
4
+ import { recommendGame } from '../recommend/game-pick.js';
5
+
6
+ export interface SimulationOptions {
7
+ pickCount: number;
8
+ minHistory: number;
9
+ randomTrials: number;
10
+ initialWeights?: ScoreWeights;
11
+ adaptive?: boolean;
12
+ /** Score only the last N periods; omit to score every training step. */
13
+ scoreWindow?: number;
14
+ }
15
+
16
+ export interface SimulationResult {
17
+ gameId: GameId;
18
+ periods: number;
19
+ warmupPeriods: number;
20
+ adaptiveAvgHitRate: number;
21
+ fixedAvgHitRate: number;
22
+ randomAvgHitRate: number;
23
+ adaptiveVsRandom: number;
24
+ adaptiveVsFixed: number;
25
+ initialWeights: ScoreWeights;
26
+ finalWeights: ScoreWeights;
27
+ firstScoredIssue: string;
28
+ lastScoredIssue: string;
29
+ }
30
+
31
+ const EMPTY_STATS: GamePick['stats'] = {
32
+ gameId: 'kl8',
33
+ sampleSize: 0,
34
+ hot: [],
35
+ cold: [],
36
+ detail: '',
37
+ };
38
+
39
+ function createRng(seed: number): () => number {
40
+ let s = seed | 0;
41
+ return () => {
42
+ s = (s + 0x6d2b79f5) | 0;
43
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
44
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
45
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
46
+ };
47
+ }
48
+
49
+ function pickUnique(rng: () => number, min: number, max: number, count: number): number[] {
50
+ const pool = [];
51
+ for (let i = min; i <= max; i++) pool.push(i);
52
+ const out: number[] = [];
53
+ for (let i = 0; i < count && pool.length; i++) {
54
+ const idx = Math.floor(rng() * pool.length);
55
+ const picked = pool.splice(idx, 1)[0];
56
+ if (picked !== undefined) out.push(picked);
57
+ }
58
+ return out.sort((a, b) => a - b);
59
+ }
60
+
61
+ function pickDigits(rng: () => number, len: number): number[] {
62
+ const out: number[] = [];
63
+ for (let i = 0; i < len; i++) out.push(Math.floor(rng() * 10));
64
+ return out;
65
+ }
66
+
67
+ export function randomDrawNumbers(gameId: GameId, pickCount: number, rng: () => number): DrawNumbers {
68
+ switch (gameId) {
69
+ case 'kl8':
70
+ return { main: pickUnique(rng, 1, 80, Math.min(10, Math.max(1, pickCount))) };
71
+ case 'ssq':
72
+ return { red: pickUnique(rng, 1, 33, 6), blue: pickUnique(rng, 1, 16, 1) };
73
+ case 'dlt':
74
+ return { front: pickUnique(rng, 1, 35, 5), back: pickUnique(rng, 1, 12, 2) };
75
+ case 'fc3d':
76
+ case 'pl3':
77
+ return { digits: pickDigits(rng, 3) };
78
+ case 'pl5':
79
+ return { digits: pickDigits(rng, 5) };
80
+ default:
81
+ return {};
82
+ }
83
+ }
84
+
85
+ function asPick(gameId: GameId, numbers: DrawNumbers): GamePick {
86
+ return {
87
+ gameId,
88
+ label: gameId,
89
+ numbers,
90
+ weights: DEFAULT_WEIGHTS,
91
+ stats: { ...EMPTY_STATS, gameId },
92
+ };
93
+ }
94
+
95
+ function stableIssueSeed(issue: string): number {
96
+ let h = 0;
97
+ for (let i = 0; i < issue.length; i++) h = (h * 31 + issue.charCodeAt(i)) >>> 0;
98
+ return h;
99
+ }
100
+
101
+ function randomRateForPeriod(
102
+ gameId: GameId,
103
+ pickCount: number,
104
+ actual: NormalizedDraw,
105
+ randomTrials: number,
106
+ ): number {
107
+ let periodRandom = 0;
108
+ const baseSeed = stableIssueSeed(actual.issue);
109
+ for (let t = 0; t < randomTrials; t++) {
110
+ const rng = createRng(baseSeed + t);
111
+ const rnd = randomDrawNumbers(gameId, pickCount, rng);
112
+ periodRandom += comparePickToDraw(asPick(gameId, rnd), actual).rate;
113
+ }
114
+ return periodRandom / randomTrials;
115
+ }
116
+
117
+ /**
118
+ * Walk-forward core: for each issue after minHistory, predict → score → tune weights.
119
+ * Training uses scoreWindow omitted (every step counts); backtest uses scoreWindow.
120
+ */
121
+ export function runAdaptiveSimulation(
122
+ chronological: NormalizedDraw[],
123
+ gameId: GameId,
124
+ options: SimulationOptions,
125
+ ): SimulationResult | null {
126
+ if (chronological.length <= options.minHistory) return null;
127
+
128
+ const trainable = chronological.length - options.minHistory;
129
+ const testCount = options.scoreWindow
130
+ ? Math.min(options.scoreWindow, trainable)
131
+ : trainable;
132
+ if (testCount <= 0) return null;
133
+
134
+ const testStartIdx = options.scoreWindow
135
+ ? chronological.length - testCount
136
+ : options.minHistory;
137
+
138
+ const useAdaptive = options.adaptive !== false;
139
+ const initialWeights = options.initialWeights
140
+ ? { ...options.initialWeights }
141
+ : { ...DEFAULT_WEIGHTS };
142
+ let adaptiveWeights = { ...initialWeights };
143
+
144
+ let adaptiveSum = 0;
145
+ let fixedSum = 0;
146
+ let randomSum = 0;
147
+ let scored = 0;
148
+ let firstScoredIssue = '';
149
+ let lastScoredIssue = '';
150
+
151
+ for (let idx = options.minHistory; idx < chronological.length; idx++) {
152
+ const actual = chronological[idx];
153
+ if (!actual) continue;
154
+ const history = chronological.slice(0, idx).reverse();
155
+ const tieSeed = `sim:${actual.issue}`;
156
+
157
+ const fixedPick = recommendGame(gameId, history, {
158
+ pickCount: options.pickCount,
159
+ tieSeed,
160
+ weights: DEFAULT_WEIGHTS,
161
+ });
162
+
163
+ const adaptivePick = useAdaptive
164
+ ? recommendGame(gameId, history, {
165
+ pickCount: options.pickCount,
166
+ tieSeed,
167
+ weights: adaptiveWeights,
168
+ })
169
+ : fixedPick;
170
+
171
+ if (idx >= testStartIdx) {
172
+ if (!firstScoredIssue) firstScoredIssue = actual.issue;
173
+ lastScoredIssue = actual.issue;
174
+ fixedSum += comparePickToDraw(fixedPick, actual).rate;
175
+ adaptiveSum += comparePickToDraw(adaptivePick, actual).rate;
176
+ randomSum += randomRateForPeriod(gameId, options.pickCount, actual, options.randomTrials);
177
+ scored++;
178
+ }
179
+
180
+ if (useAdaptive) {
181
+ adaptiveWeights = tuneWeightsFromDraw(
182
+ adaptiveWeights,
183
+ adaptivePick.numbers,
184
+ actual.numbers,
185
+ gameId,
186
+ history,
187
+ );
188
+ }
189
+ }
190
+
191
+ if (!scored) return null;
192
+ const adaptiveAvg = adaptiveSum / scored;
193
+ const fixedAvg = fixedSum / scored;
194
+ const randomAvg = randomSum / scored;
195
+
196
+ return {
197
+ gameId,
198
+ periods: scored,
199
+ warmupPeriods: testStartIdx - options.minHistory,
200
+ adaptiveAvgHitRate: adaptiveAvg,
201
+ fixedAvgHitRate: fixedAvg,
202
+ randomAvgHitRate: randomAvg,
203
+ adaptiveVsRandom: adaptiveAvg - randomAvg,
204
+ adaptiveVsFixed: adaptiveAvg - fixedAvg,
205
+ initialWeights,
206
+ finalWeights: adaptiveWeights,
207
+ firstScoredIssue,
208
+ lastScoredIssue,
209
+ };
210
+ }
@@ -0,0 +1,128 @@
1
+ import type { GameId } from '../types.js';
2
+ import { loadDraws, type LotteryDb } from '../db.js';
3
+
4
+ import { loadGameWeights } from './tracker.js';
5
+ import { runAdaptiveSimulation, type SimulationResult } from './adaptive-sim.js';
6
+ import { trainGameWeights } from './train-weights.js';
7
+ import { formatWeights } from '../stats/weights.js';
8
+
9
+ export interface BacktestOptions {
10
+ pickCount: number;
11
+ window: number;
12
+ randomTrials: number;
13
+ minHistory: number;
14
+ historyLimit?: number;
15
+ initialWeights?: import('../types.js').ScoreWeights;
16
+ adaptive?: boolean;
17
+ }
18
+
19
+ export type BacktestSummary = SimulationResult;
20
+
21
+ export async function walkForwardBacktest(
22
+ db: LotteryDb,
23
+ gameId: GameId,
24
+ options: BacktestOptions,
25
+ ): Promise<BacktestSummary | null> {
26
+ const loadLimit = options.historyLimit ?? Math.max(options.window + options.minHistory + 50, 150);
27
+ const draws = await loadDraws(db, gameId, loadLimit);
28
+ if (draws.length <= options.minHistory) return null;
29
+
30
+ const chronological = [...draws].sort((a, b) => a.issue.localeCompare(b.issue));
31
+ return runAdaptiveSimulation(chronological, gameId, {
32
+ pickCount: options.pickCount,
33
+ minHistory: options.minHistory,
34
+ randomTrials: options.randomTrials,
35
+ initialWeights: options.initialWeights,
36
+ adaptive: options.adaptive,
37
+ scoreWindow: options.window,
38
+ });
39
+ }
40
+
41
+ export async function runBacktestForGames(
42
+ db: LotteryDb,
43
+ gameIds: GameId[],
44
+ options: BacktestOptions,
45
+ ): Promise<BacktestSummary[]> {
46
+ const out: BacktestSummary[] = [];
47
+ for (const gameId of gameIds) {
48
+ const initial = await loadGameWeights(db, gameId);
49
+ const summary = await walkForwardBacktest(db, gameId, { ...options, initialWeights: initial });
50
+ if (summary) out.push(summary);
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /** Full-history training refresh (same as lottery-train) when pipeline has no live review. */
56
+ export async function refreshAndPersistWeights(
57
+ db: LotteryDb,
58
+ gameId: GameId,
59
+ options: RefreshWeightsOptions,
60
+ ): Promise<BacktestSummary | null> {
61
+ const result = await trainGameWeights(db, gameId, {
62
+ pickCount: options.pickCount,
63
+ minHistory: options.minHistory,
64
+ historyLimit: options.historyLimit ?? 500,
65
+ randomTrials: options.randomTrials,
66
+ initialWeights: options.initialWeights ?? (await loadGameWeights(db, gameId)),
67
+ holdoutWindow: options.holdoutWindow ?? options.window,
68
+ holdoutFallback: options.holdoutFallback,
69
+ persist: true,
70
+ });
71
+ return result;
72
+ }
73
+
74
+ export interface RefreshWeightsOptions extends BacktestOptions {
75
+ persist: boolean;
76
+ reviewEvaluated: number;
77
+ holdoutWindow?: number;
78
+ holdoutFallback?: boolean;
79
+ }
80
+
81
+ export async function refreshWeightsForGames(
82
+ db: LotteryDb,
83
+ gameIds: GameId[],
84
+ options: RefreshWeightsOptions,
85
+ ): Promise<BacktestSummary[]> {
86
+ if (!options.persist) return [];
87
+ const out: BacktestSummary[] = [];
88
+ for (const gameId of gameIds) {
89
+ if (options.reviewEvaluated > 0) continue;
90
+ const summary = await refreshAndPersistWeights(db, gameId, options);
91
+ if (summary) out.push(summary);
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function pct(rate: number): string {
97
+ return (rate * 100).toFixed(1);
98
+ }
99
+
100
+ function signedPct(delta: number): string {
101
+ const v = delta * 100;
102
+ return v >= 0 ? `+${v.toFixed(1)}` : v.toFixed(1);
103
+ }
104
+
105
+ export function formatBacktestLine(s: BacktestSummary): string {
106
+ const w = formatWeights(s.finalWeights);
107
+ return [
108
+ `${s.gameId}: 调优 ${pct(s.adaptiveAvgHitRate)}%`,
109
+ `固定 ${pct(s.fixedAvgHitRate)}%`,
110
+ `随机 ${pct(s.randomAvgHitRate)}%`,
111
+ `(Δ调优-随机 ${signedPct(s.adaptiveVsRandom)}%, n=${s.periods}, 热身${s.warmupPeriods}期)`,
112
+ `终权重 ${w}`,
113
+ ].join(' / ');
114
+ }
115
+
116
+ export function formatBacktestSection(summaries: BacktestSummary[]): string {
117
+ if (!summaries.length) return '';
118
+ const lines = [
119
+ '【回测可信度】近 N 期 walk-forward 抽样(全量训练请用 lottery-train)',
120
+ ...summaries.map(formatBacktestLine),
121
+ '说明:Δ 接近 0 为正常;终权重见 lottery_model_weights。',
122
+ '推荐时若近端调优输给随机,将自动回退默认 F40/O35/T25(见【权重保护】)。',
123
+ ];
124
+ return lines.join('\n');
125
+ }
126
+
127
+ export type { TrainWeightsOptions } from './train-weights.js';
128
+ export { trainGameWeights, trainAllGameWeights, formatTrainReport } from './train-weights.js';
@@ -0,0 +1,132 @@
1
+ import { DEFAULT_WEIGHTS, type DrawNumbers, type GameId, type GamePick, type HitSummary, type NormalizedDraw, type ScoreWeights } from '../types.js';
2
+
3
+ import {
4
+ affinityOfNumbers,
5
+ buildFreqMap,
6
+ buildOmitMap,
7
+ } from '../stats/pro-analysis.js';
8
+ import { analyzePoolTrend } from '../stats/engine.js';
9
+ import { mergeWeights } from '../stats/weights.js';
10
+
11
+ function intersectCount(a: number[], b: number[]): number {
12
+ const set = new Set(b);
13
+ return a.filter((n) => set.has(n)).length;
14
+ }
15
+
16
+ function digitPositionHits(pick: number[], actual: number[]): { hits: number; total: number } {
17
+ const total = Math.min(pick.length, actual.length);
18
+ let hits = 0;
19
+ for (let i = 0; i < total; i++) {
20
+ if (pick[i] === actual[i]) hits++;
21
+ }
22
+ return { hits, total };
23
+ }
24
+
25
+ export function comparePickToDraw(pick: GamePick, draw: NormalizedDraw): HitSummary {
26
+ const n = pick.numbers;
27
+ const a = draw.numbers;
28
+ switch (pick.gameId) {
29
+ case 'kl8': {
30
+ const p = n.main ?? [];
31
+ const act = a.main ?? [];
32
+ const hits = intersectCount(p, act);
33
+ return {
34
+ total: p.length,
35
+ hits,
36
+ rate: p.length ? hits / p.length : 0,
37
+ detail: `命中 ${hits}/${p.length}`,
38
+ };
39
+ }
40
+ case 'ssq': {
41
+ const redHits = intersectCount(n.red ?? [], a.red ?? []);
42
+ const blueHit = (n.blue?.[0] != null && a.blue?.includes(n.blue[0])) ? 1 : 0;
43
+ const total = 7;
44
+ const hits = redHits + blueHit;
45
+ return {
46
+ total,
47
+ hits,
48
+ rate: hits / total,
49
+ detail: `红${redHits}/6 蓝${blueHit}/1`,
50
+ };
51
+ }
52
+ case 'dlt': {
53
+ const fHits = intersectCount(n.front ?? [], a.front ?? []);
54
+ const bHits = intersectCount(n.back ?? [], a.back ?? []);
55
+ const total = 7;
56
+ const hits = fHits + bHits;
57
+ return {
58
+ total,
59
+ hits,
60
+ rate: hits / total,
61
+ detail: `前${fHits}/5 后${bHits}/2`,
62
+ };
63
+ }
64
+ case 'fc3d':
65
+ case 'pl3':
66
+ case 'pl5': {
67
+ const { hits, total } = digitPositionHits(n.digits ?? [], a.digits ?? []);
68
+ return {
69
+ total,
70
+ hits,
71
+ rate: total ? hits / total : 0,
72
+ detail: `定位 ${hits}/${total}`,
73
+ };
74
+ }
75
+ default:
76
+ return { total: 0, hits: 0, rate: 0, detail: '未知玩法' };
77
+ }
78
+ }
79
+
80
+ /** 根据开奖结果反推更优权重方向,用于渐进调优 */
81
+ export function tuneWeightsFromDraw(
82
+ current: ScoreWeights,
83
+ pick: DrawNumbers,
84
+ actual: DrawNumbers,
85
+ gameId: GameId,
86
+ draws: NormalizedDraw[],
87
+ ): ScoreWeights {
88
+ const rows = draws.map((d) => flatNumbers(d.numbers, gameId)).filter((r) => r.length);
89
+ if (!rows.length) return current;
90
+
91
+ const ranges = poolRange(gameId);
92
+ if (!ranges) {
93
+ return current;
94
+ }
95
+ const { min, max } = ranges;
96
+ const freqMap = buildFreqMap(rows, min, max);
97
+ const omitMap = buildOmitMap(rows, min, max);
98
+ const trendMap = analyzePoolTrend(rows, min, max);
99
+ const actualNums = flatNumbers(actual, gameId);
100
+ const actualAff = affinityOfNumbers(actualNums, freqMap, omitMap, trendMap);
101
+ return mergeWeights(current, actualAff, 0.04);
102
+ }
103
+
104
+ function flatNumbers(n: DrawNumbers, gameId: GameId): number[] {
105
+ if (gameId === 'kl8') return n.main ?? [];
106
+ if (gameId === 'ssq') return [...(n.red ?? []), ...(n.blue ?? [])];
107
+ if (gameId === 'dlt') return [...(n.front ?? []), ...(n.back ?? [])];
108
+ return n.digits ?? [];
109
+ }
110
+
111
+ function poolRange(gameId: GameId): { min: number; max: number } | null {
112
+ switch (gameId) {
113
+ case 'kl8':
114
+ return { min: 1, max: 80 };
115
+ case 'ssq':
116
+ return { min: 1, max: 33 };
117
+ case 'dlt':
118
+ return { min: 1, max: 35 };
119
+ case 'fc3d':
120
+ case 'pl3':
121
+ case 'pl5':
122
+ return { min: 0, max: 9 };
123
+ default:
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function formatHitSummary(gameId: GameId, hit: HitSummary, issue: string): string {
129
+ return `${gameId} 第${issue}期 ${hit.detail} (${(hit.rate * 100).toFixed(0)}%)`;
130
+ }
131
+
132
+ export { DEFAULT_WEIGHTS };
@@ -0,0 +1,120 @@
1
+ import type { GameId, NormalizedDraw } from '../types.js';
2
+ import { loadDraws, type LotteryDb } from '../db.js';
3
+
4
+ import { formatHitSummary } from './hit-rate.js';
5
+ import {
6
+ listPendingPredictions,
7
+ PREDICTIONS_TABLE,
8
+ submitAgentReview,
9
+ } from './tracker.js';
10
+
11
+ export interface EvaluatePendingResult {
12
+ evaluated: number;
13
+ skipped: boolean;
14
+ lines: string[];
15
+ }
16
+
17
+ async function isIssueAlreadyReviewed(db: LotteryDb, gameId: GameId, issue: string): Promise<boolean> {
18
+ const predModel = db.models.get(PREDICTIONS_TABLE);
19
+ if (!predModel) return false;
20
+ const rows = (await predModel.select().where({
21
+ game_id: gameId,
22
+ status: 'evaluated',
23
+ actual_issue: issue,
24
+ })) as unknown[];
25
+ return rows.length > 0;
26
+ }
27
+
28
+ function drawsForReview(
29
+ gameId: GameId,
30
+ insertedDraws: NormalizedDraw[],
31
+ latestInDb: NormalizedDraw | undefined,
32
+ stalePending: boolean,
33
+ ): NormalizedDraw[] {
34
+ const inserted = insertedDraws
35
+ .filter((d) => d.gameId === gameId)
36
+ .sort((a, b) => a.issue.localeCompare(b.issue));
37
+ if (inserted.length) return inserted;
38
+ if (stalePending && latestInDb) return [latestInDb];
39
+ return [];
40
+ }
41
+
42
+ /** Match pending picks to new or latest draws; skip when nothing pending. */
43
+ export async function evaluatePendingPredictions(
44
+ db: LotteryDb,
45
+ gameIds: GameId[],
46
+ historyLimit: number,
47
+ insertedDraws: NormalizedDraw[] = [],
48
+ ): Promise<EvaluatePendingResult> {
49
+ const lines: string[] = [];
50
+ let evaluated = 0;
51
+ let hadPending = false;
52
+ const today = new Date().toISOString().slice(0, 10);
53
+
54
+ for (const gameId of gameIds) {
55
+ const pending = (await listPendingPredictions(db, gameId)) as Array<{
56
+ predict_at: string;
57
+ predict_date: string;
58
+ }>;
59
+ if (!pending.length) continue;
60
+ hadPending = true;
61
+
62
+ const stale = pending.filter((p) => p.predict_date < today);
63
+ const todayOnly = pending.filter((p) => p.predict_date === today);
64
+ const latest = (await loadDraws(db, gameId, 1))[0];
65
+ const useStale = stale.length > 0 && latest && !(await isIssueAlreadyReviewed(db, gameId, latest.issue));
66
+ const drawsToMatch = drawsForReview(gameId, insertedDraws, latest, useStale);
67
+
68
+ if (!drawsToMatch.length) {
69
+ if (todayOnly.length) {
70
+ lines.push(`${gameId}: waiting for next draw`);
71
+ } else {
72
+ lines.push(`${gameId}: pending, no matching draw yet`);
73
+ }
74
+ continue;
75
+ }
76
+
77
+ const pool = useStale ? [...stale] : [...pending];
78
+ const orderedPending = pool.sort((a, b) => a.predict_at.localeCompare(b.predict_at));
79
+ for (const draw of drawsToMatch) {
80
+ const row = orderedPending.shift();
81
+ if (!row) break;
82
+ const hit = await submitAgentReview(
83
+ db,
84
+ {
85
+ gameId,
86
+ predictAt: row.predict_at,
87
+ actualIssue: draw.issue,
88
+ actualNumbers: draw.numbers,
89
+ analysis: '',
90
+ },
91
+ historyLimit,
92
+ );
93
+ if (hit) {
94
+ evaluated++;
95
+ lines.push(formatHitSummary(gameId, hit, draw.issue));
96
+ }
97
+ }
98
+ }
99
+
100
+ if (!hadPending) {
101
+ return { evaluated: 0, skipped: true, lines: ['no pending predictions'] };
102
+ }
103
+ return { evaluated, skipped: false, lines };
104
+ }
105
+
106
+ /** Games that still need a next-period pick (no pending row for today). */
107
+ export async function gamesNeedingPrediction(db: LotteryDb, gameIds: GameId[], date = new Date()): Promise<GameId[]> {
108
+ const today = date.toISOString().slice(0, 10);
109
+ const out: GameId[] = [];
110
+ for (const gid of gameIds) {
111
+ const pending = (await listPendingPredictions(db, gid)) as Array<{ predict_date: string }>;
112
+ if (!pending.some((p) => p.predict_date === today)) out.push(gid);
113
+ }
114
+ return out;
115
+ }
116
+
117
+ export async function latestDrawIssue(db: LotteryDb, gameId: GameId): Promise<string | null> {
118
+ const draws = await loadDraws(db, gameId, 1);
119
+ return draws[0]?.issue ?? null;
120
+ }