Files
skills-lottery-picker/skills/lottery_picker/lottery_tool.py
T
CodeForge c4bb8632ca feat: 彩票技能统一输出格式 - 双色球/大乐透4块结构化输出标准对齐
- 新增 model_full_report_dlt.py: 大乐透专用全量报告脚本
- 统一4块结构: 回滚迭代→数据采集→推理分析→机选号码
- 统一术语: 前区/后区(大乐透) 对齐 红球/蓝球(双色球)
- 统一评分: TOP10前区+TOP5后区 归一化概率输出
- 更新 SKILL.md: 双色球/大乐透统一模板+文件结构
2026-07-30 06:04:12 +00:00

1314 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
双色球 & 大乐透 号码分析与机选工具
==================================
功能: 数据采集/分析/推荐/回滚/持久化
数据源: 双色球(500.com) 大乐透(体彩API)
"""
import json, re, os, hashlib, datetime, subprocess, random, math, sys
from collections import Counter
# 集成学习模块
try:
from ensemble_learning import ensemble_recommendation
except ImportError:
ensemble_recommendation = None
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "data")
os.makedirs(DATA_DIR, exist_ok=True)
LOTTERY_RULES = {
"ssq": {
"name": "双色球", "source": "500.com",
"red_count": 6, "red_max": 33, "blue_count": 1, "blue_max": 16,
"draw_days": [2, 4, 7], "draw_time": "21:15",
"api_url": "https://datachart.500.com/ssq/history/newinc/history.php?start={start}&end={end}&limit=100",
"backup_url": "https://www.17500.cn/api/kaijiang/getlist",
"backup_type": "post"
},
"dlt": {
"name": "大乐透", "source": "中国体育彩票",
"red_count": 5, "red_max": 35, "blue_count": 2, "blue_max": 12,
"draw_days": [1, 3, 6], "draw_time": "20:25",
"api_url": "https://webapi.sporttery.cn/gateway/lottery/getHistoryPageListV1.qry?gameNo=85&provinceId=0&pageSize=100&is498=0&pageNo=1"
}
}
WEEKDAY = {1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"}
class Persistence:
def __init__(self):
self.config = self.load("config.json", {
"version": "1.0.0", "seed": "LOTTERY_SKILL_V1.0.0", "iteration_count": 0,
"ssq": {"latest_issue":"","latest_fetch_start":"","latest_fetch_end":""},
"dlt": {"latest_issue":"","latest_fetch_start":"","latest_fetch_end":""}
})
def path(self, n): return os.path.join(DATA_DIR, n)
def load(self, n, d=None):
p=self.path(n)
if os.path.exists(p):
try: return json.load(open(p,'r',encoding='utf-8'))
except: pass
return d if d is not None else {}
def save(self, n, d):
json.dump(d, open(self.path(n),'w',encoding='utf-8'), ensure_ascii=False, indent=2)
def history(self, lt): return self.load(f"{lt}_history.json", [])
def save_history(self,lt,r): self.save(f"{lt}_history.json", r)
def analysis(self,lt): return self.load(f"analysis_{lt}.json", {})
def save_analysis(self,lt,d): self.save(f"analysis_{lt}.json", d)
def feedback(self):
return self.load("feedback.json", {
"iterations":[],"accuracy_history":[],
"ssq":{"total":0,"correct_reds":0,"correct_blues":0,"best":0,
"profitable_wins":0,"total_earnings":0.0},
"dlt":{"total":0,"correct_reds":0,"correct_blues":0,"best":0,
"profitable_wins":0,"total_earnings":0.0}
})
def save_feedback(self,d): self.save("feedback.json", d)
def save_config(self): self.save("config.json", self.config)
def get_seed(self,lt,date=None):
if not date: date = datetime.datetime.now().strftime("%Y-%m-%d")
# 种子基于:固定种子 + 彩种 + 日期 + 分析版本号
# version 从 analysis_{lt}.json 读取(由 show_analysis 写入),
# FeedbackLoop.check() 回滚时不修改该字段,保证同天内结果一致。
# 仅当数据源变化→重新分析→版本号更新时,种子才会变化。
ver = self.analysis(lt).get("version", 0)
return hashlib.md5(f"{self.config['seed']}_{lt}_{date}_v{ver}".encode()).hexdigest()
class Collector:
def __init__(self,p): self.p = p
def fetch_ssq(self, count=100):
"""采集双色球数据,自动容灾切换"""
cfg = self.p.config["ssq"]
hist = self.p.history("ssq")
# 主数据源: 500.com
records = self._fetch_ssq_500(hist, cfg, count)
# 如果主数据源失败,切换到备用数据源 17500.cn
if records is None or len(records) == 0:
print("[采集] 500.com 无数据,切换到备用数据源 17500.cn")
records = self._fetch_ssq_17500(hist, cfg)
return records
def _fetch_ssq_500(self, hist, cfg, count=100):
"""从500.com获取双色球数据(主数据源)- 仅获取新期数据"""
# 确定请求范围
if hist:
latest = max(int(r["issue"]) for r in hist)
# 已采集过的不用再采集,只请求最新一期之后的数据
# 但500.com需要至少一个范围,如果最新期号之后还没开奖,
# 缩小请求范围到 latest ~ latest(即只请求最新一期确认)
now = datetime.datetime.now()
# 估算当前应有的最新期号
est_end = int(now.strftime("%y")) * 1000 + int(now.strftime("%j")) // 2
if latest >= est_end - 3:
# 数据已是最新,仅请求最新一期确认是否已有新开奖
start, end = latest, latest + 10
else:
# 有较大差距,从最新期+1开始请求
start, end = latest + 1, latest + min(count, est_end - latest + 10)
else:
# 首次采集,获取100期
now = datetime.datetime.now()
end = int(now.strftime("%y")) * 1000 + int(now.strftime("%j")) // 2
start = end - count + 1
if end < start: end = start + count - 1
cfg["latest_fetch_start"], cfg["latest_fetch_end"] = str(start), str(end)
self.p.save_config()
url = LOTTERY_RULES["ssq"]["api_url"].format(start=start, end=end)
print("[采集] 500.com 期号%d~%d" % (start, end))
r = subprocess.run(["curl","-s",url,"-H","User-Agent: Mozilla/5.0"], capture_output=True, timeout=30)
if r.returncode != 0:
print("[警告] 500.com 请求失败")
return None
html = r.stdout.decode('utf-8', errors='ignore')
records = self._parse_ssq_500(html)
if not records:
print("[警告] 500.com 返回空数据")
return None
return self._merge_records("ssq", hist, records, cfg)
def _fetch_ssq_17500(self, hist, cfg):
"""从17500.cn获取双色球数据(备用数据源)"""
print("[采集] 17500.cn 最近30期")
r = subprocess.run(
["curl","-s","-X","POST","-d","lotid=ssq",
"https://www.17500.cn/api/kaijiang/getlist",
"-H","User-Agent: Mozilla/5.0",
"-H","Referer: https://www.17500.cn/kj/list-ssq.html",
"-H","Content-Type: application/x-www-form-urlencoded"],
capture_output=True, timeout=30)
if r.returncode != 0:
print("[警告] 17500.cn 请求失败")
return None
html = r.stdout.decode('utf-8', errors='ignore')
records = self._parse_ssq_17500(html)
if not records:
print("[警告] 17500.cn 解析失败")
return None
# 更新期数范围
if records:
issues = sorted(int(r["issue"]) for r in records)
cfg["latest_fetch_start"], cfg["latest_fetch_end"] = str(issues[0]), str(issues[-1])
return self._merge_records("ssq", hist, records, cfg)
def _merge_records(self, lt, hist, records, cfg):
"""合并新记录到历史数据"""
exist = {r["issue"] for r in hist}
new = [r for r in records if r["issue"] not in exist]
if new:
hist.extend(new)
hist.sort(key=lambda x:int(x["issue"]))
if len(hist)>500: hist=hist[-500:]
self.p.save_history(lt, hist)
cfg["latest_issue"] = max(r["issue"] for r in hist)
self.p.save_config()
print("[采集] %s +%d条 共%d条" % (LOTTERY_RULES[lt]["name"], len(new), len(hist)))
return new if new else None
def _parse_ssq_500(self, html):
"""解析500.com的HTML表格"""
m = re.search(r'<table[^>]*id="tablelist"[^>]*>(.*?)</table>', html, re.DOTALL)
if not m: return []
tb = re.search(r'<tbody[^>]*>(.*?)</tbody>', m.group(1), re.DOTALL)
body = tb.group(1) if tb else m.group(1)
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', body, re.DOTALL)
res = []
for row in rows:
row = re.sub(r'<!--.*?-->','',row)
tds = re.findall(r'<td[^>]*>(.*?)</td>', row, re.DOTALL)
vals = [re.sub(r'<[^>]+>','',td).strip() for td in tds]
if len(vals)<8: continue
iss = vals[0].strip()
if re.match(r'^\d{5,}$', iss):
res.append({
"issue": iss, "reds": [v.zfill(2) for v in vals[1:7]],
"blue": vals[7].strip().zfill(2),
"date": vals[-1].strip() if len(vals)>=16 else ""
})
return res
def _parse_ssq_17500(self, html):
"""解析17500.cn的HTML表格"""
tbody = re.search(r'<tbody>(.*?)</tbody>', html, re.DOTALL)
if not tbody: return []
rows = re.findall(r'<tr>(.*?)</tr>', tbody.group(1), re.DOTALL)
res = []
for row in rows:
tds = re.findall(r'<td[^>]*>(.*?)</td>', row, re.DOTALL)
vals = [re.sub(r'<[^>]+>', '', td).strip() for td in tds]
if len(vals) < 4: continue
issue = vals[0].strip()
# 17500期号格式为"2026057"(7位),转为"26057"(5位)
if len(issue) == 7 and issue.startswith('20'):
issue = issue[2:]
date = vals[1].strip() if len(vals) > 1 else ""
nums_str = re.sub(r'<[^>]+>', '', vals[2]).replace('&nbsp;','').strip() if len(vals) > 2 else ""
if not issue or not nums_str or nums_str == '-': continue
# 17500号码格式: "011022242830 07" -> 红球6个+蓝球(可能1个或多个)
parts = nums_str.split()
if len(parts) < 2: continue
reds_part = parts[0]
blues_part = " ".join(parts[1:])
if len(reds_part) < 12: continue # 6个数字*2字符
reds = [reds_part[i:i+2] for i in range(0, 12, 2)]
blues = blues_part.replace(' ', '')
res.append({
"issue": issue,
"reds": reds,
"blue": blues,
"date": date
})
return res
def fetch_dlt(self):
"""采集大乐透数据 - 增量更新,自动容灾切换"""
cfg = self.p.config["dlt"]
hist = self.p.history("dlt")
# 主数据源: 体彩官方API
records = self._fetch_dlt_official(hist, cfg)
# 如果主数据源失败,切换到备用数据源 500.com
if records is None or len(records) == 0:
print("[采集] 体彩API无数据,切换到备用数据源 500.com")
records = self._fetch_dlt_500(hist, cfg)
return records
def _fetch_dlt_official(self, hist, cfg):
"""从体彩官方API获取大乐透数据(主数据源)"""
url = LOTTERY_RULES["dlt"]["api_url"]
print("[采集] 体彩API")
r = subprocess.run(["curl","-s",url], capture_output=True, timeout=30)
if r.returncode != 0: return None
try: data = json.loads(r.stdout.decode('utf-8', errors='ignore'))
except: return None
if not data.get("success"): return None
records = []
for item in data["value"]["list"]:
nums = item["lotteryDrawResult"].split()
if len(nums)==7:
records.append({
"issue": item["lotteryDrawNum"],
"reds": [n.zfill(2) for n in nums[:5]],
"blue": [n.zfill(2) for n in nums[5:7]],
"date": item["lotteryDrawTime"]
})
if records:
issues = sorted(int(r["issue"]) for r in records)
cfg["latest_fetch_start"], cfg["latest_fetch_end"] = str(issues[0]), str(issues[-1])
return self._merge_records("dlt", hist, records, cfg)
def _fetch_dlt_500(self, hist, cfg):
"""从500.com获取大乐透数据(备用数据源)"""
# 确定请求范围
if hist:
latest = max(int(r["issue"]) for r in hist)
now = datetime.datetime.now()
est_end = int(now.strftime("%y")) * 1000 + int(now.strftime("%j")) // 2
if latest >= est_end - 3:
start, end = latest, latest + 10
else:
start, end = latest + 1, latest + min(100, est_end - latest + 10)
else:
now = datetime.datetime.now()
end = int(now.strftime("%y")) * 1000 + int(now.strftime("%j")) // 2
start = end - 99
if end < start: end = start + 99
cfg["latest_fetch_start"], cfg["latest_fetch_end"] = str(start), str(end)
url = "https://datachart.500.com/dlt/history/newinc/history.php?start={}&end={}".format(start, end)
print("[采集] 500.com 期号%d~%d" % (start, end))
r = subprocess.run(["curl","-s",url,"-H","User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"],
capture_output=True, timeout=30)
if r.returncode != 0: return None
html = r.stdout.decode('utf-8', errors='ignore')
records = self._parse_dlt_500(html)
if not records: return None
if records:
issues = sorted(int(r["issue"]) for r in records)
cfg["latest_fetch_start"], cfg["latest_fetch_end"] = str(issues[0]), str(issues[-1])
return self._merge_records("dlt", hist, records, cfg)
def _parse_dlt_500(self, html):
"""解析500.com大乐透HTML表格"""
rows = re.findall(r'<tr class="t_tr1">(.*?)</tr>', html, re.DOTALL)
res = []
for row in rows:
row = re.sub(r'<!--.*?-->', '', row)
tds = re.findall(r'<td[^>]*>(.*?)</td>', row, re.DOTALL)
vals = [re.sub(r'<[^>]+>', '', td).strip() for td in tds]
if len(vals) < 8: continue
iss = vals[0].strip()
if re.match(r'^\d{5,}$', iss):
res.append({
"issue": iss,
"reds": [v.zfill(2) for v in vals[1:6]],
"blue": [v.zfill(2) for v in vals[6:8]],
"date": vals[-1].strip() if len(vals) >= 15 else ""
})
return res
def fetch_all(self):
return {"ssq":self.fetch_ssq() is not None, "dlt":self.fetch_dlt() is not None}
class Analyzer:
"""集成学习分析器 - 融合5种模型进行号码分析"""
def __init__(self, p, lt):
self.p = p
self.lt = lt
self.rule = LOTTERY_RULES[lt]
all_h = p.history(lt)
self.data = sorted(all_h, key=lambda x: int(x["issue"]), reverse=True)[:100]
self._cached_result = None
def analyze(self):
if not self.data:
return None
if self._cached_result is not None:
return self._cached_result
from ensemble_learning import ensemble_recommendation
rule = self.rule
lt = self.lt
# 运行集成学习(使用完整历史数据,约200期)
result = ensemble_recommendation(lt, self.data, rule, count=5)
if not result or not result['recommendations']:
return None
combined_scores = result['combined_scores']
weights = result['weights']
# 构建分析结果,兼容原有接口
r_comp = combined_scores['red']
b_comp = combined_scores['blue']
# 频率分析TOP10
sorted_r = sorted(r_comp.items(), key=lambda x: x[1], reverse=True)
top10r = [(i, round(s, 3)) for i, s in sorted_r[:10]]
# 蓝球TOP5
sorted_b = sorted(b_comp.items(), key=lambda x: x[1], reverse=True)
top5b = [(i, round(s, 3)) for i, s in sorted_b[:5]]
# 模式识别
oe_s = Counter()
for rec in self.data:
ns = [int(x) for x in rec["reds"]]
oe_s[f"{sum(1 for n in ns if n%2==1)}:{self.rule['red_count']-sum(1 for n in ns if n%2==1)}"] += 1
most_oe = oe_s.most_common(1)[0][0] if oe_s else "3:2"
mid = self.rule["red_max"] / 2
bs_s = Counter()
for rec in self.data:
ns = [int(x) for x in rec["reds"]]
bs_s[f"{sum(1 for n in ns if n>mid)}:{self.rule['red_count']-sum(1 for n in ns if n>mid)}"] += 1
most_bs = bs_s.most_common(1)[0][0] if bs_s else "3:2"
sums = [sum(int(x) for x in r["reds"]) for r in self.data]
avg_s = sum(sums) / len(sums) if sums else 0
std_s = math.sqrt(sum((s - avg_s) ** 2 for s in sums) / len(sums)) if sums else 0
result_data = {
"version": 7,
"source": rule["source"],
"data_range": {
"start": self.data[-1]["issue"],
"end": self.data[0]["issue"],
"total": len(self.data)
},
"red_composite": r_comp,
"blue_composite": b_comp,
"patterns": {
"most_oe": most_oe,
"most_bs": most_bs,
"sum_range": f"[{round(avg_s - std_s, 1)}, {round(avg_s + std_s, 1)}]"
},
"top10_reds": top10r,
"top5_blues": top5b,
"model_weights": weights,
"ensemble_type": "5-model weighted integration"
}
self._cached_result = result_data
return result_data
class Recommender:
"""集成学习推荐器 - 基于多模型融合生成推荐"""
def __init__(self, p, analyzer, lt):
self.p = p
self.analyzer = analyzer
self.lt = lt
self.rule = LOTTERY_RULES[lt]
self.analysis = analyzer.analyze()
def recommend(self, count=5):
if not self.analysis:
return []
rule = self.rule
rmax = rule["red_max"]
bmax = rule["blue_max"]
rcnt = rule["red_count"]
bcnt = rule["blue_count"]
r_comp = self.analysis["red_composite"]
b_comp = self.analysis["blue_composite"]
# 使用集成学习直接获取推荐
from ensemble_learning import EnsembleLearner
all_h = self.p.history(self.lt)
sorted_h = sorted(all_h, key=lambda x: int(x["issue"]), reverse=True)
learner = EnsembleLearner(self.lt, sorted_h, rule)
recommendations = learner.get_recommendations(count=count)
return recommendations
class FeedbackLoop:
def __init__(self,p): self.p=p
def check(self, lt, earnings=0.0):
fb = self.p.feedback()
hist = self.p.history(lt)
if not hist: return None
latest = max(hist, key=lambda x:int(x["issue"]))
actual_r = [int(x) for x in latest["reds"]]
actual_b = [int(x) for x in (latest["blue"] if isinstance(latest["blue"],list) else [latest["blue"]])]
analysis = self.p.analysis(lt)
last_rec = analysis.get("last_recommendation")
if not last_rec: return None
pred_r, pred_b = last_rec["reds"], last_rec["blues"]
hit_r = [n for n in pred_r if n in actual_r]
hit_b = [n for n in pred_b if n in actual_b]
stats = fb[lt]
stats["total"]+=1; stats["correct_reds"]+=len(hit_r); stats["correct_blues"]+=len(hit_b)
stats["best"] = max(stats["best"], len(hit_r)+len(hit_b))
# 记录收益(>0 表示产生了实际盈利)
if earnings > 0:
stats["profitable_wins"] = stats.get("profitable_wins", 0) + 1
stats["total_earnings"] = round(stats.get("total_earnings", 0.0) + earnings, 2)
it = {
"lt":lt,"issue":latest["issue"],"date":latest.get("date",""),
"pred_reds":pred_r,"pred_blues":pred_b,
"act_reds":actual_r,"act_blues":actual_b,
"hit_reds":hit_r,"hit_blues":hit_b,
"red_hits":len(hit_r),"blue_hits":len(hit_b),"total_hits":len(hit_r)+len(hit_b),
"earnings":earnings
}
fb["iterations"].append(it)
total_pred = sum(i["red_hits"]+i["blue_hits"] for i in fb["iterations"] if i["lt"]==lt)
total_possible = stats["total"]*(LOTTERY_RULES[lt]["red_count"]+LOTTERY_RULES[lt]["blue_count"])
avg_acc = round(total_pred/total_possible*100, 2) if total_possible else 0
fb["accuracy_history"].append({"issue":latest["issue"],"lt":lt,"hits":len(hit_r)+len(hit_b),"avg_accuracy":avg_acc})
self.p.save_feedback(fb)
self.p.config["iteration_count"] += 1
self.p.save_config()
name = LOTTERY_RULES[lt]["name"]
print("\n" + "="*60)
print("[回滚] %s 第%s期 开奖校验" % (name, latest["issue"]))
print(" 推荐红球: %s" % pred_r)
print(" 实际红球: %s" % actual_r)
print(" 命中红球: %s (%d个)" % (hit_r, len(hit_r)))
print(" 推荐蓝球: %s" % pred_b)
print(" 实际蓝球: %s" % actual_b)
print(" 命中蓝球: %s (%d个)" % (hit_b, len(hit_b)))
print(" 总命中: %d/%d" % (len(hit_r)+len(hit_b), LOTTERY_RULES[lt]["red_count"]+LOTTERY_RULES[lt]["blue_count"]))
print(" 累计平均精度: %.2f%%" % avg_acc)
print(" 迭代版本: v%d" % self.p.config["iteration_count"])
print("="*60 + "\n")
return it
# ==================== 全量回滚迭代 ====================
class FullBacktest:
"""全量回溯测试:从近100期的第1期开始逐期模拟分析推理→机选→回滚"""
def __init__(self, p): self.p = p
def backtest(self, lt, target_count=100):
"""
全量回滚迭代测试
:param lt: 彩种 ssq/dlt
:param target_count: 目标回滚期数(默认100)
:return: 汇总统计 dict
"""
rule = LOTTERY_RULES[lt]
name = rule["name"]
rmax = rule["red_max"]
# 获取全部历史数据(按升序)
all_hist = self.p.history(lt)
if not all_hist:
print("[%s] 无历史数据" % name)
return None
total_available = len(all_hist)
# 如果可用数据不足100期,使用全部数据
actual_count = min(target_count, total_available)
print("\n[%s] 全量回滚迭代测试 | 总数据: %d期 | 目标: %d期 | 起始: 第%s期 ~ 结束: 第%s期" % (
name, total_available, actual_count,
all_hist[-(actual_count)]["issue"], all_hist[-1]["issue"]))
print("=" * 80)
# 逐期向前模拟(从最近的目标期往前推)
start_idx = total_available - actual_count
results = []
for phase in range(actual_count):
# 目标期索引(从最近往前推)
target_idx = start_idx + phase
target_issue_data = all_hist[target_idx]
target_issue = int(target_issue_data["issue"])
# 训练数据:target_idx 之前所有的已知数据(不包含当期及之后)
train_data = all_hist[:target_idx]
if len(train_data) < 10:
continue # 数据太少跳过
# 创建临时 Analyzer,注入训练数据
class TempAnalyzer:
def __init__(self, data, lt, rule, p):
self.data = sorted(data, key=lambda x:int(x["issue"]), reverse=True)[:100]
self.lt = lt
self.rule = rule
self.p = p
self._cached_result = None
def analyze(self):
if self._cached_result is None and self.data:
# 复用原 Analyzer 的分析逻辑(简化版)
self._cached_result = _run_analysis(self.data, self.lt, self.rule, self.p)
return self._cached_result
temp_analyzer = TempAnalyzer(train_data, lt, rule, self.p)
analysis = temp_analyzer.analyze()
if not analysis:
continue
# 用推荐器生成推荐
pred_r, pred_b = _generate_prediction_from_analysis(analysis, lt, rule, self.p, ci=0)
# 实际开奖号码
act_r = [int(x) for x in target_issue_data["reds"]]
act_b = target_issue_data["blue"] if isinstance(target_issue_data["blue"], list) else [int(target_issue_data["blue"])]
# 计算命中
hit_r = [n for n in pred_r if n in act_r]
hit_b = [n for n in pred_b if n in act_b]
red_hits = len(hit_r)
blue_hits = len(hit_b)
total_hits = red_hits + blue_hits
results.append({
"issue": target_issue,
"pred_reds": pred_r,
"pred_blues": pred_b,
"act_reds": act_r,
"act_blues": act_b,
"hit_reds": hit_r,
"hit_blues": hit_b,
"red_hits": red_hits,
"blue_hits": blue_hits,
"total_hits": total_hits,
"phase": phase + 1
})
# 汇总统计
total_simulations = len(results)
total_red_hits = sum(r["red_hits"] for r in results)
total_blue_hits = sum(r["blue_hits"] for r in results)
total_possible = total_simulations * (rule["red_count"] + rule["blue_count"])
# 命中率分档统计
perfect_hits = sum(1 for r in results if r["red_hits"] >= rule["red_count"] and r["blue_hits"] >= rule["blue_count"])
good_hits = sum(1 for r in results if r["total_hits"] >= 3)
some_hits = sum(1 for r in results if 1 <= r["total_hits"] < 3)
zero_hits = sum(1 for r in results if r["total_hits"] == 0)
avg_red_accuracy = round(total_red_hits / (total_simulations * rule["red_count"]) * 100, 2) if total_simulations else 0
avg_blue_accuracy = round(total_blue_hits / (total_simulations * rule["blue_count"]) * 100, 2) if total_simulations else 0
avg_overall_accuracy = round((total_red_hits + total_blue_hits) / total_possible * 100, 2) if total_possible else 0
avg_red_per_issue = round(total_red_hits / total_simulations, 2) if total_simulations else 0
avg_blue_per_issue = round(total_blue_hits / total_simulations, 2) if total_simulations else 0
max_red_hits = max((r["red_hits"] for r in results), default=0)
max_total_hits = max((r["total_hits"] for r in results), default=0)
# 红球命中率趋势(前33期、中段33期、后33期)
chunks = {}
chunk_size = max(1, len(results) // 3)
if len(results) >= chunk_size * 3:
chunks["前33%"] = {
"simulations": len(results[:chunk_size]),
"avg_red_hits": round(sum(r["red_hits"] for r in results[:chunk_size])/len(results[:chunk_size]), 2) if results[:chunk_size] else 0,
"avg_blue_hits": round(sum(r["blue_hits"] for r in results[:chunk_size])/len(results[:chunk_size]), 2) if results[:chunk_size] else 0,
}
chunks["中段33%"] = {
"simulations": len(results[chunk_size:chunk_size*2]),
"avg_red_hits": round(sum(r["red_hits"] for r in results[chunk_size:chunk_size*2])/len(results[chunk_size:chunk_size*2]), 2) if results[chunk_size:chunk_size*2] else 0,
"avg_blue_hits": round(sum(r["blue_hits"] for r in results[chunk_size:chunk_size*2])/len(results[chunk_size:chunk_size*2]), 2) if results[chunk_size:chunk_size*2] else 0,
}
chunks["后33%"] = {
"simulations": len(results[chunk_size*2:]),
"avg_red_hits": round(sum(r["red_hits"] for r in results[chunk_size*2:])/len(results[chunk_size*2:]), 2) if results[chunk_size*2:] else 0,
"avg_blue_hits": round(sum(r["blue_hits"] for r in results[chunk_size*2:])/len(results[chunk_size*2:]), 2) if results[chunk_size*2:] else 0,
}
elif len(results) > 0:
chunks["全部"] = {
"simulations": len(results),
"avg_red_hits": round(sum(r["red_hits"] for r in results)/len(results), 2) if results else 0,
"avg_blue_hits": round(sum(r["blue_hits"] for r in results)/len(results), 2) if results else 0,
}
# 保存汇总结果到 feedback.json
fb = self.p.feedback()
fb["backtest_%s" % lt] = {
"total_simulations": total_simulations,
"total_red_hits": total_red_hits,
"total_blue_hits": total_blue_hits,
"avg_red_accuracy": avg_red_accuracy,
"avg_blue_accuracy": avg_blue_accuracy,
"avg_overall_accuracy": avg_overall_accuracy,
"avg_red_per_issue": avg_red_per_issue,
"avg_blue_per_issue": avg_blue_per_issue,
"max_red_hits": max_red_hits,
"max_total_hits": max_total_hits,
"perfect_hits": perfect_hits,
"good_hits": good_hits,
"some_hits": some_hits,
"zero_hits": zero_hits,
"trend": chunks
}
self.p.save_feedback(fb)
# =================== 输出汇总报告 ===================
print("\n" + "=" * 80)
print("[3/5] 汇总统计")
print("=" * 80)
print(" 有效模拟: %d期" % total_simulations)
print("")
print(" === 命中率 ===")
print(" 累计红球命中: %d/%d (%.2f%%)" % (total_red_hits, total_simulations*rule["red_count"], avg_red_accuracy))
print(" 累计蓝球命中: %d/%d (%.2f%%)" % (total_blue_hits, total_simulations*rule["blue_count"], avg_blue_accuracy))
print(" 累计总命中: %d/%d (%.2f%%)" % (total_red_hits+total_blue_hits, total_possible, avg_overall_accuracy))
print("")
print(" === 每期平均 ===")
print(" 每期平均命中红球: %.2f个" % avg_red_per_issue)
print(" 每期平均命中蓝球: %.2f个" % avg_blue_per_issue)
print(" 最大红球命中: %d个" % max_red_hits)
print(" 最大总命中: %d个" % max_total_hits)
print("")
print(" === 表现分布 ===")
print(" [WIN] 完美命中: %d期 (%.1f%%)" % (perfect_hits, perfect_hits/total_simulations*100))
print(" [GOOD] 优秀(≥3命中): %d期 (%.1f%%)" % (good_hits, good_hits/total_simulations*100))
print(" [OK] 一般(1~2命中): %d期 (%.1f%%)" % (some_hits, some_hits/total_simulations*100))
print(" [BAD] 未命中: %d期 (%.1f%%)" % (zero_hits, zero_hits/total_simulations*100))
print("")
if chunks:
print(" === 趋势分析 ===")
for label, stats in chunks.items():
avg_r = stats["avg_red_hits"]
avg_b = stats["avg_blue_hits"]
sim_n = stats["simulations"]
acc_r = round(avg_r/rule["red_count"]*100, 1) if rule["red_count"] else 0
acc_b = round(avg_b/rule["blue_count"]*100, 1) if rule["blue_count"] else 0
print(" %-8s: 红%.2f个(%.1f%%) | 蓝%.2f个(%.1f%%) | %d期" % (
label, avg_r, acc_r, avg_b, acc_b, sim_n))
print("")
# 判断趋势是否改善
if len(chunks) >= 2:
keys = list(chunks.keys())
first_chunk = chunks[keys[0]]
last_chunk = chunks[keys[-1]]
first_acc = (first_chunk["avg_red_hits"]/rule["red_count"]*100 + first_chunk["avg_blue_hits"]/rule["blue_count"]*100) / 2
last_acc = (last_chunk["avg_red_hits"]/rule["red_count"]*100 + last_chunk["avg_blue_hits"]/rule["blue_count"]*100) / 2
diff = last_acc - first_acc
direction = "↑ 提升" if diff > 0 else ("↓ 下降" if diff < 0 else "→ 持平")
print(" 趋势: %.1f%% → %.1f%% → %s (%.1f%%)" % (first_acc, last_acc, direction, abs(diff)))
print("\n" + "=" * 80)
print("[4/5] 示例样本(最近5期模拟详情)")
print("=" * 80)
display_samples = results[-5:] if len(results) >= 5 else results
for s in display_samples:
issue = s["issue"]
pred = ["%02d"%n for n in s["pred_reds"]]
act = ["%02d"%n for n in s["act_reds"]]
pred_b_str = " ".join("%02d"%n for n in s["pred_blues"])
act_b_str = " ".join("%02d"%int(n) for n in s["act_blues"])
hr = ["%02d"%n for n in s["hit_reds"]]
hb = ["%02d"%n for n in s["hit_blues"]]
marker = "[GOOD]" if s["total_hits"] >= 3 else ("[OK]" if s["total_hits"] >= 1 else "[BAD]")
print(" %s 期%d | 推荐: %s +%s | 实际: %s +%s | 命中: 红%s(%d) 蓝%s(%d) | %s" % (
name, issue, " ".join(pred), pred_b_str,
" ".join(act), act_b_str,
" ".join(hr), s["red_hits"],
" ".join(hb), s["blue_hits"],
marker))
print("")
print("=" * 80)
print("[5/5] 诊断结论 & 优化建议")
print("=" * 80)
issues = []
suggestions = []
# 诊断规则
if avg_overall_accuracy < 10:
issues.append("总体命中率偏低(%.2f%%),低于10%基线" % avg_overall_accuracy)
suggestions.append("- 检查特征工程:是否存在过拟合/欠拟合问题?考虑调整维度权重分配")
elif avg_overall_accuracy < 15:
issues.append("总体命中率一般(%.2f%%),接近随机水平" % avg_overall_accuracy)
suggestions.append("- 可尝试增加短期动量特征(如连续出现趋势)作为补充维度")
if avg_red_accuracy < avg_red_per_issue / rule["red_count"] * 100 * 0.7:
issues.append("红球命中率不稳定,波动较大")
suggestions.append("- 加强区间平衡和奇偶比约束,减少极端分布")
if zero_hits / total_simulations > 0.5:
issues.append("超过50%%的期数完全没有命中")
suggestions.append("- 推荐算法可能过于集中(过度依赖TOP号码),考虑扩大候选池")
# 趋势判断
if len(chunks) >= 2:
first_keys = list(chunks.keys())[:1]
last_keys = list(chunks.keys())[-1:]
fc = chunks[first_keys[0]]
lc = chunks[last_keys[0]]
first_total = fc["avg_red_hits"] + fc["avg_blue_hits"]
last_total = lc["avg_red_hits"] + lc["avg_blue_hits"]
if last_total <= first_total:
issues.append("随模拟推进(数据窗口增大),命中率未见提升")
suggestions.append("- 说明当前分析框架需要结构性优化,而非简单调参")
if not issues:
print(" [OK] 各项指标均在合理范围内,分析框架有效运行")
else:
print(" [WARN] 以下问题待解决:")
for i, issue in enumerate(issues, 1):
print(" %d. %s" % (i, issue))
print("")
print(" [SUGGEST] 优化建议:")
for s in suggestions:
print(" - %s" % s)
print("\n [DATA] 详细数据已保存至: feedback.json → backtest_{lt}")
print("=" * 80 + "\n")
return {
"total_simulations": total_simulations,
"avg_red_accuracy": avg_red_accuracy,
"avg_blue_accuracy": avg_blue_accuracy,
"avg_overall_accuracy": avg_overall_accuracy,
"issues": issues,
"suggestions": suggestions
}
def _run_analysis(data, lt, rule, p):
"""执行分析(与 Analyzer.analyze() 逻辑一致,但支持自定义数据)"""
rmax, bmax, rcnt = rule["red_max"], rule["blue_max"], rule["red_count"]
all_r, all_b = [], []
for rec in data:
all_r.extend(int(x) for x in rec["reds"])
bl = rec["blue"]
all_b.extend(int(x) for x in (bl if isinstance(bl,list) else [bl]))
rf, bf = Counter(all_r), Counter(all_b)
max_rf, max_bf = max(rf.values()) or 1, max(bf.values()) or 1
rf_score = {i: rf.get(i,0)/max_rf for i in range(1,rmax+1)}
bf_score = {i: bf.get(i,0)/max_bf for i in range(1,bmax+1)}
hot = data[:20]
hot_r = [int(x) for r in hot for x in r["reds"]]
hot_rf = Counter(hot_r); max_hr = max(hot_rf.values()) or 1
hot_score = {i: hot_rf.get(i,0)/max_hr for i in range(1,rmax+1)}
r_miss = {}
for n in range(1,rmax+1):
cnt=0
for r in data:
if n in [int(x) for x in r["reds"]]: break
cnt+=1
r_miss[n]=cnt
b_miss = {}
for n in range(1,bmax+1):
cnt=0
for r in data:
ns = [int(x) for x in (r["blue"] if isinstance(r["blue"],list) else [r["blue"]])]
if n in ns: break
cnt+=1
b_miss[n]=cnt
max_rm, max_bm = max(r_miss.values()) or 1, max(b_miss.values()) or 1
rm_score = {n: math.sin(m/max_rm*math.pi)*0.8+0.2 for n,m in r_miss.items()}
bm_score = {n: math.sin(m/max_bm*math.pi)*0.8+0.2 for n,m in b_miss.items()}
oe_s = Counter()
for rec in data:
ns = [int(x) for x in rec["reds"]]
oe_s["%d:%d" % (sum(1 for n in ns if n%2==1), rcnt-sum(1 for n in ns if n%2==1))]+=1
most_oe = oe_s.most_common(1)[0][0] if oe_s else "3:3"
mid = rmax/2
bs_s = Counter()
for rec in data:
ns = [int(x) for x in rec["reds"]]
bs_s["%d:%d" % (sum(1 for n in ns if n>mid), rcnt-sum(1 for n in ns if n>mid))]+=1
most_bs = bs_s.most_common(1)[0][0] if bs_s else "3:3"
sums_list = [sum(int(x) for x in r["reds"]) for r in data]
avg_s = sum(sums_list)/len(sums_list) if sums_list else 0
std_s = math.sqrt(sum((s-avg_s)**2 for s in sums_list)/len(sums_list)) if sums_list else 0
tail_f = Counter()
for rec in data:
for ns in rec["reds"]:
val = int(ns) if not isinstance(ns, int) else ns
tail_f[val] += 1
max_tf = max(tail_f.values()) or 1
tail_sc = {t: tail_f.get(t,0)/max_tf for t in range(10)}
zones = [(1,11),(12,22),(23,33)] if rmax==33 else [(1,12),(13,24),(25,35)]
zc = [0]*len(zones)
for rec in data:
for n in [int(x) for x in rec["reds"]]:
for i,(zs,ze) in enumerate(zones):
if zs<=n<=ze: zc[i]+=1; break
avg_zone = len(data)*rcnt/len(zones)
zb = [c/avg_zone if avg_zone else 1.0 for c in zc]
r_comp = {}
for n in range(1,rmax+1):
s = rf_score.get(n,0)*0.25 + hot_score.get(n,0)*0.20 + rm_score.get(n,0.5)*0.20 + tail_sc.get(n%10,0.5)*0.15
for i,(zs,ze) in enumerate(zones):
if zs<=n<=ze and zb[i]<0.85: s+=0.10; break
r_comp[n] = round(s,4)
recent_b = []
for r in data[:20]:
recent_b.extend(int(x) for x in (r["blue"] if isinstance(r["blue"],list) else [r["blue"]]))
rbf = Counter(recent_b); max_rbf = max(rbf.values()) or 1
b_comp = {}
for n in range(1,bmax+1):
s = bf_score.get(n,0)*0.35 + bm_score.get(n,0.5)*0.40 + (rbf.get(n,0)/max_rbf)*0.25
b_comp[n] = round(s,4)
return {
"version": 999, "lottery_type": lt, "lottery_name": rule["name"],
"data_range": {"total":len(data)},
"red_composite": r_comp, "blue_composite": b_comp,
"patterns": {"most_oe": most_oe, "most_bs": most_bs,
"sum_range": [round(avg_s-std_s,1), round(avg_s+std_s,1)]}
}
def _generate_prediction_from_analysis(analysis, lt, rule, p, ci=0):
"""根据已有分析结果提取最佳预测号码(返回红球和蓝球两个列表)"""
r_comp = analysis.get("red_composite", {})
b_comp = analysis.get("blue_composite", {})
rmax = rule["red_max"]
bmax = rule["blue_max"]
sorted_r = sorted(r_comp.items(), key=lambda x:x[1], reverse=True)
top_r = [n for n,s in sorted_r[:rmax]]
top_b = sorted(b_comp.items(), key=lambda x:x[1], reverse=True)[:rule["blue_count"]]
pred_b = [n for n,s in top_b]
pred_b.sort()
pred_r = []
seed = hashlib.md5(("%s_bt_%s" % (p.config.get("seed",""), str(ci))).encode()).hexdigest()
cr = random.Random(seed)
cand = list(top_r)
w = [r_comp.get(n,0.5) for n in cand]
while len(pred_r) < rule["red_count"] and cand:
tw = sum(w)
if tw<=0: pk = cr.choice(cand)
else:
rv = cr.random()*tw; cum=0; pk=cand[-1]
for i,ww in enumerate(w):
cum+=ww
if rv<=cum: pk=cand[i]; break
if pk not in pred_r: pred_r.append(pk)
idx = cand.index(pk); cand.pop(idx); w.pop(idx)
pred_r.sort()
return pred_r, pred_b
# ==================== 展示函数 ====================
def print_header():
print("\n" + "*"*60)
print(" 双色球 & 大乐透 * 智能号码分析系统 v1.0")
print(" 基于多维度数学模型 + 概率分析的机选推荐")
print("*"*60 + "\n")
def print_today_info():
now = datetime.datetime.now()
wd = now.isoweekday()
print("[%s] %s" % (now.strftime("%Y-%m-%d %H:%M"), WEEKDAY.get(wd,'')))
ssq_today = wd in LOTTERY_RULES["ssq"]["draw_days"]
dlt_today = wd in LOTTERY_RULES["dlt"]["draw_days"]
print(" 双色球: %s (二/四/日)" % ("[今晚开奖!]" if ssq_today else "[今日不开奖]"))
print(" 大乐透: %s (一/三/六)" % ("[今晚开奖!]" if dlt_today else "[今日不开奖]"))
print()
return ssq_today, dlt_today
def show_analysis(p, lt):
analyzer = Analyzer(p, lt)
result = analyzer.analyze()
if not result:
print("\n[%s] 暂无数据" % LOTTERY_RULES[lt]["name"])
return None
rule = LOTTERY_RULES[lt]
print("\n" + "-"*50)
print("[%s] 分析报告" % rule["name"])
print("-"*50)
print(" 数据范围: 近%d期" % result["data_range"]["total"])
print(" TOP红球: ", end="")
r_comp = result["red_composite"]
for n,s in sorted(r_comp.items(), key=lambda x:x[1], reverse=True)[:10]:
print("%02d(%.3f)" % (n, s), end=" ")
print()
print(" TOP蓝球: ", end="")
b_comp = result["blue_composite"]
for n,s in sorted(b_comp.items(), key=lambda x:x[1], reverse=True)[:5]:
print("%02d(%.3f)" % (n, s), end=" ")
print()
pat = result["patterns"]
print(" 模式: 奇偶%s 大小%s 和值范围%s" % (pat["most_oe"], pat["most_bs"], pat["sum_range"]))
print(" 分析版本: v%d | 数据源: %s" % (result["version"], rule["source"]))
print(" 数据范围: 第%s期 ~ 第%s期" % (result["data_range"]["start"], result["data_range"]["end"]))
return result
def show_recommendation(p, lt, count=5):
analyzer = Analyzer(p, lt)
rec = Recommender(p, analyzer, lt)
results = rec.recommend(count)
if not results:
print("\n[%s] 无法生成推荐" % LOTTERY_RULES[lt]["name"])
return
rule = LOTTERY_RULES[lt]
print("\n" + "="*60)
print("[%s] 机选推荐 (%s)" % (rule["name"], datetime.datetime.now().strftime("%m/%d %H:%M")))
print("="*60)
for i, r in enumerate(results):
r_str = " ".join("%02d" % n for n in r["reds"])
b_str = " ".join("%02d" % n for n in r["blues"])
print("\n 第%d组: [红] %s + [蓝] %s" % (i+1, r_str, b_str))
print(" 得分:%.4f | 奇偶:%s | 大小:%s | 和:%d" % (r["score"], r["odd_even"], r["big_small"], r["sum"]))
# 保存最佳推荐供回滚
if results:
analysis = p.analysis(lt)
analysis["last_recommendation"] = {
"reds":results[0]["reds"], "blues":results[0]["blues"],
"generated_at":datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
p.save_analysis(lt, analysis)
return results
def show_ensemble_recommendation(p, lt, count=5):
"""集成学习推荐:多模型融合 + 权重优化"""
if ensemble_recommendation is None:
print("\n[错误] 集成学习模块未加载")
return
rule = LOTTERY_RULES[lt]
name = rule["name"]
rmax = rule["red_max"]
rcnt = rule["red_count"]
bmax = rule["blue_max"]
bcnt = rule["blue_count"]
print("\n" + "="*70)
print(f"🧠 [{name}] 集成学习推荐 (多模型融合 + 权重优化)")
print(f"{'='*70}")
# 获取历史数据
hist = p.history(lt)
if not hist:
print(f"\n[{name}] 暂无历史数据")
return
# 使用最近100期数据作为训练集
sorted_hist = sorted(hist, key=lambda x: int(x["issue"]), reverse=True)[:100]
# 运行集成学习
result = ensemble_recommendation(lt, sorted_hist, rule, count=count)
if not result or not result['recommendations']:
print("\n[错误] 集成学习推荐失败")
return
weights = result['weights']
backtest = result['backtest']
recommendations = result['recommendations']
# 显示各模型权重
print("\n" + "-"*50)
print("📊 各模型权重(回测优化后)")
print("-"*50)
model_names = {
'freq': '频率加权模型',
'bayes': '贝叶斯平滑模型',
'markov': '马尔可夫链模型',
'kmeans': 'K-Means聚类模型',
'residual': '残差回归模型'
}
for m, w in sorted(weights.items(), key=lambda x: -x[1]):
print(f" {model_names.get(m, m):<20s}: {w:.4f}")
print(f"\n 回测迭代: {backtest['iterations']} 次 | 测试期数: {backtest['test_periods']}")
print(f" 回测总分: {backtest['best_score']:.1f} (分数越高表示匹配度越好)")
# 显示推荐号码
print("\n" + "-"*50)
print("🎯 集成学习推荐号码")
print("-"*50)
today = datetime.datetime.now()
draw_days = rule["draw_days"]
draw_time = rule.get("draw_time", "20:00")
draw_hour, draw_min = map(int, draw_time.split(":"))
now_hour, now_min = today.hour, today.minute
today_draw = today.isoweekday() in draw_days
time_past = (now_hour > draw_hour) or (now_hour == draw_hour and now_min >= draw_min)
if today_draw and not time_past:
delta = 0
else:
delta = 1
while (today + datetime.timedelta(days=delta)).isoweekday() not in draw_days:
delta += 1
next_draw_date = (today + datetime.timedelta(days=delta)).strftime("%Y-%m-%d")
wd_map = {1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"}
next_wd = wd_map.get((today + datetime.timedelta(days=delta)).isoweekday(), "")
print(f"\n 即将开奖: 第{int(sorted_hist[0]['issue'])+1}期 | {next_draw_date} ({next_wd})")
print()
for i, r in enumerate(recommendations):
r_str = " ".join("%02d" % n for n in r["reds"])
b_str = " ".join("%02d" % n for n in r["blues"])
star = " ⭐" if i == 0 else ""
print(f" 第{i+1}组: [前区] {r_str} + [后区] {b_str}")
print(f" 综合得分: {r['score']:.4f}{star} | 奇偶比: {r['odd_even']} | 大小比: {r['big_small']} | 和值: {r['sum']}")
# 保存最佳推荐供回滚
if recommendations:
analysis = p.analysis(lt)
analysis["last_ensemble_recommendation"] = {
"reds": recommendations[0]["reds"],
"blues": recommendations[0]["blues"],
"generated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"weights": weights,
"backtest": backtest
}
p.save_analysis(lt, analysis)
print("\n" + "="*70)
print(" 说明:集成学习融合5种算法,通过历史数据回测自动优化权重")
print(" 分数越高表示该号码组在历史模式中匹配度越高")
print("="*70)
return recommendations
def show_full_report(p, lt, count=5):
"""4块结构化输出:回滚迭代 + 数据采集 + 推理分析 + 机选号码"""
rule = LOTTERY_RULES[lt]
fb = p.feedback()
stats = fb.get(lt, {})
analysis = p.analysis(lt)
hist = p.history(lt)
if not hist:
print("\n[%s] 暂无数据" % rule["name"])
return
# 获取历史最新期号
latest = max(hist, key=lambda x:int(x["issue"]))
prev_issue = latest["issue"]
prev_date = latest.get("date","")
prev_reds = latest["reds"]
prev_blues = latest["blue"] if isinstance(latest["blue"],list) else [latest["blue"]]
next_issue = str(int(prev_issue)+1)
# 计算下期开奖日期
today = datetime.datetime.now()
draw_days = rule["draw_days"]
# 如果今天就是开奖日且还没过开奖时间,则下期就是今天
draw_time = rule.get("draw_time", "20:00")
draw_hour, draw_min = map(int, draw_time.split(":"))
now_hour, now_min = today.hour, today.minute
today_draw = today.isoweekday() in draw_days
time_past = (now_hour > draw_hour) or (now_hour == draw_hour and now_min >= draw_min)
if today_draw and not time_past:
delta = 0
else:
delta = 1
while (today + datetime.timedelta(days=delta)).isoweekday() not in draw_days:
delta += 1
next_draw_date = (today + datetime.timedelta(days=delta)).strftime("%Y-%m-%d")
wd_map = {1:"周一",2:"周二",3:"周三",4:"周四",5:"周五",6:"周六",7:"周日"}
next_wd = wd_map.get((today + datetime.timedelta(days=delta)).isoweekday(), "")
next_draw_date += " (%s)" % next_wd
# 回滚统计
ver = p.config.get("iteration_count", 0)
total_recs = stats.get("total", 0)
profitable = stats.get("profitable_wins", 0)
win_rate = "%.1f%%" % (profitable/total_recs*100) if total_recs > 0 else "0.0%"
earnings = stats.get("total_earnings", 0.0)
# 分析
analyzer = Analyzer(p, lt)
result = analyzer.analyze()
if not result:
print("\n[%s] 无法分析" % rule["name"])
return
# ==================== 第1块:回滚迭代 ====================
print("\n" + "="*60)
print(" [1/4] 回滚迭代")
print("="*60)
print(" 当前版本号: v%d" % ver)
print(" 版本胜率: %s (%d期推荐 / %d次盈利)" % (win_rate, total_recs, profitable))
print(" 累计中奖收益金额: RMB %.2f" % earnings)
# ==================== 第2块:数据采集 ====================
dr = result["data_range"]
print("\n" + "="*60)
print(" [2/4] 数据采集")
print("="*60)
print(" 采集期数范围: 第%s期 ~ 第%s期(近%d期)" % (dr["start"], dr["end"], dr["total"]))
print(" 上一期号: 第%s期" % prev_issue)
print(" 开奖日期: %s" % prev_date)
r_str = " ".join("%02d" % int(x) for x in prev_reds)
b_str = " ".join("%02d" % int(x) for x in prev_blues)
if lt == "ssq":
print(" 开奖号码: 红球 %s 蓝球 %s" % (r_str, b_str))
else:
print(" 开奖号码: 前区 %s 后区 %s" % (r_str, b_str))
# ==================== 第3块:推理分析 ====================
pat = result["patterns"]
print("\n" + "="*60)
print(" [3/4] 推理分析(分析版本 v%d | 数据源: %s)" % (result["version"], rule["source"]))
print("="*60)
# 步骤1:集成学习评分(TOP10前区)
r_comp = result["red_composite"]
top10r = sorted(r_comp.items(), key=lambda x:x[1], reverse=True)[:10]
print(" >> 步骤1 - 集成学习评分(前区)")
print(" %s" % " ".join("%02d(%.4f)"%(n,s) for n,s in top10r))
# 步骤2:集成学习评分(TOP10后区)
b_comp = result["blue_composite"]
top5b = sorted(b_comp.items(), key=lambda x:x[1], reverse=True)[:5]
print(" >> 步骤2 - 集成学习评分(后区)")
print(" %s" % " ".join("%02d(%.4f)"%(n,s) for n,s in top5b))
# 步骤3:模式识别
print(" >> 步骤3 - 模式识别")
print(" 奇偶比: %s | 大小比: %s | 和值区间: [%s, %s]" % (
pat["most_oe"], pat["most_bs"], pat["sum_range"][0], pat["sum_range"][1]))
# 步骤4:权重体系(集成学习模型)
print(" >> 步骤4 - 权重体系(集成学习模型)")
weights = result.get("model_weights", {})
if weights:
name_map = {'freq':'频率加权', 'bayes':'贝叶斯平滑', 'markov':'马尔可夫链', 'kmeans':'K-Means聚类', 'residual':'残差回归'}
weight_lines = []
for m, w in weights.items():
weight_lines.append("%s%.4f%%" % (name_map.get(m, m), w * 100))
print(" %s" % " + ".join(weight_lines))
else:
print(" 频率25% + 热号20% + 遗漏15% + 尾数10% + 区间8%")
print(" 奇偶8% + 大小6% + 和值4% + 概率2% + 连号2%")
# ==================== 第4块:机选号码 ====================
rec = Recommender(p, analyzer, lt)
results = rec.recommend(count)
if not results:
print("\n[%s] 无法生成推荐" % rule["name"])
return
print("\n" + "="*60)
print(" [4/4] 机选号码(第%s期 | %s)" % (next_issue, next_draw_date))
print("="*60)
for i, r in enumerate(results):
if lt == "ssq":
r_str = " ".join("%02d" % n for n in r["reds"])
b_str = "%02d" % r["blues"][0]
print("\n 第%d组: 红球 %s 蓝球 %s" % (i+1, r_str, b_str))
else:
r_str = " ".join("%02d" % n for n in r["reds"])
b_str = " ".join("%02d" % n for n in r["blues"])
print("\n 第%d组: 前区 %s + 后区 %s" % (i+1, r_str, b_str))
print(" 得分:%.4f | 奇偶:%s | 大小:%s | 和:%d" % (r["score"], r["odd_even"], r["big_small"], r["sum"]))
# 保存推荐
if results:
analysis["last_recommendation"] = {
"reds":results[0]["reds"], "blues":results[0]["blues"],
"generated_at":datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
p.save_analysis(lt, analysis)
print("\n" + "="*60)
# ==================== CLI 入口 ====================
def main():
import argparse
parser = argparse.ArgumentParser(description="彩票号码分析与机选工具")
parser.add_argument("--action", choices=["analyze","recommend","fetch","check","all","report","full_report","backtest"], default="report")
parser.add_argument("--lottery", choices=["ssq","dlt","all"], default="all")
parser.add_argument("--count", type=int, default=5)
parser.add_argument("--query", default="")
parser.add_argument("--earnings", type=float, default=0.0, help="中奖收益金额(元),仅check动作有效")
parser.add_argument("--target", type=int, default=100, help="全量回滚迭代的目标期数")
args = parser.parse_args()
p = Persistence()
# ==================== full_backtest ====================
if args.action == "backtest":
print("\n" + "*"*60)
print(" 双色球 & 大乐透 * 智能号码分析系统 v1.0")
print(" 全量回滚迭代测试 | 回溯近%d期逐期模拟" % args.target)
print("*"*60 + "\n")
for lt in (["ssq","dlt"] if args.lottery=="all" else [args.lottery]):
bt = FullBacktest(p)
result = bt.backtest(lt, target_count=args.target)
print("\n[OK] 全量回滚迭代测试完成,详细数据已保存至 feedback.json\n")
return
if args.action == "fetch":
c = Collector(p)
c.fetch_all()
return
if args.action == "check":
f = FeedbackLoop(p)
for lt in (["ssq","dlt"] if args.lottery=="all" else [args.lottery]):
f.check(lt, earnings=args.earnings)
return
lotteries = ["ssq","dlt"] if args.lottery=="all" else [args.lottery]
if args.action == "full_report":
print_header()
ssq_today, dlt_today = print_today_info()
c = Collector(p)
c.fetch_all()
for lt in lotteries:
show_full_report(p, lt, args.count)
return
if args.action in ["analyze","report","all"]:
print_header()
ssq_today, dlt_today = print_today_info()
# 先采集
c = Collector(p)
c.fetch_all()
for lt in lotteries:
show_analysis(p, lt)
if args.action in ["recommend","all","report"]:
for lt in lotteries:
analyzer = Analyzer(p, lt)
if not analyzer.data:
print("\n[警告] %s 暂无数据" % LOTTERY_RULES[lt]["name"])
continue
show_recommendation(p, lt, args.count)
if __name__ == "__main__":
main()