@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.
- package/README.md +114 -0
- package/agent/agent.ts +6 -0
- package/agent/instructions.md +2 -0
- package/agent/schedules/daily_pipeline.ts +34 -0
- package/agent/skills/lottery.md +36 -0
- package/agent/tools/compute_recommend.ts +11 -0
- package/agent/tools/get_model_state.ts +11 -0
- package/agent/tools/history.ts +22 -0
- package/agent/tools/list_pending.ts +11 -0
- package/agent/tools/save_prediction.ts +14 -0
- package/agent/tools/stats_snapshot.ts +11 -0
- package/agent/tools/sync.ts +18 -0
- package/evals/pipeline_smoke.eval.ts +8 -0
- package/lib/agent/agent.d.ts +3 -0
- package/lib/agent/agent.d.ts.map +1 -0
- package/lib/agent/agent.js +6 -0
- package/lib/agent/agent.js.map +1 -0
- package/lib/agent/prompts.d.ts +11 -0
- package/lib/agent/prompts.d.ts.map +1 -0
- package/lib/agent/prompts.js +11 -0
- package/lib/agent/prompts.js.map +1 -0
- package/lib/agent/read-skills.d.ts +3 -0
- package/lib/agent/read-skills.d.ts.map +1 -0
- package/lib/agent/read-skills.js +25 -0
- package/lib/agent/read-skills.js.map +1 -0
- package/lib/agent/run-turn.d.ts +14 -0
- package/lib/agent/run-turn.d.ts.map +1 -0
- package/lib/agent/run-turn.js +47 -0
- package/lib/agent/run-turn.js.map +1 -0
- package/lib/agent/schedules/daily_pipeline.d.ts +3 -0
- package/lib/agent/schedules/daily_pipeline.d.ts.map +1 -0
- package/lib/agent/schedules/daily_pipeline.js +36 -0
- package/lib/agent/schedules/daily_pipeline.js.map +1 -0
- package/lib/agent/tools/compute_recommend.d.ts +5 -0
- package/lib/agent/tools/compute_recommend.d.ts.map +1 -0
- package/lib/agent/tools/compute_recommend.js +11 -0
- package/lib/agent/tools/compute_recommend.js.map +1 -0
- package/lib/agent/tools/get_model_state.d.ts +5 -0
- package/lib/agent/tools/get_model_state.d.ts.map +1 -0
- package/lib/agent/tools/get_model_state.js +11 -0
- package/lib/agent/tools/get_model_state.js.map +1 -0
- package/lib/agent/tools/history.d.ts +6 -0
- package/lib/agent/tools/history.d.ts.map +1 -0
- package/lib/agent/tools/history.js +24 -0
- package/lib/agent/tools/history.js.map +1 -0
- package/lib/agent/tools/list_pending.d.ts +5 -0
- package/lib/agent/tools/list_pending.d.ts.map +1 -0
- package/lib/agent/tools/list_pending.js +11 -0
- package/lib/agent/tools/list_pending.js.map +1 -0
- package/lib/agent/tools/save_prediction.d.ts +6 -0
- package/lib/agent/tools/save_prediction.d.ts.map +1 -0
- package/lib/agent/tools/save_prediction.js +14 -0
- package/lib/agent/tools/save_prediction.js.map +1 -0
- package/lib/agent/tools/stats_snapshot.d.ts +5 -0
- package/lib/agent/tools/stats_snapshot.d.ts.map +1 -0
- package/lib/agent/tools/stats_snapshot.js +11 -0
- package/lib/agent/tools/stats_snapshot.js.map +1 -0
- package/lib/agent/tools/sync.d.ts +5 -0
- package/lib/agent/tools/sync.d.ts.map +1 -0
- package/lib/agent/tools/sync.js +19 -0
- package/lib/agent/tools/sync.js.map +1 -0
- package/lib/db.d.ts +33 -0
- package/lib/db.d.ts.map +1 -0
- package/lib/db.js +108 -0
- package/lib/db.js.map +1 -0
- package/lib/evals/pipeline_smoke.eval.d.ts +3 -0
- package/lib/evals/pipeline_smoke.eval.d.ts.map +1 -0
- package/lib/evals/pipeline_smoke.eval.js +8 -0
- package/lib/evals/pipeline_smoke.eval.js.map +1 -0
- package/lib/evaluate/adaptive-sim.d.ts +31 -0
- package/lib/evaluate/adaptive-sim.d.ts.map +1 -0
- package/lib/evaluate/adaptive-sim.js +155 -0
- package/lib/evaluate/adaptive-sim.js.map +1 -0
- package/lib/evaluate/backtest.d.ts +29 -0
- package/lib/evaluate/backtest.d.ts.map +1 -0
- package/lib/evaluate/backtest.js +87 -0
- package/lib/evaluate/backtest.js.map +1 -0
- package/lib/evaluate/hit-rate.d.ts +8 -0
- package/lib/evaluate/hit-rate.d.ts.map +1 -0
- package/lib/evaluate/hit-rate.js +120 -0
- package/lib/evaluate/hit-rate.js.map +1 -0
- package/lib/evaluate/review-pending.d.ts +13 -0
- package/lib/evaluate/review-pending.d.ts.map +1 -0
- package/lib/evaluate/review-pending.js +89 -0
- package/lib/evaluate/review-pending.js.map +1 -0
- package/lib/evaluate/tracker.d.ts +28 -0
- package/lib/evaluate/tracker.d.ts.map +1 -0
- package/lib/evaluate/tracker.js +265 -0
- package/lib/evaluate/tracker.js.map +1 -0
- package/lib/evaluate/train-weights.d.ts +28 -0
- package/lib/evaluate/train-weights.d.ts.map +1 -0
- package/lib/evaluate/train-weights.js +101 -0
- package/lib/evaluate/train-weights.js.map +1 -0
- package/lib/evaluate/weight-guard.d.ts +15 -0
- package/lib/evaluate/weight-guard.d.ts.map +1 -0
- package/lib/evaluate/weight-guard.js +46 -0
- package/lib/evaluate/weight-guard.js.map +1 -0
- package/lib/games/kl8-groups.d.ts +31 -0
- package/lib/games/kl8-groups.d.ts.map +1 -0
- package/lib/games/kl8-groups.js +62 -0
- package/lib/games/kl8-groups.js.map +1 -0
- package/lib/games/registry.d.ts +13 -0
- package/lib/games/registry.d.ts.map +1 -0
- package/lib/games/registry.js +45 -0
- package/lib/games/registry.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +215 -0
- package/lib/index.js.map +1 -0
- package/lib/pipeline.d.ts +36 -0
- package/lib/pipeline.d.ts.map +1 -0
- package/lib/pipeline.js +130 -0
- package/lib/pipeline.js.map +1 -0
- package/lib/push.d.ts +3 -0
- package/lib/push.d.ts.map +1 -0
- package/lib/push.js +31 -0
- package/lib/push.js.map +1 -0
- package/lib/recommend/game-pick.d.ts +14 -0
- package/lib/recommend/game-pick.d.ts.map +1 -0
- package/lib/recommend/game-pick.js +169 -0
- package/lib/recommend/game-pick.js.map +1 -0
- package/lib/recommend/report.d.ts +22 -0
- package/lib/recommend/report.d.ts.map +1 -0
- package/lib/recommend/report.js +173 -0
- package/lib/recommend/report.js.map +1 -0
- package/lib/src/agent/prompts.d.ts +11 -0
- package/lib/src/agent/prompts.d.ts.map +1 -0
- package/lib/src/agent/prompts.js +11 -0
- package/lib/src/agent/prompts.js.map +1 -0
- package/lib/src/agent/read-skills.d.ts +3 -0
- package/lib/src/agent/read-skills.d.ts.map +1 -0
- package/lib/src/agent/read-skills.js +25 -0
- package/lib/src/agent/read-skills.js.map +1 -0
- package/lib/src/agent/run-turn.d.ts +14 -0
- package/lib/src/agent/run-turn.d.ts.map +1 -0
- package/lib/src/agent/run-turn.js +47 -0
- package/lib/src/agent/run-turn.js.map +1 -0
- package/lib/src/db.d.ts +33 -0
- package/lib/src/db.d.ts.map +1 -0
- package/lib/src/db.js +108 -0
- package/lib/src/db.js.map +1 -0
- package/lib/src/evaluate/adaptive-sim.d.ts +31 -0
- package/lib/src/evaluate/adaptive-sim.d.ts.map +1 -0
- package/lib/src/evaluate/adaptive-sim.js +159 -0
- package/lib/src/evaluate/adaptive-sim.js.map +1 -0
- package/lib/src/evaluate/backtest.d.ts +29 -0
- package/lib/src/evaluate/backtest.d.ts.map +1 -0
- package/lib/src/evaluate/backtest.js +87 -0
- package/lib/src/evaluate/backtest.js.map +1 -0
- package/lib/src/evaluate/hit-rate.d.ts +8 -0
- package/lib/src/evaluate/hit-rate.d.ts.map +1 -0
- package/lib/src/evaluate/hit-rate.js +118 -0
- package/lib/src/evaluate/hit-rate.js.map +1 -0
- package/lib/src/evaluate/review-pending.d.ts +13 -0
- package/lib/src/evaluate/review-pending.d.ts.map +1 -0
- package/lib/src/evaluate/review-pending.js +89 -0
- package/lib/src/evaluate/review-pending.js.map +1 -0
- package/lib/src/evaluate/tracker.d.ts +28 -0
- package/lib/src/evaluate/tracker.d.ts.map +1 -0
- package/lib/src/evaluate/tracker.js +269 -0
- package/lib/src/evaluate/tracker.js.map +1 -0
- package/lib/src/evaluate/train-weights.d.ts +28 -0
- package/lib/src/evaluate/train-weights.d.ts.map +1 -0
- package/lib/src/evaluate/train-weights.js +101 -0
- package/lib/src/evaluate/train-weights.js.map +1 -0
- package/lib/src/evaluate/weight-guard.d.ts +15 -0
- package/lib/src/evaluate/weight-guard.d.ts.map +1 -0
- package/lib/src/evaluate/weight-guard.js +46 -0
- package/lib/src/evaluate/weight-guard.js.map +1 -0
- package/lib/src/games/kl8-groups.d.ts +31 -0
- package/lib/src/games/kl8-groups.d.ts.map +1 -0
- package/lib/src/games/kl8-groups.js +62 -0
- package/lib/src/games/kl8-groups.js.map +1 -0
- package/lib/src/games/registry.d.ts +13 -0
- package/lib/src/games/registry.d.ts.map +1 -0
- package/lib/src/games/registry.js +45 -0
- package/lib/src/games/registry.js.map +1 -0
- package/lib/src/index.d.ts +3 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +183 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/lottery-agent-deps.d.ts +24 -0
- package/lib/src/lottery-agent-deps.d.ts.map +1 -0
- package/lib/src/lottery-agent-deps.js +10 -0
- package/lib/src/lottery-agent-deps.js.map +1 -0
- package/lib/src/lottery-tool-handlers.d.ts +17 -0
- package/lib/src/lottery-tool-handlers.d.ts.map +1 -0
- package/lib/src/lottery-tool-handlers.js +155 -0
- package/lib/src/lottery-tool-handlers.js.map +1 -0
- package/lib/src/pipeline.d.ts +36 -0
- package/lib/src/pipeline.d.ts.map +1 -0
- package/lib/src/pipeline.js +130 -0
- package/lib/src/pipeline.js.map +1 -0
- package/lib/src/push.d.ts +3 -0
- package/lib/src/push.d.ts.map +1 -0
- package/lib/src/push.js +31 -0
- package/lib/src/push.js.map +1 -0
- package/lib/src/recommend/game-pick.d.ts +14 -0
- package/lib/src/recommend/game-pick.d.ts.map +1 -0
- package/lib/src/recommend/game-pick.js +169 -0
- package/lib/src/recommend/game-pick.js.map +1 -0
- package/lib/src/recommend/report.d.ts +22 -0
- package/lib/src/recommend/report.d.ts.map +1 -0
- package/lib/src/recommend/report.js +176 -0
- package/lib/src/recommend/report.js.map +1 -0
- package/lib/src/stats/engine.d.ts +18 -0
- package/lib/src/stats/engine.d.ts.map +1 -0
- package/lib/src/stats/engine.js +122 -0
- package/lib/src/stats/engine.js.map +1 -0
- package/lib/src/stats/pro-analysis.d.ts +7 -0
- package/lib/src/stats/pro-analysis.d.ts.map +1 -0
- package/lib/src/stats/pro-analysis.js +121 -0
- package/lib/src/stats/pro-analysis.js.map +1 -0
- package/lib/src/stats/weights.d.ts +8 -0
- package/lib/src/stats/weights.d.ts.map +1 -0
- package/lib/src/stats/weights.js +25 -0
- package/lib/src/stats/weights.js.map +1 -0
- package/lib/src/sync/fetch-official.d.ts +4 -0
- package/lib/src/sync/fetch-official.d.ts.map +1 -0
- package/lib/src/sync/fetch-official.js +160 -0
- package/lib/src/sync/fetch-official.js.map +1 -0
- package/lib/src/sync/run-sync.d.ts +8 -0
- package/lib/src/sync/run-sync.d.ts.map +1 -0
- package/lib/src/sync/run-sync.js +29 -0
- package/lib/src/sync/run-sync.js.map +1 -0
- package/lib/src/tools/agent-tools.d.ts +15 -0
- package/lib/src/tools/agent-tools.d.ts.map +1 -0
- package/lib/src/tools/agent-tools.js +211 -0
- package/lib/src/tools/agent-tools.js.map +1 -0
- package/lib/src/types.d.ts +84 -0
- package/lib/src/types.d.ts.map +1 -0
- package/lib/src/types.js +3 -0
- package/lib/src/types.js.map +1 -0
- package/lib/stats/engine.d.ts +18 -0
- package/lib/stats/engine.d.ts.map +1 -0
- package/lib/stats/engine.js +122 -0
- package/lib/stats/engine.js.map +1 -0
- package/lib/stats/pro-analysis.d.ts +7 -0
- package/lib/stats/pro-analysis.d.ts.map +1 -0
- package/lib/stats/pro-analysis.js +121 -0
- package/lib/stats/pro-analysis.js.map +1 -0
- package/lib/stats/weights.d.ts +8 -0
- package/lib/stats/weights.d.ts.map +1 -0
- package/lib/stats/weights.js +25 -0
- package/lib/stats/weights.js.map +1 -0
- package/lib/sync/fetch-official.d.ts +4 -0
- package/lib/sync/fetch-official.d.ts.map +1 -0
- package/lib/sync/fetch-official.js +160 -0
- package/lib/sync/fetch-official.js.map +1 -0
- package/lib/sync/run-sync.d.ts +8 -0
- package/lib/sync/run-sync.d.ts.map +1 -0
- package/lib/sync/run-sync.js +29 -0
- package/lib/sync/run-sync.js.map +1 -0
- package/lib/tools/agent-tools.d.ts +15 -0
- package/lib/tools/agent-tools.d.ts.map +1 -0
- package/lib/tools/agent-tools.js +211 -0
- package/lib/tools/agent-tools.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types.d.ts +84 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/package.json +73 -0
- package/src/agent/prompts.ts +21 -0
- package/src/agent/read-skills.ts +27 -0
- package/src/agent/run-turn.ts +74 -0
- package/src/db.ts +132 -0
- package/src/evaluate/adaptive-sim.ts +210 -0
- package/src/evaluate/backtest.ts +128 -0
- package/src/evaluate/hit-rate.ts +132 -0
- package/src/evaluate/review-pending.ts +120 -0
- package/src/evaluate/tracker.ts +311 -0
- package/src/evaluate/train-weights.ts +134 -0
- package/src/evaluate/weight-guard.ts +68 -0
- package/src/games/kl8-groups.ts +98 -0
- package/src/games/registry.ts +55 -0
- package/src/index.ts +216 -0
- package/src/lottery-agent-deps.ts +29 -0
- package/src/lottery-tool-handlers.ts +166 -0
- package/src/pipeline.ts +206 -0
- package/src/push.ts +34 -0
- package/src/recommend/game-pick.ts +248 -0
- package/src/recommend/report.ts +211 -0
- package/src/stats/engine.ts +143 -0
- package/src/stats/pro-analysis.ts +136 -0
- package/src/stats/weights.ts +29 -0
- package/src/sync/fetch-official.ts +185 -0
- package/src/sync/run-sync.ts +33 -0
- package/src/types.ts +98 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { DEFAULT_WEIGHTS, type DrawNumbers, type GameId, type GamePick, type HitSummary, type NormalizedDraw, type ScoreWeights, type AccuracySnapshot } from '../types.js';
|
|
2
|
+
|
|
3
|
+
import { loadDraws, parseStoredJson, type LotteryDb } from '../db.js';
|
|
4
|
+
|
|
5
|
+
import { comparePickToDraw, tuneWeightsFromDraw, formatHitSummary } from './hit-rate.js';
|
|
6
|
+
import { formatWeights, normalizeWeights } from '../stats/weights.js';
|
|
7
|
+
|
|
8
|
+
export const PREDICTIONS_TABLE = 'lottery_predictions';
|
|
9
|
+
export const WEIGHTS_TABLE = 'lottery_model_weights';
|
|
10
|
+
export const MEMORY_TABLE = 'lottery_agent_memory';
|
|
11
|
+
|
|
12
|
+
export function defineEvalTables(db: { define: (name: string, schema: Record<string, unknown>) => void }): void {
|
|
13
|
+
db.define(PREDICTIONS_TABLE, {
|
|
14
|
+
game_id: { type: 'text', nullable: false },
|
|
15
|
+
predict_at: { type: 'text', nullable: false },
|
|
16
|
+
predict_date: { type: 'text', default: '' },
|
|
17
|
+
numbers: { type: 'text', default: '{}' },
|
|
18
|
+
weights: { type: 'text', default: '{}' },
|
|
19
|
+
status: { type: 'text', default: 'pending' },
|
|
20
|
+
actual_issue: { type: 'text', default: '' },
|
|
21
|
+
actual_numbers: { type: 'text', default: '' },
|
|
22
|
+
hit_summary: { type: 'text', default: '' },
|
|
23
|
+
analysis: { type: 'text', default: '' },
|
|
24
|
+
evaluated_at: { type: 'text', default: '' },
|
|
25
|
+
});
|
|
26
|
+
db.define(WEIGHTS_TABLE, {
|
|
27
|
+
game_id: { type: 'text', nullable: false },
|
|
28
|
+
freq_weight: { type: 'text', default: '0.4' },
|
|
29
|
+
omit_weight: { type: 'text', default: '0.35' },
|
|
30
|
+
trend_weight: { type: 'text', default: '0.25' },
|
|
31
|
+
eval_count: { type: 'text', default: '0' },
|
|
32
|
+
avg_hit_rate: { type: 'text', default: '0' },
|
|
33
|
+
updated_at: { type: 'text', default: '' },
|
|
34
|
+
});
|
|
35
|
+
db.define(MEMORY_TABLE, {
|
|
36
|
+
game_id: { type: 'text', default: '' },
|
|
37
|
+
memory_type: { type: 'text', default: 'insight' },
|
|
38
|
+
content: { type: 'text', default: '' },
|
|
39
|
+
created_at: { type: 'text', default: '' },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadGameWeights(db: LotteryDb, gameId: GameId): Promise<ScoreWeights> {
|
|
44
|
+
const model = db.models.get(WEIGHTS_TABLE);
|
|
45
|
+
if (!model) return { ...DEFAULT_WEIGHTS };
|
|
46
|
+
const rows = (await model.select().where({ game_id: gameId })) as Array<{
|
|
47
|
+
freq_weight: string;
|
|
48
|
+
omit_weight: string;
|
|
49
|
+
trend_weight: string;
|
|
50
|
+
}>;
|
|
51
|
+
if (!rows.length) return { ...DEFAULT_WEIGHTS };
|
|
52
|
+
const r = rows[0];
|
|
53
|
+
if (!r) return { ...DEFAULT_WEIGHTS };
|
|
54
|
+
return {
|
|
55
|
+
freq: Number.parseFloat(r.freq_weight) || DEFAULT_WEIGHTS.freq,
|
|
56
|
+
omit: Number.parseFloat(r.omit_weight) || DEFAULT_WEIGHTS.omit,
|
|
57
|
+
trend: Number.parseFloat(r.trend_weight) || DEFAULT_WEIGHTS.trend,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function hasWeightsRow(db: LotteryDb, gameId: GameId): Promise<boolean> {
|
|
62
|
+
const model = db.models.get(WEIGHTS_TABLE);
|
|
63
|
+
if (!model) return false;
|
|
64
|
+
const rows = (await model.select().where({ game_id: gameId })) as unknown[];
|
|
65
|
+
return rows.length > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function saveGameWeights(
|
|
69
|
+
db: LotteryDb,
|
|
70
|
+
gameId: GameId,
|
|
71
|
+
weights: ScoreWeights,
|
|
72
|
+
evalCount: number,
|
|
73
|
+
avgHitRate: number,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const model = db.models.get(WEIGHTS_TABLE);
|
|
76
|
+
if (!model) return;
|
|
77
|
+
const normalized = normalizeWeights(weights);
|
|
78
|
+
const existing = (await model.select().where({ game_id: gameId })) as unknown[];
|
|
79
|
+
const row = {
|
|
80
|
+
game_id: gameId,
|
|
81
|
+
freq_weight: String(normalized.freq),
|
|
82
|
+
omit_weight: String(normalized.omit),
|
|
83
|
+
trend_weight: String(normalized.trend),
|
|
84
|
+
eval_count: String(evalCount),
|
|
85
|
+
avg_hit_rate: String(avgHitRate),
|
|
86
|
+
updated_at: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
if (existing.length) await model.delete().where({ game_id: gameId });
|
|
89
|
+
await model.insert(row);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function loadAccuracySnapshot(db: LotteryDb, gameId: GameId): Promise<AccuracySnapshot | undefined> {
|
|
93
|
+
const model = db.models.get(WEIGHTS_TABLE);
|
|
94
|
+
const predModel = db.models.get(PREDICTIONS_TABLE);
|
|
95
|
+
if (!model || !predModel) return undefined;
|
|
96
|
+
const wrows = (await model.select().where({ game_id: gameId })) as Array<{
|
|
97
|
+
eval_count: string;
|
|
98
|
+
avg_hit_rate: string;
|
|
99
|
+
}>;
|
|
100
|
+
const evalCount = Number.parseInt(wrows[0]?.eval_count ?? '0', 10) || 0;
|
|
101
|
+
const avgHitRate = Number.parseFloat(wrows[0]?.avg_hit_rate ?? '0') || 0;
|
|
102
|
+
const preds = (await predModel.select().where({ game_id: gameId, status: 'evaluated' })) as Array<{
|
|
103
|
+
hit_summary: string | HitSummary;
|
|
104
|
+
evaluated_at: string;
|
|
105
|
+
}>;
|
|
106
|
+
const recent = [...preds]
|
|
107
|
+
.sort((a, b) => (b.evaluated_at ?? '').localeCompare(a.evaluated_at ?? ''))
|
|
108
|
+
.slice(0, 5)
|
|
109
|
+
.map((p) => {
|
|
110
|
+
const h = parseStoredJson<HitSummary>(p.hit_summary, { total: 0, hits: 0, rate: 0, detail: '-' });
|
|
111
|
+
return `${(h.rate * 100).toFixed(0)}%`;
|
|
112
|
+
});
|
|
113
|
+
if (!evalCount) return undefined;
|
|
114
|
+
return { evalCount, avgHitRate, recent5: recent.length ? recent.join(',') : '-' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function cancelTodayPendingPredictions(
|
|
118
|
+
db: LotteryDb,
|
|
119
|
+
gameId: GameId,
|
|
120
|
+
date = new Date(),
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const model = db.models.get(PREDICTIONS_TABLE);
|
|
123
|
+
if (!model) return;
|
|
124
|
+
const today = date.toISOString().slice(0, 10);
|
|
125
|
+
const pending = (await listPendingPredictions(db, gameId)) as Array<{ predict_at: string; predict_date: string }>;
|
|
126
|
+
for (const row of pending.filter((p) => p.predict_date === today)) {
|
|
127
|
+
await model.delete().where({ game_id: gameId, predict_at: row.predict_at, status: 'pending' });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function savePrediction(db: LotteryDb, pick: GamePick, at = new Date()): Promise<void> {
|
|
132
|
+
const model = db.models.get(PREDICTIONS_TABLE);
|
|
133
|
+
if (!model) return;
|
|
134
|
+
await model.insert({
|
|
135
|
+
game_id: pick.gameId,
|
|
136
|
+
predict_at: at.toISOString(),
|
|
137
|
+
predict_date: at.toISOString().slice(0, 10),
|
|
138
|
+
numbers: JSON.stringify(pick.numbers),
|
|
139
|
+
weights: JSON.stringify(pick.weights),
|
|
140
|
+
status: 'pending',
|
|
141
|
+
actual_issue: '',
|
|
142
|
+
actual_numbers: '',
|
|
143
|
+
hit_summary: '',
|
|
144
|
+
analysis: '',
|
|
145
|
+
evaluated_at: '',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function listPendingPredictions(db: LotteryDb, gameId?: GameId): Promise<unknown[]> {
|
|
150
|
+
const model = db.models.get(PREDICTIONS_TABLE);
|
|
151
|
+
if (!model) return [];
|
|
152
|
+
if (gameId) {
|
|
153
|
+
return model.select().where({ game_id: gameId, status: 'pending' });
|
|
154
|
+
}
|
|
155
|
+
const all = (await model.select().where({ status: 'pending' })) as unknown[];
|
|
156
|
+
return all;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function submitAgentReview(
|
|
160
|
+
db: LotteryDb,
|
|
161
|
+
input: {
|
|
162
|
+
gameId: GameId;
|
|
163
|
+
predictAt: string;
|
|
164
|
+
actualIssue: string;
|
|
165
|
+
actualNumbers: DrawNumbers;
|
|
166
|
+
analysis: string;
|
|
167
|
+
adjustWeights?: boolean;
|
|
168
|
+
},
|
|
169
|
+
historyLimit: number,
|
|
170
|
+
): Promise<HitSummary | null> {
|
|
171
|
+
const predModel = db.models.get(PREDICTIONS_TABLE);
|
|
172
|
+
if (!predModel) return null;
|
|
173
|
+
const pending = (await predModel.select().where({
|
|
174
|
+
game_id: input.gameId,
|
|
175
|
+
predict_at: input.predictAt,
|
|
176
|
+
status: 'pending',
|
|
177
|
+
})) as Array<{ numbers: string | DrawNumbers; weights: string | ScoreWeights }>;
|
|
178
|
+
if (!pending.length) return null;
|
|
179
|
+
|
|
180
|
+
const row = pending[0];
|
|
181
|
+
if (!row) return null;
|
|
182
|
+
const pick: GamePick = {
|
|
183
|
+
gameId: input.gameId,
|
|
184
|
+
label: '',
|
|
185
|
+
numbers: parseStoredJson<DrawNumbers>(row.numbers, {}),
|
|
186
|
+
weights: parseStoredJson<ScoreWeights>(row.weights, DEFAULT_WEIGHTS),
|
|
187
|
+
stats: { gameId: input.gameId, sampleSize: 0, hot: [], cold: [], detail: '' },
|
|
188
|
+
};
|
|
189
|
+
const draw: NormalizedDraw = {
|
|
190
|
+
gameId: input.gameId,
|
|
191
|
+
issue: input.actualIssue,
|
|
192
|
+
drawTime: '',
|
|
193
|
+
numbers: input.actualNumbers,
|
|
194
|
+
source: 'fucai',
|
|
195
|
+
};
|
|
196
|
+
const hit = comparePickToDraw(pick, draw);
|
|
197
|
+
const history = await loadDraws(db, input.gameId, historyLimit);
|
|
198
|
+
const analysis = input.analysis.trim() || formatHitSummary(input.gameId, hit, input.actualIssue);
|
|
199
|
+
|
|
200
|
+
let evalCount = 0;
|
|
201
|
+
let avgHitRate = 0;
|
|
202
|
+
const wrows = (await db.models.get(WEIGHTS_TABLE)?.select().where({ game_id: input.gameId }) ?? []) as Array<{
|
|
203
|
+
eval_count: string;
|
|
204
|
+
avg_hit_rate: string;
|
|
205
|
+
}>;
|
|
206
|
+
if (wrows[0]) {
|
|
207
|
+
evalCount = Number.parseInt(wrows[0].eval_count, 10) || 0;
|
|
208
|
+
avgHitRate = Number.parseFloat(wrows[0].avg_hit_rate) || 0;
|
|
209
|
+
}
|
|
210
|
+
evalCount++;
|
|
211
|
+
avgHitRate = avgHitRate + (hit.rate - avgHitRate) / evalCount;
|
|
212
|
+
|
|
213
|
+
let weights = pick.weights;
|
|
214
|
+
if (input.adjustWeights !== false) {
|
|
215
|
+
weights = tuneWeightsFromDraw(weights, pick.numbers, draw.numbers, input.gameId, history);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await predModel.delete().where({ game_id: input.gameId, predict_at: input.predictAt, status: 'pending' });
|
|
219
|
+
await predModel.insert({
|
|
220
|
+
game_id: input.gameId,
|
|
221
|
+
predict_at: input.predictAt,
|
|
222
|
+
predict_date: input.predictAt.slice(0, 10),
|
|
223
|
+
numbers: JSON.stringify(pick.numbers),
|
|
224
|
+
weights: JSON.stringify(pick.weights),
|
|
225
|
+
status: 'evaluated',
|
|
226
|
+
actual_issue: input.actualIssue,
|
|
227
|
+
actual_numbers: JSON.stringify(input.actualNumbers),
|
|
228
|
+
hit_summary: JSON.stringify(hit),
|
|
229
|
+
analysis: analysis,
|
|
230
|
+
evaluated_at: new Date().toISOString(),
|
|
231
|
+
});
|
|
232
|
+
await saveGameWeights(db, input.gameId, weights, evalCount, avgHitRate);
|
|
233
|
+
return hit;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function saveAgentMemory(
|
|
237
|
+
db: LotteryDb,
|
|
238
|
+
content: string,
|
|
239
|
+
memoryType = 'insight',
|
|
240
|
+
gameId = '',
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const model = db.models.get(MEMORY_TABLE);
|
|
243
|
+
if (!model || !content.trim()) return;
|
|
244
|
+
await model.insert({
|
|
245
|
+
game_id: gameId,
|
|
246
|
+
memory_type: memoryType,
|
|
247
|
+
content: content.trim(),
|
|
248
|
+
created_at: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function loadAgentMemory(db: LotteryDb, gameId?: string, limit = 20): Promise<unknown[]> {
|
|
253
|
+
const model = db.models.get(MEMORY_TABLE);
|
|
254
|
+
if (!model) return [];
|
|
255
|
+
let rows: unknown[] = [];
|
|
256
|
+
if (gameId) {
|
|
257
|
+
rows = (await model.select().where({ game_id: gameId })) as unknown[];
|
|
258
|
+
} else {
|
|
259
|
+
const games: Array<GameId | ''> = ['kl8', 'ssq', 'dlt', 'fc3d', 'pl3', 'pl5', ''];
|
|
260
|
+
for (const g of games) {
|
|
261
|
+
const part = (await model.select().where({ game_id: g })) as unknown[];
|
|
262
|
+
rows.push(...part);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return [...rows]
|
|
266
|
+
.sort((a, b) => String((b as { created_at?: string }).created_at ?? '').localeCompare(String((a as { created_at?: string }).created_at ?? '')))
|
|
267
|
+
.slice(0, limit);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function formatReviewReport(db: LotteryDb, gameId?: GameId): Promise<string> {
|
|
271
|
+
const predModel = db.models.get(PREDICTIONS_TABLE);
|
|
272
|
+
const memModel = db.models.get(MEMORY_TABLE);
|
|
273
|
+
if (!predModel) return '数据库未就绪';
|
|
274
|
+
const games: GameId[] = gameId ? [gameId] : ['kl8', 'ssq', 'dlt', 'fc3d', 'pl3', 'pl5'];
|
|
275
|
+
const lines = ['【彩票推荐复盘】', ''];
|
|
276
|
+
for (const gid of games) {
|
|
277
|
+
const weights = await loadGameWeights(db, gid);
|
|
278
|
+
const snap = await loadAccuracySnapshot(db, gid);
|
|
279
|
+
const preds = (await predModel.select().where({ game_id: gid, status: 'evaluated' })) as Array<{
|
|
280
|
+
actual_issue: string;
|
|
281
|
+
hit_summary: string | HitSummary;
|
|
282
|
+
analysis: string;
|
|
283
|
+
evaluated_at: string;
|
|
284
|
+
}>;
|
|
285
|
+
const recent = [...preds]
|
|
286
|
+
.sort((a, b) => (b.evaluated_at ?? '').localeCompare(a.evaluated_at ?? ''))
|
|
287
|
+
.slice(0, 5);
|
|
288
|
+
lines.push(`■ ${gid} 模型 ${formatWeights(weights)}`);
|
|
289
|
+
if (snap) {
|
|
290
|
+
lines.push(` 累计 ${snap.evalCount} 期 平均命中 ${(snap.avgHitRate * 100).toFixed(1)}%`);
|
|
291
|
+
} else {
|
|
292
|
+
lines.push(' 尚无复盘数据');
|
|
293
|
+
}
|
|
294
|
+
for (const p of recent) {
|
|
295
|
+
const h = parseStoredJson<HitSummary>(p.hit_summary, { total: 0, hits: 0, rate: 0, detail: '-' });
|
|
296
|
+
lines.push(` 第${p.actual_issue}期 ${h.detail} (${(h.rate * 100).toFixed(0)}%)`);
|
|
297
|
+
if (p.analysis?.trim()) lines.push(` ${p.analysis.trim()}`);
|
|
298
|
+
}
|
|
299
|
+
lines.push('');
|
|
300
|
+
}
|
|
301
|
+
if (memModel) {
|
|
302
|
+
const mem = await loadAgentMemory(db, gameId, 5);
|
|
303
|
+
if (mem.length) {
|
|
304
|
+
lines.push('【Agent 记忆】');
|
|
305
|
+
for (const m of mem as Array<{ game_id?: string; content?: string; memory_type?: string }>) {
|
|
306
|
+
lines.push(`- [${m.game_id || 'global'}/${m.memory_type}] ${m.content}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return lines.join('\n').trim();
|
|
311
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { DEFAULT_WEIGHTS, type GameId, type ScoreWeights } from '../types.js';
|
|
2
|
+
|
|
3
|
+
import { loadDraws, type LotteryDb } from '../db.js';
|
|
4
|
+
|
|
5
|
+
import { runAdaptiveSimulation, type SimulationResult } from './adaptive-sim.js';
|
|
6
|
+
import { walkForwardBacktest } from './backtest.js';
|
|
7
|
+
import { shouldFallbackWeights } from './weight-guard.js';
|
|
8
|
+
import { saveGameWeights } from './tracker.js';
|
|
9
|
+
import { formatWeights } from '../stats/weights.js';
|
|
10
|
+
|
|
11
|
+
export interface TrainWeightsOptions {
|
|
12
|
+
pickCount: number;
|
|
13
|
+
minHistory: number;
|
|
14
|
+
historyLimit: number;
|
|
15
|
+
randomTrials: number;
|
|
16
|
+
/** Start training from these weights; default F40/O35/T25. */
|
|
17
|
+
initialWeights?: ScoreWeights;
|
|
18
|
+
persist?: boolean;
|
|
19
|
+
holdoutWindow?: number;
|
|
20
|
+
holdoutFallback?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TrainWeightsResult extends SimulationResult {
|
|
24
|
+
totalDraws: number;
|
|
25
|
+
trainSteps: number;
|
|
26
|
+
holdoutFallbackApplied?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadChronologicalDraws(db: LotteryDb, gameId: GameId, limit: number) {
|
|
30
|
+
const draws = await loadDraws(db, gameId, limit);
|
|
31
|
+
return [...draws].sort((a, b) => a.issue.localeCompare(b.issue));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Full-history training: from first predictable issue through all DB draws,
|
|
36
|
+
* predict → compare → tune weights each step; optionally persist final weights.
|
|
37
|
+
*/
|
|
38
|
+
export async function trainGameWeights(
|
|
39
|
+
db: LotteryDb,
|
|
40
|
+
gameId: GameId,
|
|
41
|
+
options: TrainWeightsOptions,
|
|
42
|
+
): Promise<TrainWeightsResult | null> {
|
|
43
|
+
const chronological = await loadChronologicalDraws(db, gameId, options.historyLimit);
|
|
44
|
+
if (chronological.length <= options.minHistory) return null;
|
|
45
|
+
|
|
46
|
+
const initial = options.initialWeights ?? { ...DEFAULT_WEIGHTS };
|
|
47
|
+
const sim = runAdaptiveSimulation(chronological, gameId, {
|
|
48
|
+
pickCount: options.pickCount,
|
|
49
|
+
minHistory: options.minHistory,
|
|
50
|
+
randomTrials: options.randomTrials,
|
|
51
|
+
initialWeights: initial,
|
|
52
|
+
adaptive: true,
|
|
53
|
+
// no scoreWindow → every training step is scored
|
|
54
|
+
});
|
|
55
|
+
if (!sim) return null;
|
|
56
|
+
|
|
57
|
+
if (options.persist !== false) {
|
|
58
|
+
let weightsToSave = sim.finalWeights;
|
|
59
|
+
let holdoutFallbackApplied = false;
|
|
60
|
+
if (options.holdoutFallback !== false && options.holdoutWindow && options.holdoutWindow > 0) {
|
|
61
|
+
const holdout = await walkForwardBacktest(db, gameId, {
|
|
62
|
+
pickCount: options.pickCount,
|
|
63
|
+
minHistory: options.minHistory,
|
|
64
|
+
randomTrials: options.randomTrials,
|
|
65
|
+
historyLimit: options.historyLimit,
|
|
66
|
+
window: options.holdoutWindow,
|
|
67
|
+
initialWeights: { ...DEFAULT_WEIGHTS },
|
|
68
|
+
});
|
|
69
|
+
if (shouldFallbackWeights(holdout)) {
|
|
70
|
+
weightsToSave = { ...DEFAULT_WEIGHTS };
|
|
71
|
+
holdoutFallbackApplied = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await saveGameWeights(db, gameId, weightsToSave, sim.periods, sim.adaptiveAvgHitRate);
|
|
75
|
+
return {
|
|
76
|
+
...sim,
|
|
77
|
+
finalWeights: weightsToSave,
|
|
78
|
+
totalDraws: chronological.length,
|
|
79
|
+
trainSteps: sim.periods,
|
|
80
|
+
holdoutFallbackApplied,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...sim,
|
|
86
|
+
totalDraws: chronological.length,
|
|
87
|
+
trainSteps: sim.periods,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function trainAllGameWeights(
|
|
92
|
+
db: LotteryDb,
|
|
93
|
+
gameIds: GameId[],
|
|
94
|
+
options: TrainWeightsOptions,
|
|
95
|
+
): Promise<TrainWeightsResult[]> {
|
|
96
|
+
const out: TrainWeightsResult[] = [];
|
|
97
|
+
for (const gameId of gameIds) {
|
|
98
|
+
const result = await trainGameWeights(db, gameId, options);
|
|
99
|
+
if (result) out.push(result);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pct(rate: number): string {
|
|
105
|
+
return (rate * 100).toFixed(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function signedPct(delta: number): string {
|
|
109
|
+
const v = delta * 100;
|
|
110
|
+
return v >= 0 ? `+${v.toFixed(1)}` : v.toFixed(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatTrainLine(r: TrainWeightsResult): string {
|
|
114
|
+
const init = formatWeights(r.initialWeights);
|
|
115
|
+
const fin = formatWeights(r.finalWeights);
|
|
116
|
+
const fallback = r.holdoutFallbackApplied ? ' [holdout→默认]' : '';
|
|
117
|
+
return [
|
|
118
|
+
`${r.gameId}: 训练${r.trainSteps}期 (${r.firstScoredIssue}→${r.lastScoredIssue})${fallback}`,
|
|
119
|
+
`命中 调优${pct(r.adaptiveAvgHitRate)}% / 固定${pct(r.fixedAvgHitRate)}% / 随机${pct(r.randomAvgHitRate)}%`,
|
|
120
|
+
`Δ调优-随机 ${signedPct(r.adaptiveVsRandom)}%`,
|
|
121
|
+
`权重 ${init} → ${fin}`,
|
|
122
|
+
].join('\n ');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatTrainReport(results: TrainWeightsResult[]): string {
|
|
126
|
+
if (!results.length) return '训练失败:历史数据不足,请先同步开奖。';
|
|
127
|
+
const lines = [
|
|
128
|
+
'【权重训练】全历史 walk-forward:预测→比对→调权,已写入 lottery_model_weights',
|
|
129
|
+
...results.map(formatTrainLine),
|
|
130
|
+
'',
|
|
131
|
+
'说明:训练≠保证未来更准;终权重供后续 lottery 推荐使用。',
|
|
132
|
+
];
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DEFAULT_WEIGHTS, type GameId, type ScoreWeights } from '../types.js';
|
|
2
|
+
|
|
3
|
+
import type { LotteryDb } from '../db.js';
|
|
4
|
+
import { loadGameWeights } from './tracker.js';
|
|
5
|
+
import type { SimulationResult } from './adaptive-sim.js';
|
|
6
|
+
import { walkForwardBacktest, type BacktestOptions } from './backtest.js';
|
|
7
|
+
|
|
8
|
+
export interface WeightPlan {
|
|
9
|
+
/** Explicit weights for recommend; only lists holdout-fallback games. */
|
|
10
|
+
overrides: Map<GameId, ScoreWeights>;
|
|
11
|
+
fallbacks: GameId[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function gamesToPredict(
|
|
15
|
+
gameIds: GameId[],
|
|
16
|
+
missingToday: GameId[],
|
|
17
|
+
weightPlan: WeightPlan,
|
|
18
|
+
): GameId[] {
|
|
19
|
+
const regenFallback = weightPlan.fallbacks.filter((g) => !missingToday.includes(g));
|
|
20
|
+
const ordered = new Set<GameId>();
|
|
21
|
+
for (const gid of gameIds) {
|
|
22
|
+
if (missingToday.includes(gid) || regenFallback.includes(gid)) ordered.add(gid);
|
|
23
|
+
}
|
|
24
|
+
return [...ordered];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function planRecommendWeights(
|
|
28
|
+
gameIds: GameId[],
|
|
29
|
+
holdoutSummaries: SimulationResult[],
|
|
30
|
+
holdoutFallback: boolean,
|
|
31
|
+
): WeightPlan {
|
|
32
|
+
const byGame = new Map(holdoutSummaries.map((s) => [s.gameId, s]));
|
|
33
|
+
const overrides = new Map<GameId, ScoreWeights>();
|
|
34
|
+
const fallbacks: GameId[] = [];
|
|
35
|
+
if (!holdoutFallback) return { overrides, fallbacks };
|
|
36
|
+
|
|
37
|
+
for (const gid of gameIds) {
|
|
38
|
+
const summary = byGame.get(gid);
|
|
39
|
+
if (summary && summary.adaptiveVsRandom < 0) {
|
|
40
|
+
overrides.set(gid, { ...DEFAULT_WEIGHTS });
|
|
41
|
+
fallbacks.push(gid);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { overrides, fallbacks };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runHoldoutBacktest(
|
|
48
|
+
db: LotteryDb,
|
|
49
|
+
gameIds: GameId[],
|
|
50
|
+
options: BacktestOptions,
|
|
51
|
+
): Promise<SimulationResult[]> {
|
|
52
|
+
const out: SimulationResult[] = [];
|
|
53
|
+
for (const gameId of gameIds) {
|
|
54
|
+
const initial = await loadGameWeights(db, gameId);
|
|
55
|
+
const summary = await walkForwardBacktest(db, gameId, { ...options, initialWeights: initial });
|
|
56
|
+
if (summary) out.push(summary);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatWeightFallbackNote(fallbacks: GameId[]): string {
|
|
62
|
+
if (!fallbacks.length) return '';
|
|
63
|
+
return `【权重保护】近端 holdout 调优未跑赢随机,以下玩法已回退默认 F40/O35/T25:${fallbacks.join(', ')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function shouldFallbackWeights(summary: SimulationResult | null | undefined): boolean {
|
|
67
|
+
return summary != null && summary.adaptiveVsRandom < 0;
|
|
68
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { DEFAULT_WEIGHTS, type DrawNumbers, type Kl8GroupStrategy, type Kl8PickGroup, type NormalizedDraw, type ScoreWeights } from '../types.js';
|
|
2
|
+
|
|
3
|
+
import { pickTopUnique, scorePool, type PoolScore } from '../stats/engine.js';
|
|
4
|
+
|
|
5
|
+
function poolDraws(draws: NormalizedDraw[], extract: (n: DrawNumbers) => number[]): number[][] {
|
|
6
|
+
return draws.map((d) => extract(d.numbers)).filter((a) => a.length > 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const KL8_STRATEGY_LABELS: Record<Kl8GroupStrategy, string> = {
|
|
10
|
+
balanced: '均衡',
|
|
11
|
+
hot: '热号',
|
|
12
|
+
cold: '冷号',
|
|
13
|
+
trend: '趋势',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const STRATEGY_PRESETS: Record<Exclude<Kl8GroupStrategy, 'balanced'>, ScoreWeights> = {
|
|
17
|
+
hot: { freq: 0.55, omit: 0.2, trend: 0.25 },
|
|
18
|
+
cold: { freq: 0.2, omit: 0.55, trend: 0.25 },
|
|
19
|
+
trend: { freq: 0.25, omit: 0.25, trend: 0.5 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface Kl8Config {
|
|
23
|
+
pickCount: number;
|
|
24
|
+
recommendGroups: number;
|
|
25
|
+
groupStrategies: Kl8GroupStrategy[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Kl8ConfigInput {
|
|
29
|
+
pickCount?: number;
|
|
30
|
+
recommendGroups?: number;
|
|
31
|
+
groupStrategies?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseKl8GroupStrategy(raw: string): Kl8GroupStrategy {
|
|
35
|
+
const s = raw.trim().toLowerCase();
|
|
36
|
+
if (s === 'balanced' || s === 'hot' || s === 'cold' || s === 'trend') return s;
|
|
37
|
+
return 'balanced';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveKl8Config(globalPickCount: number, raw?: Kl8ConfigInput): Kl8Config {
|
|
41
|
+
const strategies = (raw?.groupStrategies?.length ? raw.groupStrategies : ['balanced', 'hot', 'cold']).map(
|
|
42
|
+
parseKl8GroupStrategy,
|
|
43
|
+
);
|
|
44
|
+
const recommendGroups = Math.min(10, Math.max(1, raw?.recommendGroups ?? 3));
|
|
45
|
+
const pickCount = Math.min(10, Math.max(1, raw?.pickCount ?? globalPickCount));
|
|
46
|
+
return { pickCount, recommendGroups, groupStrategies: strategies };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function weightsForKl8Strategy(strategy: Kl8GroupStrategy, base: ScoreWeights): ScoreWeights {
|
|
50
|
+
if (strategy === 'balanced') return { ...base };
|
|
51
|
+
return { ...STRATEGY_PRESETS[strategy] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function strategiesForGroupCount(count: number, strategies: Kl8GroupStrategy[]): Kl8GroupStrategy[] {
|
|
55
|
+
const pool = strategies.length ? strategies : (['balanced'] as Kl8GroupStrategy[]);
|
|
56
|
+
return Array.from({ length: count }, (_, i) => pool[i % pool.length] ?? pool[0] ?? 'balanced');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RecommendKl8GroupsResult {
|
|
60
|
+
groups: Kl8PickGroup[];
|
|
61
|
+
scores: PoolScore[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Multi-strategy KL8 rows; groups may overlap — each picks top-N by its own weights. */
|
|
65
|
+
export function recommendKl8Groups(
|
|
66
|
+
draws: NormalizedDraw[],
|
|
67
|
+
options: {
|
|
68
|
+
pickCount: number;
|
|
69
|
+
recommendGroups: number;
|
|
70
|
+
strategies: Kl8GroupStrategy[];
|
|
71
|
+
tieSeed: string;
|
|
72
|
+
baseWeights: ScoreWeights;
|
|
73
|
+
},
|
|
74
|
+
): RecommendKl8GroupsResult {
|
|
75
|
+
const rows = poolDraws(draws, (n) => n.main ?? []);
|
|
76
|
+
const groupStrategies = strategiesForGroupCount(options.recommendGroups, options.strategies);
|
|
77
|
+
const groups: Kl8PickGroup[] = [];
|
|
78
|
+
let primaryScores: PoolScore[] = [];
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < groupStrategies.length; i++) {
|
|
81
|
+
const strategy = groupStrategies[i] ?? 'balanced';
|
|
82
|
+
const weights = weightsForKl8Strategy(strategy, options.baseWeights);
|
|
83
|
+
const scores = scorePool(rows, 1, 80, `${options.tieSeed}:${strategy}:${i}`, weights);
|
|
84
|
+
if (i === 0) primaryScores = scores;
|
|
85
|
+
const main = pickTopUnique(scores, options.pickCount);
|
|
86
|
+
groups.push({
|
|
87
|
+
index: i + 1,
|
|
88
|
+
strategy,
|
|
89
|
+
label: KL8_STRATEGY_LABELS[strategy],
|
|
90
|
+
numbers: main,
|
|
91
|
+
weights,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { groups, scores: primaryScores };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { DEFAULT_WEIGHTS };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { GameId, LotterySource } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export interface GameMeta {
|
|
4
|
+
id: GameId;
|
|
5
|
+
name: string;
|
|
6
|
+
source: LotterySource;
|
|
7
|
+
fucaiName?: string;
|
|
8
|
+
ticaiGameNo?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ALL_GAMES: GameMeta[] = [
|
|
12
|
+
{ id: 'kl8', name: '快乐8', source: 'fucai', fucaiName: 'kl8' },
|
|
13
|
+
{ id: 'ssq', name: '双色球', source: 'fucai', fucaiName: 'ssq' },
|
|
14
|
+
{ id: 'fc3d', name: '福彩3D', source: 'fucai', fucaiName: '3d' },
|
|
15
|
+
{ id: 'dlt', name: '超级大乐透', source: 'ticai', ticaiGameNo: '85' },
|
|
16
|
+
{ id: 'pl3', name: '排列3', source: 'ticai', ticaiGameNo: '35' },
|
|
17
|
+
{ id: 'pl5', name: '排列5', source: 'ticai', ticaiGameNo: '350133' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const BY_ID = new Map(ALL_GAMES.map((g) => [g.id, g]));
|
|
21
|
+
|
|
22
|
+
export function getGameMeta(id: GameId): GameMeta {
|
|
23
|
+
const meta = BY_ID.get(id);
|
|
24
|
+
if (!meta) throw new Error(`Unknown game: ${id}`);
|
|
25
|
+
return meta;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseGameId(raw: string): GameId | null {
|
|
29
|
+
const key = raw.trim().toLowerCase();
|
|
30
|
+
const aliases: Record<string, GameId> = {
|
|
31
|
+
kl8: 'kl8',
|
|
32
|
+
'快乐8': 'kl8',
|
|
33
|
+
ssq: 'ssq',
|
|
34
|
+
'双色球': 'ssq',
|
|
35
|
+
dlt: 'dlt',
|
|
36
|
+
'大乐透': 'dlt',
|
|
37
|
+
fc3d: 'fc3d',
|
|
38
|
+
'福彩3d': 'fc3d',
|
|
39
|
+
pl3: 'pl3',
|
|
40
|
+
'排列3': 'pl3',
|
|
41
|
+
pl5: 'pl5',
|
|
42
|
+
'排列5': 'pl5',
|
|
43
|
+
};
|
|
44
|
+
return aliases[key] ?? (BY_ID.has(key as GameId) ? (key as GameId) : null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveEnabledGames(configGames?: string[]): GameId[] {
|
|
48
|
+
if (!configGames?.length) return ALL_GAMES.map((g) => g.id);
|
|
49
|
+
const out: GameId[] = [];
|
|
50
|
+
for (const g of configGames) {
|
|
51
|
+
const id = parseGameId(g);
|
|
52
|
+
if (id && !out.includes(id)) out.push(id);
|
|
53
|
+
}
|
|
54
|
+
return out.length ? out : ALL_GAMES.map((x) => x.id);
|
|
55
|
+
}
|