@zhin.js/plugin-lottery 1.1.8 → 1.1.10
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.
- package/README.md +8 -0
- package/commands/lottery/[[game]]/index.js +5 -4
- package/commands/lottery/[[game]]/index.ts +5 -4
- package/commands/lottery-backtest/[[game]]/index.js +9 -4
- package/commands/lottery-backtest/[[game]]/index.ts +8 -4
- package/commands/lottery-history/[game]/index.js +6 -4
- package/commands/lottery-history/[game]/index.ts +6 -4
- package/commands/lottery-stats/[game]/index.js +11 -6
- package/commands/lottery-stats/[game]/index.ts +11 -5
- package/commands/lottery-today/index.js +8 -4
- package/commands/lottery-today/index.ts +8 -3
- package/commands/lottery-train/[[game]]/index.js +4 -3
- package/commands/lottery-train/[[game]]/index.ts +4 -3
- package/lib/command-helpers.d.ts +2 -1
- package/lib/command-helpers.d.ts.map +1 -1
- package/lib/command-helpers.js +2 -1
- package/lib/command-helpers.js.map +1 -1
- package/lib/evaluate/backtest.d.ts +3 -2
- package/lib/evaluate/backtest.d.ts.map +1 -1
- package/lib/evaluate/backtest.js +12 -11
- package/lib/evaluate/backtest.js.map +1 -1
- package/lib/evaluate/tracker.d.ts +2 -1
- package/lib/evaluate/tracker.d.ts.map +1 -1
- package/lib/evaluate/tracker.js +9 -8
- package/lib/evaluate/tracker.js.map +1 -1
- package/lib/evaluate/train-weights.d.ts +3 -2
- package/lib/evaluate/train-weights.d.ts.map +1 -1
- package/lib/evaluate/train-weights.js +12 -11
- package/lib/evaluate/train-weights.js.map +1 -1
- package/lib/games/kl8-groups.d.ts +2 -0
- package/lib/games/kl8-groups.d.ts.map +1 -1
- package/lib/games/kl8-groups.js +2 -1
- package/lib/games/kl8-groups.js.map +1 -1
- package/lib/i18n.d.ts +2 -0
- package/lib/i18n.d.ts.map +1 -0
- package/lib/i18n.js +222 -0
- package/lib/i18n.js.map +1 -0
- package/lib/lottery-tool-handlers.d.ts +4 -3
- package/lib/lottery-tool-handlers.d.ts.map +1 -1
- package/lib/lottery-tool-handlers.js +12 -7
- package/lib/lottery-tool-handlers.js.map +1 -1
- package/lib/pipeline.d.ts +3 -1
- package/lib/pipeline.d.ts.map +1 -1
- package/lib/pipeline.js +9 -7
- package/lib/pipeline.js.map +1 -1
- package/lib/recommend/game-pick.d.ts +4 -2
- package/lib/recommend/game-pick.d.ts.map +1 -1
- package/lib/recommend/game-pick.js +39 -28
- package/lib/recommend/game-pick.js.map +1 -1
- package/lib/recommend/report.d.ts +4 -2
- package/lib/recommend/report.d.ts.map +1 -1
- package/lib/recommend/report.js +17 -10
- package/lib/recommend/report.js.map +1 -1
- package/lib/runtime-state.d.ts +2 -0
- package/lib/runtime-state.d.ts.map +1 -1
- package/lib/runtime-state.js.map +1 -1
- package/lib/stats/pro-analysis.d.ts +3 -2
- package/lib/stats/pro-analysis.d.ts.map +1 -1
- package/lib/stats/pro-analysis.js +10 -9
- package/lib/stats/pro-analysis.js.map +1 -1
- package/lib/sync/run-sync.d.ts +2 -1
- package/lib/sync/run-sync.d.ts.map +1 -1
- package/lib/sync/run-sync.js +4 -3
- package/lib/sync/run-sync.js.map +1 -1
- package/lib/types.d.ts +2 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/package.json +8 -8
- package/plugin.js +4 -0
- package/skills/lottery/tools/compute-recommend/index.js +1 -1
- package/skills/lottery/tools/compute-recommend/index.ts +1 -1
- package/skills/lottery/tools/history/index.js +2 -1
- package/skills/lottery/tools/history/index.ts +2 -1
- package/skills/lottery/tools/save-prediction/index.js +1 -1
- package/skills/lottery/tools/save-prediction/index.ts +1 -1
- package/skills/lottery/tools/stats-snapshot/index.js +1 -1
- package/skills/lottery/tools/stats-snapshot/index.ts +1 -1
- package/skills/lottery/tools/sync/index.js +1 -1
- package/skills/lottery/tools/sync/index.ts +1 -0
- package/src/command-helpers.ts +3 -0
- package/src/evaluate/backtest.ts +13 -11
- package/src/evaluate/tracker.ts +10 -8
- package/src/evaluate/train-weights.ts +13 -11
- package/src/games/kl8-groups.ts +4 -1
- package/src/i18n.ts +222 -0
- package/src/lottery-tool-handlers.ts +12 -7
- package/src/pipeline.ts +10 -7
- package/src/recommend/game-pick.ts +40 -28
- package/src/recommend/report.ts +18 -8
- package/src/runtime-state.ts +2 -0
- package/src/stats/pro-analysis.ts +11 -7
- package/src/sync/run-sync.ts +5 -2
- package/src/types.ts +2 -0
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { createMessages } from 'zhin.js/i18n';
|
|
2
|
+
|
|
3
|
+
export const messages = createMessages({
|
|
4
|
+
dbNotReady: { 'zh-CN': '数据库未就绪', en: 'Database is not ready.' },
|
|
5
|
+
syncResult: { 'zh-CN': '同步 {result}', en: 'Sync {result}' },
|
|
6
|
+
reviewTitle: { 'zh-CN': '【彩票推荐复盘】', en: '[Lottery recommendation review]' },
|
|
7
|
+
reviewModel: { 'zh-CN': '■ {game} 模型 {weights}', en: '■ {game} model {weights}' },
|
|
8
|
+
reviewRate: { 'zh-CN': ' 累计 {count} 期 平均命中 {rate}%', en: ' {count} draws reviewed; average hit rate {rate}%' },
|
|
9
|
+
reviewEmpty: { 'zh-CN': ' 尚无复盘数据', en: ' No reviews yet.' },
|
|
10
|
+
reviewDraw: { 'zh-CN': ' 第{issue}期 {detail} ({rate}%)', en: ' Draw {issue}: {detail} ({rate}%)' },
|
|
11
|
+
agentMemory: { 'zh-CN': '【Agent 记忆】', en: '[Agent memory]' },
|
|
12
|
+
pickLine: { 'zh-CN': '■ {label}:{numbers}', en: '■ {label}: {numbers}' },
|
|
13
|
+
noPicks: { 'zh-CN': '暂无推荐号码,请确认已同步开奖数据。', en: 'No picks available. Make sure draw results have been synchronized.' },
|
|
14
|
+
noReport: { 'zh-CN': '今日尚无推荐,请等待定时任务。', en: 'No report for today yet. Wait for the scheduled run.' },
|
|
15
|
+
noReportCommand: { 'zh-CN': '今日尚无推荐,可执行 {command} 或等待定时任务。', en: 'No report for today yet. Run {command} or wait for the scheduled run.' },
|
|
16
|
+
gameRequired: { 'zh-CN': '请指定玩法', en: 'Specify a lottery game.' },
|
|
17
|
+
invalidGame: { 'zh-CN': '无效的玩法', en: 'Invalid lottery game.' },
|
|
18
|
+
noData: { 'zh-CN': '暂无数据', en: 'No data available.' },
|
|
19
|
+
syncRequired: { 'zh-CN': '历史数据不足,请先同步开奖。', en: 'Not enough history. Synchronize draw results first.' },
|
|
20
|
+
syncCommand: { 'zh-CN': '历史数据不足,请先执行 {command} 同步开奖。', en: 'Not enough history. Run {command} to synchronize draw results first.' },
|
|
21
|
+
sample: { 'zh-CN': '样本 {count} 期', en: 'Sample: {count} draws' },
|
|
22
|
+
"zones": {
|
|
23
|
+
"zh-CN": "三区 {zones}",
|
|
24
|
+
"en": "Zones {zones}"
|
|
25
|
+
},
|
|
26
|
+
"oddEven": {
|
|
27
|
+
"zh-CN": "奇偶 {odd}:{even}",
|
|
28
|
+
"en": "Odd/even {odd}:{even}"
|
|
29
|
+
},
|
|
30
|
+
"bigSmall": {
|
|
31
|
+
"zh-CN": "大小 {big}:{small}",
|
|
32
|
+
"en": "High/low {big}:{small}"
|
|
33
|
+
},
|
|
34
|
+
"recentHot": {
|
|
35
|
+
"zh-CN": "近10期热号 {numbers}",
|
|
36
|
+
"en": "Hot numbers in last 10 draws: {numbers}"
|
|
37
|
+
},
|
|
38
|
+
"weights": {
|
|
39
|
+
"zh-CN": "权重 {weights}",
|
|
40
|
+
"en": "Weights {weights}"
|
|
41
|
+
},
|
|
42
|
+
"digitPos": {
|
|
43
|
+
"zh-CN": "第{position}位[{numbers}]",
|
|
44
|
+
"en": "Position {position} [{numbers}]"
|
|
45
|
+
},
|
|
46
|
+
"strategy.balanced": {
|
|
47
|
+
"zh-CN": "均衡",
|
|
48
|
+
"en": "Balanced"
|
|
49
|
+
},
|
|
50
|
+
"strategy.hot": {
|
|
51
|
+
"zh-CN": "热号",
|
|
52
|
+
"en": "Hot"
|
|
53
|
+
},
|
|
54
|
+
"strategy.cold": {
|
|
55
|
+
"zh-CN": "冷号",
|
|
56
|
+
"en": "Cold"
|
|
57
|
+
},
|
|
58
|
+
"strategy.trend": {
|
|
59
|
+
"zh-CN": "趋势",
|
|
60
|
+
"en": "Trend"
|
|
61
|
+
},
|
|
62
|
+
"pickLabel": {
|
|
63
|
+
"zh-CN": "{name}(选{count})",
|
|
64
|
+
"en": "{name} (pick {count})"
|
|
65
|
+
},
|
|
66
|
+
"pickGroupsLabel": {
|
|
67
|
+
"zh-CN": "{name}(选{count}·{groups}组)",
|
|
68
|
+
"en": "{name} (pick {count} · {groups} groups)"
|
|
69
|
+
},
|
|
70
|
+
"basicDetail": {
|
|
71
|
+
"zh-CN": "频率+遗漏+趋势 | 和值{sum} 跨度{span}",
|
|
72
|
+
"en": "Frequency + omission + trend | Sum {sum}, span {span}"
|
|
73
|
+
},
|
|
74
|
+
"multiDetail": {
|
|
75
|
+
"zh-CN": "多策略 {strategies} | 和值{sum} 跨度{span}",
|
|
76
|
+
"en": "Strategies {strategies} | Sum {sum}, span {span}"
|
|
77
|
+
},
|
|
78
|
+
"redDetail": {
|
|
79
|
+
"zh-CN": "红球 {oddEven} {bigSmall} | 蓝 {blue}",
|
|
80
|
+
"en": "Red {oddEven} {bigSmall} | Blue {blue}"
|
|
81
|
+
},
|
|
82
|
+
"frontDetail": {
|
|
83
|
+
"zh-CN": "前区 {oddEven} {zones}",
|
|
84
|
+
"en": "Front {oddEven} {zones}"
|
|
85
|
+
},
|
|
86
|
+
"digitDetail": {
|
|
87
|
+
"zh-CN": "直选 {positions} | 和值{sum}",
|
|
88
|
+
"en": "Straight {positions} | Sum {sum}"
|
|
89
|
+
},
|
|
90
|
+
"groupLine": {
|
|
91
|
+
"zh-CN": " 组{index}·{label}:{numbers}",
|
|
92
|
+
"en": " Group {index} · {label}: {numbers}"
|
|
93
|
+
},
|
|
94
|
+
"redLine": {
|
|
95
|
+
"zh-CN": "■ {label}:红 {red} + 蓝 {blue}",
|
|
96
|
+
"en": "■ {label}: Red {red} + Blue {blue}"
|
|
97
|
+
},
|
|
98
|
+
"frontLine": {
|
|
99
|
+
"zh-CN": "■ {label}:前 {front} + 后 {back}",
|
|
100
|
+
"en": "■ {label}: Front {front} + Back {back}"
|
|
101
|
+
},
|
|
102
|
+
"hot": {
|
|
103
|
+
"zh-CN": "热号 {numbers}",
|
|
104
|
+
"en": "Hot {numbers}"
|
|
105
|
+
},
|
|
106
|
+
"cold": {
|
|
107
|
+
"zh-CN": "冷号 {numbers}",
|
|
108
|
+
"en": "Cold {numbers}"
|
|
109
|
+
},
|
|
110
|
+
"accuracy": {
|
|
111
|
+
"zh-CN": "历史命中 {rate}% ({count}期) 近5期 {recent}",
|
|
112
|
+
"en": "Historical hit rate {rate}% ({count} draws), last 5: {recent}"
|
|
113
|
+
},
|
|
114
|
+
"game.kl8": {
|
|
115
|
+
"zh-CN": "快乐8",
|
|
116
|
+
"en": "Happy 8"
|
|
117
|
+
},
|
|
118
|
+
"game.ssq": {
|
|
119
|
+
"zh-CN": "双色球",
|
|
120
|
+
"en": "Double Color Ball"
|
|
121
|
+
},
|
|
122
|
+
"game.dlt": {
|
|
123
|
+
"zh-CN": "超级大乐透",
|
|
124
|
+
"en": "Super Lotto"
|
|
125
|
+
},
|
|
126
|
+
"game.fc3d": {
|
|
127
|
+
"zh-CN": "福彩3D",
|
|
128
|
+
"en": "Fucai 3D"
|
|
129
|
+
},
|
|
130
|
+
"game.pl3": {
|
|
131
|
+
"zh-CN": "排列3",
|
|
132
|
+
"en": "Pick 3"
|
|
133
|
+
},
|
|
134
|
+
"game.pl5": {
|
|
135
|
+
"zh-CN": "排列5",
|
|
136
|
+
"en": "Pick 5"
|
|
137
|
+
},
|
|
138
|
+
"dailyTitle": {
|
|
139
|
+
"zh-CN": "【彩票每日推荐】{date}",
|
|
140
|
+
"en": "[Daily lottery report] {date}"
|
|
141
|
+
},
|
|
142
|
+
"weightFallback": {
|
|
143
|
+
"zh-CN": "【权重保护】近端 holdout 调优未跑赢随机,已回退默认 F40/O35/T25:{games}",
|
|
144
|
+
"en": "[Weight safeguard] Recent holdout tuning did not beat random; reverted to F40/O35/T25: {games}"
|
|
145
|
+
},
|
|
146
|
+
"accuracyOverview": {
|
|
147
|
+
"zh-CN": "【模型复盘】累计平均命中 {overview}",
|
|
148
|
+
"en": "[Model review] Cumulative average hit rate: {overview}"
|
|
149
|
+
},
|
|
150
|
+
"statsExplanation": {
|
|
151
|
+
"zh-CN": "【统计说明】号码由频率+遗漏+近期趋势综合打分,并根据历史命中自动调权。",
|
|
152
|
+
"en": "[Method] Numbers are scored by frequency, omission and recent trends; weights adapt to historical hits."
|
|
153
|
+
},
|
|
154
|
+
"disclaimer": {
|
|
155
|
+
"zh-CN": "免责声明:本推荐由历史数据统计生成,仅供参考,不构成投注建议,不保证中奖。请理性购彩。",
|
|
156
|
+
"en": "Disclaimer: These picks are generated from historical statistics for reference only, not betting advice or a guarantee of winning. Play responsibly."
|
|
157
|
+
},
|
|
158
|
+
"backtestAdaptive": {
|
|
159
|
+
"zh-CN": "{game}: 调优 {rate}%",
|
|
160
|
+
"en": "{game}: adaptive {rate}%"
|
|
161
|
+
},
|
|
162
|
+
"backtestFixed": {
|
|
163
|
+
"zh-CN": "固定 {rate}%",
|
|
164
|
+
"en": "fixed {rate}%"
|
|
165
|
+
},
|
|
166
|
+
"backtestRandom": {
|
|
167
|
+
"zh-CN": "随机 {rate}%",
|
|
168
|
+
"en": "random {rate}%"
|
|
169
|
+
},
|
|
170
|
+
"backtestDelta": {
|
|
171
|
+
"zh-CN": "(Δ调优-随机 {delta}%, n={count}, 热身{warmup}期)",
|
|
172
|
+
"en": "(Δ adaptive-random {delta}%, n={count}, warmup {warmup} draws)"
|
|
173
|
+
},
|
|
174
|
+
"finalWeights": {
|
|
175
|
+
"zh-CN": "终权重 {weights}",
|
|
176
|
+
"en": "final weights {weights}"
|
|
177
|
+
},
|
|
178
|
+
"backtestTitle": {
|
|
179
|
+
"zh-CN": "【回测可信度】近 N 期 walk-forward 抽样",
|
|
180
|
+
"en": "[Backtest confidence] Walk-forward sampling of the latest N draws"
|
|
181
|
+
},
|
|
182
|
+
"backtestNote": {
|
|
183
|
+
"zh-CN": "说明:Δ 接近 0 为正常;终权重见 lottery_model_weights。",
|
|
184
|
+
"en": "Note: Δ near zero is normal; final weights are stored in lottery_model_weights."
|
|
185
|
+
},
|
|
186
|
+
"backtestFallback": {
|
|
187
|
+
"zh-CN": "推荐时若近端调优输给随机,将自动回退默认 F40/O35/T25(见【权重保护】)。",
|
|
188
|
+
"en": "If recent tuning underperforms random, recommendations revert to F40/O35/T25 (see Weight safeguard)."
|
|
189
|
+
},
|
|
190
|
+
"trainFallback": {
|
|
191
|
+
"zh-CN": " [holdout→默认]",
|
|
192
|
+
"en": " [holdout→default]"
|
|
193
|
+
},
|
|
194
|
+
"trainGame": {
|
|
195
|
+
"zh-CN": "{game}: 训练{steps}期 ({first}→{last}){fallback}",
|
|
196
|
+
"en": "{game}: trained on {steps} draws ({first}→{last}){fallback}"
|
|
197
|
+
},
|
|
198
|
+
"trainHit": {
|
|
199
|
+
"zh-CN": "命中 调优{adaptive}% / 固定{fixed}% / 随机{random}%",
|
|
200
|
+
"en": "Hit rate: adaptive {adaptive}% / fixed {fixed}% / random {random}%"
|
|
201
|
+
},
|
|
202
|
+
"trainDelta": {
|
|
203
|
+
"zh-CN": "Δ调优-随机 {delta}%",
|
|
204
|
+
"en": "Δ adaptive-random {delta}%"
|
|
205
|
+
},
|
|
206
|
+
"trainWeights": {
|
|
207
|
+
"zh-CN": "权重 {initial} → {final}",
|
|
208
|
+
"en": "Weights {initial} → {final}"
|
|
209
|
+
},
|
|
210
|
+
"trainEmpty": {
|
|
211
|
+
"zh-CN": "训练失败:历史数据不足,请先同步开奖。",
|
|
212
|
+
"en": "Training failed: not enough history. Synchronize draw results first."
|
|
213
|
+
},
|
|
214
|
+
"trainTitle": {
|
|
215
|
+
"zh-CN": "【权重训练】全历史 walk-forward:预测→比对→调权,已写入 lottery_model_weights",
|
|
216
|
+
"en": "[Weight training] Full-history walk-forward: predict → compare → tune; saved to lottery_model_weights"
|
|
217
|
+
},
|
|
218
|
+
"trainNote": {
|
|
219
|
+
"zh-CN": "说明:训练≠保证未来更准;终权重供后续推荐使用。",
|
|
220
|
+
"en": "Note: training does not guarantee better future results. Final weights are used for later recommendations."
|
|
221
|
+
}
|
|
222
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveLocale, type Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t } from './i18n.js';
|
|
1
3
|
import type { GameId } from './types.js';
|
|
2
4
|
|
|
3
5
|
import { parseGameId } from './games/registry.js';
|
|
@@ -25,14 +27,15 @@ export async function handleGetModelState(runtime: LotteryRuntime, game?: string
|
|
|
25
27
|
return JSON.stringify(out);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export async function handleComputeRecommend(runtime: LotteryRuntime, game: string): Promise<string> {
|
|
30
|
+
export async function handleComputeRecommend(runtime: LotteryRuntime, game: string, locale: Locale = resolveLocale(runtime.locale)): Promise<string> {
|
|
29
31
|
const gid = parseGameId(game);
|
|
30
|
-
if (!gid) return
|
|
32
|
+
if (!gid) return t(locale, 'invalidGame');
|
|
31
33
|
const { db, config: cfg } = runtime;
|
|
32
34
|
const kl8 = lotteryKl8(cfg);
|
|
33
35
|
const draws = await loadDraws(db, gid, cfg.historyLimit);
|
|
34
|
-
if (!draws.length) return
|
|
36
|
+
if (!draws.length) return t(locale, 'syncRequired');
|
|
35
37
|
const pick = recommendGame(gid, draws, {
|
|
38
|
+
locale,
|
|
36
39
|
pickCount: gid === 'kl8' ? kl8.pickCount : cfg.pickCount,
|
|
37
40
|
tieSeed: new Date().toISOString().slice(0, 10),
|
|
38
41
|
weights: await loadGameWeights(db, gid),
|
|
@@ -42,13 +45,14 @@ export async function handleComputeRecommend(runtime: LotteryRuntime, game: stri
|
|
|
42
45
|
return JSON.stringify(pick);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
export async function handleSavePrediction(runtime: LotteryRuntime, game: string, numbersJson: string): Promise<string> {
|
|
48
|
+
export async function handleSavePrediction(runtime: LotteryRuntime, game: string, numbersJson: string, locale: Locale = resolveLocale(runtime.locale)): Promise<string> {
|
|
46
49
|
const gid = parseGameId(game);
|
|
47
|
-
if (!gid) return
|
|
50
|
+
if (!gid) return t(locale, 'invalidGame');
|
|
48
51
|
const { db } = runtime;
|
|
49
52
|
const numbers = JSON.parse(numbersJson);
|
|
50
53
|
const weights = await loadGameWeights(db, gid);
|
|
51
54
|
await savePrediction(db, {
|
|
55
|
+
locale,
|
|
52
56
|
gameId: gid,
|
|
53
57
|
label: gid,
|
|
54
58
|
numbers,
|
|
@@ -64,13 +68,14 @@ export async function handleListPending(runtime: LotteryRuntime, game?: string):
|
|
|
64
68
|
return JSON.stringify(await listPendingPredictions(db, gid ?? undefined));
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
export async function handleStatsSnapshot(runtime: LotteryRuntime, game: string): Promise<string> {
|
|
71
|
+
export async function handleStatsSnapshot(runtime: LotteryRuntime, game: string, locale: Locale = resolveLocale(runtime.locale)): Promise<string> {
|
|
68
72
|
const gid = parseGameId(game);
|
|
69
|
-
if (!gid) return
|
|
73
|
+
if (!gid) return t(locale, 'invalidGame');
|
|
70
74
|
const { db, config: cfg } = runtime;
|
|
71
75
|
const kl8 = lotteryKl8(cfg);
|
|
72
76
|
const draws = await loadDraws(db, gid, Math.min(30, cfg.historyLimit));
|
|
73
77
|
const pick = recommendGame(gid, draws, {
|
|
78
|
+
locale,
|
|
74
79
|
pickCount: gid === 'kl8' ? kl8.pickCount : cfg.pickCount,
|
|
75
80
|
tieSeed: 'snapshot',
|
|
76
81
|
weights: await loadGameWeights(db, gid),
|
package/src/pipeline.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveLocale, type Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t } from './i18n.js';
|
|
1
3
|
import type { GameId } from './types.js';
|
|
2
4
|
import { syncGame, type LotteryDb } from './db.js';
|
|
3
5
|
|
|
@@ -19,6 +21,7 @@ import { cancelTodayPendingPredictions } from './evaluate/tracker.js';
|
|
|
19
21
|
import type { Kl8Config } from './games/kl8-groups.js';
|
|
20
22
|
|
|
21
23
|
export interface PipelineDeps {
|
|
24
|
+
locale?: Locale;
|
|
22
25
|
db: LotteryDb;
|
|
23
26
|
enabledGames: () => GameId[];
|
|
24
27
|
historyLimit: number;
|
|
@@ -57,7 +60,7 @@ async function attachBacktestOverview(
|
|
|
57
60
|
const summaries = holdoutSummaries.length
|
|
58
61
|
? holdoutSummaries
|
|
59
62
|
: await runBacktestForGames(db, gameIds, backtestOpts(deps));
|
|
60
|
-
return formatBacktestSection(summaries);
|
|
63
|
+
return formatBacktestSection(summaries, deps.locale);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export interface PipelineOptions {
|
|
@@ -128,11 +131,11 @@ export async function runLotteryPipeline(
|
|
|
128
131
|
let recommendText: string;
|
|
129
132
|
|
|
130
133
|
if (!needPredict.length) {
|
|
131
|
-
const existing = await resolveTodayReport(db, gameIds);
|
|
134
|
+
const existing = await resolveTodayReport(db, gameIds, new Date(), deps.locale);
|
|
132
135
|
if (existing) {
|
|
133
136
|
recommendText = 'reused today picks';
|
|
134
137
|
existing.weightFallbackGames = weightPlan.fallbacks.length ? weightPlan.fallbacks : undefined;
|
|
135
|
-
existing.backtestOverview = await attachBacktestOverview(db, gameIds, deps, holdoutSummaries);
|
|
138
|
+
existing.backtestOverview = await attachBacktestOverview(db, gameIds, { ...deps, locale: resolveLocale(existing.locale) }, holdoutSummaries);
|
|
136
139
|
reportText = formatDailyReportText(existing, '');
|
|
137
140
|
} else {
|
|
138
141
|
recommendText = 'skipped (no picks available)';
|
|
@@ -141,7 +144,7 @@ export async function runLotteryPipeline(
|
|
|
141
144
|
const report = await buildDailyReport(
|
|
142
145
|
db,
|
|
143
146
|
needPredict,
|
|
144
|
-
{ pickCount: deps.pickCount, historyLimit: deps.historyLimit, kl8: deps.kl8 },
|
|
147
|
+
{ pickCount: deps.pickCount, historyLimit: deps.historyLimit, kl8: deps.kl8, locale: deps.locale },
|
|
145
148
|
new Date(),
|
|
146
149
|
true,
|
|
147
150
|
weightPlan.overrides,
|
|
@@ -150,7 +153,7 @@ export async function runLotteryPipeline(
|
|
|
150
153
|
report.backtestOverview = await attachBacktestOverview(db, gameIds, deps, holdoutSummaries);
|
|
151
154
|
|
|
152
155
|
if (needPredict.length < gameIds.length) {
|
|
153
|
-
const existing = await resolveTodayReport(db, gameIds);
|
|
156
|
+
const existing = await resolveTodayReport(db, gameIds, new Date(), deps.locale);
|
|
154
157
|
if (existing?.picks?.length) {
|
|
155
158
|
const regen = new Set(needPredict);
|
|
156
159
|
report.picks = [
|
|
@@ -184,7 +187,7 @@ export async function runLotteryPipeline(
|
|
|
184
187
|
};
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
export function formatPipelineReply(result: PipelineStepResult): string {
|
|
190
|
+
export function formatPipelineReply(result: PipelineStepResult, locale?: Locale): string {
|
|
188
191
|
if (result.reportText.trim()) return result.reportText.trim();
|
|
189
|
-
return '
|
|
192
|
+
return t(locale, 'noPicks');
|
|
190
193
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveLocale, type Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t } from '../i18n.js';
|
|
1
3
|
import { type DrawNumbers, type GameId, type GamePick, type NormalizedDraw, type ScoreWeights, type StatsSnapshot, type AccuracySnapshot } from '../types.js';
|
|
2
4
|
import { getGameMeta } from '../games/registry.js';
|
|
3
5
|
import { recommendKl8Groups, resolveKl8Config, type Kl8Config } from '../games/kl8-groups.js';
|
|
@@ -30,6 +32,7 @@ function poolDraws(draws: NormalizedDraw[], extract: (n: DrawNumbers) => number[
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface RecommendOptions {
|
|
35
|
+
locale?: Locale;
|
|
33
36
|
pickCount: number;
|
|
34
37
|
tieSeed: string;
|
|
35
38
|
weights?: ScoreWeights;
|
|
@@ -42,7 +45,9 @@ export function recommendGame(
|
|
|
42
45
|
draws: NormalizedDraw[],
|
|
43
46
|
options: RecommendOptions,
|
|
44
47
|
): GamePick {
|
|
48
|
+
const locale = resolveLocale(options.locale);
|
|
45
49
|
const meta = getGameMeta(gameId);
|
|
50
|
+
const name = locale === 'en' ? t(locale, `game.${gameId}`) : meta.name;
|
|
46
51
|
const tieSeed = `${options.tieSeed}:${gameId}`;
|
|
47
52
|
const weights = options.weights ?? { freq: 0.4, omit: 0.35, trend: 0.25 };
|
|
48
53
|
|
|
@@ -55,10 +60,11 @@ export function recommendGame(
|
|
|
55
60
|
const pick = pickTopUnique(scores, kl8.pickCount);
|
|
56
61
|
const hot = scores.slice(0, 5).map((s) => s.value);
|
|
57
62
|
const cold = [...scores].reverse().slice(0, 5).map((s) => s.value);
|
|
58
|
-
const pro = computeProMetrics(gameId, pick, draws, weights, 1, 80, 40);
|
|
63
|
+
const pro = computeProMetrics(gameId, pick, draws, weights, 1, 80, 40, locale);
|
|
59
64
|
return {
|
|
60
65
|
gameId,
|
|
61
|
-
|
|
66
|
+
locale,
|
|
67
|
+
label: t(locale, 'pickLabel', { name, count: pick.length }),
|
|
62
68
|
numbers: { main: pick },
|
|
63
69
|
weights,
|
|
64
70
|
stats: buildSnapshot(
|
|
@@ -66,7 +72,7 @@ export function recommendGame(
|
|
|
66
72
|
draws,
|
|
67
73
|
hot,
|
|
68
74
|
cold,
|
|
69
|
-
|
|
75
|
+
t(locale, 'basicDetail', { sum: pro.sum, span: pro.span }),
|
|
70
76
|
pro,
|
|
71
77
|
options.accuracy,
|
|
72
78
|
weights,
|
|
@@ -79,15 +85,17 @@ export function recommendGame(
|
|
|
79
85
|
strategies: kl8.groupStrategies,
|
|
80
86
|
tieSeed,
|
|
81
87
|
baseWeights: weights,
|
|
88
|
+
locale,
|
|
82
89
|
});
|
|
83
90
|
const primary = multi.groups[0]?.numbers ?? [];
|
|
84
91
|
const hot = multi.scores.slice(0, 5).map((s) => s.value);
|
|
85
92
|
const cold = [...multi.scores].reverse().slice(0, 5).map((s) => s.value);
|
|
86
|
-
const pro = computeProMetrics(gameId, primary, draws, weights, 1, 80, 40);
|
|
93
|
+
const pro = computeProMetrics(gameId, primary, draws, weights, 1, 80, 40, locale);
|
|
87
94
|
const strategyNote = multi.groups.map((g) => g.label).join('/');
|
|
88
95
|
return {
|
|
89
96
|
gameId,
|
|
90
|
-
|
|
97
|
+
locale,
|
|
98
|
+
label: t(locale, 'pickGroupsLabel', { name, count: kl8.pickCount, groups: multi.groups.length }),
|
|
91
99
|
numbers: { main: primary },
|
|
92
100
|
weights,
|
|
93
101
|
kl8Groups: multi.groups,
|
|
@@ -96,7 +104,7 @@ export function recommendGame(
|
|
|
96
104
|
draws,
|
|
97
105
|
hot,
|
|
98
106
|
cold,
|
|
99
|
-
|
|
107
|
+
t(locale, 'multiDetail', { strategies: strategyNote, sum: pro.sum, span: pro.span }),
|
|
100
108
|
pro,
|
|
101
109
|
options.accuracy,
|
|
102
110
|
weights,
|
|
@@ -110,10 +118,11 @@ export function recommendGame(
|
|
|
110
118
|
const blueScores = scorePool(blueRows, 1, 16, `${tieSeed}:blue`, weights);
|
|
111
119
|
const red = pickTopUnique(redScores, 6);
|
|
112
120
|
const blue = pickTopUnique(blueScores, 1);
|
|
113
|
-
const pro = computeProMetrics(gameId, red, draws, weights, 1, 33, 17);
|
|
121
|
+
const pro = computeProMetrics(gameId, red, draws, weights, 1, 33, 17, locale);
|
|
114
122
|
return {
|
|
115
123
|
gameId,
|
|
116
|
-
|
|
124
|
+
locale,
|
|
125
|
+
label: name,
|
|
117
126
|
numbers: { red, blue },
|
|
118
127
|
weights,
|
|
119
128
|
stats: buildSnapshot(
|
|
@@ -121,7 +130,7 @@ export function recommendGame(
|
|
|
121
130
|
draws,
|
|
122
131
|
redScores.slice(0, 5).map((s) => s.value),
|
|
123
132
|
redScores.slice(-5).map((s) => s.value),
|
|
124
|
-
|
|
133
|
+
t(locale, 'redDetail', { oddEven: pro.oddEven, bigSmall: pro.bigSmall, blue: blue.map(pad2).join(' ') }),
|
|
125
134
|
pro,
|
|
126
135
|
options.accuracy,
|
|
127
136
|
weights,
|
|
@@ -135,10 +144,11 @@ export function recommendGame(
|
|
|
135
144
|
const backScores = scorePool(backRows, 1, 12, `${tieSeed}:back`, weights);
|
|
136
145
|
const front = pickTopUnique(frontScores, 5);
|
|
137
146
|
const back = pickTopUnique(backScores, 2);
|
|
138
|
-
const pro = computeProMetrics(gameId, front, draws, weights, 1, 35, 18);
|
|
147
|
+
const pro = computeProMetrics(gameId, front, draws, weights, 1, 35, 18, locale);
|
|
139
148
|
return {
|
|
140
149
|
gameId,
|
|
141
|
-
|
|
150
|
+
locale,
|
|
151
|
+
label: name,
|
|
142
152
|
numbers: { front, back },
|
|
143
153
|
weights,
|
|
144
154
|
stats: buildSnapshot(
|
|
@@ -146,7 +156,7 @@ export function recommendGame(
|
|
|
146
156
|
draws,
|
|
147
157
|
frontScores.slice(0, 5).map((s) => s.value),
|
|
148
158
|
frontScores.slice(-5).map((s) => s.value),
|
|
149
|
-
|
|
159
|
+
t(locale, 'frontDetail', { oddEven: pro.oddEven, zones: pro.zones ?? '' }).trim(),
|
|
150
160
|
pro,
|
|
151
161
|
options.accuracy,
|
|
152
162
|
weights,
|
|
@@ -157,10 +167,11 @@ export function recommendGame(
|
|
|
157
167
|
case 'pl3': {
|
|
158
168
|
const rows = poolDraws(draws, (n) => n.digits ?? []);
|
|
159
169
|
const digits = scoreDigits(rows, 3, tieSeed, weights);
|
|
160
|
-
const pro = computeDigitProMetrics(digits, draws, weights);
|
|
170
|
+
const pro = computeDigitProMetrics(digits, draws, weights, locale);
|
|
161
171
|
return {
|
|
162
172
|
gameId,
|
|
163
|
-
|
|
173
|
+
locale,
|
|
174
|
+
label: name,
|
|
164
175
|
numbers: { digits },
|
|
165
176
|
weights,
|
|
166
177
|
stats: buildSnapshot(
|
|
@@ -168,7 +179,7 @@ export function recommendGame(
|
|
|
168
179
|
draws,
|
|
169
180
|
digits,
|
|
170
181
|
[],
|
|
171
|
-
|
|
182
|
+
t(locale, 'digitDetail', { positions: pro.digitPos ?? '', sum: pro.sum }),
|
|
172
183
|
pro,
|
|
173
184
|
options.accuracy,
|
|
174
185
|
weights,
|
|
@@ -178,10 +189,11 @@ export function recommendGame(
|
|
|
178
189
|
case 'pl5': {
|
|
179
190
|
const rows = poolDraws(draws, (n) => n.digits ?? []);
|
|
180
191
|
const digits = scoreDigits(rows, 5, tieSeed, weights);
|
|
181
|
-
const pro = computeDigitProMetrics(digits, draws, weights);
|
|
192
|
+
const pro = computeDigitProMetrics(digits, draws, weights, locale);
|
|
182
193
|
return {
|
|
183
194
|
gameId,
|
|
184
|
-
|
|
195
|
+
locale,
|
|
196
|
+
label: name,
|
|
185
197
|
numbers: { digits },
|
|
186
198
|
weights,
|
|
187
199
|
stats: buildSnapshot(
|
|
@@ -189,7 +201,7 @@ export function recommendGame(
|
|
|
189
201
|
draws,
|
|
190
202
|
digits,
|
|
191
203
|
[],
|
|
192
|
-
|
|
204
|
+
t(locale, 'digitDetail', { positions: pro.digitPos ?? '', sum: pro.sum }),
|
|
193
205
|
pro,
|
|
194
206
|
options.accuracy,
|
|
195
207
|
weights,
|
|
@@ -201,36 +213,36 @@ export function recommendGame(
|
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
export function formatPickLine(pick: GamePick): string {
|
|
216
|
+
export function formatPickLine(pick: GamePick, locale: Locale = resolveLocale(pick.locale)): string {
|
|
205
217
|
const n = pick.numbers;
|
|
206
218
|
switch (pick.gameId) {
|
|
207
219
|
case 'kl8': {
|
|
208
220
|
if (pick.kl8Groups?.length) {
|
|
209
221
|
const header = `■ ${pick.label}`;
|
|
210
222
|
const rows = pick.kl8Groups.map(
|
|
211
|
-
(g) =>
|
|
223
|
+
(g) => t(locale, 'groupLine', { index: g.index, label: g.label, numbers: g.numbers.map(pad2).join(' ') }),
|
|
212
224
|
);
|
|
213
225
|
return [header, ...rows].join('\n');
|
|
214
226
|
}
|
|
215
|
-
return
|
|
227
|
+
return t(locale, 'pickLine', { label: pick.label, numbers: (n.main ?? []).map(pad2).join(' ') });
|
|
216
228
|
}
|
|
217
229
|
case 'ssq':
|
|
218
|
-
return
|
|
230
|
+
return t(locale, 'redLine', { label: pick.label, red: (n.red ?? []).map(pad2).join(' '), blue: (n.blue ?? []).map(pad2).join(' ') });
|
|
219
231
|
case 'dlt':
|
|
220
|
-
return
|
|
232
|
+
return t(locale, 'frontLine', { label: pick.label, front: (n.front ?? []).map(pad2).join(' '), back: (n.back ?? []).map(pad2).join(' ') });
|
|
221
233
|
case 'fc3d':
|
|
222
234
|
case 'pl3':
|
|
223
235
|
case 'pl5':
|
|
224
|
-
return
|
|
236
|
+
return t(locale, 'pickLine', { label: pick.label, numbers: (n.digits ?? []).join(' ') });
|
|
225
237
|
default:
|
|
226
238
|
return `■ ${pick.label}`;
|
|
227
239
|
}
|
|
228
240
|
}
|
|
229
241
|
|
|
230
|
-
export function formatPickStats(pick: GamePick): string {
|
|
242
|
+
export function formatPickStats(pick: GamePick, locale: Locale = resolveLocale(pick.locale)): string {
|
|
231
243
|
const parts: string[] = [];
|
|
232
|
-
const hot = pick.stats.hot.length ?
|
|
233
|
-
const cold = pick.stats.cold.length ?
|
|
244
|
+
const hot = pick.stats.hot.length ? t(locale, 'hot', { numbers: pick.stats.hot.join(',') }) : '';
|
|
245
|
+
const cold = pick.stats.cold.length ? t(locale, 'cold', { numbers: pick.stats.cold.join(',') }) : '';
|
|
234
246
|
if (hot) parts.push(hot);
|
|
235
247
|
if (cold) parts.push(cold);
|
|
236
248
|
if (pick.stats.pro) {
|
|
@@ -241,7 +253,7 @@ export function formatPickStats(pick: GamePick): string {
|
|
|
241
253
|
}
|
|
242
254
|
if (pick.stats.accuracy) {
|
|
243
255
|
const a = pick.stats.accuracy;
|
|
244
|
-
parts.push(
|
|
256
|
+
parts.push(t(locale, 'accuracy', { rate: (a.avgHitRate * 100).toFixed(1), count: a.evalCount, recent: a.recent5 }));
|
|
245
257
|
}
|
|
246
258
|
parts.push(pick.stats.detail);
|
|
247
259
|
return ` ${parts.filter(Boolean).join('\n ')}`;
|
package/src/recommend/report.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveLocale, type Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { messages as t } from '../i18n.js';
|
|
3
|
+
import { DEFAULT_WEIGHTS, type DailyReport, type DrawNumbers, type GameId, type GamePick, type ScoreWeights } from '../types.js';
|
|
2
4
|
|
|
3
5
|
import { recommendGame, formatPickLine, formatPickStats } from './game-pick.js';
|
|
4
6
|
import { resolveKl8Config, type Kl8Config } from '../games/kl8-groups.js';
|
|
@@ -8,6 +10,7 @@ import { loadDraws, REPORTS_TABLE, parseStoredJson, type LotteryDb } from '../db
|
|
|
8
10
|
import { listPendingPredictions, loadAccuracySnapshot, loadGameWeights, savePrediction } from '../evaluate/tracker.js';
|
|
9
11
|
|
|
10
12
|
export interface LotteryConfigSlice {
|
|
13
|
+
locale?: Locale;
|
|
11
14
|
pickCount: number;
|
|
12
15
|
historyLimit: number;
|
|
13
16
|
kl8?: Kl8Config;
|
|
@@ -31,6 +34,7 @@ export async function buildDailyReport(
|
|
|
31
34
|
const accuracy = await loadAccuracySnapshot(db, gameId);
|
|
32
35
|
const kl8 = config.kl8 ?? resolveKl8Config(config.pickCount);
|
|
33
36
|
const pick = recommendGame(gameId, draws, {
|
|
37
|
+
locale: config.locale,
|
|
34
38
|
pickCount: gameId === 'kl8' ? kl8.pickCount : config.pickCount,
|
|
35
39
|
tieSeed,
|
|
36
40
|
weights,
|
|
@@ -46,28 +50,30 @@ export async function buildDailyReport(
|
|
|
46
50
|
.join(' | ');
|
|
47
51
|
return {
|
|
48
52
|
date: dateStr,
|
|
53
|
+
locale: resolveLocale(config.locale),
|
|
49
54
|
picks,
|
|
50
|
-
disclaimer:
|
|
55
|
+
disclaimer: t(config.locale, 'disclaimer'),
|
|
51
56
|
accuracyOverview: accuracyOverview || undefined,
|
|
52
57
|
};
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function formatDailyReportText(report: DailyReport, aiExplanation?: string): string {
|
|
61
|
+
const locale = resolveLocale(report.locale);
|
|
56
62
|
if (report.body?.trim()) {
|
|
57
63
|
const extra = aiExplanation?.trim();
|
|
58
64
|
return extra ? `${report.body.trim()}\n\n${extra}` : report.body.trim();
|
|
59
65
|
}
|
|
60
|
-
const lines = [
|
|
66
|
+
const lines = [t(locale, 'dailyTitle', { date: report.date }), ''];
|
|
61
67
|
if (report.backtestOverview?.trim()) {
|
|
62
68
|
lines.push(report.backtestOverview.trim());
|
|
63
69
|
lines.push('');
|
|
64
70
|
}
|
|
65
71
|
if (report.weightFallbackGames?.length) {
|
|
66
|
-
lines.push(
|
|
72
|
+
lines.push(t(locale, 'weightFallback', { games: report.weightFallbackGames.join(', ') }));
|
|
67
73
|
lines.push('');
|
|
68
74
|
}
|
|
69
75
|
if (report.accuracyOverview) {
|
|
70
|
-
lines.push(
|
|
76
|
+
lines.push(t(locale, 'accuracyOverview', { overview: report.accuracyOverview }));
|
|
71
77
|
lines.push('');
|
|
72
78
|
}
|
|
73
79
|
for (const pick of report.picks) {
|
|
@@ -79,7 +85,7 @@ export function formatDailyReportText(report: DailyReport, aiExplanation?: strin
|
|
|
79
85
|
lines.push(aiExplanation.trim());
|
|
80
86
|
lines.push('');
|
|
81
87
|
} else {
|
|
82
|
-
lines.push('
|
|
88
|
+
lines.push(t(locale, 'statsExplanation'));
|
|
83
89
|
lines.push('');
|
|
84
90
|
}
|
|
85
91
|
lines.push(report.disclaimer);
|
|
@@ -128,6 +134,7 @@ export async function loadTodayPendingPicks(
|
|
|
128
134
|
db: LotteryDb,
|
|
129
135
|
gameIds: GameId[],
|
|
130
136
|
date = new Date(),
|
|
137
|
+
locale?: Locale,
|
|
131
138
|
): Promise<GamePick[]> {
|
|
132
139
|
const today = date.toISOString().slice(0, 10);
|
|
133
140
|
const picks: GamePick[] = [];
|
|
@@ -139,6 +146,7 @@ export async function loadTodayPendingPicks(
|
|
|
139
146
|
}>;
|
|
140
147
|
for (const row of pending.filter((p) => p.predict_date === today)) {
|
|
141
148
|
picks.push({
|
|
149
|
+
locale: resolveLocale(locale),
|
|
142
150
|
gameId,
|
|
143
151
|
label: gameId,
|
|
144
152
|
numbers: parseStoredJson<DrawNumbers>(row.numbers, {}),
|
|
@@ -155,15 +163,17 @@ export async function resolveTodayReport(
|
|
|
155
163
|
db: LotteryDb,
|
|
156
164
|
gameIds: GameId[],
|
|
157
165
|
date = new Date(),
|
|
166
|
+
locale?: Locale,
|
|
158
167
|
): Promise<DailyReport | null> {
|
|
159
168
|
const saved = await loadTodayReport(db, date);
|
|
160
169
|
if (saved) return saved;
|
|
161
|
-
const picks = await loadTodayPendingPicks(db, gameIds, date);
|
|
170
|
+
const picks = await loadTodayPendingPicks(db, gameIds, date, locale);
|
|
162
171
|
if (!picks.length) return null;
|
|
163
172
|
return {
|
|
164
173
|
date: date.toISOString().slice(0, 10),
|
|
174
|
+
locale: resolveLocale(locale),
|
|
165
175
|
picks,
|
|
166
|
-
disclaimer:
|
|
176
|
+
disclaimer: t(locale, 'disclaimer'),
|
|
167
177
|
};
|
|
168
178
|
}
|
|
169
179
|
|