- 新增 model_full_report_dlt.py: 大乐透专用全量报告脚本 - 统一4块结构: 回滚迭代→数据采集→推理分析→机选号码 - 统一术语: 前区/后区(大乐透) 对齐 红球/蓝球(双色球) - 统一评分: TOP10前区+TOP5后区 归一化概率输出 - 更新 SKILL.md: 双色球/大乐透统一模板+文件结构
1314 lines
56 KiB
Python
1314 lines
56 KiB
Python
#!/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(' ','').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() |