From 8b2590c5b5a216943ea6c750449a05eda2d07ee2 Mon Sep 17 00:00:00 2001
From: leon <916117771@qq.com>
Date: Fri, 1 May 2026 23:17:24 +0800
Subject: [PATCH] =?UTF-8?q?docs(predictV3):=20=E6=B7=BB=E5=8A=A0predictV3?=
=?UTF-8?q?=E7=AE=97=E6=B3=95=E4=BC=98=E5=8C=96=E7=A0=94=E7=A9=B6=E6=96=87?=
=?UTF-8?q?=E6=A1=A3=E5=92=8C=E5=89=8D=E7=AB=AF=E5=8A=9F=E8=83=BD=E5=AE=9E?=
=?UTF-8?q?=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 完成Phase 11: predictV3算法优化研究文档,涵盖6个优化方向的技术分析
- 实现置信度评估功能,提供历史命中率、得分分布、多维度一致性置信度指标
- 扩展回测指标体系,新增NDCG@K、MRR、命中率分布等排名质量评估指标
- 优化转移概率算法,引入二阶马尔可夫链和多属性联合转移增强预测准确性
- 设计权重训练机制,支持网格搜索和遗传算法进行数据驱动的参数优化
- 集成组合特征挖掘功能,采用关联规则和序列模式发现号码间潜在关联
- 实现完整的前端交互界面,支持预测结果显示、置信度展示和回测验证功能
- 建立性能优化策略,包括预计算缓存、批量计算和降级策略保障响应速度
---
.omc/project-memory.json | 163 ++++
.../76146475-cfaa-4328-af26-aad39250b506.json | 8 +
.omc/state/hud-stdin-cache.json | 1 +
.../hud-state.json | 6 +
.../session-started.json | 6 +
.planning/phases/11-predictv3/.gitkeep | 0
.planning/phases/11-predictv3/11-01-PLAN.md | 328 +++++++
.planning/phases/11-predictv3/11-02-PLAN.md | 325 +++++++
.planning/phases/11-predictv3/11-03-PLAN.md | 278 ++++++
.planning/phases/11-predictv3/11-04-PLAN.md | 346 +++++++
.planning/phases/11-predictv3/11-05-PLAN.md | 455 ++++++++++
.planning/phases/11-predictv3/11-RESEARCH.md | 6 +-
.planning/phases/11-predictv3/11-REVIEWS.md | 158 ++++
.../phases/11-predictv3/11-VALIDATION.md | 97 ++
.../quick/20260430-history-predict/PLAN.md | 55 ++
.../quick/20260430-history-predict/SUMMARY.md | 37 +
.../20260501-history-predict/ANALYSIS.md | 39 +
analysis/predict_analysis.php | 858 ++++++++++++++++++
analysis/predict_analysis.py | 589 ++++++++++++
analysis_history.php | 395 ++++++++
analysis_history.py | 286 ++++++
analysis_result.txt | 204 +++++
application/admin/controller/History.php | 26 +-
application/admin/model/History.php | 538 +++++++++++
application/admin/view/history/index.html | 2 +
public/assets/js/backend/history.js | 203 +++++
26 files changed, 5407 insertions(+), 2 deletions(-)
create mode 100644 .omc/project-memory.json
create mode 100644 .omc/sessions/76146475-cfaa-4328-af26-aad39250b506.json
create mode 100644 .omc/state/hud-stdin-cache.json
create mode 100644 .omc/state/sessions/76146475-cfaa-4328-af26-aad39250b506/hud-state.json
create mode 100644 .omc/state/sessions/a1d5e02a-7411-4199-815c-5ae711cf7291/session-started.json
create mode 100644 .planning/phases/11-predictv3/.gitkeep
create mode 100644 .planning/phases/11-predictv3/11-01-PLAN.md
create mode 100644 .planning/phases/11-predictv3/11-02-PLAN.md
create mode 100644 .planning/phases/11-predictv3/11-03-PLAN.md
create mode 100644 .planning/phases/11-predictv3/11-04-PLAN.md
create mode 100644 .planning/phases/11-predictv3/11-05-PLAN.md
create mode 100644 .planning/phases/11-predictv3/11-REVIEWS.md
create mode 100644 .planning/phases/11-predictv3/11-VALIDATION.md
create mode 100644 .planning/quick/20260430-history-predict/PLAN.md
create mode 100644 .planning/quick/20260430-history-predict/SUMMARY.md
create mode 100644 .planning/quick/20260501-history-predict/ANALYSIS.md
create mode 100644 analysis/predict_analysis.php
create mode 100644 analysis/predict_analysis.py
create mode 100644 analysis_history.php
create mode 100644 analysis_history.py
create mode 100644 analysis_result.txt
diff --git a/.omc/project-memory.json b/.omc/project-memory.json
new file mode 100644
index 0000000..3af87ff
--- /dev/null
+++ b/.omc/project-memory.json
@@ -0,0 +1,163 @@
+{
+ "version": "1.0.0",
+ "lastScanned": 1777648534656,
+ "projectRoot": "D:\\code\\php\\amlhc",
+ "techStack": {
+ "languages": [
+ {
+ "name": "JavaScript/TypeScript",
+ "version": null,
+ "confidence": "high",
+ "markers": [
+ "package.json"
+ ]
+ },
+ {
+ "name": "PHP",
+ "version": null,
+ "confidence": "high",
+ "markers": [
+ "composer.json"
+ ]
+ }
+ ],
+ "frameworks": [],
+ "packageManager": "composer",
+ "runtime": null
+ },
+ "build": {
+ "buildCommand": "npm run build",
+ "testCommand": null,
+ "lintCommand": null,
+ "devCommand": null,
+ "scripts": {
+ "build": "grunt"
+ }
+ },
+ "conventions": {
+ "namingStyle": null,
+ "importStyle": null,
+ "testPattern": null,
+ "fileOrganization": null
+ },
+ "structure": {
+ "isMonorepo": false,
+ "workspaces": [],
+ "mainDirectories": [
+ "public"
+ ],
+ "gitBranches": null
+ },
+ "customNotes": [],
+ "directoryMap": {
+ "addons": {
+ "path": "addons",
+ "purpose": null,
+ "fileCount": 1,
+ "lastAccessed": 1777648534596,
+ "keyFiles": []
+ },
+ "analysis": {
+ "path": "analysis",
+ "purpose": null,
+ "fileCount": 2,
+ "lastAccessed": 1777648534608,
+ "keyFiles": [
+ "predict_analysis.php",
+ "predict_analysis.py"
+ ]
+ },
+ "application": {
+ "path": "application",
+ "purpose": null,
+ "fileCount": 8,
+ "lastAccessed": 1777648534609,
+ "keyFiles": [
+ "build.php",
+ "command.php",
+ "common.php",
+ "config.php",
+ "database.php"
+ ]
+ },
+ "extend": {
+ "path": "extend",
+ "purpose": null,
+ "fileCount": 1,
+ "lastAccessed": 1777648534611,
+ "keyFiles": []
+ },
+ "public": {
+ "path": "public",
+ "purpose": "Public files",
+ "fileCount": 5,
+ "lastAccessed": 1777648534612,
+ "keyFiles": [
+ "admin.php",
+ "index.php",
+ "robots.txt",
+ "router.php"
+ ]
+ },
+ "runtime": {
+ "path": "runtime",
+ "purpose": null,
+ "fileCount": 1,
+ "lastAccessed": 1777648534612,
+ "keyFiles": []
+ },
+ "sql": {
+ "path": "sql",
+ "purpose": null,
+ "fileCount": 2,
+ "lastAccessed": 1777648534612,
+ "keyFiles": [
+ "amlhc.sql",
+ "macaujc_history.sql"
+ ]
+ },
+ "thinkphp": {
+ "path": "thinkphp",
+ "purpose": null,
+ "fileCount": 15,
+ "lastAccessed": 1777648534613,
+ "keyFiles": [
+ "base.php",
+ "codecov.yml",
+ "composer.json",
+ "console.php",
+ "CONTRIBUTING.md"
+ ]
+ },
+ "vendor": {
+ "path": "vendor",
+ "purpose": "Third-party code",
+ "fileCount": 1,
+ "lastAccessed": 1777648534613,
+ "keyFiles": [
+ "autoload.php"
+ ]
+ },
+ "application\\api": {
+ "path": "application\\api",
+ "purpose": "API routes",
+ "fileCount": 2,
+ "lastAccessed": 1777648534614,
+ "keyFiles": [
+ "common.php",
+ "config.php"
+ ]
+ },
+ "public\\assets": {
+ "path": "public\\assets",
+ "purpose": "Static assets",
+ "fileCount": 1,
+ "lastAccessed": 1777648534615,
+ "keyFiles": [
+ "index.html"
+ ]
+ }
+ },
+ "hotPaths": [],
+ "userDirectives": []
+}
\ No newline at end of file
diff --git a/.omc/sessions/76146475-cfaa-4328-af26-aad39250b506.json b/.omc/sessions/76146475-cfaa-4328-af26-aad39250b506.json
new file mode 100644
index 0000000..428077e
--- /dev/null
+++ b/.omc/sessions/76146475-cfaa-4328-af26-aad39250b506.json
@@ -0,0 +1,8 @@
+{
+ "session_id": "76146475-cfaa-4328-af26-aad39250b506",
+ "ended_at": "2026-05-01T15:15:29.199Z",
+ "reason": "prompt_input_exit",
+ "agents_spawned": 0,
+ "agents_completed": 0,
+ "modes_used": []
+}
\ No newline at end of file
diff --git a/.omc/state/hud-stdin-cache.json b/.omc/state/hud-stdin-cache.json
new file mode 100644
index 0000000..ecc289c
--- /dev/null
+++ b/.omc/state/hud-stdin-cache.json
@@ -0,0 +1 @@
+{"session_id":"a1d5e02a-7411-4199-815c-5ae711cf7291","transcript_path":"C:\\Users\\91611\\.claude\\projects\\D--code-php-amlhc\\a1d5e02a-7411-4199-815c-5ae711cf7291.jsonl","cwd":"D:\\code\\php\\amlhc","model":{"id":"qwen3.6-plus[1m]","display_name":"qwen3.6-plus[1m]"},"workspace":{"current_dir":"D:\\code\\php\\amlhc","project_dir":"D:\\code\\php\\amlhc","added_dirs":[]},"version":"2.1.126","output_style":{"name":"default"},"cost":{"total_cost_usd":0,"total_duration_ms":1008,"total_api_duration_ms":0,"total_lines_added":0,"total_lines_removed":0},"context_window":{"total_input_tokens":0,"total_output_tokens":0,"context_window_size":1000000,"current_usage":null,"used_percentage":null,"remaining_percentage":null},"exceeds_200k_tokens":false,"fast_mode":false,"effort":{"level":"high"},"thinking":{"enabled":true}}
\ No newline at end of file
diff --git a/.omc/state/sessions/76146475-cfaa-4328-af26-aad39250b506/hud-state.json b/.omc/state/sessions/76146475-cfaa-4328-af26-aad39250b506/hud-state.json
new file mode 100644
index 0000000..711b999
--- /dev/null
+++ b/.omc/state/sessions/76146475-cfaa-4328-af26-aad39250b506/hud-state.json
@@ -0,0 +1,6 @@
+{
+ "timestamp": "2026-05-01T15:08:59.373Z",
+ "backgroundTasks": [],
+ "sessionStartTimestamp": "2026-05-01T15:05:35.977Z",
+ "sessionId": "76146475-cfaa-4328-af26-aad39250b506"
+}
\ No newline at end of file
diff --git a/.omc/state/sessions/a1d5e02a-7411-4199-815c-5ae711cf7291/session-started.json b/.omc/state/sessions/a1d5e02a-7411-4199-815c-5ae711cf7291/session-started.json
new file mode 100644
index 0000000..3733f07
--- /dev/null
+++ b/.omc/state/sessions/a1d5e02a-7411-4199-815c-5ae711cf7291/session-started.json
@@ -0,0 +1,6 @@
+{
+ "session_id": "a1d5e02a-7411-4199-815c-5ae711cf7291",
+ "started_at": "2026-05-01T15:15:34.560Z",
+ "cwd": "D:\\code\\php\\amlhc",
+ "pid": 23096
+}
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/.gitkeep b/.planning/phases/11-predictv3/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/.planning/phases/11-predictv3/11-01-PLAN.md b/.planning/phases/11-predictv3/11-01-PLAN.md
new file mode 100644
index 0000000..64026c6
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-01-PLAN.md
@@ -0,0 +1,328 @@
+---
+phase: 11-predictv3
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - application/admin/model/History.php
+autonomous: true
+requirements:
+ - PRED-02
+ - PRED-05
+must_haves:
+ truths:
+ - "用户可以在回测结果中看到 NDCG@5 指标"
+ - "用户可以在回测结果中看到 MRR 指标"
+ - "用户可以看到各排名位置的命中分布统计"
+ - "系统在数据不足时返回合理的默认值或提示"
+ artifacts:
+ - path: "application/admin/model/History.php"
+ provides: "NDCG、MRR、命中分布计算方法"
+ contains: "_calculateNDCG|_calculateMRR|_calculateHitDistribution"
+ key_links:
+ - from: "_runBacktestV3"
+ to: "_calculateNDCG, _calculateMRR, _calculateHitDistribution"
+ via: "method call in return statement"
+---
+
+# Phase 11 - Plan 01: 回测指标扩展
+
+## Objective
+
+扩展 `_runBacktestV3` 方法的回测指标,新增 NDCG@5、MRR、命中率分布等排名质量评估指标,提升算法评估能力。
+
+**Purpose:** 当前回测仅返回命中率(Top5)和平均排名,缺少排名质量评估指标。NDCG、MRR 是成熟的推荐系统评估指标,能更全面反映预测排名质量。
+
+**Output:** `History.php` 中新增 3 个计算方法,`_runBacktestV3` 返回结果扩展。
+
+## Tasks
+
+### Task 1: 实现 NDCG@5 计算(含空预测保护和公式文档)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 3495-3560, _runBacktestV3 方法)
+
+
+
+在 `History.php` 文件末尾(类内)新增 `_calculateNDCG` 方法:
+
+```php
+/**
+ * 计算 NDCG@K (Normalized Discounted Cumulative Gain)
+ *
+ * 公式说明:
+ * - DCG (Discounted Cumulative Gain) = Σ(rel_i / log2(rank_i + 1))
+ * 其中 rel_i = 1 (命中) 或 0 (未命中),rank_i 为预测排名位置
+ * - IDCG (Ideal DCG) = Σ(1 / log2(i + 1)) for i = 1..min(hits, K)
+ * 即理想情况下所有命中的号码都排在最前面的DCG值
+ * - NDCG = DCG / IDCG,范围 0-1,越接近1表示排名质量越好
+ *
+ * @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int}
+ * @param int $K Top-K 参数,默认5,评估前K个预测位置的排名质量
+ * @return float NDCG值 (0-1范围),空数据时返回0
+ */
+private function _calculateNDCG($backtestDetails, $K = 5)
+{
+ // 边缘情况处理:空预测或无效参数
+ if (empty($backtestDetails) || $K <= 0) {
+ return 0;
+ }
+
+ $dcg = 0;
+ $idcg = 0;
+
+ // 计算 DCG: 命中号码的排名折损累积值
+ foreach ($backtestDetails as $detail) {
+ if (!isset($detail['hit']) || !isset($detail['rank'])) {
+ continue; // 跳过无效数据
+ }
+ if ($detail['hit'] && $detail['rank'] > 0 && $detail['rank'] <= $K) {
+ // DCG公式: rel / log2(rank + 1),命中时 rel=1
+ $dcg += 1 / log($detail['rank'] + 1, 2);
+ }
+ }
+
+ // 计算 IDCG: 最理想情况下所有命中的 DCG(假设都排在第1位)
+ $hitCount = 0;
+ foreach ($backtestDetails as $detail) {
+ if (isset($detail['hit']) && $detail['hit']) {
+ $hitCount++;
+ }
+ }
+
+ for ($i = 1; $i <= min($hitCount, $K); $i++) {
+ $idcg += 1 / log($i + 1, 2);
+ }
+
+ // 返回标准化值,IDCG为0时返回0避免除零错误
+ return $idcg > 0 ? round($dcg / $idcg, 4) : 0;
+}
+```
+
+实现要点:
+- 公式:DCG = Σ(1/log2(rank+1)),IDCG = Σ(1/log2(i+1)) for i=1..hits
+- 添加空预测保护:检查 $backtestDetails 是否为空
+- 添加数据完整性检查:确保 hit 和 rank 字段存在
+- 使用 log(rank + 1, 2) 作为折损函数,排名越靠前权重越高
+- 返回 0-1 范围的标准化值,越接近 1 表示排名质量越好
+
+
+
+- grep 正则匹配: `_calculateNDCG\s*\(` 在 History.php 中存在
+- grep 匹配: `empty($backtestDetails)` 在方法中存在(空预测保护)
+- 方法返回 float 类型值
+- 包含函数级注释说明 NDCG 计算逻辑和公式
+
+
+### Task 2: 实现 MRR 和命中分布计算(含边缘情况处理)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (新增的 _calculateNDCG 方法位置)
+
+
+
+在 `_calculateNDCG` 方法后继续新增 `_calculateMRR` 和 `_calculateHitDistribution` 方法:
+
+```php
+/**
+ * 计算 MRR (Mean Reciprocal Rank)
+ * 平均倒数排名,关注命中号码的具体排名位置
+ *
+ * 公式说明:
+ * - MRR = Σ(1/rank_i) / N,其中 rank_i 为命中号码的排名,N 为测试总数
+ * - 未命中的测试项贡献 0 到倒数排名
+ * - MRR 范围 0-1,越接近1表示命中号码平均排名越靠前
+ *
+ * @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int}
+ * @return float MRR值 (0-1范围),空数据时返回0
+ */
+private function _calculateMRR($backtestDetails)
+{
+ // 边缘情况处理:空预测
+ if (empty($backtestDetails)) {
+ return 0;
+ }
+
+ $reciprocalRanks = [];
+
+ foreach ($backtestDetails as $detail) {
+ if (!isset($detail['hit']) || !isset($detail['rank'])) {
+ continue; // 跳过无效数据
+ }
+ if ($detail['hit'] && $detail['rank'] > 0) {
+ $reciprocalRanks[] = 1 / $detail['rank'];
+ } else {
+ $reciprocalRanks[] = 0; // 未命中记为0
+ }
+ }
+
+ return count($reciprocalRanks) > 0
+ ? round(array_sum($reciprocalRanks) / count($reciprocalRanks), 4)
+ : 0;
+}
+
+/**
+ * 计算命中率分布
+ * 统计各排名位置(1-5)的命中次数分布
+ *
+ * 结构定义:
+ * - 返回格式: {rank_1: n, rank_2: n, rank_3: n, rank_4: n, rank_5: n}
+ * - rank_N 表示预测排名第N位的命中次数
+ * - 用于前端柱状图可视化展示
+ *
+ * @param array $backtestDetails 回测详情数组,每项包含 {hit: bool, rank: int}
+ * @return array 各排名(1-5)的命中次数统计,键名为 rank_1 到 rank_5
+ */
+private function _calculateHitDistribution($backtestDetails)
+{
+ // 边缘情况处理:空预测返回全0分布
+ if (empty($backtestDetails)) {
+ return [
+ 'rank_1' => 0,
+ 'rank_2' => 0,
+ 'rank_3' => 0,
+ 'rank_4' => 0,
+ 'rank_5' => 0
+ ];
+ }
+
+ // 初始化分布数组,键名使用 rank_N 格式便于前端解析
+ $distribution = [
+ 'rank_1' => 0,
+ 'rank_2' => 0,
+ 'rank_3' => 0,
+ 'rank_4' => 0,
+ 'rank_5' => 0
+ ];
+
+ foreach ($backtestDetails as $detail) {
+ if (!isset($detail['hit']) || !isset($detail['rank'])) {
+ continue; // 跳过无效数据
+ }
+ if ($detail['hit'] && $detail['rank'] >= 1 && $detail['rank'] <= 5) {
+ $key = 'rank_' . $detail['rank'];
+ $distribution[$key]++;
+ }
+ }
+
+ return $distribution;
+}
+```
+
+实现要点:
+- MRR: 命中号码排名倒数平均值,公式 Σ(1/rank)/N
+- 命中分布: 明确结构为 `{rank_1: n, rank_2: n, ..., rank_5: n}`
+- 两个方法均添加空预测保护和无效数据跳过逻辑
+- hit_distribution 使用 rank_N 键名格式,便于前端柱状图渲染
+
+
+
+- grep 正则匹配: `_calculateMRR\s*\(` 在 History.php 中存在
+- grep 正则匹配: `_calculateHitDistribution\s*\(` 在 History.php 中存在
+- grep 匹配: `empty($backtestDetails)` 在两个方法中均存在(空预测保护)
+- grep 匹配: `rank_1|rank_2|rank_3|rank_4|rank_5` 在 _calculateHitDistribution 中存在
+- 两个方法均包含函数级注释
+
+
+### Task 3: 扩展 _runBacktestV3 返回结果(含数据量检查)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 3549-3556, _runBacktestV3 返回语句)
+
+
+
+修改 `_runBacktestV3` 方法的返回语句,在原有返回结构中添加新指标和数据量验证:
+
+找到以下代码段(约 line 3549-3556):
+```php
+return [
+ 'hit_rate' => $hitRate,
+ 'avg_rank' => $avgRank,
+ 'total_tests' => $testCount,
+ 'total_hits' => $hits,
+ 'details' => $details
+];
+```
+
+替换为:
+```php
+// 计算新增指标(添加数据量检查)
+$minDataThreshold = 50; // 置信度计算最小数据量阈值
+
+// 如果测试数据不足,返回默认值并添加警告
+if ($testCount < $minDataThreshold) {
+ $ndcg5 = 0;
+ $mrr = 0;
+ $hitDistribution = [
+ 'rank_1' => 0,
+ 'rank_2' => 0,
+ 'rank_3' => 0,
+ 'rank_4' => 0,
+ 'rank_5' => 0
+ ];
+ $dataWarning = '回测数据不足(' . $testCount . '期),建议至少50期以获得可靠指标';
+} else {
+ $ndcg5 = $this->_calculateNDCG($details, 5);
+ $mrr = $this->_calculateMRR($details);
+ $hitDistribution = $this->_calculateHitDistribution($details);
+ $dataWarning = null;
+}
+
+$precision5 = $testCount > 0 ? round($hits / ($testCount * 5) * 100, 2) : 0;
+
+return [
+ 'hit_rate' => $hitRate,
+ 'avg_rank' => $avgRank,
+ 'total_tests' => $testCount,
+ 'total_hits' => $hits,
+ 'details' => $details,
+ // 新增排名质量指标
+ 'ndcg_5' => $ndcg5,
+ 'mrr' => $mrr,
+ 'hit_distribution' => $hitDistribution,
+ 'precision_5' => $precision5,
+ // 数据量警告(不足时提示)
+ 'data_warning' => $dataWarning,
+ 'data_sufficient' => $testCount >= $minDataThreshold
+];
+```
+
+注意:
+- 新增指标计算放在 return 语句之前,确保 $details 数组已完整构建
+- 添加最小数据量检查(50期),不足时返回默认值和警告提示
+- 新增 data_warning 和 data_sufficient 字段供前端展示
+
+
+
+- grep 匹配: `ndcg_5` 在 _runBacktestV3 返回结构中存在
+- grep 匹配: `mrr` 在 _runBacktestV3 返回结构中存在
+- grep 匹配: `hit_distribution` 在 _runBacktestV3 返回结构中存在
+- grep 匹配: `precision_5` 在 _runBacktestV3 返回结构中存在
+- grep 匹配: `data_warning` 在 _runBacktestV3 返回结构中存在
+- grep 匹配: `minDataThreshold` 变量在方法中存在
+
+
+## Verification
+
+执行预测接口验证新指标返回:
+
+```bash
+curl -s "http://127.0.0.1:8000/admin/history/predictV3?periods=200&backtest=10" | grep -E "ndcg_5|mrr|hit_distribution|precision_5|data_warning"
+```
+
+预期结果:返回 JSON 中包含 ndcg_5、mrr、hit_distribution、precision_5、data_warning 字段。
+
+## Success Criteria
+
+1. `_calculateNDCG`、`_calculateMRR`、`_calculateHitDistribution` 三个方法已实现
+2. 所有计算方法包含空预测保护和数据完整性检查
+3. NDCG 公式在注释中完整说明:DCG = Σ(1/log2(rank+1))
+4. hit_distribution 结构明确为 `{rank_1..rank_5: counts}` 格式
+5. `_runBacktestV3` 返回结构包含 ndcg_5、mrr、hit_distribution、precision_5、data_warning 字段
+6. 添加数据量检查,不足50期时返回警告
+7. 所有新增方法包含函数级注释
+
+## Output
+
+完成后创建 `.planning/phases/11-predictv3/11-01-SUMMARY.md`
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/11-02-PLAN.md b/.planning/phases/11-predictv3/11-02-PLAN.md
new file mode 100644
index 0000000..446b1bc
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-02-PLAN.md
@@ -0,0 +1,325 @@
+---
+phase: 11-predictv3
+plan: 02
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - application/admin/model/History.php
+autonomous: true
+requirements:
+ - PRED-01
+must_haves:
+ truths:
+ - "用户可以看到每个预测号码的置信度百分比"
+ - "用户可以看到 Top5 预测的整体置信度"
+ - "置信度基于历史命中率、得分集中度、得分分布三个维度计算"
+ - "系统在数据不足时提供合理的置信度估算"
+ artifacts:
+ - path: "application/admin/model/History.php"
+ provides: "置信度计算方法"
+ contains: "_calculateConfidence|_getHistoricalHitRateByRank|_getScoreDistributionConfidence|_getScoreConcentration"
+ key_links:
+ - from: "getPredictionV3"
+ to: "_calculateConfidence"
+ via: "method call before return"
+---
+
+# Phase 11 - Plan 02: 置信度评估实现
+
+## Objective
+
+为预测结果添加置信度评估,帮助用户判断预测可靠性。置信度基于历史排名命中率、得分分布、得分集中度三个维度计算。
+
+**Purpose:** 当前预测结果只有排名和得分,缺少置信度指标。用户无法判断预测结果的可信程度,置信度评估能有效辅助用户决策。
+
+**Output:** `History.php` 中新增置信度计算方法,`getPredictionV3` 返回结果扩展 confidence 字段。
+
+## Tasks
+
+### Task 1: 实现置信度核心计算方法(含明确维度定义和数据量检查)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 2436-2444, getPredictionV3 返回语句)
+- D:\code\php\amlhc\application\admin\model\History.php (line 3495-3556, _runBacktestV3 方法)
+
+
+
+在 `History.php` 类末尾新增置信度计算相关方法:
+
+```php
+/**
+ * 计算预测置信度
+ *
+ * 置信度组成(三个维度加权平均):
+ * - 维度1: 历史排名命中率 (权重0.4) - 基于回测数据统计各排名位置的命中率
+ * - 维度2: 得分分布置信度 (权重0.3) - 当前号码得分与Top5得分范围的比例关系
+ * - 维度3: 得分集中度 (权重0.3) - Top5得分与平均得分的差距,差距越大置信度越高
+ *
+ * 加权公式:
+ * confidence = 0.4 * historical_hit_rate + 0.3 * score_distribution + 0.3 * score_concentration
+ *
+ * 阈值定义:
+ * - 高置信度: >= 70% (绿色展示)
+ * - 中置信度: 50-70% (橙色展示)
+ * - 低置信度: < 50% (红色展示)
+ *
+ * @param array $predictions 预测结果数组(Top5)
+ * @param array $backtest 回测结果
+ * @param array $scoresAll 所有号码得分详情(可选,用于集中度计算)
+ * @param int $minDataThreshold 最小数据量阈值,默认50期
+ * @return array {confidence_scores: [], overall_confidence: float, data_warning: string|null}
+ */
+private function _calculateConfidence($predictions, $backtest, $scoresAll = null, $minDataThreshold = 50)
+{
+ // 数据量检查
+ $dataWarning = null;
+ $hasBacktest = $backtest && !empty($backtest['details']) && $backtest['total_tests'] > 0;
+
+ if (!$hasBacktest || $backtest['total_tests'] < $minDataThreshold) {
+ $dataWarning = '回测数据不足(' . ($backtest['total_tests'] ?? 0) . '期),置信度基于估算,建议至少50期';
+ }
+
+ $confidenceScores = [];
+
+ // 计算Top5平均得分(用于集中度计算)
+ $avgScore = 0;
+ if (!empty($predictions)) {
+ $totalScore = array_sum(array_column($predictions, 'score'));
+ $avgScore = $totalScore / count($predictions);
+ }
+
+ foreach ($predictions as $idx => $pred) {
+ $rank = $idx + 1;
+ $num = $pred['num'];
+ $score = $pred['score'];
+
+ // 维度1: 历史排名命中率 (权重0.4)
+ $rankHitRate = $this->_getHistoricalHitRateByRank($rank, $backtest);
+
+ // 维度2: 得分分布置信度 (权重0.3) - 得分比例
+ $scoreDistribution = $this->_getScoreDistributionConfidence($score, $predictions);
+
+ // 维度3: 得分集中度 (权重0.3) - Top得分与平均得分的差距比例
+ $scoreConcentration = $this->_getScoreConcentration($score, $avgScore, $predictions);
+
+ // 综合置信度(加权平均)
+ $overallConfidence = $rankHitRate * 0.4 + $scoreDistribution * 0.3 + $scoreConcentration * 0.3;
+
+ $confidenceScores[] = [
+ 'num' => $num,
+ 'rank' => $rank,
+ 'confidence' => round($overallConfidence * 100, 1),
+ 'rank_hit_rate' => round($rankHitRate * 100, 1),
+ 'score_distribution' => round($scoreDistribution * 100, 1),
+ 'score_concentration' => round($scoreConcentration * 100, 1)
+ ];
+ }
+
+ // 整体置信度(Top5平均)
+ $overallConfidence = count($confidenceScores) > 0
+ ? round(array_sum(array_column($confidenceScores, 'confidence')) / count($confidenceScores), 1)
+ : 0;
+
+ return [
+ 'confidence_scores' => $confidenceScores,
+ 'overall_confidence' => $overallConfidence,
+ 'data_warning' => $dataWarning
+ ];
+}
+
+/**
+ * 基于历史排名获取命中率
+ *
+ * 计算方法:
+ * - 有回测数据时: 统计各排名的历史命中次数 / 总测试次数
+ * - 无回测数据时: 根据排名估算,排名越靠前置信度越高
+ * 估算公式: 1 - (rank - 1) * 0.15,即第1名估算85%,第5名估算25%
+ *
+ * @param int $rank 排名位置 (1-5)
+ * @param array $backtest 回测结果
+ * @return float 该排名的历史命中率 (0-1)
+ */
+private function _getHistoricalHitRateByRank($rank, $backtest)
+{
+ if (!$backtest || empty($backtest['details']) || $backtest['total_tests'] == 0) {
+ // 无回测数据时,根据排名估算(排名越靠前置信度越高)
+ // 估算公式: 1 - (rank - 1) * 0.15
+ // 第1名: 1.0, 第2名: 0.85, 第3名: 0.70, 第4名: 0.55, 第5名: 0.40
+ return max(0, 1 - ($rank - 1) * 0.15);
+ }
+
+ // 统计各排名的历史命中次数
+ $rankHits = array_fill(1, 5, 0);
+ foreach ($backtest['details'] as $detail) {
+ if ($detail['hit'] && $detail['rank'] >= 1 && $detail['rank'] <= 5) {
+ $rankHits[$detail['rank']]++;
+ }
+ }
+
+ $totalTests = $backtest['total_tests'];
+ return $totalTests > 0 ? $rankHits[$rank] / $totalTests : 0;
+}
+
+/**
+ * 计算得分分布置信度
+ *
+ * 计算方法:
+ * - 得分比例 = (score - bottomScore) / (topScore - bottomScore)
+ * - 得分越接近第一名,置信度越高
+ * - 所有得分相同时返回1
+ *
+ * @param float $score 当前号码得分
+ * @param array $predictions 所有预测结果
+ * @return float 得分置信度 (0-1)
+ */
+private function _getScoreDistributionConfidence($score, $predictions)
+{
+ if (empty($predictions)) return 0;
+
+ $topScore = $predictions[0]['score'];
+ $bottomScore = end($predictions)['score'];
+
+ if ($topScore == $bottomScore) return 1; // 所有得分相同
+
+ // 得分比例:(score - bottom) / (top - bottom)
+ $ratio = ($score - $bottomScore) / ($topScore - $bottomScore);
+ return max(0, min(1, $ratio));
+}
+
+/**
+ * 计算得分集中度
+ *
+ * 计算方法:
+ * - 集中度 = (score - avgScore) / (topScore - avgScore) 如果 score > avgScore
+ * - 集中度 = 0 如果 score <= avgScore
+ * - Top得分与平均得分差距越大,集中度越高,表示预测结果区分度明显
+ *
+ * @param float $score 当前号码得分
+ * @param float $avgScore Top5平均得分
+ * @param array $predictions 所有预测结果
+ * @return float 集中度置信度 (0-1)
+ */
+private function _getScoreConcentration($score, $avgScore, $predictions)
+{
+ if (empty($predictions)) return 0;
+
+ $topScore = $predictions[0]['score'];
+
+ // 如果得分低于平均,集中度为0
+ if ($score <= $avgScore) {
+ return 0;
+ }
+
+ // 如果Top得分等于平均,所有得分相同,集中度为0.5
+ if ($topScore == $avgScore) {
+ return $score == $topScore ? 0.5 : 0;
+ }
+
+ // 集中度 = (score - avg) / (top - avg)
+ $concentration = ($score - $avgScore) / ($topScore - $avgScore);
+ return max(0, min(1, $concentration));
+}
+```
+
+实现要点:
+- **维度重命名**:将"多维度一致性"改为"得分集中度",更明确地表示Top得分与平均得分的差距
+- **加权公式明确**:`confidence = 0.4*历史命中率 + 0.3*得分分布 + 0.3*得分集中度`
+- **数据量检查**:回测数据不足50期时返回警告
+- **阈值明确**:>=70%高、50-70%中、<50%低
+- **无数据fallback**:回测缺失时使用估算公式
+
+
+
+- grep 正则匹配: `_calculateConfidence\s*\(` 在 History.php 中存在
+- grep 正则匹配: `_getHistoricalHitRateByRank\s*\(` 在 History.php 中存在
+- grep 正则匹配: `_getScoreDistributionConfidence\s*\(` 在 History.php 中存在
+- grep 正则匹配: `_getScoreConcentration\s*\(` 在 History.php 中存在(替代原_getDimensionConsistency)
+- grep 匹配: `minDataThreshold` 在方法中存在(数据量阈值)
+- grep 匹配: `score_concentration` 在返回结构中存在(替代原consistency)
+- 所有方法包含函数级注释,注释中包含加权公式说明
+
+
+### Task 2: 在 getPredictionV3 中调用置信度计算(含数据量传递)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 2413-2444, getPredictionV3 返回部分)
+
+
+
+在 `getPredictionV3` 方法中,找到以下代码段(约 line 2413-2444):
+
+```php
+// ====== 10. 历史回测验证 ======
+$backtest = $skipBacktest ? null : $this->_runBacktestV3($periods, $weights, $backtestCount, $cutoffTime);
+
+// 计算命中情况
+$hitInfo = null;
+...
+return [
+ 'predictions' => $predictions,
+ ...
+];
+```
+
+在 `$backtest` 计算后、`$hitInfo` 计算前,插入置信度计算代码:
+
+```php
+// ====== 10. 历史回测验证 ======
+$backtest = $skipBacktest ? null : $this->_runBacktestV3($periods, $weights, $backtestCount, $cutoffTime);
+
+// ====== 11. 置信度评估(新增)======
+// 最小数据量阈值设为50期,不足时置信度基于估算
+$minDataThreshold = 50;
+$confidence = $this->_calculateConfidence($predictions, $backtest, null, $minDataThreshold);
+
+// 计算命中情况
+$hitInfo = null;
+...
+```
+
+并修改返回结构,添加 `confidence` 字段:
+
+```php
+return [
+ 'predictions' => $predictions,
+ 'last_special' => $lastSpecial,
+ 'last_expect' => $lastExpect,
+ 'analysis' => $analysis,
+ 'actual_result' => $actualResult,
+ 'hit_info' => $hitInfo,
+ 'backtest' => $backtest,
+ 'confidence' => $confidence // 新增置信度字段
+];
+```
+
+
+
+- grep 匹配: `_calculateConfidence` 在 getPredictionV3 方法中被调用
+- grep 匹配: `$minDataThreshold` 在 getPredictionV3 中存在
+- grep 匹配: `'confidence'` 在 getPredictionV3 返回结构中存在
+- 置信度计算在回测验证之后执行
+
+
+## Verification
+
+执行预测接口验证置信度字段返回:
+
+```bash
+curl -s "http://127.0.0.1:8000/admin/history/predictV3?periods=200&backtest=10" | grep -E "confidence|overall_confidence|confidence_scores|score_concentration"
+```
+
+预期结果:返回 JSON 中包含 confidence、overall_confidence、confidence_scores、score_concentration 字段。
+
+## Success Criteria
+
+1. `_calculateConfidence` 及 4 个辅助方法已实现
+2. 置信度维度重命名为:历史排名命中率、得分分布、得分集中度
+3. 加权公式在注释中明确:`confidence = 0.4*历史 + 0.3*分布 + 0.3*集中度`
+4. 添加数据量检查,不足50期时返回警告
+5. `getPredictionV3` 返回结构包含 confidence 字段
+6. 所有新增方法包含函数级注释
+
+## Output
+
+完成后创建 `.planning/phases/11-predictv3/11-02-SUMMARY.md`
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/11-03-PLAN.md b/.planning/phases/11-predictv3/11-03-PLAN.md
new file mode 100644
index 0000000..f1eaa5f
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-03-PLAN.md
@@ -0,0 +1,278 @@
+---
+phase: 11-predictv3
+plan: 03
+type: execute
+wave: 2
+depends_on:
+ - 11-01
+ - 11-02
+files_modified:
+ - public/assets/js/backend/history.js
+autonomous: true
+requirements:
+ - PRED-01
+ - PRED-02
+must_haves:
+ truths:
+ - "用户可以在预测弹窗中看到每个号码的置信度百分比"
+ - "用户可以在回测结果区域看到 NDCG@5 和 MRR 指标"
+ - "用户可以看到各排名位置的命中分布柱状图"
+ - "用户可以看到数据不足时的警告提示"
+ artifacts:
+ - path: "public/assets/js/backend/history.js"
+ provides: "置信度和新回测指标前端展示"
+ contains: "renderPredict|confidence|ndcg_5|mrr|hit_distribution|data_warning"
+ key_links:
+ - from: "renderPredict"
+ to: "backtest.ndcg_5, backtest.mrr, confidence, backtest.data_warning"
+ via: "property access in rendering logic"
+---
+
+# Phase 11 - Plan 03: 前端展示优化
+
+## Objective
+
+更新前端 `renderPredict` 方法,展示新增的置信度指标和扩展的回测指标(NDCG、MRR、命中分布、数据警告)。
+
+**Purpose:** 后端已计算置信度和新回测指标,前端需要将这些数据可视化呈现给用户,提升预测结果的可读性和决策辅助价值。
+
+**Output:** `history.js` 中的 `renderPredict` 方法扩展,新增置信度展示区域和回测指标扩展展示。
+
+## Tasks
+
+### Task 1: 添加置信度展示区域(含数据警告提示)
+
+
+- D:\code\php\amlhc\public\assets\js\backend\history.js (line 1700-1871, renderPredict 方法)
+
+
+
+在 `renderPredict` 方法中,找到以下变量声明位置(约 line 1701):
+
+```javascript
+var predictions = data.predictions || [];
+var analysis = data.analysis || {};
+var hitInfo = data.hit_info || null;
+var actualResult = data.actual_result || null;
+var backtest = data.backtest || null;
+```
+
+在 `backtest` 声明后添加 `confidence` 变量:
+
+```javascript
+var confidence = data.confidence || null;
+```
+
+然后在回测验证结果展示区域(约 line 1732-1751)之前,插入置信度展示区域:
+
+在 line 1732 之前(即 `// 回测验证结果` 注释之前)插入:
+
+```javascript
+// 置信度评估展示(V2和V3版本)
+if (confidence && (version === 'v2' || version === 'v3')) {
+ html += '';
+ html += '
预测置信度评估
';
+
+ // 数据警告提示(数据不足时显示)
+ if (confidence.data_warning) {
+ html += '
' + confidence.data_warning + '
';
+ }
+
+ html += '
';
+ html += '
' + confidence.overall_confidence + '%
整体置信度
';
+
+ // 各排名置信度(使用得分集中度维度)
+ if (confidence.confidence_scores && confidence.confidence_scores.length > 0) {
+ html += '
';
+ for (var i = 0; i < confidence.confidence_scores.length; i++) {
+ var cs = confidence.confidence_scores[i];
+ // 阈值定义:>=70%高(绿)、50-70%中(橙)、<50%低(红)
+ var confLevel = cs.confidence >= 70 ? '高' : (cs.confidence >= 50 ? '中' : '低');
+ var confColor = cs.confidence >= 70 ? '#4caf50' : (cs.confidence >= 50 ? '#ff9800' : '#f44336');
+ html += '
';
+ html += '
' + cs.confidence + '%
';
+ html += '
#' + cs.rank + '
';
+ html += '
';
+ }
+ html += '
';
+ }
+ html += '
';
+}
+```
+
+实现要点:
+- 整体置信度以大数字展示,各排名置信度以小卡片形式横向排列
+- 置信度分三级:高(>=70%, 绿色)、中(50-70%, 橙色)、低(<50%, 红色)
+- 只在 V2 和 V3 版本中显示
+- 新增 data_warning 展示:数据不足时显示红色警告提示
+
+
+
+- grep 匹配: `confidence.overall_confidence` 在 history.js renderPredict 方法中存在
+- grep 匹配: `confidence.confidence_scores` 在 history.js renderPredict 方法中存在
+- grep 匹配: `confidence.data_warning` 在 history.js renderPredict 方法中存在
+- grep 匹配: `confLevel` 和 `confColor` 变量在 history.js 中存在
+- 置信度展示区域在回测验证结果之前显示
+
+
+### Task 2: 扩展回测指标展示区域(含数据警告和命中分布柱状图)
+
+
+- D:\code\php\amlhc\public\assets\js\backend\history.js (line 1732-1751, 回测验证结果展示区域)
+
+
+
+在 `renderPredict` 方法中,找到回测验证结果展示区域(约 line 1732-1751),现有代码展示命中率、命中次数、平均排名三个指标。
+
+找到以下代码段:
+
+```javascript
+html += '';
+html += '
' + backtest.hit_rate + '%
命中率(Top5)
';
+html += '
' + backtest.total_hits + '/' + backtest.total_tests + '
命中次数
';
+html += '
' + (backtest.avg_rank || '—') + '
平均排名
';
+html += '
';
+```
+
+替换为:
+
+```javascript
+// 回测数据警告提示
+if (backtest.data_warning) {
+ html += ' ' + backtest.data_warning + '
';
+}
+
+html += '';
+html += '
' + backtest.hit_rate + '%
命中率(Top5)
';
+html += '
' + backtest.total_hits + '/' + backtest.total_tests + '
命中次数
';
+html += '
' + (backtest.avg_rank || '—') + '
平均排名
';
+
+// 新增指标:NDCG@5 和 MRR(百分比展示)
+if (backtest.ndcg_5 !== undefined) {
+ html += '
' + (backtest.ndcg_5 * 100).toFixed(1) + '%
NDCG@5
';
+}
+if (backtest.mrr !== undefined) {
+ html += '
' + (backtest.mrr * 100).toFixed(1) + '%
MRR
';
+}
+
+// 转移概率阶数显示(来自11-05的transition_order字段)
+if (analysis && analysis.transition_order !== undefined) {
+ html += '' + analysis.transition_order + '阶
转移概率
';
+}
+html += '';
+
+// 命中分布柱状图(使用rank_1..rank_5键名)
+if (backtest.hit_distribution && Object.keys(backtest.hit_distribution).length > 0) {
+ var distribution = backtest.hit_distribution;
+ var maxHit = 0;
+ // 找最大值用于计算柱状图高度比例
+ for (var r = 1; r <= 5; r++) {
+ var key = 'rank_' + r;
+ if (distribution[key] > maxHit) {
+ maxHit = distribution[key];
+ }
+ }
+
+ html += '命中分布(各排名命中次数):
';
+ html += '';
+ for (var r = 1; r <= 5; r++) {
+ var key = 'rank_' + r;
+ var hitCount = distribution[key] || 0;
+ var barHeight = maxHit > 0 ? (hitCount / maxHit * 45) : 0;
+ var barColor = hitCount > 0 ? '#4caf50' : '#e0e0e0';
+ html += '
';
+ html += '
';
+ html += '
#' + r + '
';
+ html += '
' + hitCount + '
';
+ html += '
';
+ }
+ html += '
';
+}
+```
+
+实现要点:
+- NDCG@5 和 MRR 以百分比形式展示(乘100后保留1位小数)
+- 命中分布以简单柱状图展示,排名1-5横向排列
+- 柱状图高度按最大命中次数比例计算
+- 使用 rank_1..rank_5 键名格式解析分布数据
+- 新增 data_warning 展示:回测数据不足时显示警告
+- 转移概率阶数从 analysis.transition_order 获取(而非 backtest)
+
+
+
+- grep 匹配: `backtest.ndcg_5` 在 history.js renderPredict 方法中存在
+- grep 匹配: `backtest.mrr` 在 history.js renderPredict 方法中存在
+- grep 匹配: `backtest.hit_distribution` 在 history.js renderPredict 方法中存在
+- grep 匹配: `backtest.data_warning` 在 history.js renderPredict 方法中存在
+- grep 匹配: `rank_1|rank_2|rank_3|rank_4|rank_5` 在命中分布解析中存在
+- grep 匹配: `analysis.transition_order` 在 history.js renderPredict 方法中存在
+- 命中分布柱状图使用 div 元素实现
+
+
+### Task 3: 在预测号码卡片中显示置信度(含得分集中度展示)
+
+
+- D:\code\php\amlhc\public\assets\js\backend\history.js (line 1828-1866, 预测号码列表渲染)
+
+
+
+在预测号码列表渲染区域(约 line 1828-1866),找到号码卡片渲染代码:
+
+在现有得分显示后添加置信度显示。找到以下代码段:
+
+```javascript
+html += '得分:' + p.score + '
';
+```
+
+替换为:
+
+```javascript
+html += '得分:' + p.score + '
';
+
+// 显示置信度(V3版本)
+if (version === 'v3' && confidence && confidence.confidence_scores) {
+ var csForNum = confidence.confidence_scores.find(function(c) { return c.num === p.num; });
+ if (csForNum) {
+ // 阈值定义:>=70%高(绿)、50-70%中(橙)、<50%低(红)
+ var confLevel = csForNum.confidence >= 70 ? '高' : (csForNum.confidence >= 50 ? '中' : '低');
+ var confColor = csForNum.confidence >= 70 ? '#4caf50' : (csForNum.confidence >= 50 ? '#ff9800' : '#f44336');
+ html += '置信度:' + confLevel + ' (' + csForNum.confidence + '%)
';
+ }
+}
+```
+
+实现要点:
+- 在号码卡片中显示置信度等级(高/中/低)和具体百分比
+- 使用与整体置信度展示相同的颜色映射阈值
+- 只在 V3 版本中显示
+- 置信度字段使用 score_concentration(得分集中度)维度
+
+
+
+- grep 匹配: `csForNum` 变量在 history.js 中存在
+- grep 匹配: `置信度:` 在号码卡片渲染代码中存在
+- 置信度显示在得分下方
+- grep 匹配: `csForNum.confidence` 在号码卡片渲染中存在
+
+
+## Verification
+
+1. 打开 history 页面,点击"智能预测"按钮
+2. 使用 V3 版本执行预测,验证:
+ - 置信度评估区域是否显示
+ - 数据不足时是否显示警告提示
+ - 回测结果中是否显示 NDCG@5、MRR、命中分布柱状图
+ - 预测号码卡片中是否显示置信度等级和百分比
+
+## Success Criteria
+
+1. `renderPredict` 方法已更新,新增置信度展示区域
+2. 回测结果展示区域已扩展,包含 NDCG@5、MRR、命中分布、数据警告
+3. 预测号码卡片显示置信度等级和百分比
+4. 命中分布使用 rank_1..rank_5 键名格式解析
+5. 所有新增展示样式与原有 UI 风格一致
+6. 数据警告以红色背景突出显示
+
+## Output
+
+完成后创建 `.planning/phases/11-predictv3/11-03-SUMMARY.md`
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/11-04-PLAN.md b/.planning/phases/11-predictv3/11-04-PLAN.md
new file mode 100644
index 0000000..08a855f
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-04-PLAN.md
@@ -0,0 +1,346 @@
+---
+phase: 11-predictv3
+plan: 04
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - application/admin/model/History.php
+ - application/admin/controller/History.php
+autonomous: true
+requirements:
+ - PRED-04
+must_haves:
+ truths:
+ - "用户可以通过接口获取最优权重配置"
+ - "系统返回基于历史回测的权重优化结果"
+ - "优化结果包含各权重配置的命中率、NDCG评估"
+ - "网格搜索有超时保护机制"
+ artifacts:
+ - path: "application/admin/model/History.php"
+ provides: "权重网格搜索优化方法"
+ contains: "_optimizeWeightsGridSearch"
+ - path: "application/admin/controller/History.php"
+ provides: "权重优化接口入口"
+ contains: "optimizeWeights"
+ key_links:
+ - from: "optimizeWeights controller"
+ to: "_optimizeWeightsGridSearch model"
+ via: "method call"
+---
+
+# Phase 11 - Plan 04: 权重网格搜索优化
+
+## Objective
+
+实现权重网格搜索优化功能,通过预定义权重组合批量回测,找出最优权重配置,提升算法预测准确性。
+
+**Purpose:** 当前权重为手动配置,缺乏数据驱动优化。网格搜索是一种成熟的参数优化方法,能基于历史回测数据找到更优权重组合。
+
+**Output:** `History.php` 新增 `_optimizeWeightsGridSearch` 方法,`History.php` controller 新增 `optimizeWeights` 接口入口。
+
+## Tasks
+
+### Task 1: 实现权重网格搜索方法(含5种具体配置和超时保护)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 2094-2113, 默认权重配置)
+- D:\code\php\amlhc\application\admin\model\History.php (line 3495-3556, _runBacktestV3 方法)
+
+
+
+在 `History.php` 类末尾新增权重网格搜索优化方法:
+
+```php
+/**
+ * 权重网格搜索优化
+ *
+ * 优化目标定义:
+ * - 综合评估得分 = hit_rate * 0.6 + ndcg_5 * 100 * 0.4
+ * - 命中率权重60%,NDCG权重40%
+ * - 返回综合得分最高的权重配置
+ *
+ * 5种预定义权重配置:
+ * - 配置1: 遗漏优先型 - omit_regression权重最高(0.25)
+ * - 配置2: 转移概率优先型 - transition_prob权重最高(0.25)
+ * - 配置3: 走势方向优先型 - trend_direction权重最高(0.25)
+ * - 配置4: 平衡型 - 各维度权重较均衡
+ * - 配置5: 组合特征优先型 - combination权重最高(0.20)
+ *
+ * @param int $periods 统计期数,范围50-500
+ * @param int $backtestCount 回测期数,范围10-100
+ * @param int $timeoutSeconds 超时限制秒数,默认60秒
+ * @return array {best_weights: [], best_hit_rate: float, best_ndcg: float, all_results: [], timed_out: bool}
+ */
+private function _optimizeWeightsGridSearch($periods = 200, $backtestCount = 50, $timeoutSeconds = 60)
+{
+ // 超时保护:记录开始时间
+ $startTime = microtime(true);
+ $timedOut = false;
+
+ // 5种预定义权重配置(具体权重值明确)
+ $weightConfigs = [
+ // 配置1: 遗漏优先型 - 遗漏回归权重最高
+ [
+ 'omit_regression' => 0.25, // 遗漏回归权重25%
+ 'freq_regression' => 0.12, // 频率回归权重12%
+ 'transition_prob' => 0.15, // 转移概率权重15%
+ 'trend_direction' => 0.12, // 走势方向权重12%
+ 'oddeven_balance' => 0.08, // 单双平衡权重8%
+ 'bigsmall_balance' => 0.08, // 大小平衡权重8%
+ 'zone_balance' => 0.05, // 区域平衡权重5%
+ 'color_balance' => 0.05, // 波色平衡权重5%
+ 'combination' => 0.10 // 组合特征权重10%
+ ],
+ // 配置2: 转移概率优先型 - 转移概率权重最高
+ [
+ 'omit_regression' => 0.15,
+ 'freq_regression' => 0.10,
+ 'transition_prob' => 0.25, // 转移概率权重25%(最高)
+ 'trend_direction' => 0.12,
+ 'oddeven_balance' => 0.08,
+ 'bigsmall_balance' => 0.08,
+ 'zone_balance' => 0.04,
+ 'color_balance' => 0.04,
+ 'combination' => 0.14
+ ],
+ // 配置3: 走势方向优先型 - 走势方向权重最高
+ [
+ 'omit_regression' => 0.12,
+ 'freq_regression' => 0.10,
+ 'transition_prob' => 0.15,
+ 'trend_direction' => 0.25, // 走势方向权重25%(最高)
+ 'oddeven_balance' => 0.08,
+ 'bigsmall_balance' => 0.08,
+ 'zone_balance' => 0.04,
+ 'color_balance' => 0.04,
+ 'combination' => 0.12
+ ],
+ // 配置4: 平衡型(默认配置)- 各维度权重较均衡
+ [
+ 'omit_regression' => 0.18,
+ 'freq_regression' => 0.12,
+ 'transition_prob' => 0.18,
+ 'trend_direction' => 0.14,
+ 'oddeven_balance' => 0.08,
+ 'bigsmall_balance' => 0.08,
+ 'zone_balance' => 0.04,
+ 'color_balance' => 0.04,
+ 'combination' => 0.10
+ ],
+ // 配置5: 组合特征优先型 - 组合特征权重最高
+ [
+ 'omit_regression' => 0.15,
+ 'freq_regression' => 0.10,
+ 'transition_prob' => 0.15,
+ 'trend_direction' => 0.12,
+ 'oddeven_balance' => 0.06,
+ 'bigsmall_balance' => 0.06,
+ 'zone_balance' => 0.03,
+ 'color_balance' => 0.03,
+ 'combination' => 0.20 // 组合特征权重20%(最高)
+ ]
+ ];
+
+ $bestWeights = [];
+ $bestHitRate = 0;
+ $bestNdcg = 0;
+ $bestCombinedScore = 0;
+ $allResults = [];
+
+ // 执行每种配置的回测(添加超时检查)
+ foreach ($weightConfigs as $configIdx => $weights) {
+ // 超时检查:超过限制时间则停止
+ $elapsedTime = microtime(true) - $startTime;
+ if ($elapsedTime > $timeoutSeconds) {
+ $timedOut = true;
+ break;
+ }
+
+ // 执行回测
+ $backtest = $this->_runBacktestV3($periods, $weights, $backtestCount);
+
+ $hitRate = $backtest['hit_rate'] ?? 0;
+ $ndcg = $backtest['ndcg_5'] ?? 0;
+ $avgRank = $backtest['avg_rank'] ?? 0;
+ $mrr = $backtest['mrr'] ?? 0;
+
+ // 综合评估得分:命中率60% + NDCG40%
+ $combinedScore = $hitRate * 0.6 + $ndcg * 100 * 0.4;
+
+ $result = [
+ 'config_name' => $configIdx + 1,
+ 'config_type' => ['遗漏优先型', '转移概率优先型', '走势方向优先型', '平衡型', '组合特征优先型'][$configIdx],
+ 'weights' => $weights,
+ 'hit_rate' => $hitRate,
+ 'avg_rank' => $avgRank,
+ 'ndcg_5' => $ndcg,
+ 'mrr' => $mrr,
+ 'combined_score' => round($combinedScore, 2),
+ 'total_hits' => $backtest['total_hits'] ?? 0
+ ];
+
+ $allResults[] = $result;
+
+ // 更新最优配置
+ if ($combinedScore > $bestCombinedScore) {
+ $bestCombinedScore = $combinedScore;
+ $bestHitRate = $hitRate;
+ $bestNdcg = $ndcg;
+ $bestWeights = $weights;
+ }
+ }
+
+ // 按综合得分降序排序结果
+ usort($allResults, function($a, $b) {
+ return $b['combined_score'] - $a['combined_score'];
+ });
+
+ return [
+ 'best_weights' => $bestWeights,
+ 'best_hit_rate' => $bestHitRate,
+ 'best_ndcg' => $bestNdcg,
+ 'best_combined_score' => round($bestCombinedScore, 2),
+ 'all_results' => $allResults,
+ 'periods' => $periods,
+ 'backtest_count' => $backtestCount,
+ 'timeout_seconds' => $timeoutSeconds,
+ 'timed_out' => $timedOut,
+ 'elapsed_time' => round(microtime(true) - $startTime, 2)
+ ];
+}
+```
+
+实现要点:
+- **5种具体权重配置**:每种配置的权重值在代码中明确列出
+- **优化目标明确**:综合得分 = hit_rate*0.6 + ndcg_5*100*0.4
+- **超时保护**:添加 $timeoutSeconds 参数,默认60秒,超时后停止剩余配置测试
+- **配置类型命名**:遗漏优先型、转移概率优先型、走势方向优先型、平衡型、组合特征优先型
+- 返回 timed_out 标志和 elapsed_time 供前端判断
+
+
+
+- grep 正则匹配: `_optimizeWeightsGridSearch\s*\(` 在 History.php 中存在
+- grep 匹配: `$weightConfigs` 数组包含5个配置项
+- grep 匹配: `$timeoutSeconds` 参数在方法签名中存在
+- grep 匹配: `$timedOut` 变量在方法中存在
+- grep 匹配: `combined_score` 在返回结果中存在
+- grep 匹配: `config_type` 在结果中存在
+- 方法调用 `_runBacktestV3` 进行回测
+- 方法包含函数级注释说明优化目标和配置类型
+
+
+### Task 2: 新增权重优化接口入口(含参数验证和超时警告)
+
+
+- D:\code\php\amlhc\application\admin\controller\History.php (line 25, noNeedRight 数组)
+- D:\code\php\amlhc\application\admin\controller\History.php (line 460-489, predictV3 方法)
+
+
+
+在 `History.php` controller 中:
+
+1. 将 `optimizeWeights` 添加到 `noNeedRight` 数组(约 line 25):
+
+找到:
+```php
+protected $noNeedRight = ['missingNum', 'trendData', ..., 'predictV3'];
+```
+
+在 `predictV3` 后添加 `'optimizeWeights'`:
+
+```php
+protected $noNeedRight = ['missingNum', 'trendData', ..., 'predictV3', 'optimizeWeights'];
+```
+
+2. 在 `predictV3` 方法后(约 line 489 之后)新增 `optimizeWeights` 方法:
+
+```php
+/**
+ * 权重网格搜索优化接口
+ * 执行多权重配置回测,返回最优权重组合
+ *
+ * 参数说明:
+ * - periods: 统计期数,范围50-500,默认200
+ * - backtest: 回测期数,范围10-100,默认30
+ * - timeout: 超时秒数,范围10-120,默认60
+ *
+ * 返回说明:
+ * - best_weights: 最优权重配置
+ * - best_hit_rate: 最优配置命中率
+ * - all_results: 所有配置测试结果
+ * - timed_out: 是否超时中断
+ */
+public function optimizeWeights()
+{
+ if ($this->request->isAjax()) {
+ // 参数验证
+ $periods = $this->request->get('periods', 200, 'intval');
+ if ($periods < 50 || $periods > 500) {
+ $this->error('期数范围必须在 50-500 之间');
+ }
+
+ $backtestCount = $this->request->get('backtest', 30, 'intval');
+ if ($backtestCount < 10 || $backtestCount > 100) {
+ $backtestCount = 30; // 使用默认值而非报错
+ }
+
+ $timeoutSeconds = $this->request->get('timeout', 60, 'intval');
+ if ($timeoutSeconds < 10 || $timeoutSeconds > 120) {
+ $timeoutSeconds = 60; // 使用默认值
+ }
+
+ // 执行优化
+ $result = $this->model->_optimizeWeightsGridSearch($periods, $backtestCount, $timeoutSeconds);
+
+ // 超时警告提示
+ $message = '优化完成';
+ if ($result['timed_out']) {
+ $message = '优化超时中断,已完成' . count($result['all_results']) . '种配置测试';
+ }
+
+ $this->success($message, null, $result);
+ }
+}
+```
+
+实现要点:
+- 接口参数:periods(统计期数)、backtest(回测期数)、timeout(超时秒数)
+- 参数范围验证,超出范围时使用默认值或报错
+- 返回最优权重配置及所有测试结果
+- 超时时返回警告消息和已完成的测试数量
+- 需添加到 noNeedRight 允许无权限访问
+
+
+
+- grep 匹配: `optimizeWeights` 在 noNeedRight 数组中存在
+- grep 正则匹配: `public function optimizeWeights\s*\(` 在 controller 中存在
+- grep 匹配: `$timeoutSeconds` 在方法中存在
+- grep 匹配: `$result['timed_out']` 在方法中存在
+- 方法调用 `$this->model->_optimizeWeightsGridSearch`
+- 方法包含函数级注释
+
+
+## Verification
+
+执行权重优化接口验证返回结果:
+
+```bash
+curl -s "http://127.0.0.1:8000/admin/history/optimizeWeights?periods=200&backtest=20&timeout=60" | grep -E "best_weights|best_hit_rate|best_ndcg|all_results|timed_out|config_type"
+```
+
+预期结果:返回 JSON 中包含 best_weights、best_hit_rate、best_ndcg、all_results、timed_out、config_type 字段。
+
+## Success Criteria
+
+1. `_optimizeWeightsGridSearch` 方法已实现,包含 5 种预定义权重配置(权重值明确)
+2. 优化目标明确:综合得分 = hit_rate*0.6 + ndcg_5*100*0.4
+3. 超时保护机制已添加,默认60秒
+4. `optimizeWeights` controller 接口已实现,含参数验证
+5. 接口能正常返回最优权重配置及测试结果
+6. 超时时返回警告消息
+7. 所有新增方法包含函数级注释
+
+## Output
+
+完成后创建 `.planning/phases/11-predictv3/11-04-SUMMARY.md`
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/11-05-PLAN.md b/.planning/phases/11-predictv3/11-05-PLAN.md
new file mode 100644
index 0000000..479ccd4
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-05-PLAN.md
@@ -0,0 +1,455 @@
+---
+phase: 11-predictv3
+plan: 05
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - application/admin/model/History.php
+autonomous: true
+requirements:
+ - PRED-03
+must_haves:
+ truths:
+ - "转移概率计算考虑前两期状态联合决定"
+ - "系统在数据充足时使用二阶马尔可夫,数据不足时回退一阶"
+ - "预测结果中显示使用的转移概率阶数"
+ - "二阶马尔可夫有状态对观察次数检查,不足时回退一阶"
+ artifacts:
+ - path: "application/admin/model/History.php"
+ provides: "二阶马尔可夫转移矩阵构建方法"
+ contains: "_getTransitionMatrix2ndOrder|_calcTransitionScore2ndOrder"
+ key_links:
+ - from: "getPredictionV3"
+ to: "_getTransitionMatrix2ndOrder"
+ via: "conditional call based on data availability and state pair count"
+---
+
+# Phase 11 - Plan 05: 二阶马尔可夫转移概率增强
+
+## Objective
+
+改进现有一阶马尔可夫链转移概率计算,新增二阶马尔可夫链实现。考虑前两期状态联合决定当前转移概率,提升转移概率预测准确性。
+
+**Purpose:** 现有 `_getTransitionMatrix` 仅考虑上一期状态,预测信息有限。二阶马尔可夫链利用更长历史序列,理论上预测更精准。
+
+**Important:** 本计划是独立功能增强,不依赖其他计划。可独立执行。
+
+**Output:** `History.php` 新增 `_getTransitionMatrix2ndOrder` 和 `_calcTransitionScore2ndOrder` 方法,`getPredictionV3` 中根据数据量和状态对观察次数选择使用一阶或二阶转移概率。
+
+## Tasks
+
+### Task 1: 实现二阶马尔可夫转移矩阵构建方法(含状态对观察次数检查)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 2468-2493, _getTransitionMatrix 方法)
+- D:\code\php\amlhc\application\admin\model\History.php (line 2452-2460, _getHeadIdx 方法)
+
+
+
+在 `History.php` 类末尾新增二阶马尔可夫转移矩阵构建方法:
+
+```php
+/**
+ * 构建二阶马尔可夫转移矩阵
+ * 考虑前两期状态联合决定当前转移概率
+ *
+ * 状态空间说明:
+ * - 一阶马尔可夫: N个状态 (zone:5, tail:10, head:5)
+ * - 二阶马尔可夫: N^2个状态对 (zone:25, tail:100, head:25)
+ * - 状态键格式: "prev1-prev2",如 "2-3" 表示前一期区域2、前两期区域3
+ *
+ * 数据量阈值说明:
+ * - 建议历史数据 >= 200期以获得稳定的二阶概率估计
+ * - 状态对观察次数 >= 5 才使用该状态对的二阶概率
+ * - 观察次数不足时返回 state_pair_insufficient 标志,供调用者回退一阶
+ *
+ * @param array $history 历史数据(降序,最新在前)
+ * @param string $type 类型:zone/tail/head
+ * @param int $minStatePairCount 状态对最小观察次数,默认5
+ * @return array {matrix: [], prob_matrix: [], state_totals: [], num_categories: int, sufficient_pairs: int, total_pairs: int, min_threshold: int}
+ */
+private function _getTransitionMatrix2ndOrder($history, $type, $minStatePairCount = 5)
+{
+ // 升序排列(从旧到新)
+ $historyAsc = array_reverse($history);
+
+ // 确定类别数量和索引函数
+ switch ($type) {
+ case 'zone':
+ $numCategories = 5;
+ $getIdx = function ($num) {
+ if ($num <= 10) return 0;
+ if ($num <= 20) return 1;
+ if ($num <= 30) return 2;
+ if ($num <= 40) return 3;
+ return 4;
+ };
+ break;
+ case 'tail':
+ $numCategories = 10;
+ $getIdx = function ($num) { return $num % 10; };
+ break;
+ case 'head':
+ $numCategories = 5;
+ $getIdx = function ($num) {
+ if ($num <= 9) return 0;
+ if ($num <= 19) return 1;
+ if ($num <= 29) return 2;
+ if ($num <= 39) return 3;
+ return 4;
+ };
+ break;
+ default:
+ return [
+ 'matrix' => [],
+ 'prob_matrix' => [],
+ 'state_totals' => [],
+ 'num_categories' => 0,
+ 'sufficient_pairs' => 0,
+ 'total_pairs' => 0,
+ 'min_threshold' => $minStatePairCount
+ ];
+ }
+
+ // 状态空间: (prev1, prev2) -> current,共 numCategories^2 个前置状态
+ $matrix = [];
+ $stateTotals = [];
+
+ // 初始化矩阵结构
+ for ($i = 0; $i < $numCategories; $i++) {
+ for ($j = 0; $j < $numCategories; $j++) {
+ $stateKey = $i . '-' . $j;
+ $matrix[$stateKey] = array_fill(0, $numCategories, 0);
+ $stateTotals[$stateKey] = 0;
+ }
+ }
+
+ // 统计二阶转移
+ for ($i = 0; $i < count($historyAsc) - 2; $i++) {
+ $prev1 = $getIdx((int)$historyAsc[$i]['num7']);
+ $prev2 = $getIdx((int)$historyAsc[$i + 1]['num7']);
+ $current = $getIdx((int)$historyAsc[$i + 2]['num7']);
+
+ if ($prev1 < 0 || $prev2 < 0 || $current < 0) continue;
+
+ $stateKey = $prev1 . '-' . $prev2;
+ $matrix[$stateKey][$current]++;
+ $stateTotals[$stateKey]++;
+ }
+
+ // 统计充分观察的状态对数量(观察次数 >= minStatePairCount)
+ $sufficientPairs = 0;
+ $totalPairs = $numCategories * $numCategories;
+ foreach ($stateTotals as $stateKey => $count) {
+ if ($count >= $minStatePairCount) {
+ $sufficientPairs++;
+ }
+ }
+
+ // 拉普拉斯平滑处理
+ $probMatrix = [];
+ foreach ($matrix as $stateKey => $counts) {
+ $smoothTotal = $stateTotals[$stateKey] + $numCategories;
+ $probMatrix[$stateKey] = [];
+ for ($j = 0; $j < $numCategories; $j++) {
+ $probMatrix[$stateKey][$j] = ($counts[$j] + 1) / $smoothTotal;
+ }
+ }
+
+ return [
+ 'matrix' => $matrix,
+ 'prob_matrix' => $probMatrix,
+ 'state_totals' => $stateTotals,
+ 'num_categories' => $numCategories,
+ 'sufficient_pairs' => $sufficientPairs,
+ 'total_pairs' => $totalPairs,
+ 'min_threshold' => $minStatePairCount
+ ];
+}
+```
+
+实现要点:
+- 状态空间从 N 扩展到 N^2(zone: 25状态,tail: 100状态,head: 25状态)
+- 使用拉普拉斯平滑处理避免零概率问题
+- 状态键格式为 "prev1-prev2"
+- **新增状态对观察次数检查**:统计 sufficient_pairs(观察>=5次的状态对数量)
+- 返回 sufficient_pairs、total_pairs、min_threshold 供调用者判断是否足够稳定
+
+
+
+- grep 正则匹配: `_getTransitionMatrix2ndOrder\s*\(` 在 History.php 中存在
+- grep 匹配: `$minStatePairCount` 参数在方法签名中存在
+- grep 匹配: `sufficient_pairs` 在返回结构中存在
+- grep 匹配: `total_pairs` 在返回结构中存在
+- 方法包含 stateKey 变量(格式为 prev1-prev2)
+- 方法包含函数级注释,说明状态空间和数据量阈值
+
+
+### Task 2: 实现二阶转移概率得分计算方法
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (新增的 _getTransitionMatrix2ndOrder 方法)
+- D:\code\php\amlhc\application\admin\model\History.php (查找 _calcTransitionScore 方法位置)
+
+
+
+使用 Grep 找到 `_calcTransitionScore` 方法位置后,在其附近新增二阶转移概率得分计算方法:
+
+```bash
+grep -n "_calcTransitionScore" application/admin/model/History.php
+```
+
+新增 `_calcTransitionScore2ndOrder` 方法:
+
+```php
+/**
+ * 计算二阶转移概率得分
+ *
+ * 计算方法:
+ * - 综合区域、尾号、首号三个维度的二阶转移概率
+ * - 各维度权重: 区域40%、尾号35%、首号25%
+ * - 得分范围: 0-100
+ *
+ * @param int $num 当前号码
+ * @param int $prev1Zone 前一期区域索引
+ * @param int $prev2Zone 前两期区域索引
+ * @param int $prev1Tail 前一期尾号索引
+ * @param int $prev2Tail 前两期尾号索引
+ * @param int $prev1Head 前一期首号索引
+ * @param int $prev2Head 前两期首号索引
+ * @param array $zoneTrans2nd 二阶区域转移矩阵
+ * @param array $tailTrans2nd 二阶尾号转移矩阵
+ * @param array $headTrans2nd 二阶首号转移矩阵
+ * @param array $zoneMap 号码区域映射
+ * @param array $tailMap 号码尾号映射
+ * @param array $headMap 号码首号映射
+ * @return float 综合转移得分 (0-100)
+ */
+private function _calcTransitionScore2ndOrder(
+ $num,
+ $prev1Zone, $prev2Zone,
+ $prev1Tail, $prev2Tail,
+ $prev1Head, $prev2Head,
+ $zoneTrans2nd, $tailTrans2nd, $headTrans2nd,
+ $zoneMap, $tailMap, $headMap
+)
+{
+ $zone = $zoneMap[$num];
+ $tail = $tailMap[$num];
+ $head = $headMap[$num];
+
+ $score = 0;
+
+ // 区域二阶转移得分(权重40%)
+ $zoneStateKey = $prev1Zone . '-' . $prev2Zone;
+ if (isset($zoneTrans2nd['prob_matrix'][$zoneStateKey][$zone])) {
+ $prob = $zoneTrans2nd['prob_matrix'][$zoneStateKey][$zone];
+ $score += $prob * 40;
+ }
+
+ // 尾号二阶转移得分(权重35%)
+ $tailStateKey = $prev1Tail . '-' . $prev2Tail;
+ if (isset($tailTrans2nd['prob_matrix'][$tailStateKey][$tail])) {
+ $prob = $tailTrans2nd['prob_matrix'][$tailStateKey][$tail];
+ $score += $prob * 35;
+ }
+
+ // 首号二阶转移得分(权重25%)
+ $headStateKey = $prev1Head . '-' . $prev2Head;
+ if (isset($headTrans2nd['prob_matrix'][$headStateKey][$head])) {
+ $prob = $headTrans2nd['prob_matrix'][$headStateKey][$head];
+ $score += $prob * 25;
+ }
+
+ return round($score, 2);
+}
+```
+
+实现要点:
+- 综合区域、尾号、首号三个维度
+- 各维度权重:区域40%、尾号35%、首号25%
+- 使用 prob_matrix 中对应状态键的概率值
+
+
+
+- grep 正则匹配: `_calcTransitionScore2ndOrder\s*\(` 在 History.php 中存在
+- 方法参数包含 prev1Zone、prev2Zone 等二阶状态参数
+- 方法包含 zoneStateKey、tailStateKey、headStateKey 变量
+- 方法包含函数级注释说明权重分配
+
+
+### Task 3: 在 getPredictionV3 中集成二阶马尔可夫(含200期阈值和状态对检查)
+
+
+- D:\code\php\amlhc\application\admin\model\History.php (line 2230-2239, 转移概率分析部分)
+- D:\code\php\amlhc\application\admin\model\History.php (line 2159-2161, 历史数据量检查)
+
+
+
+在 `getPredictionV3` 方法中修改转移概率分析部分(约 line 2230-2239):
+
+1. 找到以下代码段:
+```php
+// ====== 3. 转移概率分析(新增)======
+// 获取转移概率矩阵数据
+$zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone');
+$tailTransition = $this->_getTransitionMatrix($allHistory, 'tail');
+$headTransition = $this->_getTransitionMatrix($allHistory, 'head');
+
+// 上期号码的各类属性
+$lastZone = $this->_getZoneIdx($lastSpecial);
+$lastTail = $lastSpecial % 10;
+$lastHead = $this->_getHeadIdx($lastSpecial);
+```
+
+替换为:
+```php
+// ====== 3. 转移概率分析 ======
+// 根据历史数据量决定使用一阶或二阶马尔可夫
+// 阈值条件:总期数 >= 200 且 状态对观察次数充足(>=5次的比例>=30%)
+$minPeriodsThreshold = 200; // 二阶马尔可夫最小历史期数阈值(从100提升到200)
+$minStatePairCount = 5; // 状态对最小观察次数
+$use2ndOrder = false;
+$secondOrderAvailable = false;
+
+// 获取一阶转移概率矩阵(始终计算,作为fallback)
+$zoneTransition = $this->_getTransitionMatrix($allHistory, 'zone');
+$tailTransition = $this->_getTransitionMatrix($allHistory, 'tail');
+$headTransition = $this->_getTransitionMatrix($allHistory, 'head');
+
+// 获取二阶转移概率矩阵(数据充足时)
+$zoneTransition2nd = null;
+$tailTransition2nd = null;
+$headTransition2nd = null;
+$prev2Zone = 0;
+$prev2Tail = 0;
+$prev2Head = 0;
+
+if (count($allHistory) >= $minPeriodsThreshold && count($allHistory) >= 2) {
+ // 获取前两期号码属性
+ $prev2Special = (int)$allHistory[1]['num7'];
+ $prev2Zone = $this->_getZoneIdx($prev2Special);
+ $prev2Tail = $prev2Special % 10;
+ $prev2Head = $this->_getHeadIdx($prev2Special);
+
+ // 构建二阶转移矩阵
+ $zoneTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'zone', $minStatePairCount);
+ $tailTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'tail', $minStatePairCount);
+ $headTransition2nd = $this->_getTransitionMatrix2ndOrder($allHistory, 'head', $minStatePairCount);
+
+ // 检查状态对观察次数是否充足(至少30%的状态对有足够观察)
+ // tail类型状态空间最大(100),以tail为基准判断
+ if ($tailTransition2nd['total_pairs'] > 0) {
+ $sufficientRatio = $tailTransition2nd['sufficient_pairs'] / $tailTransition2nd['total_pairs'];
+ $secondOrderAvailable = $sufficientRatio >= 0.3; // 至少30%状态对观察>=5次
+ }
+
+ $use2ndOrder = $secondOrderAvailable;
+}
+
+// 上期号码的各类属性
+$lastZone = $this->_getZoneIdx($lastSpecial);
+$lastTail = $lastSpecial % 10;
+$lastHead = $this->_getHeadIdx($lastSpecial);
+```
+
+2. 在 analysis 数组中添加转移阶数信息(约 line 2297-2317):
+
+找到:
+```php
+$analysis = [
+ 'last_special' => $lastSpecial,
+ 'last_expect' => $lastExpect,
+ 'weights' => $weights,
+ ...
+];
+```
+
+在 `trend_direction` 后添加:
+```php
+$analysis = [
+ ...
+ 'trend_direction' => $trendDirection,
+ 'transition_order' => $use2ndOrder ? 2 : 1, // 新增:转移概率阶数
+ 'transition_available' => $secondOrderAvailable, // 二阶是否可用
+ 'history_count' => count($allHistory), // 历史期数
+ 'min_periods_threshold' => $minPeriodsThreshold, // 阈值
+ 'last_zone' => $zoneLabels[$lastZone] ?? '',
+ ...
+];
+```
+
+3. 在得分计算循环中(约 line 2342-2349)修改转移概率得分计算:
+
+找到:
+```php
+// === 转移概率得分 ===
+$transScore = $this->_calcTransitionScore(
+ $num, $lastZone, $lastTail, $lastHead,
+ $zoneTransition, $tailTransition, $headTransition,
+ $zoneMap, $tailMap, $headMap
+);
+```
+
+替换为:
+```php
+// === 转移概率得分(根据阶数选择计算方法)===
+if ($use2ndOrder && $zoneTransition2nd && $tailTransition2nd && $headTransition2nd) {
+ $transScore = $this->_calcTransitionScore2ndOrder(
+ $num, $lastZone, $prev2Zone, $lastTail, $prev2Tail, $lastHead, $prev2Head,
+ $zoneTransition2nd, $tailTransition2nd, $headTransition2nd,
+ $zoneMap, $tailMap, $headMap
+ );
+ $detail['trans_order'] = 2;
+} else {
+ $transScore = $this->_calcTransitionScore(
+ $num, $lastZone, $lastTail, $lastHead,
+ $zoneTransition, $tailTransition, $headTransition,
+ $zoneMap, $tailMap, $headMap
+ );
+ $detail['trans_order'] = 1;
+}
+```
+
+
+
+- grep 匹配: `minPeriodsThreshold` 变量在 getPredictionV3 中存在(值为200)
+- grep 匹配: `minStatePairCount` 变量在 getPredictionV3 中存在(值为5)
+- grep 匹配: `$secondOrderAvailable` 变量在 getPredictionV3 中存在
+- grep 匹配: `sufficientRatio` 在 getPredictionV3 中存在(状态对观察比例)
+- grep 匹配: `_getTransitionMatrix2ndOrder` 在 getPredictionV3 中被调用
+- grep 匹配: `transition_order` 在 analysis 数组中存在
+- grep 匹配: `transition_available` 在 analysis 数组中存在
+- grep 匹配: `_calcTransitionScore2ndOrder` 在得分计算中被调用
+- 数据量阈值设置为 200 期(而非原100期)
+- 状态对观察次数检查 >= 5,比例 >= 30%
+
+
+## Verification
+
+执行预测接口验证二阶马尔可夫使用情况:
+
+```bash
+curl -s "http://127.0.0.1:8000/admin/history/predictV3?periods=300&backtest=10" | grep -E "transition_order|transition_available|history_count"
+```
+
+预期结果:
+- periods >= 200 且状态对观察充足时,返回 transition_order: 2
+- periods < 200 或状态对观察不足时,返回 transition_order: 1
+- transition_available 显示二阶是否可用
+
+## Success Criteria
+
+1. `_getTransitionMatrix2ndOrder` 方法已实现,包含二阶状态空间构建
+2. `_calcTransitionScore2ndOrder` 方法已实现
+3. `getPredictionV3` 根据数据量和状态对观察次数自动选择一阶或二阶马尔可夫
+4. 数据量阈值提升到 200 期(而非原100期)
+5. 状态对观察次数检查 >= 5,比例 >= 30% 才使用二阶
+6. analysis 返回中包含 transition_order、transition_available 字段
+7. 所有新增方法包含函数级注释
+8. depends_on 已修正为空数组(独立功能)
+
+## Output
+
+完成后创建 `.planning/phases/11-predictv3/11-05-SUMMARY.md`
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/11-RESEARCH.md b/.planning/phases/11-predictv3/11-RESEARCH.md
index 8bd1139..7399302 100644
--- a/.planning/phases/11-predictv3/11-RESEARCH.md
+++ b/.planning/phases/11-predictv3/11-RESEARCH.md
@@ -844,25 +844,29 @@ foreach ($weights as $key => $value) {
---
-## Open Questions
+## Open Questions (RESOLVED)
1. **历史数据量是否足够支撑高级优化?**
- 当前默认200期统计,二阶马尔可夫和关联规则挖掘建议500期+
- 需检查数据库中实际可用的历史期数
- 推荐: 查询 `SELECT COUNT(*) FROM fa_history` 确认数据量
+ - **Resolution:** 11-05 Task 3 设置100期阈值,数据不足时回退一阶马尔可夫,已在plan中处理
2. **权重优化结果如何持久化?**
- 选项A: 存储到 `application/extra/predict.php` 配置文件
- 选项B: 存储到数据库配置表
- 选项C: 每次预测时动态计算(性能成本高)
+ - **Resolution:** 11-04 采用选项C(动态计算)+ 返回结果给前端展示,不持久化。设计决策:避免过拟合特定时间段,每次获取最新优化结果
3. **置信度阈值如何定义?**
- 当前假设: >=70%为高,50-70%为中,<50%为低
- 需根据实际回测数据调整阈值
+ - **Resolution:** 11-02 Task 1 明确阈值定义:>=70%高(绿色)、50-70%中(橙色)、<50%低(红色),前端11-03使用相同映射
4. **前端如何展示新增的回测指标(NDCG、MRR)?**
- 需设计用户友好的展示方式
- 可考虑简化为"预测质量评分"单一指标
+ - **Resolution:** 11-03 Task 2 实现百分比显示 + 柱状图:NDCG@5/MRR以百分比展示,命中分布以柱状图可视化
---
diff --git a/.planning/phases/11-predictv3/11-REVIEWS.md b/.planning/phases/11-predictv3/11-REVIEWS.md
new file mode 100644
index 0000000..248bebf
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-REVIEWS.md
@@ -0,0 +1,158 @@
+---
+phase: 11
+reviewers: [codex, opencode]
+reviewed_at: 2026-05-01T12:30:00+08:00
+plans_reviewed: [11-01-PLAN.md, 11-02-PLAN.md, 11-03-PLAN.md, 11-04-PLAN.md, 11-05-PLAN.md]
+---
+
+# Cross-AI Plan Review — Phase 11: predictV3算法优化
+
+## Codex Review
+
+### Summary
+
+计划技术上是合理的,但有几个需要注意的地方:依赖关系修复、数据验证、性能保护和边缘情况处理。整体架构遵循现有模式良好,NDCG@5 和 MRR 作为排名评估指标是适当的。
+
+### Strengths
+
+- 计划结构清晰,任务分解合理
+- NDCG@5、MRR、命中分布都是业界标准的排名质量评估指标
+- 扩展字段明确(ndcg_5、mrr、hit_distribution、precision_5)
+- 整体架构遵循现有代码模式
+
+### Concerns
+
+| Severity | Concern |
+|----------|---------|
+| HIGH | **依赖关系错误**: Plan 05 (二阶马尔可夫) 不应依赖 Plans 01 和 03 — 它是独立的增强功能 |
+| HIGH | **数据验证缺失**: 需要最小样本量检查 — 置信度计算建议 50+ 期,二阶马尔可夫建议 150+ 期 |
+| MEDIUM | **性能保护**: 网格搜索需要超时保护和最优权重缓存机制 |
+| MEDIUM | **边缘情况**: NDCG 计算需要空预测保护;命中分布需要定义明确的统计桶 |
+
+### Suggestions
+
+- 修复 Plan 05 的依赖关系,使其独立执行
+- 添加数据量阈值验证,不足时返回提示或回退策略
+- 为网格搜索添加执行超时限制(如 60 秒)
+- 在 NDCG/MRR 计算前检查 `$details` 是否为空
+
+### Risk Assessment
+
+**MEDIUM** — 依赖关系和边缘情况需要修复,否则执行可能失败。
+
+---
+
+## OpenCode Review
+
+### Summary
+
+Phase 11 计划在功能扩展方向上合理,在现有 V3 预测算法(9维度 + 动态权重)基础上增加置信度评估、回测指标扩展、权重优化和二阶马尔可夫链增强。计划整体结构清晰,依赖关系明确,但存在若干实现细节缺失和潜在风险需要补充。
+
+### Strengths
+
+- Plan 01 任务分解合理,指标选择专业
+- Plan 02 与回测指标扩展解耦,可独立实现
+- Plan 03 依赖明确,在现有弹窗架构内实现
+- Plan 04 网格搜索是系统化的超参数优化方法
+- Plan 05 二阶马尔可夫是合理的算法增强方向
+
+### Concerns
+
+| Severity | Concern |
+|----------|---------|
+| HIGH | **Plan 01: NDCG 计算公式不明确** — 需要确认基于什么"理想排名"计算,relevance score 定义模糊 |
+| HIGH | **Plan 01: hit_distribution 定义模糊** — 具体指什么分布?按命中排名?按命中/未命中按期数分布? |
+| HIGH | **Plan 02: "历史排名命中率"数据来源不明** — 现有 `_runBacktestV3` 不保存历史命中率数据,需要明确是实时计算还是缓存 |
+| HIGH | **Plan 02: "多维度一致性"定义不明** — 具体指哪些维度之间的一致性?如何量化? |
+| HIGH | **Plan 04: 5 种权重配置未明确** — 缺少具体配置清单 |
+| HIGH | **Plan 04: 优化目标不明确** — 用哪个指标作为优化目标?hit_rate?NDCG?MRR? |
+| HIGH | **Plan 05: 状态空间爆炸** — 二阶马尔可夫状态空间大(尾数 10×10=100),100 期历史数据下很多状态对从未出现,概率估计不准确 |
+| HIGH | **Plan 05: 100 期阈值缺乏依据** — 二阶马尔可夫需要更长历史,建议至少 200-300 期 |
+| MEDIUM | **Plan 01: precision_5 与 hit_rate 关系** — 两者都是 Top-5 命中,是否重复?建议区分或合并 |
+| MEDIUM | **Plan 02: 置信度百分比计算公式** — 三个维度如何加权组合?权重比例未明确 |
+| MEDIUM | **Plan 02: 数据量要求** — 历史 < 20 期时置信度不准确,缺少 fallback 策略 |
+| MEDIUM | **Plan 03: UI 设计细节缺失** — 未提供具体布局建议,置信度展示样式不明确 |
+| MEDIUM | **Plan 03: 命中分布图表实现** — 未说明使用 ECharts 还是 CSS,数据量大时需考虑性能 |
+| MEDIUM | **Plan 04: 结果持久化** — 每次调用都重新计算?建议增加缓存 |
+| MEDIUM | **Plan 04: 计算量** — 5 配置 × 50 期回测 = 250 次预测,响应时间长,建议异步 |
+| MEDIUM | **Plan 05: 一阶/二阶切换逻辑** — 判断条件不明确:总期数阈值还是状态对观察次数阈值? |
+| MEDIUM | **Plan 05: 与现有一阶权重关系** — 二阶是独立维度还是替换一阶? |
+| LOW | **Plan 01: 计算性能** — 每次回测批量计算可能影响性能 |
+| LOW | **Plan 02: 返回值结构** — 是否每个号码都提供置信度? |
+| LOW | **Plan 03: 前端数据量** — 命中分布是否需要分页/滚动加载? |
+| LOW | **Plan 03: 国际化** — 新增 UI 文本是否需要多语言? |
+| LOW | **Plan 05: 性能影响** — 二阶计算复杂度高于一阶 |
+
+### Suggestions
+
+**Plan 01:**
+- 补充 NDCG 公式:DCG = Σ(1/log2(rank+1)),IDCG = Σ(1/log2(i+1)) for i=1..hits
+- 明确 hit_distribution 结构:`{rank_1: n, rank_2: n, ..., rank_5: n, miss: n}`
+- 添加回测结果缓存机制
+
+**Plan 02:**
+- 将"多维度一致性"改为"预测得分集中度",基于 Top-5 得分与平均得分差距计算
+- 明确加权公式:`confidence = 0.4*historical + 0.3*score + 0.3*consistency`
+- 历史命中率作为 `_runBacktestV3` 副产品,调用一次获取
+
+**Plan 03:**
+- 补充 UI 设计 mockup 或具体说明
+- 使用现有 ECharts 展示命中分布
+- 置信度用红/黄/绿三色表示高/中/低
+
+**Plan 04:**
+- 补充 5 种预定义配置的具体权重值
+- 定义优化目标:综合 hit_rate(60%) + avg_rank(40%)
+- 使用后台队列异步处理,返回 task_id
+
+**Plan 05:**
+- 重新评估二阶马尔可夫必要性 — 49选1彩票数据稀疏性严重
+- 替代方案:加权 N 阶马尔可夫(70% 二阶 + 30% 一阶)
+- 明确判断标准:`count($history) >= 200 && $statePairCount >= 5`
+
+### Risk Assessment
+
+**MEDIUM** — 实现复杂度中等,数据稀疏性问题高,多处定义不明确需补充。建议优先实现 Plan 01、02、04,将 Plan 05 作为可选高阶优化或重新评估必要性。
+
+---
+
+## Consensus Summary
+
+### Agreed Strengths
+
+- 计划结构清晰,任务分解合理 ✓
+- NDCG@5、MRR 是适当的排名质量评估指标 ✓
+- 整体架构遵循现有代码模式 ✓
+- 网格搜索是系统化的参数优化方法 ✓
+
+### Agreed Concerns (Highest Priority)
+
+| Priority | Concern | Source |
+|----------|---------|--------|
+| 1 | **Plan 05 依赖关系错误** — 不应依赖 01、03,是独立功能 | Codex + OpenCode |
+| 2 | **数据量验证缺失** — 置信度需 50+,二阶马尔可夫需 150-200+ | Codex + OpenCode |
+| 3 | **边缘情况处理** — NDCG/MRR 空预测保护,hit_distribution 定义模糊 | Codex + OpenCode |
+| 4 | **Plan 05 状态空间爆炸** — 100 期数据下二阶马尔可夫概率估计不准 | OpenCode |
+| 5 | **Plan 02 置信度维度定义不明** — "多维度一致性"如何量化 | OpenCode |
+| 6 | **Plan 04 配置未明确** — 5 种权重具体值缺失,优化目标不明 | OpenCode |
+| 7 | **性能影响** — 网格搜索需超时/异步,二阶马尔可夫计算量大 | Codex + OpenCode |
+
+### Divergent Views
+
+| Issue | Codex | OpenCode |
+|-------|-------|----------|
+| Plan 05 数据阈值 | 建议 150+ | 建议 200-300+,并要求状态对观察次数 >= 5 |
+| precision_5 与 hit_rate | 未提及 | 认为可能重复,建议区分 |
+
+---
+
+## Action Items for Replanning
+
+1. **Fix Plan 05 depends_on** → 改为 `depends_on: []`
+2. **Add data validation** → 所有计算方法添加最小数据量检查和 fallback
+3. **Clarify NDCG formula** → 补充完整公式到 Plan 01 Task 1
+4. **Clarify hit_distribution** → 明确结构为 `{rank_1..rank_5: counts}`
+5. **Clarify confidence dimensions** → 重命名"多维度一致性"为"得分集中度"
+6. **Add weight configs** → Plan 04 补充 5 种具体权重配置值
+7. **Raise 2nd-order threshold** → Plan 05 改为 200 期 + 状态对观察次数检查
+8. **Add performance protection** → 网格搜索添加超时限制,考虑异步
\ No newline at end of file
diff --git a/.planning/phases/11-predictv3/11-VALIDATION.md b/.planning/phases/11-predictv3/11-VALIDATION.md
new file mode 100644
index 0000000..c893440
--- /dev/null
+++ b/.planning/phases/11-predictv3/11-VALIDATION.md
@@ -0,0 +1,97 @@
+---
+phase: 11
+phase_slug: predictv3
+created: 2026-05-01
+---
+
+# Phase 11: predictV3算法优化 - Validation Strategy
+
+## Overview
+
+本阶段为算法优化,验证重点在于:
+1. 新增指标计算准确性(NDCG、MRR、置信度)
+2. 二阶马尔可夫转移矩阵构建正确性
+3. 权重优化结果有效性
+4. 回测命中率提升验证
+
+## Test Framework
+
+| Property | Value |
+|----------|-------|
+| Framework | PHPUnit (ThinkPHP内置) |
+| Config file | 无独立配置,通过 `php think unit` 运行 |
+| Quick run command | `php think unit --filter HistoryTest` |
+| Full suite command | `php think unit` |
+
+## Phase Requirements → Test Map
+
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| PRED-01 | 置信度计算准确性 | unit | `php think unit --filter testConfidenceCalculation` | ❌ Wave 0 |
+| PRED-02 | NDCG计算准确性 | unit | `php think unit --filter testNDCGCalculation` | ❌ Wave 0 |
+| PRED-03 | 二阶马尔可夫转移矩阵构建 | unit | `php think unit --filter testTransitionMatrix2ndOrder` | ❌ Wave 0 |
+| PRED-04 | 权重优化收敛性 | unit | `php think unit --filter testWeightOptimization` | ❌ Wave 0 |
+| PRED-05 | 回测结果完整性 | unit | `php think unit --filter testBacktestV3Extended` | ❌ Wave 0 |
+
+## Sampling Rate
+
+- **Per task commit:** 快速单元测试覆盖核心方法
+- **Per wave merge:** 回测验证使用真实历史数据
+- **Phase gate:** 全量回测(100期)验证整体命中率提升
+
+## Wave 0 Gaps
+
+- [ ] `tests/HistoryTest.php` — 核心预测方法单元测试
+- [ ] `tests/ConfidenceTest.php` — 置信度计算测试
+- [ ] `tests/BacktestMetricsTest.php` — NDCG、MRR等指标计算测试
+- [ ] 共享fixtures: 历史数据模拟生成器
+
+## Validation Dimensions
+
+### Dimension 8: Nyquist Test Coverage
+
+**Target:** 每个 PLAN.md 至少有一个可验证的测试命令
+
+| Plan | Primary Test | Coverage |
+|------|--------------|----------|
+| 11-01 | testBacktestV3Extended | NDCG/MRR 计算准确性 |
+| 11-02 | testConfidenceCalculation | 置信度计算准确性 |
+| 11-03 | 手动 UI 验证 | 前端展示正确性 |
+| 11-04 | testWeightOptimization | 权重优化收敛性 |
+| 11-05 | testTransitionMatrix2ndOrder | 二阶马尔可夫正确性 |
+
+### Dimension 9: Integration Verification
+
+**End-to-End Flow:**
+
+1. 后端 `getPredictionV3()` 返回完整数据结构(predictions + confidence + backtest)
+2. 前端 `renderPredict()` 正确渲染所有新增指标
+3. 回测命中率可量化对比(V2 vs V3)
+
+**Verification Command:**
+
+```bash
+# 验证后端接口返回结构完整性
+curl -s "http://localhost/history/predictV3?periods=100" | jq '.data | keys'
+# 期望输出包含: predictions, confidence, backtest, analysis
+
+# 验证 NDCG/MRR 存在
+curl -s "http://localhost/history/predictV3?periods=100" | jq '.data.backtest | keys'
+# 期望输出包含: hit_rate, avg_rank, ndcg_5, mrr, hit_distribution
+```
+
+## Acceptance Criteria
+
+### Phase Gate
+
+- [ ] NDCG@5 计算准确(单元测试通过)
+- [ ] MRR 计算准确(单元测试通过)
+- [ ] 置信度阈值正确(>=70%高、50-70%中、<50%低)
+- [ ] 二阶马尔可夫在数据充足时启用
+- [ ] 前端展示包含所有新增指标
+- [ ] 回测命中率有提升(相比 V2)
+
+---
+
+*Phase: 11-predictv3*
+*Validation strategy created: 2026-05-01*
\ No newline at end of file
diff --git a/.planning/quick/20260430-history-predict/PLAN.md b/.planning/quick/20260430-history-predict/PLAN.md
new file mode 100644
index 0000000..d212960
--- /dev/null
+++ b/.planning/quick/20260430-history-predict/PLAN.md
@@ -0,0 +1,55 @@
+---
+name: history-predict
+created: 2026-04-30
+type: quick
+---
+
+# 预测号码功能规划
+
+## 目标
+在 history 页面新增预测号码功能,综合历史记录多维度分析给出号码预测建议。
+
+## 分析维度
+现有系统已具备以下转移概率分析:
+1. **区域转移** - zoneTransition (1-10, 11-20, 21-30, 31-40, 41-49)
+2. **生肖转移** - zodiacTransition (12生肖)
+3. **尾号转移** - tailNumberTransition (尾号0-9)
+4. **首号转移** - headNumberTransition (首号0-4)
+5. **波色转移** - colorWaveTransition (红/蓝/绿)
+
+## 预测算法
+基于最近N期特码,结合各维度转移概率矩阵:
+- 根据上一期特码所在维度(区域、生肖、尾号、首号),查找转移概率最高的目标维度
+- 综合各维度预测结果,计算每个号码的综合得分
+- 得分 = 区域概率权重 + 生肖概率权重 + 尾号概率权重 + 首号概率权重 + 波色概率权重
+
+## 实现步骤
+
+### 1. 后端 Model 新增方法
+- `getPrediction($periods, $weights)` - 综合预测计算方法
+ - 输入:历史期数、各维度权重配置
+ - 输出:预测号码列表(按得分排序)
+
+### 2. 后端 Controller 新增接口
+- `predict()` - AJAX 接口
+ - 参数:periods, weights (可选)
+ - 返回:预测号码列表 + 各维度分析详情
+
+### 3. 前端 JS 新增功能
+- 预测弹窗 `showPredictDialog()`
+- 权重配置面板
+- 预测结果渲染(号码球 + 得分 + 各维度分析说明)
+
+### 4. 权重配置
+默认权重:
+- 区域转移:0.25
+- 生肖转移:0.20
+- 尾号转移:0.20
+- 首号转移:0.15
+- 波色转移:0.10
+- 冷热系数:0.10
+
+## 文件修改清单
+1. `application/admin/model/History.php` - 新增 getPrediction 方法
+2. `application/admin/controller/History.php` - 新增 predict 接口,更新 noNeedRight
+3. `public/assets/js/backend/history.js` - 新增预测弹窗和渲染逻辑
\ No newline at end of file
diff --git a/.planning/quick/20260430-history-predict/SUMMARY.md b/.planning/quick/20260430-history-predict/SUMMARY.md
new file mode 100644
index 0000000..34a7bc1
--- /dev/null
+++ b/.planning/quick/20260430-history-predict/SUMMARY.md
@@ -0,0 +1,37 @@
+---
+status: complete
+created: 2026-04-30
+slug: history-predict
+---
+
+# 预测号码功能完成
+
+## 实现内容
+
+### 1. 后端 Model (History.php)
+新增 `getPrediction($periods, $weights)` 方法:
+- 基于 6 个维度计算综合预测得分
+- 区域转移、生肖转移、尾号转移、首号转移、波色转移、冷热系数
+- 返回 Top 20 预测号码及其详细得分分析
+
+### 2. 后端 Controller (History.php)
+新增 `predict()` 接口:
+- 支持 AJAX 请求
+- 可配置统计期数和权重参数
+- 已加入 `noNeedRight` 白名单
+
+### 3. 前端 JS (history.js)
+新增预测功能:
+- `showPredictDialog()` - 预测弹窗
+- `queryPredict()` - AJAX 查询
+- `renderPredict()` - 结果渲染
+- 支持自定义权重配置
+
+### 4. 视图 (index.html)
+新增"智能预测"按钮
+
+## 文件变更
+- `application/admin/model/History.php` (+150行)
+- `application/admin/controller/History.php` (+25行)
+- `public/assets/js/backend/history.js` (+180行)
+- `application/admin/view/history/index.html` (+1行)
\ No newline at end of file
diff --git a/.planning/quick/20260501-history-predict/ANALYSIS.md b/.planning/quick/20260501-history-predict/ANALYSIS.md
new file mode 100644
index 0000000..e4ed823
--- /dev/null
+++ b/.planning/quick/20260501-history-predict/ANALYSIS.md
@@ -0,0 +1,39 @@
+## 正码与特码关联规律分析
+
+### 数据范围
+- 总期数:约500期(2025111-2026120)
+- 每期数据:num1-6(正码) + num7(特码)
+
+### 分析维度
+
+#### 1. 正码平均值与特码差值
+分析每一期的正码平均值(avg)与特码(num7)的差值分布:
+- 差值 = num7 - avg(num1-6)
+- 统计差值的高频范围
+
+#### 2. 正码范围与特码关系
+分析正码的[min, max]范围与特码的关系:
+- 特码是否在正码范围内?
+- 特码距离正码范围的距离分布
+
+#### 3. 正码排序后与特码距离
+将num1-6排序,分析特码与最近正码的距离:
+- 最短距离分布
+- 特码是否等于某个正码?
+
+#### 4. 和值尾数与特码尾数
+分析正码和值的尾数与特码尾数的关系:
+- 同尾概率?
+- 差值分布?
+
+#### 5. 正码区间覆盖分析
+将1-49分为5个区间,分析正码覆盖的区间与特码所在区间的关系:
+- 特码是否出现在正码未覆盖的区间?
+
+#### 6. 波色/生肖关联
+分析正码中各波色/生肖的数量与特码波色/生肖的关系
+
+---
+
+### 数据提取准备
+从SQL中提取所有INSERT数据进行统计分析。
\ No newline at end of file
diff --git a/analysis/predict_analysis.php b/analysis/predict_analysis.php
new file mode 100644
index 0000000..75d5988
--- /dev/null
+++ b/analysis/predict_analysis.php
@@ -0,0 +1,858 @@
+ '红', 2 => '红', 3 => '蓝', 4 => '蓝', 5 => '绿', 6 => '绿',
+ 7 => '红', 8 => '红', 9 => '蓝', 10 => '蓝', 11 => '绿', 12 => '红',
+ 13 => '红', 14 => '蓝', 15 => '蓝', 16 => '绿', 17 => '绿', 18 => '红',
+ 19 => '红', 20 => '蓝', 21 => '绿', 22 => '绿', 23 => '红', 24 => '红',
+ 25 => '蓝', 26 => '蓝', 27 => '绿', 28 => '绿', 29 => '红', 30 => '红',
+ 31 => '蓝', 32 => '绿', 33 => '绿', 34 => '红', 35 => '红', 36 => '蓝',
+ 37 => '蓝', 38 => '绿', 39 => '绿', 40 => '红', 41 => '蓝', 42 => '蓝',
+ 43 => '绿', 44 => '绿', 45 => '红', 46 => '红', 47 => '蓝', 48 => '蓝',
+ 49 => '绿'
+];
+
+// 区间划分(1-10小号,11-30中号,31-49大号)
+function getRange($num) {
+ if ($num >= 1 && $num <= 10) return '小号(1-10)';
+ if ($num >= 11 && $num <= 30) return '中号(11-30)';
+ return '大号(31-49)';
+}
+
+// 获取数字的尾数
+function getTail($num) {
+ return $num % 10;
+}
+
+// 从SQL数据提取历史记录
+$history = [];
+$sqlFile = 'C:\Users\91611\Desktop\fa_history.sql';
+$content = file_get_contents($sqlFile);
+
+// 解析INSERT语句
+preg_match_all("/INSERT INTO `fa_history` VALUES \((\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), '([^']+)'\)/", $content, $matches);
+
+for ($i = 0; $i < count($matches[0]); $i++) {
+ $history[] = [
+ 'expect' => (int)$matches[1][$i],
+ 'num1' => (int)$matches[2][$i],
+ 'num2' => (int)$matches[3][$i],
+ 'num3' => (int)$matches[4][$i],
+ 'num4' => (int)$matches[5][$i],
+ 'num5' => (int)$matches[6][$i],
+ 'num6' => (int)$matches[7][$i],
+ 'num7' => (int)$matches[8][$i],
+ 'openTime' => $matches[9][$i]
+ ];
+}
+
+// 按期号排序
+usort($history, function($a, $b) {
+ return $a['expect'] - $b['expect'];
+});
+
+echo "=== 数据概览 ===\n";
+echo "总期数: " . count($history) . "\n";
+echo "期号范围: {$history[0]['expect']} - {$history[count($history)-1]['expect']}\n\n";
+
+// 分析结果存储
+$analysisResults = [];
+
+// ============ 维度1: 上期正码平均值与下期特码的差值分布 ============
+echo "=== 维度1: 上期正码平均值与下期特码的差值分布 ===\n";
+
+$avgDifferences = [];
+$avgDiffPredictions = []; // 基于平均值的预测命中率
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ // 计算上期正码平均值
+ $avg = ($current['num1'] + $current['num2'] + $current['num3'] +
+ $current['num4'] + $current['num5'] + $current['num6']) / 6;
+
+ // 与下期特码的差值
+ $diff = $next['num7'] - $avg;
+ $avgDifferences[] = round($diff);
+}
+
+// 统计差值分布
+$avgDiffStats = array_count_values($avgDifferences);
+ksort($avgDiffStats);
+
+echo "差值分布(差值=下期特码-上期正码平均值):\n";
+foreach ($avgDiffStats as $diff => $count) {
+ $percent = round($count / (count($history) - 1) * 100, 2);
+ echo " 差值 $diff: $count次 ($percent%)\n";
+}
+
+// 分析差值范围预测效果
+echo "\n差值范围预测效果:\n";
+$diffRanges = [
+ '[-40,-20]' => 0,
+ '[-20,-10]' => 0,
+ '[-10,0]' => 0,
+ '[0,10]' => 0,
+ '[10,20]' => 0,
+ '[20,40]' => 0
+];
+
+foreach ($avgDifferences as $diff) {
+ if ($diff >= -40 && $diff < -20) $diffRanges['[-40,-20]']++;
+ elseif ($diff >= -20 && $diff < -10) $diffRanges['[-20,-10]']++;
+ elseif ($diff >= -10 && $diff < 0) $diffRanges['[-10,0]']++;
+ elseif ($diff >= 0 && $diff < 10) $diffRanges['[0,10]']++;
+ elseif ($diff >= 10 && $diff < 20) $diffRanges['[10,20]']++;
+ elseif ($diff >= 20 && $diff <= 40) $diffRanges['[20,40]']++;
+}
+
+$total = count($avgDifferences);
+foreach ($diffRanges as $range => $count) {
+ $percent = round($count / $total * 100, 2);
+ echo " $range: $count次 ($percent%)\n";
+}
+
+// 基于平均值±10范围的预测命中率分析
+$hitCount = 0;
+$predictionRange = 10; // 预测范围
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $avg = ($current['num1'] + $current['num2'] + $current['num3'] +
+ $current['num4'] + $current['num5'] + $current['num6']) / 6;
+
+ // 预测范围:平均值±10
+ $predictMin = max(1, floor($avg) - $predictionRange);
+ $predictMax = min(49, floor($avg) + $predictionRange);
+
+ // 检查下期特码是否在预测范围内
+ if ($next['num7'] >= $predictMin && $next['num7'] <= $predictMax) {
+ $hitCount++;
+ }
+}
+echo "\n基于平均值±$predictionRange范围的预测命中率: " . round($hitCount / (count($history) - 1) * 100, 2) . "% ($hitCount/" . (count($history)-1) . ")\n";
+
+echo "\n";
+
+// ============ 维度2: 上期正码范围[min,max]与下期特码的关系 ============
+echo "=== 维度2: 上期正码范围[min,max]与下期特码的关系 ===\n";
+
+$inRangeCount = 0;
+$belowRangeCount = 0;
+$aboveRangeCount = 0;
+$rangeStats = [];
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ // 上期正码范围
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+ $min = min($nums);
+ $max = max($nums);
+ $rangeWidth = $max - $min;
+
+ // 下期特码与范围的关系
+ if ($next['num7'] >= $min && $next['num7'] <= $max) {
+ $inRangeCount++;
+ $relation = '在范围内';
+ } elseif ($next['num7'] < $min) {
+ $belowRangeCount++;
+ $relation = '低于范围';
+ } else {
+ $aboveRangeCount++;
+ $relation = '高于范围';
+ }
+
+ // 统计范围宽度与关系
+ $rangeKey = "范围宽度$rangeWidth";
+ if (!isset($rangeStats[$rangeKey])) {
+ $rangeStats[$rangeKey] = ['在范围内' => 0, '低于范围' => 0, '高于范围' => 0];
+ }
+ $rangeStats[$rangeKey][$relation]++;
+}
+
+$total = count($history) - 1;
+echo "下期特码位置分布:\n";
+echo " 在上期正码范围内: $inRangeCount次 (" . round($inRangeCount / $total * 100, 2) . "%)\n";
+echo " 低于上期正码范围: $belowRangeCount次 (" . round($belowRangeCount / $total * 100, 2) . "%)\n";
+echo " 高于上期正码范围: $aboveRangeCount次 (" . round($aboveRangeCount / $total * 100, 2) . "%)\n";
+
+echo "\n范围宽度与特码位置关系:\n";
+ksort($rangeStats);
+foreach ($rangeStats as $width => $stats) {
+ $widthTotal = array_sum($stats);
+ echo " $width (共$widthTotal期):\n";
+ foreach ($stats as $relation => $count) {
+ $percent = round($count / $widthTotal * 100, 2);
+ echo " $relation: $count次 ($percent%)\n";
+ }
+}
+
+// 计算范围宽度的平均值
+$avgRangeWidth = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+ $avgRangeWidth += max($nums) - min($nums);
+}
+$avgRangeWidth = round($avgRangeWidth / (count($history) - 1), 2);
+echo "\n平均范围宽度: $avgRangeWidth\n";
+
+echo "\n";
+
+// ============ 维度3: 上期正码与下期特码的最短距离分布 ============
+echo "=== 维度3: 上期正码与下期特码的最短距离分布 ===\n";
+
+$minDistances = [];
+$distanceStats = [];
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ // 计算上期正码与下期特码的最短距离
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ $minDist = PHP_INT_MAX;
+ foreach ($nums as $num) {
+ $dist = abs($next['num7'] - $num);
+ if ($dist < $minDist) $minDist = $dist;
+ }
+ $minDistances[] = $minDist;
+
+ if (!isset($distanceStats[$minDist])) {
+ $distanceStats[$minDist] = 0;
+ }
+ $distanceStats[$minDist]++;
+}
+
+ksort($distanceStats);
+echo "最短距离分布:\n";
+foreach ($distanceStats as $dist => $count) {
+ $percent = round($count / (count($history) - 1) * 100, 2);
+ echo " 距离 $dist: $count次 ($percent%)\n";
+}
+
+// 分析距离≤5的命中率
+$closeHitCount = count(array_filter($minDistances, function($d) { return $d <= 5; }));
+echo "\n最短距离≤5的比例: " . round($closeHitCount / (count($history) - 1) * 100, 2) . "% ($closeHitCount/" . (count($history)-1) . ")\n";
+
+$veryCloseHitCount = count(array_filter($minDistances, function($d) { return $d <= 3; }));
+echo "最短距离≤3的比例: " . round($veryCloseHitCount / (count($history) - 1) * 100, 2) . "% ($veryCloseHitCount/" . (count($history)-1) . ")\n";
+
+// 基于最短距离预测(围绕上期正码±3的范围)
+$predictionHit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ // 预测范围:每个正码±3
+ $predicted = [];
+ foreach ($nums as $num) {
+ for ($p = max(1, $num - 3); $p <= min(49, $num + 3); $p++) {
+ $predicted[$p] = true;
+ }
+ }
+
+ if (isset($predicted[$next['num7']])) {
+ $predictionHit++;
+ }
+}
+echo "基于正码±3范围预测命中率: " . round($predictionHit / (count($history) - 1) * 100, 2) . "% ($predictionHit/" . (count($history)-1) . ")\n";
+echo "预测范围大小: 约" . count($predicted) . "个数字\n";
+
+echo "\n";
+
+// ============ 维度4: 上期正码和值尾数与下期特码尾数的关系 ============
+echo "=== 维度4: 上期正码和值尾数与下期特码尾数的关系 ===\n";
+
+$sumTailRelations = [];
+$tailSameCount = 0;
+$tailDiff1Count = 0;
+$tailDiff2Count = 0;
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ // 上期正码和值
+ $sum = $current['num1'] + $current['num2'] + $current['num3'] +
+ $current['num4'] + $current['num5'] + $current['num6'];
+ $sumTail = getTail($sum);
+
+ // 下期特码尾数
+ $nextTail = getTail($next['num7']);
+
+ // 统计尾数关系
+ $tailDiff = abs($sumTail - $nextTail);
+ if ($tailDiff > 5) $tailDiff = 10 - $tailDiff; // 考虑环形差异
+
+ if (!isset($sumTailRelations[$sumTail])) {
+ $sumTailRelations[$sumTail] = [];
+ }
+ if (!isset($sumTailRelations[$sumTail][$nextTail])) {
+ $sumTailRelations[$sumTail][$nextTail] = 0;
+ }
+ $sumTailRelations[$sumTail][$nextTail]++;
+
+ if ($tailDiff == 0) $tailSameCount++;
+ elseif ($tailDiff == 1) $tailDiff1Count++;
+ elseif ($tailDiff == 2) $tailDiff2Count++;
+}
+
+$total = count($history) - 1;
+echo "尾数关系分布:\n";
+echo " 尾数相同: $tailSameCount次 (" . round($tailSameCount / $total * 100, 2) . "%)\n";
+echo " 尾数相差1: $tailDiff1Count次 (" . round($tailDiff1Count / $total * 100, 2) . "%)\n";
+echo " 尾数相差2: $tailDiff2Count次 (" . round($tailDiff2Count / $total * 100, 2) . "%)\n";
+
+echo "\n上期和值尾数→下期特码尾数转移矩阵:\n";
+for ($sumTail = 0; $sumTail <= 9; $sumTail++) {
+ echo " 和值尾数$sumTail → ";
+ if (isset($sumTailRelations[$sumTail])) {
+ $maxTail = -1;
+ $maxCount = 0;
+ foreach ($sumTailRelations[$sumTail] as $nextTail => $count) {
+ if ($count > $maxCount) {
+ $maxCount = $count;
+ $maxTail = $nextTail;
+ }
+ }
+ $totalCount = array_sum($sumTailRelations[$sumTail]);
+ $percent = round($maxCount / $totalCount * 100, 2);
+ echo "最可能尾数$maxTail ($maxCount次, $percent%), 其他: ";
+ $others = [];
+ foreach ($sumTailRelations[$sumTail] as $nextTail => $count) {
+ if ($nextTail != $maxTail) {
+ $others[] = "$nextTail($count)";
+ }
+ }
+ echo implode(', ', $others) . "\n";
+ } else {
+ echo "无数据\n";
+ }
+}
+
+// 基于尾数预测命中率
+$tailPredictionHit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $sum = $current['num1'] + $current['num2'] + $current['num3'] +
+ $current['num4'] + $current['num5'] + $current['num6'];
+ $sumTail = getTail($sum);
+
+ // 预测:和值尾数±2范围内的尾数
+ $predictTails = [
+ $sumTail,
+ ($sumTail + 1) % 10,
+ ($sumTail - 1 + 10) % 10,
+ ($sumTail + 2) % 10,
+ ($sumTail - 2 + 10) % 10
+ ];
+
+ $nextTail = getTail($next['num7']);
+ if (in_array($nextTail, $predictTails)) {
+ $tailPredictionHit++;
+ }
+}
+echo "\n基于和值尾数±2范围的尾数预测命中率: " . round($tailPredictionHit / (count($history) - 1) * 100, 2) . "% ($tailPredictionHit/" . (count($history)-1) . ")\n";
+echo "预测范围: 5个尾数,每个尾数对应约5个数字,共约25个数字\n";
+
+echo "\n";
+
+// ============ 维度5: 上期正码覆盖区间与下期特码所在区间的关系 ============
+echo "=== 维度5: 上期正码覆盖区间与下期特码所在区间的关系 ===\n";
+
+$rangeCoverStats = [];
+$rangeTransferStats = [];
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ // 上期正码覆盖的区间
+ $covers = [];
+ foreach ($nums as $num) {
+ $covers[getRange($num)] = true;
+ }
+ $coverStr = implode('+', array_keys($covers));
+
+ // 下期特码所在区间
+ $nextRange = getRange($next['num7']);
+
+ if (!isset($rangeCoverStats[$coverStr])) {
+ $rangeCoverStats[$coverStr] = [];
+ }
+ if (!isset($rangeCoverStats[$coverStr][$nextRange])) {
+ $rangeCoverStats[$coverStr][$nextRange] = 0;
+ }
+ $rangeCoverStats[$coverStr][$nextRange]++;
+
+ // 统计覆盖度与特码位置
+ $coverCount = count($covers);
+ if (!isset($rangeTransferStats[$coverCount])) {
+ $rangeTransferStats[$coverCount] = [
+ '小号(1-10)' => 0,
+ '中号(11-30)' => 0,
+ '大号(31-49)' => 0
+ ];
+ }
+ $rangeTransferStats[$coverCount][$nextRange]++;
+}
+
+echo "上期正码覆盖区间→下期特码区间转移:\n";
+foreach ($rangeCoverStats as $cover => $nextStats) {
+ $totalCover = array_sum($nextStats);
+ echo " $cover (共$totalCover期):\n";
+ foreach ($nextStats as $nextRange => $count) {
+ $percent = round($count / $totalCover * 100, 2);
+ echo " → $nextRange: $count次 ($percent%)\n";
+ }
+}
+
+echo "\n上期正码覆盖区间数量与下期特码分布:\n";
+foreach ($rangeTransferStats as $coverCount => $stats) {
+ $totalCover = array_sum($stats);
+ echo " 覆盖$coverCount个区间 (共$totalCover期):\n";
+ foreach ($stats as $range => $count) {
+ $percent = round($count / $totalCover * 100, 2);
+ echo " $range: $count次 ($percent%)\n";
+ }
+}
+
+// 分析特码是否在上期正码覆盖的区间内
+$hitInCoveredRange = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ $covers = [];
+ foreach ($nums as $num) {
+ $covers[getRange($num)] = true;
+ }
+
+ $nextRange = getRange($next['num7']);
+ if (isset($covers[$nextRange])) {
+ $hitInCoveredRange++;
+ }
+}
+echo "\n下期特码在上期正码覆盖区间内的比例: " . round($hitInCoveredRange / (count($history) - 1) * 100, 2) . "% ($hitInCoveredRange/" . (count($history)-1) . ")\n";
+
+echo "\n";
+
+// ============ 维度6: 上期正码波色分布与下期特码波色的关系 ============
+echo "=== 维度6: 上期正码波色分布与下期特码波色的关系 ===\n";
+
+$colorDistributionStats = [];
+$colorTransferStats = [];
+$dominantColorStats = [];
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ // 上期正码波色分布
+ $colors = [];
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+ foreach ($nums as $num) {
+ $color = $colorMap[$num];
+ if (!isset($colors[$color])) {
+ $colors[$color] = 0;
+ }
+ $colors[$color]++;
+ }
+
+ // 波色分布字符串
+ $colorStr = "红{$colors['红']}蓝{$colors['蓝']}绿{$colors['绿']}";
+
+ // 下期特码波色
+ $nextColor = $colorMap[$next['num7']];
+
+ // 统计波色分布→特码波色
+ if (!isset($colorDistributionStats[$colorStr])) {
+ $colorDistributionStats[$colorStr] = [];
+ }
+ if (!isset($colorDistributionStats[$colorStr][$nextColor])) {
+ $colorDistributionStats[$colorStr][$nextColor] = 0;
+ }
+ $colorDistributionStats[$colorStr][$nextColor]++;
+
+ // 统计主导波色→特码波色
+ $dominantColor = array_keys($colors, max($colors))[0];
+ if (!isset($dominantColorStats[$dominantColor])) {
+ $dominantColorStats[$dominantColor] = [];
+ }
+ if (!isset($dominantColorStats[$dominantColor][$nextColor])) {
+ $dominantColorStats[$dominantColor][$nextColor] = 0;
+ }
+ $dominantColorStats[$dominantColor][$nextColor]++;
+}
+
+echo "上期正码波色分布→下期特码波色转移:\n";
+// 按出现次数排序
+$sortedColorDist = $colorDistributionStats;
+uasort($sortedColorDist, function($a, $b) {
+ return array_sum($b) - array_sum($a);
+});
+
+foreach ($sortedColorDist as $dist => $nextStats) {
+ $totalDist = array_sum($nextStats);
+ if ($totalDist >= 5) { // 只显示出现5次以上的分布
+ echo " $dist (共$totalDist期):\n";
+ foreach ($nextStats as $nextColor => $count) {
+ $percent = round($count / $totalDist * 100, 2);
+ echo " → $nextColor: $count次 ($percent%)\n";
+ }
+ }
+}
+
+echo "\n上期主导波色→下期特码波色转移:\n";
+foreach ($dominantColorStats as $dominant => $nextStats) {
+ $totalDom = array_sum($nextStats);
+ echo " 主导$dominant (共$totalDom期):\n";
+ foreach ($nextStats as $nextColor => $count) {
+ $percent = round($count / $totalDom * 100, 2);
+ echo " → $nextColor: $count次 ($percent%)\n";
+ }
+}
+
+// 基于主导波色预测命中率
+$dominantPredictionHit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+ $colors = [];
+ foreach ($nums as $num) {
+ $color = $colorMap[$num];
+ if (!isset($colors[$color])) $colors[$color] = 0;
+ $colors[$color]++;
+ }
+
+ $dominantColor = array_keys($colors, max($colors))[0];
+ $nextColor = $colorMap[$next['num7']];
+
+ if ($dominantColor == $nextColor) {
+ $dominantPredictionHit++;
+ }
+}
+echo "\n主导波色预测命中率: " . round($dominantPredictionHit / (count($history) - 1) * 100, 2) . "% ($dominantPredictionHit/" . (count($history)-1) . ")\n";
+
+// 基于上期正码波色预测(扩展到两种最可能波色)
+$expandedColorPredictionHit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+ $colors = ['红' => 0, '蓝' => 0, '绿' => 0];
+ foreach ($nums as $num) {
+ $colors[$colorMap[$num]]++;
+ }
+
+ // 选择出现次数最多的两种波色
+ arsort($colors);
+ $topColors = array_keys(array_slice($colors, 0, 2, true));
+
+ $nextColor = $colorMap[$next['num7']];
+ if (in_array($nextColor, $topColors)) {
+ $expandedColorPredictionHit++;
+ }
+}
+echo "扩展到两种主导波色预测命中率: " . round($expandedColorPredictionHit / (count($history) - 1) * 100, 2) . "% ($expandedColorPredictionHit/" . (count($history)-1) . ")\n";
+
+echo "\n";
+
+// ============ 维度7: 上期特码与下期特码的转移关系(马尔可夫分析) ============
+echo "=== 维度7: 上期特码与下期特码的转移关系(马尔可夫分析) ===\n";
+
+$specialTransfer = [];
+$specialRangeTransfer = [];
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $currentSpecial = $current['num7'];
+ $nextSpecial = $next['num7'];
+
+ // 精确转移统计
+ if (!isset($specialTransfer[$currentSpecial])) {
+ $specialTransfer[$currentSpecial] = [];
+ }
+ if (!isset($specialTransfer[$currentSpecial][$nextSpecial])) {
+ $specialTransfer[$currentSpecial][$nextSpecial] = 0;
+ }
+ $specialTransfer[$currentSpecial][$nextSpecial]++;
+
+ // 区间转移统计
+ $currentRange = getRange($currentSpecial);
+ $nextRange = getRange($nextSpecial);
+
+ if (!isset($specialRangeTransfer[$currentRange])) {
+ $specialRangeTransfer[$currentRange] = [];
+ }
+ if (!isset($specialRangeTransfer[$currentRange][$nextRange])) {
+ $specialRangeTransfer[$currentRange][$nextRange] = 0;
+ }
+ $specialRangeTransfer[$currentRange][$nextRange]++;
+}
+
+echo "特码区间转移矩阵:\n";
+foreach ($specialRangeTransfer as $fromRange => $toStats) {
+ $totalFrom = array_sum($toStats);
+ echo " $fromRange → :\n";
+ foreach ($toStats as $toRange => $count) {
+ $percent = round($count / $totalFrom * 100, 2);
+ echo " $toRange: $count次 ($percent%)\n";
+ }
+}
+
+// 找出高频转移
+echo "\n高频特码转移(出现2次以上):\n";
+foreach ($specialTransfer as $from => $toStats) {
+ foreach ($toStats as $to => $count) {
+ if ($count >= 2) {
+ echo " 特码$from → 特码$to: $count次\n";
+ }
+ }
+}
+
+// 基于特码区间预测
+$specialRangePredictionHit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $currentRange = getRange($current['num7']);
+
+ // 找出该区间最可能转移到的两个区间
+ $transferStats = $specialRangeTransfer[$currentRange];
+ arsort($transferStats);
+ $topRanges = array_keys(array_slice($transferStats, 0, 2, true));
+
+ $nextRange = getRange($next['num7']);
+ if (in_array($nextRange, $topRanges)) {
+ $specialRangePredictionHit++;
+ }
+}
+echo "\n基于特码区间转移预测(前2区间)命中率: " . round($specialRangePredictionHit / (count($history) - 1) * 100, 2) . "% ($specialRangePredictionHit/" . (count($history)-1) . ")\n";
+
+echo "\n";
+
+// ============ 综合分析:寻找40%以上命中率的规律 ============
+echo "=== 综合分析:寻找40%以上命中率的规律 ===\n\n";
+
+// 综合预测模型
+echo "【综合预测模型测试】\n\n";
+
+$combinedHits = [
+ '平均值±10范围' => $hitCount,
+ '正码±3范围' => $predictionHit,
+ '和值尾数±2尾数范围' => $tailPredictionHit,
+ '覆盖区间预测' => $hitInCoveredRange,
+ '主导波色预测' => $dominantPredictionHit,
+ '双波色预测' => $expandedColorPredictionHit,
+ '特码区间转移' => $specialRangePredictionHit
+];
+
+echo "各维度预测命中率汇总:\n";
+$totalPredictions = count($history) - 1;
+foreach ($combinedHits as $name => $hit) {
+ $percent = round($hit / $totalPredictions * 100, 2);
+ $status = $percent >= 40 ? '【达标】' : '';
+ echo " $name: $percent% ($hit/$totalPredictions) $status\n";
+}
+
+// 组合预测测试
+echo "\n组合预测测试:\n";
+
+$comboHits = 0;
+$comboPlusHits = 0;
+
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ // 方法1:平均值±15范围
+ $avg = array_sum($nums) / 6;
+ $avgRange = range(max(1, floor($avg) - 15), min(49, floor($avg) + 15));
+
+ // 方法2:正码±5范围
+ $numRange = [];
+ foreach ($nums as $num) {
+ for ($p = max(1, $num - 5); $p <= min(49, $num + 5); $p++) {
+ $numRange[$p] = true;
+ }
+ }
+
+ // 方法3:和值尾数±3范围的所有数字
+ $sum = array_sum($nums);
+ $sumTail = getTail($sum);
+ $tailRange = [];
+ for ($t = $sumTail - 3; $t <= $sumTail + 3; $t++) {
+ $actualTail = ($t + 10) % 10;
+ for ($n = 1; $n <= 49; $n++) {
+ if (getTail($n) == $actualTail) {
+ $tailRange[$n] = true;
+ }
+ }
+ }
+
+ // 组合1:平均值范围 OR 正码范围
+ if (in_array($next['num7'], $avgRange) || isset($numRange[$next['num7']])) {
+ $comboHits++;
+ }
+
+ // 组合2:平均值范围 OR 正码范围 OR 尾数范围
+ if (in_array($next['num7'], $avgRange) || isset($numRange[$next['num7']]) || isset($tailRange[$next['num7']])) {
+ $comboPlusHits++;
+ }
+}
+
+echo "组合1(平均值±15 OR 正码±5)命中率: " . round($comboHits / $totalPredictions * 100, 2) . "% ($comboHits/$totalPredictions)\n";
+echo "组合2(平均值±15 OR 正码±5 OR 尾数±3)命中率: " . round($comboPlusHits / $totalPredictions * 100, 2) . "% ($comboPlusHits/$totalPredictions)\n";
+
+// 波色+区间组合
+echo "\n波色+区间组合预测:\n";
+$colorRangeComboHit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ // 获取上期正码覆盖的波色和区间
+ $colors = ['红' => 0, '蓝' => 0, '绿' => 0];
+ $ranges = ['小号(1-10)' => 0, '中号(11-30)' => 0, '大号(31-49)' => 0];
+ foreach ($nums as $num) {
+ $colors[$colorMap[$num]]++;
+ $ranges[getRange($num)]++;
+ }
+
+ // 选择最可能的波色和区间
+ arsort($colors);
+ arsort($ranges);
+ $topColor = array_keys($colors)[0];
+ $topRange = array_keys($ranges)[0];
+
+ // 预测:该波色+该区间的交集
+ $predictNums = [];
+ foreach (range(1, 49) as $n) {
+ if ($colorMap[$n] == $topColor && getRange($n) == $topRange) {
+ $predictNums[$n] = true;
+ }
+ }
+
+ if (isset($predictNums[$next['num7']])) {
+ $colorRangeComboHit++;
+ }
+}
+echo "波色+区间交集预测命中率: " . round($colorRangeComboHit / $totalPredictions * 100, 2) . "% ($colorRangeComboHit/$totalPredictions)\n";
+echo "预测范围大小: " . count($predictNums) . "个数字\n";
+
+// 扩展波色+区间(前2波色+前2区间)
+$colorRangeCombo2Hit = 0;
+for ($i = 0; $i < count($history) - 1; $i++) {
+ $current = $history[$i];
+ $next = $history[$i + 1];
+
+ $nums = [$current['num1'], $current['num2'], $current['num3'],
+ $current['num4'], $current['num5'], $current['num6']];
+
+ $colors = ['红' => 0, '蓝' => 0, '绿' => 0];
+ $ranges = ['小号(1-10)' => 0, '中号(11-30)' => 0, '大号(31-49)' => 0];
+ foreach ($nums as $num) {
+ $colors[$colorMap[$num]]++;
+ $ranges[getRange($num)]++;
+ }
+
+ arsort($colors);
+ arsort($ranges);
+ $top2Colors = array_keys(array_slice($colors, 0, 2, true));
+ $top2Ranges = array_keys(array_slice($ranges, 0, 2, true));
+
+ $predictNums2 = [];
+ foreach (range(1, 49) as $n) {
+ if (in_array($colorMap[$n], $top2Colors) && in_array(getRange($n), $top2Ranges)) {
+ $predictNums2[$n] = true;
+ }
+ }
+
+ if (isset($predictNums2[$next['num7']])) {
+ $colorRangeCombo2Hit++;
+ }
+}
+echo "前2波色+前2区间交集预测命中率: " . round($colorRangeCombo2Hit / $totalPredictions * 100, 2) . "% ($colorRangeCombo2Hit/$totalPredictions)\n";
+
+echo "\n";
+
+// ============ 总结:40%以上命中率的规律 ============
+echo "=== 总结:达到40%以上命中率的规律 ===\n\n";
+
+$highHitRules = [];
+foreach ($combinedHits as $name => $hit) {
+ $percent = round($hit / $totalPredictions * 100, 2);
+ if ($percent >= 40) {
+ $highHitRules[] = [
+ 'name' => $name,
+ 'percent' => $percent,
+ 'hit' => $hit,
+ 'total' => $totalPredictions
+ ];
+ }
+}
+
+if (count($highHitRules) > 0) {
+ foreach ($highHitRules as $rule) {
+ echo "【{$rule['name']}】命中率: {$rule['percent']}% ({$rule['hit']}/{$rule['total']})\n";
+ }
+} else {
+ echo "单维度分析中没有达到40%以上命中率的规律\n";
+}
+
+// 组合规律
+echo "\n组合规律命中率:\n";
+echo "组合1(平均值±15 OR 正码±5): " . round($comboHits / $totalPredictions * 100, 2) . "%\n";
+echo "组合2(平均值±15 OR 正码±5 OR 尾数±3): " . round($comboPlusHits / $totalPredictions * 100, 2) . "%\n";
+echo "前2波色+前2区间交集: " . round($colorRangeCombo2Hit / $totalPredictions * 100, 2) . "%\n";
+
+echo "\n分析完成!\n";
\ No newline at end of file
diff --git a/analysis/predict_analysis.py b/analysis/predict_analysis.py
new file mode 100644
index 0000000..de4768d
--- /dev/null
+++ b/analysis/predict_analysis.py
@@ -0,0 +1,589 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+上期正码与当期特码关联规律分析脚本
+
+分析维度:
+1. 上期正码平均值与下期特码的差值分布
+2. 上期正码范围[min,max]与下期特码的关系
+3. 上期正码与下期特码的最短距离分布
+4. 上期正码和值尾数与下期特码尾数的关系
+5. 上期正码覆盖区间与下期特码所在区间的关系
+6. 上期正码波色分布与下期特码波色的关系
+7. 上期特码与下期特码的转移关系
+"""
+
+import re
+from collections import defaultdict
+from pathlib import Path
+
+# 波色映射表
+COLOR_MAP = {
+ 1: '红', 2: '红', 3: '蓝', 4: '蓝', 5: '绿', 6: '绿',
+ 7: '红', 8: '红', 9: '蓝', 10: '蓝', 11: '绿', 12: '红',
+ 13: '红', 14: '蓝', 15: '蓝', 16: '绿', 17: '绿', 18: '红',
+ 19: '红', 20: '蓝', 21: '绿', 22: '绿', 23: '红', 24: '红',
+ 25: '蓝', 26: '蓝', 27: '绿', 28: '绿', 29: '红', 30: '红',
+ 31: '蓝', 32: '绿', 33: '绿', 34: '红', 35: '红', 36: '蓝',
+ 37: '蓝', 38: '绿', 39: '绿', 40: '红', 41: '蓝', 42: '蓝',
+ 43: '绿', 44: '绿', 45: '红', 46: '红', 47: '蓝', 48: '蓝',
+ 49: '绿'
+}
+
+def get_range(num):
+ """获取数字所在的区间"""
+ if 1 <= num <= 10:
+ return '小号(1-10)'
+ elif 11 <= num <= 30:
+ return '中号(11-30)'
+ else:
+ return '大号(31-49)'
+
+def get_tail(num):
+ """获取数字的尾数"""
+ return num % 10
+
+def parse_sql_file(filepath):
+ """解析SQL文件,提取历史数据"""
+ content = Path(filepath).read_text(encoding='utf-8')
+
+ # 解析INSERT语句
+ pattern = r"INSERT INTO `fa_history` VALUES \((\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), '([^']+)'"
+ matches = re.findall(pattern, content)
+
+ history = []
+ for match in matches:
+ history.append({
+ 'expect': int(match[0]),
+ 'num1': int(match[1]),
+ 'num2': int(match[2]),
+ 'num3': int(match[3]),
+ 'num4': int(match[4]),
+ 'num5': int(match[5]),
+ 'num6': int(match[6]),
+ 'num7': int(match[7]),
+ 'openTime': match[8]
+ })
+
+ # 按期号排序
+ history.sort(key=lambda x: x['expect'])
+ return history
+
+def analyze():
+ """主分析函数"""
+ # 解析数据
+ history = parse_sql_file(r'C:\Users\91611\Desktop\fa_history.sql')
+
+ print("=== 数据概览 ===")
+ print(f"总期数: {len(history)}")
+ print(f"期号范围: {history[0]['expect']} - {history[-1]['expect']}")
+ print()
+
+ total_predictions = len(history) - 1
+
+ # ============ 维度1: 上期正码平均值与下期特码的差值分布 ============
+ print("=== 维度1: 上期正码平均值与下期特码的差值分布 ===")
+
+ avg_diffs = []
+ hit_count_avg = 0
+ prediction_range = 10
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ # 计算上期正码平均值
+ avg = (current['num1'] + current['num2'] + current['num3'] +
+ current['num4'] + current['num5'] + current['num6']) / 6
+
+ # 与下期特码的差值
+ diff = next_record['num7'] - avg
+ avg_diffs.append(round(diff))
+
+ # 预测范围:平均值±10
+ predict_min = max(1, int(avg) - prediction_range)
+ predict_max = min(49, int(avg) + prediction_range)
+
+ if predict_min <= next_record['num7'] <= predict_max:
+ hit_count_avg += 1
+
+ # 统计差值分布
+ diff_stats = defaultdict(int)
+ for d in avg_diffs:
+ diff_stats[d] += 1
+
+ print("差值分布(差值=下期特码-上期正码平均值):")
+ for diff in sorted(diff_stats.keys()):
+ count = diff_stats[diff]
+ percent = round(count / total_predictions * 100, 2)
+ print(f" 差值 {diff}: {count}次 ({percent}%)")
+
+ # 差值范围分布
+ print("\n差值范围分布:")
+ ranges = [
+ ('[-40,-20]', lambda d: -40 <= d < -20),
+ ('[-20,-10]', lambda d: -20 <= d < -10),
+ ('[-10,0]', lambda d: -10 <= d < 0),
+ ('[0,10]', lambda d: 0 <= d < 10),
+ ('[10,20]', lambda d: 10 <= d < 20),
+ ('[20,40]', lambda d: 20 <= d <= 40)
+ ]
+ for range_name, condition in ranges:
+ count = sum(1 for d in avg_diffs if condition(d))
+ percent = round(count / total_predictions * 100, 2)
+ print(f" {range_name}: {count}次 ({percent}%)")
+
+ print(f"\n基于平均值±{prediction_range}范围的预测命中率: {round(hit_count_avg / total_predictions * 100, 2)}% ({hit_count_avg}/{total_predictions})")
+ print()
+
+ # ============ 维度2: 上期正码范围[min,max]与下期特码的关系 ============
+ print("=== 维度2: 上期正码范围[min,max]与下期特码的关系 ===")
+
+ in_range_count = 0
+ below_range_count = 0
+ above_range_count = 0
+ range_width_stats = defaultdict(lambda: {'在范围内': 0, '低于范围': 0, '高于范围': 0})
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ nums = [current['num1'], current['num2'], current['num3'],
+ current['num4'], current['num5'], current['num6']]
+ min_num = min(nums)
+ max_num = max(nums)
+ range_width = max_num - min_num
+
+ if min_num <= next_record['num7'] <= max_num:
+ in_range_count += 1
+ relation = '在范围内'
+ elif next_record['num7'] < min_num:
+ below_range_count += 1
+ relation = '低于范围'
+ else:
+ above_range_count += 1
+ relation = '高于范围'
+
+ range_width_stats[range_width][relation] += 1
+
+ print("下期特码位置分布:")
+ print(f" 在上期正码范围内: {in_range_count}次 ({round(in_range_count / total_predictions * 100, 2)}%)")
+ print(f" 低于上期正码范围: {below_range_count}次 ({round(below_range_count / total_predictions * 100, 2)}%)")
+ print(f" 高于上期正码范围: {above_range_count}次 ({round(above_range_count / total_predictions * 100, 2)}%)")
+
+ print("\n范围宽度与特码位置关系:")
+ for width in sorted(range_width_stats.keys()):
+ stats = range_width_stats[width]
+ width_total = sum(stats.values())
+ print(f" 范围宽度{width} (共{width_total}期):")
+ for relation, count in stats.items():
+ percent = round(count / width_total * 100, 2)
+ print(f" {relation}: {count}次 ({percent}%)")
+
+ avg_range_width = sum(max([h['num1'], h['num2'], h['num3'], h['num4'], h['num5'], h['num6']]) -
+ min([h['num1'], h['num2'], h['num3'], h['num4'], h['num5'], h['num6']])
+ for h in history[:-1]) / total_predictions
+ print(f"\n平均范围宽度: {round(avg_range_width, 2)}")
+ print()
+
+ # ============ 维度3: 上期正码与下期特码的最短距离分布 ============
+ print("=== 维度3: 上期正码与下期特码的最短距离分布 ===")
+
+ min_distances = []
+ prediction_hit_dist = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ nums = [current['num1'], current['num2'], current['num3'],
+ current['num4'], current['num5'], current['num6']]
+
+ min_dist = min(abs(next_record['num7'] - num) for num in nums)
+ min_distances.append(min_dist)
+
+ # 预测范围:每个正码±3
+ predicted = set()
+ for num in nums:
+ for p in range(max(1, num - 3), min(50, num + 4)):
+ predicted.add(p)
+
+ if next_record['num7'] in predicted:
+ prediction_hit_dist += 1
+
+ dist_stats = defaultdict(int)
+ for d in min_distances:
+ dist_stats[d] += 1
+
+ print("最短距离分布:")
+ for dist in sorted(dist_stats.keys()):
+ count = dist_stats[dist]
+ percent = round(count / total_predictions * 100, 2)
+ print(f" 距离 {dist}: {count}次 ({percent}%)")
+
+ close_hit = sum(1 for d in min_distances if d <= 5)
+ very_close_hit = sum(1 for d in min_distances if d <= 3)
+
+ print(f"\n最短距离≤5的比例: {round(close_hit / total_predictions * 100, 2)}% ({close_hit}/{total_predictions})")
+ print(f"最短距离≤3的比例: {round(very_close_hit / total_predictions * 100, 2)}% ({very_close_hit}/{total_predictions})")
+ print(f"基于正码±3范围预测命中率: {round(prediction_hit_dist / total_predictions * 100, 2)}% ({prediction_hit_dist}/{total_predictions})")
+
+ # 计算预测范围大小
+ nums_sample = [history[0]['num1'], history[0]['num2'], history[0]['num3'],
+ history[0]['num4'], history[0]['num5'], history[0]['num6']]
+ predicted_sample = set()
+ for num in nums_sample:
+ for p in range(max(1, num - 3), min(50, num + 4)):
+ predicted_sample.add(p)
+ print(f"预测范围大小: 约{len(predicted_sample)}个数字")
+ print()
+
+ # ============ 维度4: 上期正码和值尾数与下期特码尾数的关系 ============
+ print("=== 维度4: 上期正码和值尾数与下期特码尾数的关系 ===")
+
+ sum_tail_relations = defaultdict(lambda: defaultdict(int))
+ tail_same_count = 0
+ tail_diff1_count = 0
+ tail_diff2_count = 0
+ tail_prediction_hit = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ sum_val = current['num1'] + current['num2'] + current['num3'] + \
+ current['num4'] + current['num5'] + current['num6']
+ sum_tail = get_tail(sum_val)
+ next_tail = get_tail(next_record['num7'])
+
+ sum_tail_relations[sum_tail][next_tail] += 1
+
+ # 计算尾数差异(考虑环形)
+ tail_diff = abs(sum_tail - next_tail)
+ if tail_diff > 5:
+ tail_diff = 10 - tail_diff
+
+ if tail_diff == 0:
+ tail_same_count += 1
+ elif tail_diff == 1:
+ tail_diff1_count += 1
+ elif tail_diff == 2:
+ tail_diff2_count += 1
+
+ # 预测:和值尾数±2范围内的尾数
+ predict_tails = {sum_tail, (sum_tail + 1) % 10, (sum_tail - 1 + 10) % 10,
+ (sum_tail + 2) % 10, (sum_tail - 2 + 10) % 10}
+ if next_tail in predict_tails:
+ tail_prediction_hit += 1
+
+ print("尾数关系分布:")
+ print(f" 尾数相同: {tail_same_count}次 ({round(tail_same_count / total_predictions * 100, 2)}%)")
+ print(f" 尾数相差1: {tail_diff1_count}次 ({round(tail_diff1_count / total_predictions * 100, 2)}%)")
+ print(f" 尾数相差2: {tail_diff2_count}次 ({round(tail_diff2_count / total_predictions * 100, 2)}%)")
+
+ print("\n上期和值尾数→下期特码尾数转移矩阵:")
+ for sum_tail in range(10):
+ if sum_tail in sum_tail_relations:
+ stats = sum_tail_relations[sum_tail]
+ max_tail = max(stats, key=stats.get)
+ max_count = stats[max_tail]
+ total = sum(stats.values())
+ percent = round(max_count / total * 100, 2)
+ others = [f"{t}({c})" for t, c in stats.items() if t != max_tail]
+ print(f" 和值尾数{sum_tail} → 最可能尾数{max_tail} ({max_count}次, {percent}%), 其他: {', '.join(others)}")
+
+ print(f"\n基于和值尾数±2范围的尾数预测命中率: {round(tail_prediction_hit / total_predictions * 100, 2)}% ({tail_prediction_hit}/{total_predictions})")
+ print("预测范围: 5个尾数,每个尾数对应约5个数字,共约25个数字")
+ print()
+
+ # ============ 维度5: 上期正码覆盖区间与下期特码所在区间的关系 ============
+ print("=== 维度5: 上期正码覆盖区间与下期特码所在区间的关系 ===")
+
+ range_cover_stats = defaultdict(lambda: defaultdict(int))
+ range_transfer_stats = defaultdict(lambda: defaultdict(int))
+ hit_in_covered_range = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ nums = [current['num1'], current['num2'], current['num3'],
+ current['num4'], current['num5'], current['num6']]
+
+ covers = set(get_range(num) for num in nums)
+ cover_str = '+'.join(sorted(covers))
+ next_range = get_range(next_record['num7'])
+
+ range_cover_stats[cover_str][next_range] += 1
+ range_transfer_stats[len(covers)][next_range] += 1
+
+ if next_range in covers:
+ hit_in_covered_range += 1
+
+ print("上期正码覆盖区间→下期特码区间转移:")
+ for cover, next_stats in sorted(range_cover_stats.items(), key=lambda x: -sum(x[1].values())):
+ total_cover = sum(next_stats.values())
+ print(f" {cover} (共{total_cover}期):")
+ for next_range, count in next_stats.items():
+ percent = round(count / total_cover * 100, 2)
+ print(f" → {next_range}: {count}次 ({percent}%)")
+
+ print("\n上期正码覆盖区间数量与下期特码分布:")
+ for cover_count, stats in sorted(range_transfer_stats.items()):
+ total_cover = sum(stats.values())
+ print(f" 覆盖{cover_count}个区间 (共{total_cover}期):")
+ for range_name, count in stats.items():
+ percent = round(count / total_cover * 100, 2)
+ print(f" {range_name}: {count}次 ({percent}%)")
+
+ print(f"\n下期特码在上期正码覆盖区间内的比例: {round(hit_in_covered_range / total_predictions * 100, 2)}% ({hit_in_covered_range}/{total_predictions})")
+ print()
+
+ # ============ 维度6: 上期正码波色分布与下期特码波色的关系 ============
+ print("=== 维度6: 上期正码波色分布与下期特码波色的关系 ===")
+
+ color_distribution_stats = defaultdict(lambda: defaultdict(int))
+ dominant_color_stats = defaultdict(lambda: defaultdict(int))
+ dominant_prediction_hit = 0
+ expanded_color_prediction_hit = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ nums = [current['num1'], current['num2'], current['num3'],
+ current['num4'], current['num5'], current['num6']]
+
+ colors = defaultdict(int)
+ for num in nums:
+ colors[COLOR_MAP[num]] += 1
+
+ color_str = f"红{colors['红']}蓝{colors['蓝']}绿{colors['绿']}"
+ next_color = COLOR_MAP[next_record['num7']]
+
+ color_distribution_stats[color_str][next_color] += 1
+
+ # 主导波色
+ dominant_color = max(colors, key=colors.get)
+ dominant_color_stats[dominant_color][next_color] += 1
+
+ if dominant_color == next_color:
+ dominant_prediction_hit += 1
+
+ # 扩展到两种波色
+ top2_colors = sorted(colors, key=colors.get, reverse=True)[:2]
+ if next_color in top2_colors:
+ expanded_color_prediction_hit += 1
+
+ print("上期正码波色分布→下期特码波色转移 (出现5次以上的):")
+ sorted_color_dist = sorted(color_distribution_stats.items(),
+ key=lambda x: -sum(x[1].values()))
+ for dist, next_stats in sorted_color_dist:
+ total_dist = sum(next_stats.values())
+ if total_dist >= 5:
+ print(f" {dist} (共{total_dist}期):")
+ for next_color, count in next_stats.items():
+ percent = round(count / total_dist * 100, 2)
+ print(f" → {next_color}: {count}次 ({percent}%)")
+
+ print("\n上期主导波色→下期特码波色转移:")
+ for dominant, next_stats in dominant_color_stats.items():
+ total_dom = sum(next_stats.values())
+ print(f" 主导{dominant} (共{total_dom}期):")
+ for next_color, count in next_stats.items():
+ percent = round(count / total_dom * 100, 2)
+ print(f" → {next_color}: {count}次 ({percent}%)")
+
+ print(f"\n主导波色预测命中率: {round(dominant_prediction_hit / total_predictions * 100, 2)}% ({dominant_prediction_hit}/{total_predictions})")
+ print(f"扩展到两种主导波色预测命中率: {round(expanded_color_prediction_hit / total_predictions * 100, 2)}% ({expanded_color_prediction_hit}/{total_predictions})")
+ print()
+
+ # ============ 维度7: 上期特码与下期特码的转移关系 ============
+ print("=== 维度7: 上期特码与下期特码的转移关系(马尔可夫分析) ===")
+
+ special_transfer = defaultdict(lambda: defaultdict(int))
+ special_range_transfer = defaultdict(lambda: defaultdict(int))
+ special_range_prediction_hit = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ current_special = current['num7']
+ next_special = next_record['num7']
+
+ special_transfer[current_special][next_special] += 1
+
+ current_range = get_range(current_special)
+ next_range = get_range(next_special)
+ special_range_transfer[current_range][next_range] += 1
+
+ print("特码区间转移矩阵:")
+ for from_range, to_stats in special_range_transfer.items():
+ total_from = sum(to_stats.values())
+ print(f" {from_range} → :")
+ for to_range, count in to_stats.items():
+ percent = round(count / total_from * 100, 2)
+ print(f" {to_range}: {count}次 ({percent}%)")
+
+ # 高频特码转移
+ print("\n高频特码转移(出现2次以上):")
+ for from_num, to_stats in special_transfer.items():
+ for to_num, count in to_stats.items():
+ if count >= 2:
+ print(f" 特码{from_num} → 特码{to_num}: {count}次")
+
+ # 基于特码区间预测
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ current_range = get_range(current['num7'])
+ transfer_stats = special_range_transfer[current_range]
+ top_ranges = sorted(transfer_stats, key=transfer_stats.get, reverse=True)[:2]
+
+ next_range = get_range(next_record['num7'])
+ if next_range in top_ranges:
+ special_range_prediction_hit += 1
+
+ print(f"\n基于特码区间转移预测(前2区间)命中率: {round(special_range_prediction_hit / total_predictions * 100, 2)}% ({special_range_prediction_hit}/{total_predictions})")
+ print()
+
+ # ============ 综合分析 ============
+ print("=== 综合分析:寻找40%以上命中率的规律 ===")
+ print()
+
+ combined_hits = {
+ '平均值±10范围': hit_count_avg,
+ '正码±3范围': prediction_hit_dist,
+ '和值尾数±2尾数范围': tail_prediction_hit,
+ '覆盖区间预测': hit_in_covered_range,
+ '主导波色预测': dominant_prediction_hit,
+ '双波色预测': expanded_color_prediction_hit,
+ '特码区间转移': special_range_prediction_hit
+ }
+
+ print("各维度预测命中率汇总:")
+ for name, hit in combined_hits.items():
+ percent = round(hit / total_predictions * 100, 2)
+ status = '【达标】' if percent >= 40 else ''
+ print(f" {name}: {percent}% ({hit}/{total_predictions}) {status}")
+
+ # 组合预测测试
+ print("\n组合预测测试:")
+
+ combo_hits = 0
+ combo_plus_hits = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ nums = [current['num1'], current['num2'], current['num3'],
+ current['num4'], current['num5'], current['num6']]
+
+ # 方法1:平均值±15范围
+ avg = sum(nums) / 6
+ avg_range = set(range(max(1, int(avg) - 15), min(50, int(avg) + 16)))
+
+ # 方法2:正码±5范围
+ num_range = set()
+ for num in nums:
+ for p in range(max(1, num - 5), min(50, num + 6)):
+ num_range.add(p)
+
+ # 方法3:和值尾数±3范围
+ sum_val = sum(nums)
+ sum_tail = get_tail(sum_val)
+ tail_range = set()
+ for t in range(sum_tail - 3, sum_tail + 4):
+ actual_tail = (t + 10) % 10
+ for n in range(1, 50):
+ if get_tail(n) == actual_tail:
+ tail_range.add(n)
+
+ # 组合1
+ if next_record['num7'] in avg_range or next_record['num7'] in num_range:
+ combo_hits += 1
+
+ # 组合2
+ if next_record['num7'] in avg_range or next_record['num7'] in num_range or next_record['num7'] in tail_range:
+ combo_plus_hits += 1
+
+ print(f"组合1(平均值±15 OR 正码±5)命中率: {round(combo_hits / total_predictions * 100, 2)}% ({combo_hits}/{total_predictions})")
+ print(f"组合2(平均值±15 OR 正码±5 OR 尾数±3)命中率: {round(combo_plus_hits / total_predictions * 100, 2)}% ({combo_plus_hits}/{total_predictions})")
+
+ # 波色+区间组合
+ print("\n波色+区间组合预测:")
+
+ color_range_combo_hit = 0
+ color_range_combo2_hit = 0
+ last_predict_count = 0
+ last_predict_count2 = 0
+
+ for i in range(len(history) - 1):
+ current = history[i]
+ next_record = history[i + 1]
+
+ nums = [current['num1'], current['num2'], current['num3'],
+ current['num4'], current['num5'], current['num6']]
+
+ colors = defaultdict(int)
+ ranges = defaultdict(int)
+ for num in nums:
+ colors[COLOR_MAP[num]] += 1
+ ranges[get_range(num)] += 1
+
+ # 前1波色+前1区间
+ top_color = max(colors, key=colors.get)
+ top_range = max(ranges, key=ranges.get)
+
+ predict_nums = {n for n in range(1, 50) if COLOR_MAP[n] == top_color and get_range(n) == top_range}
+
+ if next_record['num7'] in predict_nums:
+ color_range_combo_hit += 1
+ last_predict_count = len(predict_nums)
+
+ # 前2波色+前2区间
+ top2_colors = sorted(colors, key=colors.get, reverse=True)[:2]
+ top2_ranges = sorted(ranges, key=ranges.get, reverse=True)[:2]
+
+ predict_nums2 = {n for n in range(1, 50)
+ if COLOR_MAP[n] in top2_colors and get_range(n) in top2_ranges}
+
+ if next_record['num7'] in predict_nums2:
+ color_range_combo2_hit += 1
+ last_predict_count2 = len(predict_nums2)
+
+ print(f"波色+区间交集预测命中率: {round(color_range_combo_hit / total_predictions * 100, 2)}% ({color_range_combo_hit}/{total_predictions})")
+ print(f"预测范围大小: {last_predict_count}个数字")
+ print(f"前2波色+前2区间交集预测命中率: {round(color_range_combo2_hit / total_predictions * 100, 2)}% ({color_range_combo2_hit}/{total_predictions})")
+ print(f"预测范围大小: {last_predict_count2}个数字")
+
+ print()
+
+ # ============ 总结 ============
+ print("=== 总结:达到40%以上命中率的规律 ===")
+ print()
+
+ high_hit_rules = []
+ for name, hit in combined_hits.items():
+ percent = round(hit / total_predictions * 100, 2)
+ if percent >= 40:
+ high_hit_rules.append((name, percent, hit, total_predictions))
+
+ if high_hit_rules:
+ for name, percent, hit, total in high_hit_rules:
+ print(f"【{name}】命中率: {percent}% ({hit}/{total})")
+ else:
+ print("单维度分析中没有达到40%以上命中率的规律")
+
+ print("\n组合规律命中率:")
+ print(f"组合1(平均值±15 OR 正码±5): {round(combo_hits / total_predictions * 100, 2)}%")
+ print(f"组合2(平均值±15 OR 正码±5 OR 尾数±3): {round(combo_plus_hits / total_predictions * 100, 2)}%")
+ print(f"前2波色+前2区间交集: {round(color_range_combo2_hit / total_predictions * 100, 2)}%")
+
+ print("\n分析完成!")
+
+if __name__ == '__main__':
+ analyze()
\ No newline at end of file
diff --git a/analysis_history.php b/analysis_history.php
new file mode 100644
index 0000000..c9f805b
--- /dev/null
+++ b/analysis_history.php
@@ -0,0 +1,395 @@
+ '红', 2 => '红', 3 => '蓝', 4 => '蓝', 5 => '绿', 6 => '绿',
+ 7 => '红', 8 => '红', 9 => '蓝', 10 => '蓝', 11 => '绿', 12 => '红',
+ 13 => '红', 14 => '蓝', 15 => '蓝', 16 => '绿', 17 => '绿', 18 => '红',
+ 19 => '红', 20 => '蓝', 21 => '绿', 22 => '绿', 23 => '红', 24 => '红',
+ 25 => '蓝', 26 => '蓝', 27 => '绿', 28 => '绿', 29 => '红', 30 => '红',
+ 31 => '蓝', 32 => '绿', 33 => '绿', 34 => '红', 35 => '红', 36 => '蓝',
+ 37 => '蓝', 38 => '绿', 39 => '绿', 40 => '红', 41 => '蓝', 42 => '蓝',
+ 43 => '绿', 44 => '绿', 45 => '红', 46 => '红', 47 => '蓝', 48 => '蓝',
+ 49 => '绿'
+];
+
+// 生肖映射表
+$animalMap = [
+ 1 => '马', 2 => '蛇', 3 => '龙', 4 => '兔', 5 => '虎', 6 => '牛',
+ 7 => '鼠', 8 => '猪', 9 => '狗', 10 => '鸡', 11 => '猴', 12 => '羊',
+ 13 => '马', 14 => '蛇', 15 => '龙', 16 => '兔', 17 => '虎', 18 => '牛',
+ 19 => '鼠', 20 => '猪', 21 => '狗', 22 => '鸡', 23 => '猴', 24 => '羊',
+ 25 => '马', 26 => '蛇', 27 => '龙', 28 => '兔', 29 => '虎', 30 => '牛',
+ 31 => '鼠', 32 => '猪', 33 => '狗', 34 => '鸡', 35 => '猴', 36 => '羊',
+ 37 => '马', 38 => '蛇', 39 => '龙', 40 => '兔', 41 => '虎', 42 => '牛',
+ 43 => '鼠', 44 => '猪', 45 => '狗', 46 => '鸡', 47 => '猴', 48 => '羊',
+ 49 => '马'
+];
+
+/**
+ * 获取数字所在的区间 (1-10, 11-20, 21-30, 31-40, 41-49)
+ */
+function getZone($num) {
+ if ($num >= 1 && $num <= 10) return 1;
+ if ($num >= 11 && $num <= 20) return 2;
+ if ($num >= 21 && $num <= 30) return 3;
+ if ($num >= 31 && $num <= 40) return 4;
+ if ($num >= 41 && $num <= 49) return 5;
+ return 0;
+}
+
+/**
+ * 计算特码到最近正码的距离
+ */
+function getMinDistance($sortedNums, $num7) {
+ $minDist = 49;
+ foreach ($sortedNums as $num) {
+ $dist = abs($num7 - $num);
+ if ($dist < $minDist) {
+ $minDist = $dist;
+ }
+ }
+ return $minDist;
+}
+
+// 解析SQL文件中的数据
+$sqlFile = 'C:\Users\91611\Desktop\fa_history.sql';
+$content = file_get_contents($sqlFile);
+
+// 提取INSERT语句中的数据
+$pattern = '/INSERT INTO `fa_history` VALUES \((\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), \'([^\']+)\'\);/';
+preg_match_all($pattern, $content, $matches);
+
+$data = [];
+for ($i = 0; $i < count($matches[0]); $i++) {
+ $data[] = [
+ 'expect' => (int)$matches[1][$i],
+ 'num1' => (int)$matches[2][$i],
+ 'num2' => (int)$matches[3][$i],
+ 'num3' => (int)$matches[4][$i],
+ 'num4' => (int)$matches[5][$i],
+ 'num5' => (int)$matches[6][$i],
+ 'num6' => (int)$matches[7][$i],
+ 'num7' => (int)$matches[8][$i],
+ 'openTime' => $matches[9][$i]
+ ];
+}
+
+$total = count($data);
+echo "==================== 正码与特码关联规律统计分析 ====================\n";
+echo "数据总量: {$total} 期 (从 {$data[0]['expect']} 到 {$data[$total-1]['expect']})\n\n";
+
+// ==================== 1. 正码平均值与特码差值分布 ====================
+echo "==================== 1. 正码平均值与特码差值分布 ====================\n";
+
+$diffCounts = [];
+$inRange5 = 0;
+
+foreach ($data as $row) {
+ $avg = ($row['num1'] + $row['num2'] + $row['num3'] + $row['num4'] + $row['num5'] + $row['num6']) / 6;
+ $diff = round($row['num7'] - $avg); // 四舍五入
+
+ $diffKey = $diff;
+ if (!isset($diffCounts[$diffKey])) {
+ $diffCounts[$diffKey] = 0;
+ }
+ $diffCounts[$diffKey]++;
+
+ if ($diff >= -5 && $diff <= 5) {
+ $inRange5++;
+ }
+}
+
+// 按差值排序
+ksort($diffCounts);
+
+echo "差值分布统计:\n";
+foreach ($diffCounts as $diff => $count) {
+ $pct = round($count / $total * 100, 2);
+ echo " 差值 {$diff}: {$count} 次 ({$pct}%)\n";
+}
+
+echo "\n差值在 [-5, +5] 范围内的概率: " . round($inRange5 / $total * 100, 2) . "% ($inRange5/$total)\n";
+
+// 找出高频差值区间 (>5%)
+echo "\n高频差值区间 (>5%):\n";
+foreach ($diffCounts as $diff => $count) {
+ $pct = round($count / $total * 100, 2);
+ if ($pct > 5) {
+ echo " 差值 {$diff}: {$pct}%\n";
+ }
+}
+
+// ==================== 2. 特码是否在正码范围内 ====================
+echo "\n==================== 2. 特码是否在正码范围内 ====================\n";
+
+$inRange = 0;
+$outRange = 0;
+
+foreach ($data as $row) {
+ $nums = [$row['num1'], $row['num2'], $row['num3'], $row['num4'], $row['num5'], $row['num6']];
+ $min = min($nums);
+ $max = max($nums);
+
+ if ($row['num7'] >= $min && $row['num7'] <= $max) {
+ $inRange++;
+ } else {
+ $outRange++;
+ }
+}
+
+echo "特码在正码范围内 [min(num1-6), max(num1-6)]:\n";
+echo " 是: {$inRange} 次 (" . round($inRange / $total * 100, 2) . "%)\n";
+echo " 否: {$outRange} 次 (" . round($outRange / $total * 100, 2) . "%)\n";
+
+// ==================== 3. 特码与最近正码的距离分布 ====================
+echo "\n==================== 3. 特码与最近正码的距离分布 ====================\n";
+
+$distCounts = [];
+$equalCount = 0; // 特码等于某正码
+
+foreach ($data as $row) {
+ $nums = [$row['num1'], $row['num2'], $row['num3'], $row['num4'], $row['num5'], $row['num6']];
+ sort($nums);
+
+ $minDist = getMinDistance($nums, $row['num7']);
+
+ if (!isset($distCounts[$minDist])) {
+ $distCounts[$minDist] = 0;
+ }
+ $distCounts[$minDist]++;
+
+ if ($minDist == 0) {
+ $equalCount++;
+ }
+}
+
+ksort($distCounts);
+
+echo "距离分布统计:\n";
+foreach ($distCounts as $dist => $count) {
+ $pct = round($count / $total * 100, 2);
+ echo " 距离 {$dist}: {$count} 次 ({$pct}%)\n";
+}
+
+echo "\n特码等于某正码 (距离=0) 的概率: " . round($equalCount / $total * 100, 2) . "% ($equalCount/$total)\n";
+
+// 距离<=5的概率
+$distLE5 = 0;
+for ($i = 0; $i <= 5; $i++) {
+ if (isset($distCounts[$i])) {
+ $distLE5 += $distCounts[$i];
+ }
+}
+echo "距离 <= 5 的概率: " . round($distLE5 / $total * 100, 2) . "% ($distLE5/$total)\n";
+
+// ==================== 4. 和值尾数关系 ====================
+echo "\n==================== 4. 和值尾数关系 ====================\n";
+
+$sameTail = 0;
+$tailDiffCounts = [];
+
+foreach ($data as $row) {
+ $sum = $row['num1'] + $row['num2'] + $row['num3'] + $row['num4'] + $row['num5'] + $row['num6'];
+ $sumTail = $sum % 10;
+ $num7Tail = $row['num7'] % 10;
+
+ $tailDiff = abs($sumTail - $num7Tail);
+
+ if (!isset($tailDiffCounts[$tailDiff])) {
+ $tailDiffCounts[$tailDiff] = 0;
+ }
+ $tailDiffCounts[$tailDiff]++;
+
+ if ($sumTail == $num7Tail) {
+ $sameTail++;
+ }
+}
+
+ksort($tailDiffCounts);
+
+echo "和值尾数与特码尾数同尾概率: " . round($sameTail / $total * 100, 2) . "% ($sameTail/$total)\n";
+echo "\n尾数差值分布:\n";
+foreach ($tailDiffCounts as $diff => $count) {
+ $pct = round($count / $total * 100, 2);
+ echo " 尾数差 {$diff}: {$count} 次 ({$pct}%)\n";
+}
+
+// 尾数差 <= 3 的概率
+$tailDiffLE3 = 0;
+for ($i = 0; $i <= 3; $i++) {
+ if (isset($tailDiffCounts[$i])) {
+ $tailDiffLE3 += $tailDiffCounts[$i];
+ }
+}
+echo "\n尾数差 <= 3 的概率: " . round($tailDiffLE3 / $total * 100, 2) . "% ($tailDiffLE3/$total)\n";
+
+// ==================== 5. 区间覆盖分析 ====================
+echo "\n==================== 5. 区间覆盖分析 ====================\n";
+
+$zoneCoveredCounts = [0, 0, 0, 0, 0, 0]; // 覆盖0-5个区间
+$zone7Covered = 0; // 特码所在区间被正码覆盖
+
+foreach ($data as $row) {
+ $nums = [$row['num1'], $row['num2'], $row['num3'], $row['num4'], $row['num5'], $row['num6']];
+ $zones = [];
+ foreach ($nums as $num) {
+ $zone = getZone($num);
+ $zones[$zone] = true;
+ }
+
+ $coverCount = count($zones);
+ $zoneCoveredCounts[$coverCount]++;
+
+ $zone7 = getZone($row['num7']);
+ if (isset($zones[$zone7])) {
+ $zone7Covered++;
+ }
+}
+
+echo "正码覆盖区间数量分布:\n";
+for ($i = 0; $i <= 5; $i++) {
+ $count = $zoneCoveredCounts[$i];
+ $pct = round($count / $total * 100, 2);
+ echo " 覆盖 {$i} 个区间: {$count} 次 ({$pct}%)\n";
+}
+
+echo "\n特码所在区间被正码覆盖的概率: " . round($zone7Covered / $total * 100, 2) . "% ($zone7Covered/$total)\n";
+
+// ==================== 6. 波色/生肖关联 ====================
+echo "\n==================== 6. 波色/生肖关联 ====================\n";
+
+$color7InNums = 0; // 特码波色在正码中出现过
+$animal7InNums = 0; // 特码生肖在正码中出现过
+
+$colorMatchCounts = []; // 正码中某波色数量与特码波色匹配情况
+$color7Counts = ['红' => 0, '蓝' => 0, '绿' => 0]; // 特码波色分布
+
+foreach ($data as $row) {
+ $nums = [$row['num1'], $row['num2'], $row['num3'], $row['num4'], $row['num5'], $row['num6']];
+
+ $numColors = [];
+ $numAnimals = [];
+ $colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0];
+
+ foreach ($nums as $num) {
+ $color = $colorMap[$num];
+ $animal = $animalMap[$num];
+ $numColors[$color] = true;
+ $numAnimals[$animal] = true;
+ $colorCounts[$color]++;
+ }
+
+ $color7 = $colorMap[$row['num7']];
+ $animal7 = $animalMap[$row['num7']];
+
+ $color7Counts[$color7]++;
+
+ // 特码波色是否在正码中出现过
+ if (isset($numColors[$color7])) {
+ $color7InNums++;
+ }
+
+ // 特码生肖是否在正码中出现过
+ if (isset($numAnimals[$animal7])) {
+ $animal7InNums++;
+ }
+
+ // 正码中某波色数量与特码波色
+ $key = $colorCounts[$color7] . '_' . $color7;
+ if (!isset($colorMatchCounts[$key])) {
+ $colorMatchCounts[$key] = 0;
+ }
+ $colorMatchCounts[$key]++;
+}
+
+echo "特码波色分布:\n";
+foreach ($color7Counts as $color => $count) {
+ $pct = round($count / $total * 100, 2);
+ echo " {$color}: {$count} 次 ({$pct}%)\n";
+}
+
+echo "\n特码波色在正码中出现的概率: " . round($color7InNums / $total * 100, 2) . "% ($color7InNums/$total)\n";
+echo "特码生肖在正码中出现的概率: " . round($animal7InNums / $total * 100, 2) . "% ($animal7InNums/$total)\n";
+
+echo "\n正码中特码同波色数量分布:\n";
+ksort($colorMatchCounts);
+foreach ($colorMatchCounts as $key => $count) {
+ $pct = round($count / $total * 100, 2);
+ echo " {$key}: {$count} 次 ({$pct}%)\n";
+}
+
+// ==================== 总结: 高命中率规律 ====================
+echo "\n==================== 总结: 可能达到40%命中率以上的规律 ====================\n";
+
+$highHitRules = [];
+
+// 检查各规律
+if ($inRange5 / $total >= 0.4) {
+ $highHitRules[] = "差值在[-5,+5]: " . round($inRange5 / $total * 100, 2) . "%";
+}
+
+if ($distLE5 / $total >= 0.4) {
+ $highHitRules[] = "距离<=5: " . round($distLE5 / $total * 100, 2) . "%";
+}
+
+if ($zone7Covered / $total >= 0.4) {
+ $highHitRules[] = "特码区间被正码覆盖: " . round($zone7Covered / $total * 100, 2) . "%";
+}
+
+if ($color7InNums / $total >= 0.4) {
+ $highHitRules[] = "特码波色在正码中出现: " . round($color7InNums / $total * 100, 2) . "%";
+}
+
+if ($animal7InNums / $total >= 0.4) {
+ $highHitRules[] = "特码生肖在正码中出现: " . round($animal7InNums / $total * 100, 2) . "%";
+}
+
+if ($tailDiffLE3 / $total >= 0.4) {
+ $highHitRules[] = "尾数差<=3: " . round($tailDiffLE3 / $total * 100, 2) . "%";
+}
+
+if (count($highHitRules) > 0) {
+ foreach ($highHitRules as $rule) {
+ echo "- {$rule}\n";
+ }
+} else {
+ echo "没有发现明显超过40%命中率的规律,以下是接近40%的规律:\n";
+
+ $allRules = [
+ "差值在[-5,+5]" => $inRange5 / $total,
+ "距离<=5" => $distLE5 / $total,
+ "距离<=10" => 0,
+ "特码区间被正码覆盖" => $zone7Covered / $total,
+ "特码波色在正码中出现" => $color7InNums / $total,
+ "特码生肖在正码中出现" => $animal7InNums / $total,
+ "尾数差<=3" => $tailDiffLE3 / $total,
+ ];
+
+ // 计算距离<=10
+ $distLE10 = 0;
+ for ($i = 0; $i <= 10; $i++) {
+ if (isset($distCounts[$i])) {
+ $distLE10 += $distCounts[$i];
+ }
+ }
+ $allRules["距离<=10"] = $distLE10 / $total;
+
+ arsort($allRules);
+ foreach ($allRules as $rule => $rate) {
+ echo "- {$rule}: " . round($rate * 100, 2) . "%\n";
+ }
+}
+
+echo "\n==================== 分析完成 ====================\n";
\ No newline at end of file
diff --git a/analysis_history.py b/analysis_history.py
new file mode 100644
index 0000000..4c8de4c
--- /dev/null
+++ b/analysis_history.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+正码与特码关联规律统计分析脚本
+"""
+
+import re
+from collections import defaultdict
+
+# 波色映射表
+color_map = {
+ 1: '红', 2: '红', 3: '蓝', 4: '蓝', 5: '绿', 6: '绿',
+ 7: '红', 8: '红', 9: '蓝', 10: '蓝', 11: '绿', 12: '红',
+ 13: '红', 14: '蓝', 15: '蓝', 16: '绿', 17: '绿', 18: '红',
+ 19: '红', 20: '蓝', 21: '绿', 22: '绿', 23: '红', 24: '红',
+ 25: '蓝', 26: '蓝', 27: '绿', 28: '绿', 29: '红', 30: '红',
+ 31: '蓝', 32: '绿', 33: '绿', 34: '红', 35: '红', 36: '蓝',
+ 37: '蓝', 38: '绿', 39: '绿', 40: '红', 41: '蓝', 42: '蓝',
+ 43: '绿', 44: '绿', 45: '红', 46: '红', 47: '蓝', 48: '蓝',
+ 49: '绿'
+}
+
+# 生肖映射表
+animal_map = {
+ 1: '马', 2: '蛇', 3: '龙', 4: '兔', 5: '虎', 6: '牛',
+ 7: '鼠', 8: '猪', 9: '狗', 10: '鸡', 11: '猴', 12: '羊',
+ 13: '马', 14: '蛇', 15: '龙', 16: '兔', 17: '虎', 18: '牛',
+ 19: '鼠', 20: '猪', 21: '狗', 22: '鸡', 23: '猴', 24: '羊',
+ 25: '马', 26: '蛇', 27: '龙', 28: '兔', 29: '虎', 30: '牛',
+ 31: '鼠', 32: '猪', 33: '狗', 34: '鸡', 35: '猴', 36: '羊',
+ 37: '马', 38: '蛇', 39: '龙', 40: '兔', 41: '虎', 42: '牛',
+ 43: '鼠', 44: '猪', 45: '狗', 46: '鸡', 47: '猴', 48: '羊',
+ 49: '马'
+}
+
+def get_zone(num):
+ if 1 <= num <= 10: return 1
+ elif 11 <= num <= 20: return 2
+ elif 21 <= num <= 30: return 3
+ elif 31 <= num <= 40: return 4
+ elif 41 <= num <= 49: return 5
+ return 0
+
+def get_min_distance(nums, num7):
+ return min(abs(num7 - num) for num in nums)
+
+# 解析SQL文件
+sql_file = r'C:\Users\91611\Desktop\fa_history.sql'
+with open(sql_file, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+pattern = r"INSERT INTO `fa_history` VALUES \((\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), (\d+), '([^']+)'\);"
+matches = re.findall(pattern, content)
+
+data = []
+for match in matches:
+ data.append({
+ 'expect': int(match[0]),
+ 'num1': int(match[1]), 'num2': int(match[2]), 'num3': int(match[3]),
+ 'num4': int(match[4]), 'num5': int(match[5]), 'num6': int(match[6]),
+ 'num7': int(match[7]), 'openTime': match[8]
+ })
+
+total = len(data)
+
+# 输出到文件
+output_file = r'D:\code\php\amlhc\analysis_result.txt'
+with open(output_file, 'w', encoding='utf-8') as f:
+ f.write("==================== 正码与特码关联规律统计分析 ====================\n")
+ f.write(f"数据总量: {total} 期 (从 {data[0]['expect']} 到 {data[-1]['expect']})\n\n")
+
+ # ==================== 1. 正码平均值与特码差值分布 ====================
+ f.write("==================== 1. 正码平均值与特码差值分布 ====================\n")
+
+ diff_counts = defaultdict(int)
+ in_range_5 = 0
+
+ for row in data:
+ avg = (row['num1'] + row['num2'] + row['num3'] + row['num4'] + row['num5'] + row['num6']) / 6
+ diff = round(row['num7'] - avg)
+ diff_counts[diff] += 1
+ if -5 <= diff <= 5:
+ in_range_5 += 1
+
+ f.write("差值分布统计:\n")
+ for diff in sorted(diff_counts.keys()):
+ count = diff_counts[diff]
+ pct = round(count / total * 100, 2)
+ f.write(f" 差值 {diff}: {count} 次 ({pct}%)\n")
+
+ f.write(f"\n差值在 [-5, +5] 范围内的概率: {round(in_range_5 / total * 100, 2)}% ({in_range_5}/{total})\n")
+
+ in_range_10 = sum(diff_counts[d] for d in diff_counts if -10 <= d <= 10)
+ f.write(f"差值在 [-10, +10] 范围内的概率: {round(in_range_10 / total * 100, 2)}% ({in_range_10}/{total})\n")
+
+ # ==================== 2. 特码是否在正码范围内 ====================
+ f.write("\n==================== 2. 特码是否在正码范围内 ====================\n")
+
+ in_range = 0
+ for row in data:
+ nums = [row['num1'], row['num2'], row['num3'], row['num4'], row['num5'], row['num6']]
+ if min(nums) <= row['num7'] <= max(nums):
+ in_range += 1
+
+ f.write("特码在正码范围内 [min(num1-6), max(num1-6)]:\n")
+ f.write(f" 是: {in_range} 次 ({round(in_range / total * 100, 2)}%)\n")
+ f.write(f" 否: {total - in_range} 次 ({round((total - in_range) / total * 100, 2)}%)\n")
+
+ # ==================== 3. 特码与最近正码的距离分布 ====================
+ f.write("\n==================== 3. 特码与最近正码的距离分布 ====================\n")
+
+ dist_counts = defaultdict(int)
+ for row in data:
+ nums = [row['num1'], row['num2'], row['num3'], row['num4'], row['num5'], row['num6']]
+ min_dist = get_min_distance(nums, row['num7'])
+ dist_counts[min_dist] += 1
+
+ f.write("距离分布统计:\n")
+ for dist in sorted(dist_counts.keys()):
+ count = dist_counts[dist]
+ pct = round(count / total * 100, 2)
+ f.write(f" 距离 {dist}: {count} 次 ({pct}%)\n")
+
+ equal_count = dist_counts.get(0, 0)
+ dist_le5 = sum(dist_counts[i] for i in range(0, 6) if i in dist_counts)
+ dist_le10 = sum(dist_counts[i] for i in range(0, 11) if i in dist_counts)
+ dist_le15 = sum(dist_counts[i] for i in range(0, 16) if i in dist_counts)
+
+ f.write(f"\n特码等于某正码 (距离=0) 的概率: {round(equal_count / total * 100, 2)}% ({equal_count}/{total})\n")
+ f.write(f"距离 <= 5 的概率: {round(dist_le5 / total * 100, 2)}% ({dist_le5}/{total})\n")
+ f.write(f"距离 <= 10 的概率: {round(dist_le10 / total * 100, 2)}% ({dist_le10}/{total})\n")
+ f.write(f"距离 <= 15 的概率: {round(dist_le15 / total * 100, 2)}% ({dist_le15}/{total})\n")
+
+ # ==================== 4. 和值尾数关系 ====================
+ f.write("\n==================== 4. 和值尾数关系 ====================\n")
+
+ same_tail = 0
+ tail_diff_counts = defaultdict(int)
+
+ for row in data:
+ sum_val = row['num1'] + row['num2'] + row['num3'] + row['num4'] + row['num5'] + row['num6']
+ sum_tail = sum_val % 10
+ num7_tail = row['num7'] % 10
+ tail_diff = abs(sum_tail - num7_tail)
+ tail_diff_counts[tail_diff] += 1
+ if sum_tail == num7_tail:
+ same_tail += 1
+
+ f.write(f"和值尾数与特码尾数同尾概率: {round(same_tail / total * 100, 2)}% ({same_tail}/{total})\n")
+ f.write("\n尾数差值分布:\n")
+ for diff in sorted(tail_diff_counts.keys()):
+ count = tail_diff_counts[diff]
+ pct = round(count / total * 100, 2)
+ f.write(f" 尾数差 {diff}: {count} 次 ({pct}%)\n")
+
+ tail_diff_le3 = sum(tail_diff_counts[i] for i in range(0, 4) if i in tail_diff_counts)
+ f.write(f"\n尾数差 <= 3 的概率: {round(tail_diff_le3 / total * 100, 2)}% ({tail_diff_le3}/{total})\n")
+
+ # ==================== 5. 区间覆盖分析 ====================
+ f.write("\n==================== 5. 区间覆盖分析 ====================\n")
+
+ zone_covered_counts = defaultdict(int)
+ zone7_covered = 0
+
+ for row in data:
+ nums = [row['num1'], row['num2'], row['num3'], row['num4'], row['num5'], row['num6']]
+ zones = set(get_zone(num) for num in nums)
+ zone_covered_counts[len(zones)] += 1
+ zone7 = get_zone(row['num7'])
+ if zone7 in zones:
+ zone7_covered += 1
+
+ f.write("正码覆盖区间数量分布:\n")
+ for i in range(1, 6):
+ count = zone_covered_counts.get(i, 0)
+ pct = round(count / total * 100, 2)
+ f.write(f" 覆盖 {i} 个区间: {count} 次 ({pct}%)\n")
+
+ f.write(f"\n特码所在区间被正码覆盖的概率: {round(zone7_covered / total * 100, 2)}% ({zone7_covered}/{total})\n")
+
+ # ==================== 6. 波色/生肖关联 ====================
+ f.write("\n==================== 6. 波色/生肖关联 ====================\n")
+
+ color7_in_nums = 0
+ animal7_in_nums = 0
+ color_match_counts = defaultdict(int)
+ color7_counts = defaultdict(int)
+
+ for row in data:
+ nums = [row['num1'], row['num2'], row['num3'], row['num4'], row['num5'], row['num6']]
+ num_colors = set()
+ num_animals = set()
+ color_counts = defaultdict(int)
+
+ for num in nums:
+ color = color_map[num]
+ animal = animal_map[num]
+ num_colors.add(color)
+ num_animals.add(animal)
+ color_counts[color] += 1
+
+ color7 = color_map[row['num7']]
+ animal7 = animal_map[row['num7']]
+ color7_counts[color7] += 1
+
+ if color7 in num_colors:
+ color7_in_nums += 1
+ if animal7 in num_animals:
+ animal7_in_nums += 1
+
+ key = f"{color_counts[color7]}_{color7}"
+ color_match_counts[key] += 1
+
+ f.write("特码波色分布:\n")
+ for color in ['红', '蓝', '绿']:
+ count = color7_counts[color]
+ pct = round(count / total * 100, 2)
+ f.write(f" {color}: {count} 次 ({pct}%)\n")
+
+ f.write(f"\n特码波色在正码中出现的概率: {round(color7_in_nums / total * 100, 2)}% ({color7_in_nums}/{total})\n")
+ f.write(f"特码生肖在正码中出现的概率: {round(animal7_in_nums / total * 100, 2)}% ({animal7_in_nums}/{total})\n")
+
+ f.write("\n正码中特码同波色数量分布:\n")
+ for key in sorted(color_match_counts.keys()):
+ count = color_match_counts[key]
+ pct = round(count / total * 100, 2)
+ f.write(f" {key}: {count} 次 ({pct}%)\n")
+
+ # ==================== 总结 ====================
+ f.write("\n==================== 总结: 达到40%命中率以上的规律 ====================\n")
+
+ all_rules = {
+ "距离<=15": dist_le15 / total,
+ "距离<=10": dist_le10 / total,
+ "特码波色在正码中出现": color7_in_nums / total,
+ "距离<=5": dist_le5 / total,
+ "特码区间被正码覆盖": zone7_covered / total,
+ "特码在正码范围内": in_range / total,
+ "尾数差<=3": tail_diff_le3 / total,
+ "差值在[-10,+10]": in_range_10 / total,
+ "差值在[-5,+5]": in_range_5 / total,
+ "同尾": same_tail / total,
+ "特码生肖在正码中出现": animal7_in_nums / total,
+ }
+
+ sorted_rules = sorted(all_rules.items(), key=lambda x: x[1], reverse=True)
+
+ f.write("规律按命中率排序:\n")
+ for rule, rate in sorted_rules:
+ status = "[达标]" if rate >= 0.4 else ""
+ f.write(f" - {rule}: {round(rate * 100, 2)}% {status}\n")
+
+ f.write("\n==================== 关键发现 ====================\n")
+ f.write(f"""
+1. 【特码波色重复规律】命中率最高 90.67%
+ - 特码波色在正码中出现的概率约为 90.67%
+ - 如果正码中有红色号码,特码有90%概率是红色波色
+
+2. 【近距离规律】命中率很高 94.13%
+ - 特码距离最近正码<=10的概率约为 94.13%
+ - 特码往往不会离正码太远,基本在10个数字以内
+
+3. 【区间覆盖规律】命中率较高 74.13%
+ - 特码所在区间被正码覆盖的概率约为 74.13%
+ - 将1-49分为5区间,特码有74%概率落在正码覆盖的区间
+
+4. 【正码范围规律】命中率中等 70.67%
+ - 特码在正码[min, max]范围内的概率约为 70.67%
+ - 特码有70%概率落在正码的最小值和最大值之间
+
+5. 【尾数差规律】命中率54.13%
+ - 和值尾数与特码尾数差<=3的概率约为 54.13%
+ - 特码尾数与正码和值尾数相差不超过3
+
+6. 【生肖重复规律】命中率较低 36.27%
+ - 特码生肖在正码中出现的概率约为 36.27%
+ - 生肖关联性不如波色明显
+
+7. 【特码等于正码】命中率极低 0%
+ - 特码等于某正码的概率为 0%
+ - 特码与正码完全不重复(六合彩规则)
+""")
+
+ f.write("\n==================== 分析完成 ====================\n")
+
+print(f"分析完成,结果已保存到: {output_file}")
\ No newline at end of file
diff --git a/analysis_result.txt b/analysis_result.txt
new file mode 100644
index 0000000..fcd748c
--- /dev/null
+++ b/analysis_result.txt
@@ -0,0 +1,204 @@
+==================== 正码与特码关联规律统计分析 ====================
+数据总量: 375 期 (从 2025111 到 2026120)
+
+==================== 1. 正码平均值与特码差值分布 ====================
+差值分布统计:
+ 差值 -33: 1 次 (0.27%)
+ 差值 -32: 2 次 (0.53%)
+ 差值 -31: 1 次 (0.27%)
+ 差值 -30: 3 次 (0.8%)
+ 差值 -29: 3 次 (0.8%)
+ 差值 -28: 1 次 (0.27%)
+ 差值 -27: 3 次 (0.8%)
+ 差值 -26: 2 次 (0.53%)
+ 差值 -25: 4 次 (1.07%)
+ 差值 -24: 5 次 (1.33%)
+ 差值 -23: 3 次 (0.8%)
+ 差值 -22: 4 次 (1.07%)
+ 差值 -21: 4 次 (1.07%)
+ 差值 -20: 8 次 (2.13%)
+ 差值 -19: 9 次 (2.4%)
+ 差值 -18: 5 次 (1.33%)
+ 差值 -17: 10 次 (2.67%)
+ 差值 -16: 9 次 (2.4%)
+ 差值 -15: 3 次 (0.8%)
+ 差值 -14: 13 次 (3.47%)
+ 差值 -13: 8 次 (2.13%)
+ 差值 -12: 9 次 (2.4%)
+ 差值 -11: 8 次 (2.13%)
+ 差值 -10: 9 次 (2.4%)
+ 差值 -9: 4 次 (1.07%)
+ 差值 -8: 13 次 (3.47%)
+ 差值 -7: 9 次 (2.4%)
+ 差值 -6: 10 次 (2.67%)
+ 差值 -5: 5 次 (1.33%)
+ 差值 -4: 5 次 (1.33%)
+ 差值 -3: 2 次 (0.53%)
+ 差值 -2: 8 次 (2.13%)
+ 差值 -1: 4 次 (1.07%)
+ 差值 0: 12 次 (3.2%)
+ 差值 1: 6 次 (1.6%)
+ 差值 2: 11 次 (2.93%)
+ 差值 3: 5 次 (1.33%)
+ 差值 4: 11 次 (2.93%)
+ 差值 5: 3 次 (0.8%)
+ 差值 6: 7 次 (1.87%)
+ 差值 7: 8 次 (2.13%)
+ 差值 8: 8 次 (2.13%)
+ 差值 9: 4 次 (1.07%)
+ 差值 10: 6 次 (1.6%)
+ 差值 11: 5 次 (1.33%)
+ 差值 12: 7 次 (1.87%)
+ 差值 13: 4 次 (1.07%)
+ 差值 14: 9 次 (2.4%)
+ 差值 15: 4 次 (1.07%)
+ 差值 16: 6 次 (1.6%)
+ 差值 17: 6 次 (1.6%)
+ 差值 18: 18 次 (4.8%)
+ 差值 19: 5 次 (1.33%)
+ 差值 20: 6 次 (1.6%)
+ 差值 21: 11 次 (2.93%)
+ 差值 22: 8 次 (2.13%)
+ 差值 23: 4 次 (1.07%)
+ 差值 24: 2 次 (0.53%)
+ 差值 25: 1 次 (0.27%)
+ 差值 26: 2 次 (0.53%)
+ 差值 27: 3 次 (0.8%)
+ 差值 28: 2 次 (0.53%)
+ 差值 29: 1 次 (0.27%)
+ 差值 31: 2 次 (0.53%)
+ 差值 32: 1 次 (0.27%)
+
+差值在 [-5, +5] 范围内的概率: 19.2% (72/375)
+差值在 [-10, +10] 范围内的概率: 40.0% (150/375)
+
+==================== 2. 特码是否在正码范围内 ====================
+特码在正码范围内 [min(num1-6), max(num1-6)]:
+ 是: 265 次 (70.67%)
+ 否: 110 次 (29.33%)
+
+==================== 3. 特码与最近正码的距离分布 ====================
+距离分布统计:
+ 距离 1: 103 次 (27.47%)
+ 距离 2: 62 次 (16.53%)
+ 距离 3: 56 次 (14.93%)
+ 距离 4: 37 次 (9.87%)
+ 距离 5: 38 次 (10.13%)
+ 距离 6: 20 次 (5.33%)
+ 距离 7: 12 次 (3.2%)
+ 距离 8: 12 次 (3.2%)
+ 距离 9: 7 次 (1.87%)
+ 距离 10: 6 次 (1.6%)
+ 距离 11: 6 次 (1.6%)
+ 距离 12: 5 次 (1.33%)
+ 距离 13: 4 次 (1.07%)
+ 距离 14: 4 次 (1.07%)
+ 距离 15: 1 次 (0.27%)
+ 距离 20: 1 次 (0.27%)
+ 距离 21: 1 次 (0.27%)
+
+特码等于某正码 (距离=0) 的概率: 0.0% (0/375)
+距离 <= 5 的概率: 78.93% (296/375)
+距离 <= 10 的概率: 94.13% (353/375)
+距离 <= 15 的概率: 99.47% (373/375)
+
+==================== 4. 和值尾数关系 ====================
+和值尾数与特码尾数同尾概率: 8.27% (31/375)
+
+尾数差值分布:
+ 尾数差 0: 31 次 (8.27%)
+ 尾数差 1: 70 次 (18.67%)
+ 尾数差 2: 53 次 (14.13%)
+ 尾数差 3: 49 次 (13.07%)
+ 尾数差 4: 43 次 (11.47%)
+ 尾数差 5: 53 次 (14.13%)
+ 尾数差 6: 42 次 (11.2%)
+ 尾数差 7: 20 次 (5.33%)
+ 尾数差 8: 10 次 (2.67%)
+ 尾数差 9: 4 次 (1.07%)
+
+尾数差 <= 3 的概率: 54.13% (203/375)
+
+==================== 5. 区间覆盖分析 ====================
+正码覆盖区间数量分布:
+ 覆盖 1 个区间: 0 次 (0.0%)
+ 覆盖 2 个区间: 9 次 (2.4%)
+ 覆盖 3 个区间: 103 次 (27.47%)
+ 覆盖 4 个区间: 194 次 (51.73%)
+ 覆盖 5 个区间: 69 次 (18.4%)
+
+特码所在区间被正码覆盖的概率: 74.13% (278/375)
+
+==================== 6. 波色/生肖关联 ====================
+特码波色分布:
+ 红: 131 次 (34.93%)
+ 蓝: 122 次 (32.53%)
+ 绿: 122 次 (32.53%)
+
+特码波色在正码中出现的概率: 90.67% (340/375)
+特码生肖在正码中出现的概率: 36.27% (136/375)
+
+正码中特码同波色数量分布:
+ 0_红: 7 次 (1.87%)
+ 0_绿: 11 次 (2.93%)
+ 0_蓝: 17 次 (4.53%)
+ 1_红: 35 次 (9.33%)
+ 1_绿: 42 次 (11.2%)
+ 1_蓝: 37 次 (9.87%)
+ 2_红: 53 次 (14.13%)
+ 2_绿: 46 次 (12.27%)
+ 2_蓝: 38 次 (10.13%)
+ 3_红: 30 次 (8.0%)
+ 3_绿: 17 次 (4.53%)
+ 3_蓝: 26 次 (6.93%)
+ 4_红: 3 次 (0.8%)
+ 4_绿: 5 次 (1.33%)
+ 4_蓝: 4 次 (1.07%)
+ 5_红: 3 次 (0.8%)
+ 5_绿: 1 次 (0.27%)
+
+==================== 总结: 达到40%命中率以上的规律 ====================
+规律按命中率排序:
+ - 距离<=15: 99.47% [达标]
+ - 距离<=10: 94.13% [达标]
+ - 特码波色在正码中出现: 90.67% [达标]
+ - 距离<=5: 78.93% [达标]
+ - 特码区间被正码覆盖: 74.13% [达标]
+ - 特码在正码范围内: 70.67% [达标]
+ - 尾数差<=3: 54.13% [达标]
+ - 差值在[-10,+10]: 40.0% [达标]
+ - 特码生肖在正码中出现: 36.27%
+ - 差值在[-5,+5]: 19.2%
+ - 同尾: 8.27%
+
+==================== 关键发现 ====================
+
+1. 【特码波色重复规律】命中率最高 90.67%
+ - 特码波色在正码中出现的概率约为 90.67%
+ - 如果正码中有红色号码,特码有90%概率是红色波色
+
+2. 【近距离规律】命中率很高 94.13%
+ - 特码距离最近正码<=10的概率约为 94.13%
+ - 特码往往不会离正码太远,基本在10个数字以内
+
+3. 【区间覆盖规律】命中率较高 74.13%
+ - 特码所在区间被正码覆盖的概率约为 74.13%
+ - 将1-49分为5区间,特码有74%概率落在正码覆盖的区间
+
+4. 【正码范围规律】命中率中等 70.67%
+ - 特码在正码[min, max]范围内的概率约为 70.67%
+ - 特码有70%概率落在正码的最小值和最大值之间
+
+5. 【尾数差规律】命中率54.13%
+ - 和值尾数与特码尾数差<=3的概率约为 54.13%
+ - 特码尾数与正码和值尾数相差不超过3
+
+6. 【生肖重复规律】命中率较低 36.27%
+ - 特码生肖在正码中出现的概率约为 36.27%
+ - 生肖关联性不如波色明显
+
+7. 【特码等于正码】命中率极低 0%
+ - 特码等于某正码的概率为 0%
+ - 特码与正码完全不重复(六合彩规则)
+
+==================== 分析完成 ====================
diff --git a/application/admin/controller/History.php b/application/admin/controller/History.php
index 8044dec..3d9fd2d 100644
--- a/application/admin/controller/History.php
+++ b/application/admin/controller/History.php
@@ -22,7 +22,7 @@ class History extends Backend
* 无需额外权限检查的方法(但仍在 admin 模块内,需要 admin 登录)
* @var array
*/
- protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap', 'specialHotColdAction', 'zoneTransition', 'colorWaveTransition', 'zoneToColorTransition', 'zodiacTransition', 'tailNumberTransition', 'headNumberTransition', 'predict', 'predictV2', 'predictV3', 'optimizeWeights'];
+ protected $noNeedRight = ['missingNum', 'trendData', 'hotColdNumbers', 'colorWaveAnalysis', 'zodiacAnalysis', 'oddEvenAnalysis', 'bigSmallAnalysis', 'specialTrend', 'consecutiveNumbers', 'tailNumbers', 'dashboard', 'specialHeatmap', 'specialHotColdAction', 'zoneTransition', 'colorWaveTransition', 'zoneToColorTransition', 'zodiacTransition', 'tailNumberTransition', 'headNumberTransition', 'predict', 'predictV2', 'predictV3', 'optimizeWeights', 'predictByNormalRelation'];
public function _initialize()
{
@@ -535,5 +535,29 @@ class History extends Backend
}
}
+ /**
+ * 基于正码关联规律的特码预测
+ * 核心规律:
+ * - 波色重复规律:90.67%特码波色与正码中某号码波色相同
+ * - 距离规律:94.13%特码与最近正码距离<=10
+ * - 区间覆盖规律:74.13%特码落在正码覆盖的区间
+ * - 正码范围规律:70.67%特码在正码min-max之间
+ */
+ public function predictByNormalRelation()
+ {
+ if ($this->request->isAjax()) {
+ $periods = $this->request->get('periods', 100, 'intval');
+ if ($periods < 30 || $periods > 500) {
+ $periods = 100;
+ }
+ $targetExpect = $this->request->get('target_expect', '', 'trim');
+ $result = $this->model->getPredictionByNormalRelation($periods, $targetExpect);
+ if (isset($result['error'])) {
+ $this->error($result['error']);
+ }
+ $this->success('查询成功', null, $result);
+ }
+ }
+
}
diff --git a/application/admin/model/History.php b/application/admin/model/History.php
index 5ebc11d..1f29da2 100644
--- a/application/admin/model/History.php
+++ b/application/admin/model/History.php
@@ -4290,5 +4290,543 @@ class History extends Model
];
}
+ /**
+ * 基于正码关联规律的特码预测方法(修正版)
+ * 核心规律:上期正码 → 当期特码
+ * - 覆盖区间规律:91.44% 当期特码在上期正码覆盖的区间内
+ * - 正码±3距离:59.36% 当期特码与上期正码某号码距离≤3
+ * - 双波色预测:69.52% 当期特码波色在上期正码前2种主导波色内
+ * - 特码区间转移:77.54% 基于上期特码区间预测当期特码区间
+ * - 平均值±10:41.98% 当期特码在上期正码平均值±10范围
+ * - 尾数±2:50% 和值尾数与特码尾数差≤2
+ * @param int $periods 统计期数(用于验证历史命中率)
+ * @param string $targetExpect 目标期号(可选,用于回测验证)
+ * @return array {predictions: [], analysis: {}, hit_info: {}, backtest: {}}
+ */
+ public function getPredictionByNormalRelation($periods = 100, $targetExpect = '')
+ {
+ $num_model = new Num();
+ $colorMap = $num_model->column('color', 'num');
+ $animalMap = $num_model->column('animal', 'num');
+
+ // 区间划分:大号(31-49)、中号(11-30)、小号(1-10)
+ $getBigZone = function ($num) {
+ if ($num <= 10) return 'small';
+ if ($num <= 30) return 'mid';
+ return 'big';
+ };
+
+ // 细区间划分:1-10, 11-20, 21-30, 31-40, 41-49
+ $getFineZone = function ($num) {
+ if ($num <= 10) return 0;
+ if ($num <= 20) return 1;
+ if ($num <= 30) return 2;
+ if ($num <= 40) return 3;
+ return 4;
+ };
+
+ // 确定预测基准
+ $actualResult = null;
+ $lastNormals = [];
+ $lastSpecial = 0;
+ $lastExpect = '';
+ $cutoffTime = null;
+
+ if ($targetExpect) {
+ $targetRow = $this->where('expect', $targetExpect)->find();
+ if (!$targetRow) {
+ return ['predictions' => [], 'error' => '期号不存在', 'target_expect' => $targetExpect];
+ }
+ $cutoffTime = $targetRow['openTime'];
+ $actualResult = [
+ 'expect' => (string)$targetRow['expect'],
+ 'num7' => (int)$targetRow['num7'],
+ 'color' => $colorMap[$targetRow['num7']] ?? '',
+ 'animal' => $animalMap[$targetRow['num7']] ?? '',
+ 'bigZone' => $getBigZone($targetRow['num7']),
+ 'openTime' => $targetRow['openTime']
+ ];
+ // 获取上一期数据作为预测基准
+ $prevRow = $this->where('openTime', '<', $cutoffTime)->order('openTime', 'desc')->limit(1)->find();
+ if (!$prevRow) {
+ return ['predictions' => [], 'error' => '没有历史数据'];
+ }
+ for ($i = 1; $i <= 6; $i++) {
+ $lastNormals[] = (int)$prevRow['num' . $i];
+ }
+ $lastSpecial = (int)$prevRow['num7'];
+ $lastExpect = (string)$prevRow['expect'];
+ } else {
+ // 使用最新一期作为预测基准
+ $latest = $this->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime')
+ ->order('openTime', 'desc')->limit(1)->find();
+ if (!$latest) {
+ return ['predictions' => [], 'error' => '没有历史数据'];
+ }
+ for ($i = 1; $i <= 6; $i++) {
+ $lastNormals[] = (int)$latest['num' . $i];
+ }
+ $lastSpecial = (int)$latest['num7'];
+ $lastExpect = (string)$latest['expect'];
+ }
+
+ // 分析上期正码特征
+ $normalMin = min($lastNormals);
+ $normalMax = max($lastNormals);
+ $normalAvg = round(array_sum($lastNormals) / 6, 2);
+ $normalSum = array_sum($lastNormals);
+
+ // 统计上期正码波色分布(找出前2种主导波色)
+ $colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0];
+ foreach ($lastNormals as $n) {
+ $color = $colorMap[$n] ?? '';
+ if (strpos($color, '红') !== false) $colorCounts['红']++;
+ elseif (strpos($color, '蓝') !== false) $colorCounts['蓝']++;
+ elseif (strpos($color, '绿') !== false) $colorCounts['绿']++;
+ }
+ // 按数量排序,取前2种
+ $sortedColors = [];
+ foreach ($colorCounts as $c => $cnt) {
+ $sortedColors[] = ['color' => $c, 'count' => $cnt];
+ }
+ usort($sortedColors, function ($a, $b) { return $b['count'] - $a['count']; });
+ $top2Colors = [$sortedColors[0]['color'], $sortedColors[1]['color']];
+
+ // 获取上期正码覆盖的细区间
+ $normalFineZones = [];
+ foreach ($lastNormals as $n) {
+ $zoneIdx = $getFineZone($n);
+ if (!in_array($zoneIdx, $normalFineZones)) {
+ $normalFineZones[] = $zoneIdx;
+ }
+ }
+
+ // 获取上期正码覆盖的大区间
+ $normalBigZones = [];
+ foreach ($lastNormals as $n) {
+ $bigZone = $getBigZone($n);
+ if (!in_array($bigZone, $normalBigZones)) {
+ $normalBigZones[] = $bigZone;
+ }
+ }
+
+ // 上期特码所在大区间
+ $lastSpecialBigZone = $getBigZone($lastSpecial);
+
+ // 特码区间转移概率矩阵(基于历史分析)
+ $zoneTransMatrix = [
+ 'big' => ['big' => 35.77, 'mid' => 37.96, 'small' => 26.28],
+ 'mid' => ['big' => 33.77, 'mid' => 43.51, 'small' => 22.73],
+ 'small' => ['big' => 42.17, 'mid' => 42.17, 'small' => 15.66]
+ ];
+
+ // 计算每个号码的预测评分
+ $predictions = [];
+ for ($num = 1; $num <= 49; $num++) {
+ $numColor = $colorMap[$num] ?? '';
+ $numBigZone = $getBigZone($num);
+ $numFineZone = $getFineZone($num);
+
+ // 规律1:覆盖区间规律(91.44%命中)- 细区间覆盖
+ $fineZoneCovered = in_array($numFineZone, $normalFineZones);
+ $zoneCoverScore = $fineZoneCovered ? 91 : 0;
+
+ // 规律2:正码±3距离(59.36%命中)
+ $minDistance = 49;
+ foreach ($lastNormals as $n) {
+ $dist = abs($num - $n);
+ if ($dist < $minDistance) $minDistance = $dist;
+ }
+ $distScore = $minDistance <= 3 ? 59 : ($minDistance <= 5 ? 40 : 0);
+
+ // 规律3:双波色预测(69.52%命中)
+ $colorInTop2 = false;
+ foreach ($top2Colors as $tc) {
+ if (strpos($numColor, $tc) !== false) {
+ $colorInTop2 = true;
+ break;
+ }
+ }
+ $colorScore = $colorInTop2 ? 69 : 0;
+
+ // 规律4:特码区间转移(77.54%命中)
+ $transProb = $zoneTransMatrix[$lastSpecialBigZone][$numBigZone] ?? 0;
+ // 取该区间最高转移概率的2个区间
+ $transProbs = $zoneTransMatrix[$lastSpecialBigZone];
+ $sortedTrans = [];
+ foreach ($transProbs as $z => $p) {
+ $sortedTrans[] = ['zone' => $z, 'prob' => $p];
+ }
+ usort($sortedTrans, function ($a, $b) { return $b['prob'] - $a['prob']; });
+ $top2TransZones = [$sortedTrans[0]['zone'], $sortedTrans[1]['zone']];
+ $transMatch = in_array($numBigZone, $top2TransZones);
+ $transScore = $transMatch ? 77 : 0;
+
+ // 规律5:平均值±10(41.98%命中)
+ $avgDiff = abs($num - $normalAvg);
+ $avgScore = $avgDiff <= 10 ? 42 : 0;
+
+ // 规律6:尾数±2(50%命中)
+ $numTail = $num % 10;
+ $sumTail = $normalSum % 10;
+ $tailDiff = abs($numTail - $sumTail);
+ $tailDiff = min($tailDiff, 10 - $tailDiff);
+ $tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0);
+
+ // 综合评分(加权求和)
+ $totalScore = $zoneCoverScore * 0.30 // 覆盖区间权重最高
+ + $transScore * 0.25 // 特码区间转移
+ + $colorScore * 0.20 // 双波色
+ + $distScore * 0.12 // 距离
+ + $avgScore * 0.08 // 平均值
+ + $tailScore * 0.05; // 尾数
+
+ $predictions[] = [
+ 'num' => $num,
+ 'score' => round($totalScore, 2),
+ 'color' => $numColor,
+ 'animal' => $animalMap[$num] ?? '',
+ 'big_zone' => $numBigZone,
+ 'fine_zone' => $numFineZone,
+ 'zone_covered' => $fineZoneCovered,
+ 'min_distance' => $minDistance,
+ 'color_in_top2' => $colorInTop2,
+ 'trans_match' => $transMatch,
+ 'trans_prob' => $transProb,
+ 'avg_diff' => round($avgDiff, 2),
+ 'tail_diff' => $tailDiff
+ ];
+ }
+
+ // 按评分降序排序
+ usort($predictions, function ($a, $b) {
+ return $b['score'] - $a['score'];
+ });
+
+ // 返回Top15推荐号码
+ $topPredictions = array_slice($predictions, 0, 15);
+
+ // 分析信息
+ $analysis = [
+ 'last_expect' => $lastExpect,
+ 'last_normals' => $lastNormals,
+ 'last_special' => $lastSpecial,
+ 'last_special_zone' => $lastSpecialBigZone,
+ 'normal_min' => $normalMin,
+ 'normal_max' => $normalMax,
+ 'normal_avg' => $normalAvg,
+ 'normal_sum' => $normalSum,
+ 'top2_colors' => $top2Colors,
+ 'color_counts' => $colorCounts,
+ 'normal_fine_zones' => $normalFineZones,
+ 'normal_big_zones' => $normalBigZones,
+ 'zone_trans_matrix' => $zoneTransMatrix,
+ 'rules' => [
+ ['name' => '覆盖区间', 'rate' => '91.44%', 'desc' => '当期特码在上期正码覆盖的细区间内'],
+ ['name' => '特码区间转移', 'rate' => '77.54%', 'desc' => '基于上期特码区间预测当期特码所在大区间'],
+ ['name' => '双波色预测', 'rate' => '69.52%', 'desc' => '当期特码波色在上期正码前2种主导波色内'],
+ ['name' => '正码±3距离', 'rate' => '59.36%', 'desc' => '当期特码与上期正码某号码距离≤3'],
+ ['name' => '尾数±2', 'rate' => '50%', 'desc' => '上期正码和值尾数与当期特码尾数差≤2'],
+ ['name' => '平均值±10', 'rate' => '41.98%', 'desc' => '当期特码在上期正码平均值±10范围']
+ ],
+ 'predict_next_expect' => $lastExpect ? (string)(intval($lastExpect) + 1) : ''
+ ];
+
+ // 命中验证
+ $hitInfo = null;
+ if ($actualResult) {
+ $hitRank = -1;
+ foreach ($topPredictions as $idx => $p) {
+ if ($p['num'] === $actualResult['num7']) {
+ $hitRank = $idx + 1;
+ break;
+ }
+ }
+ $fullRank = -1;
+ foreach ($predictions as $idx => $p) {
+ if ($p['num'] === $actualResult['num7']) {
+ $fullRank = $idx + 1;
+ break;
+ }
+ }
+ // 分析实际结果的规律命中情况
+ $actualAnalysis = null;
+ foreach ($predictions as $p) {
+ if ($p['num'] === $actualResult['num7']) {
+ $actualAnalysis = $p;
+ break;
+ }
+ }
+ $hitInfo = [
+ 'hit' => $hitRank > 0,
+ 'rank_in_top' => $hitRank,
+ 'rank_in_all' => $fullRank,
+ 'actual_num' => $actualResult['num7'],
+ 'actual_color' => $actualResult['color'],
+ 'actual_animal' => $actualResult['animal'],
+ 'actual_expect' => $actualResult['expect'],
+ 'actual_zone_covered' => $actualAnalysis ? $actualAnalysis['zone_covered'] : false,
+ 'actual_min_distance' => $actualAnalysis ? $actualAnalysis['min_distance'] : 99,
+ 'actual_color_in_top2' => $actualAnalysis ? $actualAnalysis['color_in_top2'] : false,
+ 'actual_trans_match' => $actualAnalysis ? $actualAnalysis['trans_match'] : false,
+ 'actual_tail_diff' => $actualAnalysis ? $actualAnalysis['tail_diff'] : 99,
+ 'actual_avg_diff' => $actualAnalysis ? $actualAnalysis['avg_diff'] : 99
+ ];
+ }
+
+ // 回测验证(默认显示前50期命中详情)
+ $backtest = null;
+ if ($periods >= 30) {
+ $backtest = $this->_runBacktestNormalRelation($periods, 50);
+ }
+
+ return [
+ 'predictions' => $topPredictions,
+ 'all_predictions' => $predictions,
+ 'analysis' => $analysis,
+ 'actual_result' => $actualResult,
+ 'hit_info' => $hitInfo,
+ 'backtest' => $backtest
+ ];
+ }
+
+ /**
+ * 执行正码关联规律的历史回测(修正版)
+ * 使用正确规律:上期正码 → 当期特码
+ * @param int $periods 回测期数
+ * @param int $detailLimit 返回详情条数
+ * @return array {hit_rate, avg_rank, details, rule_stats}
+ */
+ private function _runBacktestNormalRelation($periods = 100, $detailLimit = 20)
+ {
+ $history = $this->field('expect,num1,num2,num3,num4,num5,num6,num7,openTime')
+ ->order('openTime', 'desc')
+ ->limit($periods + 1)
+ ->select();
+
+ if (count($history) < 2) {
+ return ['error' => '数据不足'];
+ }
+
+ $num_model = new Num();
+ $colorMap = $num_model->column('color', 'num');
+
+ // 大区间划分
+ $getBigZone = function ($num) {
+ if ($num <= 10) return 'small';
+ if ($num <= 30) return 'mid';
+ return 'big';
+ };
+
+ // 细区间划分
+ $getFineZone = function ($num) {
+ if ($num <= 10) return 0;
+ if ($num <= 20) return 1;
+ if ($num <= 30) return 2;
+ if ($num <= 40) return 3;
+ return 4;
+ };
+
+ // 特码区间转移概率矩阵
+ $zoneTransMatrix = [
+ 'big' => ['big' => 35.77, 'mid' => 37.96, 'small' => 26.28],
+ 'mid' => ['big' => 33.77, 'mid' => 43.51, 'small' => 22.73],
+ 'small' => ['big' => 42.17, 'mid' => 42.17, 'small' => 15.66]
+ ];
+
+ $hits = 0;
+ $ranks = [];
+ $details = [];
+ $ruleHits = [
+ 'zone_cover' => 0,
+ 'trans_match' => 0,
+ 'color_top2' => 0,
+ 'dist_3' => 0,
+ 'tail_2' => 0,
+ 'avg_10' => 0
+ ];
+
+ for ($i = 0; $i < count($history) - 1; $i++) {
+ $currentRow = $history[$i];
+ $prevRow = $history[$i + 1];
+
+ // 使用上一期的正码预测当期的特码
+ $lastNormals = [];
+ for ($j = 1; $j <= 6; $j++) {
+ $lastNormals[] = (int)$prevRow['num' . $j];
+ }
+ $lastSpecial = (int)$prevRow['num7'];
+ $actualSpecial = (int)$currentRow['num7'];
+
+ // 分析上期正码特征
+ $normalMin = min($lastNormals);
+ $normalMax = max($lastNormals);
+ $normalAvg = array_sum($lastNormals) / 6;
+ $normalSum = array_sum($lastNormals);
+
+ // 波色分布
+ $colorCounts = ['红' => 0, '蓝' => 0, '绿' => 0];
+ foreach ($lastNormals as $n) {
+ $color = $colorMap[$n] ?? '';
+ if (strpos($color, '红') !== false) $colorCounts['红']++;
+ elseif (strpos($color, '蓝') !== false) $colorCounts['蓝']++;
+ elseif (strpos($color, '绿') !== false) $colorCounts['绿']++;
+ }
+ $sortedColors = [];
+ foreach ($colorCounts as $c => $cnt) {
+ $sortedColors[] = ['color' => $c, 'count' => $cnt];
+ }
+ usort($sortedColors, function ($a, $b) { return $b['count'] - $a['count']; });
+ $top2Colors = [$sortedColors[0]['color'], $sortedColors[1]['color']];
+
+ // 上期正码覆盖的细区间
+ $normalFineZones = [];
+ foreach ($lastNormals as $n) {
+ $zoneIdx = $getFineZone($n);
+ if (!in_array($zoneIdx, $normalFineZones)) {
+ $normalFineZones[] = $zoneIdx;
+ }
+ }
+
+ // 上期特码所在大区间
+ $lastSpecialBigZone = $getBigZone($lastSpecial);
+
+ // 计算每个号码评分
+ $scores = [];
+ for ($num = 1; $num <= 49; $num++) {
+ $numColor = $colorMap[$num] ?? '';
+ $numBigZone = $getBigZone($num);
+ $numFineZone = $getFineZone($num);
+
+ // 规律1:覆盖区间(91%)
+ $zoneCovered = in_array($numFineZone, $normalFineZones);
+ $zoneCoverScore = $zoneCovered ? 91 : 0;
+
+ // 规律2:特码区间转移(77%)
+ $transProbs = $zoneTransMatrix[$lastSpecialBigZone];
+ $sortedTrans = [];
+ foreach ($transProbs as $z => $p) {
+ $sortedTrans[] = ['zone' => $z, 'prob' => $p];
+ }
+ usort($sortedTrans, function ($a, $b) { return $b['prob'] - $a['prob']; });
+ $top2TransZones = [$sortedTrans[0]['zone'], $sortedTrans[1]['zone']];
+ $transMatch = in_array($numBigZone, $top2TransZones);
+ $transScore = $transMatch ? 77 : 0;
+
+ // 规律3:双波色(69%)
+ $colorInTop2 = false;
+ foreach ($top2Colors as $tc) {
+ if (strpos($numColor, $tc) !== false) {
+ $colorInTop2 = true;
+ break;
+ }
+ }
+ $colorScore = $colorInTop2 ? 69 : 0;
+
+ // 规律4:距离≤3(59%)
+ $minDist = 49;
+ foreach ($lastNormals as $n) {
+ $dist = abs($num - $n);
+ if ($dist < $minDist) $minDist = $dist;
+ }
+ $distScore = $minDist <= 3 ? 59 : ($minDist <= 5 ? 40 : 0);
+
+ // 规律5:尾数≤2(50%)
+ $tailDiff = abs($num % 10 - $normalSum % 10);
+ $tailDiff = min($tailDiff, 10 - $tailDiff);
+ $tailScore = $tailDiff <= 2 ? 50 : ($tailDiff <= 3 ? 30 : 0);
+
+ // 规律6:平均值±10(42%)
+ $avgDiff = abs($num - $normalAvg);
+ $avgScore = $avgDiff <= 10 ? 42 : 0;
+
+ // 综合评分
+ $score = $zoneCoverScore * 0.30
+ + $transScore * 0.25
+ + $colorScore * 0.20
+ + $distScore * 0.12
+ + $avgScore * 0.08
+ + $tailScore * 0.05;
+
+ $scores[$num] = $score;
+ }
+
+ // 排序找排名
+ $sorted = [];
+ for ($num = 1; $num <= 49; $num++) {
+ $sorted[] = ['num' => $num, 'score' => $scores[$num]];
+ }
+ usort($sorted, function ($a, $b) {
+ return $b['score'] - $a['score'];
+ });
+
+ $rank = -1;
+ foreach ($sorted as $idx => $item) {
+ if ($item['num'] === $actualSpecial) {
+ $rank = $idx + 1;
+ break;
+ }
+ }
+
+ if ($rank > 0 && $rank <= 15) {
+ $hits++;
+ }
+ $ranks[] = $rank;
+
+ // 统计各规律命中情况
+ $actualFineZone = $getFineZone($actualSpecial);
+ $actualBigZone = $getBigZone($actualSpecial);
+ $actualColor = $colorMap[$actualSpecial] ?? '';
+ $actualMinDist = 49;
+ foreach ($lastNormals as $n) {
+ $dist = abs($actualSpecial - $n);
+ if ($dist < $actualMinDist) $actualMinDist = $dist;
+ }
+ $actualTailDiff = abs($actualSpecial % 10 - $normalSum % 10);
+ $actualTailDiff = min($actualTailDiff, 10 - $actualTailDiff);
+ $actualAvgDiff = abs($actualSpecial - $normalAvg);
+
+ if (in_array($actualFineZone, $normalFineZones)) $ruleHits['zone_cover']++;
+ if (in_array($actualBigZone, $top2TransZones)) $ruleHits['trans_match']++;
+ $actualColorInTop2 = false;
+ foreach ($top2Colors as $tc) {
+ if (strpos($actualColor, $tc) !== false) $actualColorInTop2 = true;
+ }
+ if ($actualColorInTop2) $ruleHits['color_top2']++;
+ if ($actualMinDist <= 3) $ruleHits['dist_3']++;
+ if ($actualTailDiff <= 2) $ruleHits['tail_2']++;
+ if ($actualAvgDiff <= 10) $ruleHits['avg_10']++;
+
+ $details[] = [
+ 'expect' => (string)$currentRow['expect'],
+ 'actual' => $actualSpecial,
+ 'rank' => $rank,
+ 'hit' => $rank > 0 && $rank <= 15
+ ];
+ }
+
+ $totalPeriods = count($ranks);
+ $hitRate = $totalPeriods > 0 ? round($hits / $totalPeriods * 100, 2) : 0;
+ $avgRank = $totalPeriods > 0 ? round(array_sum($ranks) / $totalPeriods, 2) : 0;
+
+ // 计算各规律实际命中率
+ $ruleStats = [];
+ foreach ($ruleHits as $rule => $count) {
+ $ruleStats[$rule] = [
+ 'hits' => $count,
+ 'rate' => $totalPeriods > 0 ? round($count / $totalPeriods * 100, 2) : 0
+ ];
+ }
+
+ return [
+ 'periods' => $totalPeriods,
+ 'hits' => $hits,
+ 'hit_rate' => $hitRate,
+ 'avg_rank' => $avgRank,
+ 'rule_stats' => $ruleStats,
+ 'details' => array_slice($details, 0, $detailLimit)
+ ];
+ }
+
}
diff --git a/application/admin/view/history/index.html b/application/admin/view/history/index.html
index e9a1500..1f1844c 100644
--- a/application/admin/view/history/index.html
+++ b/application/admin/view/history/index.html
@@ -19,6 +19,8 @@
{:__('Tail Numbers')}
特码冷热
筛号器
+ 智能预测
+ 正码关联预测
diff --git a/public/assets/js/backend/history.js b/public/assets/js/backend/history.js
index e62fa52..a6365ce 100644
--- a/public/assets/js/backend/history.js
+++ b/public/assets/js/backend/history.js
@@ -107,6 +107,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
$(document).off('click', '.btn-predict').on('click', '.btn-predict', function () {
Controller.api.showPredictDialog();
});
+
+ // 正码关联预测按钮事件
+ $(document).off('click', '.btn-normal-relation').on('click', '.btn-normal-relation', function () {
+ Controller.api.showNormalRelationDialog();
+ });
},
add: function () {
Controller.api.bindevent();
@@ -1960,6 +1965,204 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
html += '';
$('#predict-result', layero).html(html);
+ },
+
+ /**
+ * 显示正码关联预测弹窗(修正版)
+ * 核心规律:上期正码 → 当期特码
+ */
+ showNormalRelationDialog: function () {
+ var html = '
' +
+ '
' +
+ '
正码关联预测算法(修正版)
' +
+ '
' +
+ ' 核心逻辑:用上期正码(num1-6)预测当期特码(num7)
' +
+ ' 1. 覆盖区间规律(91.44%):当期特码在上期正码覆盖的细区间内
' +
+ ' 2. 特码区间转移(77.54%):基于上期特码区间预测当期特码大区间
' +
+ ' 3. 双波色预测(69.52%):当期特码波色在上期正码前2种主导波色内
' +
+ ' 4. 正码±3距离(59.36%):当期特码与上期正码某号码距离≤3
' +
+ ' 5. 尾数±2(50%):上期正码和值尾数与当期特码尾数差≤2
' +
+ ' 6. 平均值±10(41.98%):当期特码在上期正码平均值±10范围' +
+ '
' +
+ '
' +
+ '
' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '
' +
+ '
' +
+ '
';
+
+ Layer.open({
+ type: 1,
+ title: '正码关联预测(上期正码→当期特码)',
+ area: ['900px', '750px'],
+ content: html,
+ shadeClose: true,
+ success: function (layero, index) {
+ $('#btn-nr-query', layero).on('click', function () {
+ Controller.api.queryNormalRelation(layero);
+ });
+ // 自动执行一次查询
+ Controller.api.queryNormalRelation(layero);
+ }
+ });
+ },
+
+ /**
+ * 查询正码关联预测
+ */
+ queryNormalRelation: function (layero) {
+ var $btn = $('#btn-nr-query', layero);
+ $btn.prop('disabled', true);
+ $('#nr-result', layero).html(' 正在分析...
');
+
+ var periods = parseInt($('#nr-periods', layero).val()) || 100;
+ var targetExpect = $('#nr-target', layero).val().trim();
+
+ $.ajax({
+ url: 'history/predictByNormalRelation',
+ type: 'GET',
+ data: { periods: periods, target_expect: targetExpect },
+ dataType: 'json',
+ success: function (ret) {
+ if (ret.code == 1) {
+ Controller.api.renderNormalRelation(ret.data, layero);
+ } else {
+ $('#nr-result', layero).html('' + (ret.msg || '查询失败') + '
');
+ }
+ },
+ error: function () {
+ $('#nr-result', layero).html('查询失败
');
+ },
+ complete: function () {
+ $btn.prop('disabled', false);
+ }
+ });
+ },
+
+ /**
+ * 渲染正码关联预测结果
+ */
+ renderNormalRelation: function (data, layero) {
+ if (!data || !data.predictions || data.predictions.length === 0) {
+ $('#nr-result', layero).html('无预测结果
');
+ return;
+ }
+
+ var analysis = data.analysis || {};
+ var predictions = data.predictions;
+ var hitInfo = data.hit_info;
+ var backtest = data.backtest;
+
+ var html = '';
+
+ // 分析信息
+ html += '';
+ html += '
上期开奖信息(预测基准)
';
+ html += '
';
+ html += '期号:' + analysis.last_expect + ' | ';
+ html += '正码:' + analysis.last_normals.join(', ') + ' | ';
+ html += '特码:' + analysis.last_special + '
';
+ html += '正码范围:' + analysis.normal_min + ' ~ ' + analysis.normal_max + ' | ';
+ html += '平均值:' + analysis.normal_avg + ' | ';
+ html += '和值:' + analysis.normal_sum + '
';
+ html += '正码波色:' + (analysis.top2_colors || analysis.normal_colors || []).join('/') + ' | ';
+ html += '覆盖区间:' + (analysis.normal_fine_zones || analysis.normal_zones || []).map(function(z) {
+ return ['1-10','11-20','21-30','31-40','41-49'][z];
+ }).join(',');
+ html += '
';
+
+ // 预测号码展示
+ html += '';
+ html += '
推荐号码(Top 15)
';
+ html += '
';
+
+ for (var i = 0; i < predictions.length; i++) {
+ var p = predictions[i];
+ var colorHex = Controller.api.getColorByNum(p.num);
+ var animal = Controller.api.animalMap[p.num] || '';
+
+ var bgColor = '#fff';
+ var borderColor = '1px solid #ddd';
+ if (i < 5) {
+ bgColor = '#fff8e1';
+ borderColor = '2px solid #ffc107';
+ }
+
+ html += '
';
+ html += '
' + p.num + '';
+ html += '
' + animal + '
';
+ html += '
得分:' + p.score + '
';
+ html += '
';
+ html += (p.color_in_top2 || p.color_match) ? '✓波色 ' : '';
+ html += '距离' + (p.min_distance || 0);
+ html += '
';
+ html += '
';
+ }
+ html += '
';
+
+ // 规律命中情况(如果有回测)
+ if (backtest) {
+ html += '';
+ html += '
回测验证(最近' + backtest.periods + '期)
';
+ html += '
';
+ html += '命中率(Top15内):' + backtest.hit_rate + '% (' + backtest.hits + '/' + backtest.periods + ')
';
+ html += '平均排名:' + backtest.avg_rank + ' / 49
';
+ html += '注:命中率越高越好,平均排名越低越好';
+ html += '
';
+
+ // 显示前50期命中详情表格
+ if (backtest.details && backtest.details.length > 0) {
+ html += '
';
+ html += '
';
+ html += '| 期号 | 特码 | 排名 | 命中 |
';
+ html += '';
+ for (var d = 0; d < backtest.details.length; d++) {
+ var det = backtest.details[d];
+ var hitBadge = det.hit ? '✓' : '✗';
+ var rankColor = det.rank <= 5 ? '#2e7d32' : (det.rank <= 15 ? '#ff9800' : '#c62828');
+ html += '';
+ html += '| ' + det.expect + ' | ';
+ html += '' + det.actual + ' | ';
+ html += '' + det.rank + ' | ';
+ html += '' + hitBadge + ' | ';
+ html += '
';
+ }
+ html += '
';
+ html += '
';
+ }
+ html += '
';
+ }
+
+ // 实际命中情况(如果有目标期号)
+ if (hitInfo) {
+ var hitBg = hitInfo.hit ? '#e8f5e9' : '#ffebee';
+ var hitColor = hitInfo.hit ? '#2e7d32' : '#c62828';
+ html += '';
+ html += '
';
+ html += hitInfo.hit ? '✓ 命中!排名:' + hitInfo.rank_in_top + ' / 15' : '✗ 未命中,排名:' + hitInfo.rank_in_all + ' / 49';
+ html += '
';
+ html += '
实际特码:' + hitInfo.actual_num + ' (' + hitInfo.actual_color + '/' + hitInfo.actual_animal + ') 期号:' + hitInfo.actual_expect + '
';
+ html += '
';
+ }
+
+ // 规律说明表
+ if (analysis.rules) {
+ html += '';
+ html += '
规律命中率表
';
+ html += '
';
+ html += '| 规律名称 | 命中率 | 说明 |
';
+ for (var r = 0; r < analysis.rules.length; r++) {
+ var rule = analysis.rules[r];
+ html += '| ' + rule.name + ' | ' + rule.rate + ' | ' + rule.desc + ' |
';
+ }
+ html += '
';
+ }
+
+ $('#nr-result', layero).html(html);
}
}
};