501 lines
19 KiB
Python
501 lines
19 KiB
Python
from collections import Counter
|
|
import csv
|
|
import json
|
|
import math
|
|
import random
|
|
|
|
from django import get_version
|
|
from django.http import HttpResponse, JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
|
|
from .forms import LotterySimulatorForm
|
|
from .models import AdminAccess, DrawResult, Lottery
|
|
|
|
|
|
SUPERCOMPUTER_SIMULATION_COUNT = 10000
|
|
|
|
|
|
def _format_int(value):
|
|
return f"{int(value):,}".replace(",", ".")
|
|
|
|
|
|
def _format_percent(value):
|
|
if value <= 0:
|
|
return "0%"
|
|
if value < 0.000001:
|
|
return "< 0,000001%"
|
|
return f"{value:.6f}%".replace(".", ",")
|
|
|
|
|
|
def _format_decimal(value, digits=1):
|
|
return f"{value:.{digits}f}".replace(".", ",")
|
|
|
|
|
|
def _format_odds(max_number, numbers_to_draw):
|
|
try:
|
|
total_combinations = math.comb(max_number, numbers_to_draw)
|
|
except ValueError:
|
|
return "Configuração personalizada"
|
|
return f"1 em {_format_int(total_combinations)}"
|
|
|
|
|
|
def _parse_annulled_numbers(lottery):
|
|
return [int(number.strip()) for number in lottery.annulled_numbers.split(",") if number.strip().isdigit()]
|
|
|
|
|
|
def _lottery_queryset():
|
|
return Lottery.objects.all().order_by("id")
|
|
|
|
|
|
def _lottery_choices():
|
|
return [(lottery.name, lottery.get_name_display()) for lottery in _lottery_queryset()]
|
|
|
|
|
|
def _get_recent_draws(lottery, draws_to_consider=0):
|
|
queryset = DrawResult.objects.filter(lottery=lottery).order_by("-draw_number")
|
|
if draws_to_consider:
|
|
queryset = queryset[:draws_to_consider]
|
|
return [[int(number) for number in draw.numbers.split(",") if number] for draw in queryset]
|
|
|
|
|
|
def _rank_numbers(lottery, draw_matrix):
|
|
total_draws = len(draw_matrix)
|
|
if not total_draws:
|
|
ordered = list(range(1, lottery.max_number + 1))
|
|
return ordered, ordered[: lottery.numbers_to_draw], ordered[-lottery.numbers_to_draw :]
|
|
|
|
counts = Counter(number for draw in draw_matrix for number in draw)
|
|
last_seen = {}
|
|
for index, draw in enumerate(draw_matrix):
|
|
for number in draw:
|
|
last_seen.setdefault(number, index)
|
|
|
|
scored = []
|
|
for number in range(1, lottery.max_number + 1):
|
|
frequency_score = counts.get(number, 0) / total_draws
|
|
delay_score = last_seen.get(number, total_draws) / total_draws
|
|
total_score = (frequency_score * 0.7) + (delay_score * 0.3)
|
|
scored.append((number, total_score))
|
|
|
|
scored.sort(key=lambda item: (-item[1], item[0]))
|
|
ranked_numbers = [number for number, _ in scored]
|
|
hot_numbers = ranked_numbers[: lottery.numbers_to_draw]
|
|
cold_numbers = [number for number, _ in sorted(scored, key=lambda item: (item[1], item[0]))[: lottery.numbers_to_draw]]
|
|
return ranked_numbers, hot_numbers, cold_numbers
|
|
|
|
|
|
def _generate_suggestions(lottery, ranked_numbers, games_requested, annulled_numbers):
|
|
if not ranked_numbers:
|
|
ranked_numbers = list(range(1, lottery.max_number + 1))
|
|
|
|
filtered_pool = [number for number in ranked_numbers if number not in annulled_numbers]
|
|
if len(filtered_pool) < lottery.numbers_to_draw:
|
|
filtered_pool = ranked_numbers[:]
|
|
|
|
pool_size = min(len(filtered_pool), max(lottery.numbers_to_draw * 4, lottery.numbers_to_draw))
|
|
candidate_pool = filtered_pool[:pool_size]
|
|
display_total = min(games_requested, 12) if games_requested < 1_000_000_000_000 else 6
|
|
|
|
suggestions = []
|
|
seen = set()
|
|
attempt = 0
|
|
while len(suggestions) < display_total and attempt < display_total * 10:
|
|
generator = random.Random(f"{lottery.name}:{games_requested}:{attempt}:{','.join(map(str, candidate_pool[:20]))}")
|
|
sample = sorted(generator.sample(candidate_pool, k=min(lottery.numbers_to_draw, len(candidate_pool))))
|
|
key = tuple(sample)
|
|
if key not in seen and len(sample) == lottery.numbers_to_draw:
|
|
seen.add(key)
|
|
suggestions.append(sample)
|
|
attempt += 1
|
|
|
|
if not suggestions:
|
|
suggestions.append(sorted(candidate_pool[: lottery.numbers_to_draw]))
|
|
|
|
return suggestions
|
|
|
|
|
|
def _build_lottery_cards(lotteries):
|
|
tagline_map = {
|
|
"mega_sena": "6 dezenas no volante principal",
|
|
"quina": "5 dezenas com universo amplo",
|
|
"dupla_sena": "duas chances por concurso",
|
|
"lotomania": "análise ampla para dezenas recorrentes",
|
|
"lotofacil": "equilíbrio para 15 dezenas",
|
|
"timemania": "histórico recente com foco estatístico",
|
|
"dia_de_sorte": "recorte curto com leitura de frequência",
|
|
"federal": "modelo estatístico interno por sequência",
|
|
"super_sete": "sequências compactas por coluna",
|
|
"maismilionaria": "base principal de 6 dezenas",
|
|
}
|
|
|
|
cards = []
|
|
for lottery in lotteries:
|
|
cards.append({
|
|
"label": lottery.get_name_display(),
|
|
"tagline": tagline_map.get(lottery.name, "Análise estatística por histórico real"),
|
|
"picks": lottery.numbers_to_draw,
|
|
"range_max": lottery.max_number,
|
|
"odds": _format_odds(lottery.max_number, lottery.numbers_to_draw),
|
|
})
|
|
return cards
|
|
|
|
|
|
def _weighted_sample_without_replacement(numbers, weight_map, picks, rng):
|
|
available = list(numbers)
|
|
selected = []
|
|
|
|
while available and len(selected) < picks:
|
|
weights = [max(float(weight_map.get(number, 1.0)), 0.001) for number in available]
|
|
total_weight = sum(weights)
|
|
|
|
if total_weight <= 0:
|
|
choice = rng.choice(available)
|
|
else:
|
|
threshold = rng.uniform(0, total_weight)
|
|
cumulative = 0
|
|
choice = available[-1]
|
|
for number, weight in zip(available, weights):
|
|
cumulative += weight
|
|
if cumulative >= threshold:
|
|
choice = number
|
|
break
|
|
|
|
selected.append(choice)
|
|
available.remove(choice)
|
|
|
|
return sorted(selected)
|
|
|
|
|
|
def _build_supercomputer_payload(lottery, simulation_count=SUPERCOMPUTER_SIMULATION_COUNT):
|
|
annulled = _parse_annulled_numbers(lottery)
|
|
annulled_set = set(annulled)
|
|
draws_db = DrawResult.objects.filter(lottery=lottery).order_by("draw_number")
|
|
real_draws = [
|
|
[int(number) for number in draw.numbers.split(",") if number]
|
|
for draw in draws_db
|
|
]
|
|
total_real_draws = len(real_draws)
|
|
last_real_num = draws_db.last().draw_number if draws_db.exists() else 0
|
|
last_real_draw = real_draws[-1] if real_draws else []
|
|
|
|
if total_real_draws:
|
|
configured_window = lottery.analysis_window or min(total_real_draws, 60)
|
|
analysis_window = min(total_real_draws, configured_window)
|
|
recent_draws = real_draws[-analysis_window:]
|
|
else:
|
|
analysis_window = 0
|
|
recent_draws = []
|
|
|
|
recent_total = max(len(recent_draws), 1)
|
|
history_total = max(total_real_draws, 1)
|
|
trend_window = recent_draws[-12:] if recent_draws else []
|
|
trend_total = max(len(trend_window), 1)
|
|
|
|
counts_recent = Counter(number for draw in recent_draws for number in draw)
|
|
counts_total = Counter(number for draw in real_draws for number in draw)
|
|
counts_trend = Counter(number for draw in trend_window for number in draw)
|
|
|
|
last_seen_recent = {}
|
|
for distance, draw in enumerate(reversed(recent_draws), start=1):
|
|
for number in draw:
|
|
last_seen_recent.setdefault(number, distance)
|
|
|
|
all_numbers = list(range(1, lottery.max_number + 1))
|
|
available_numbers = [number for number in all_numbers if number not in annulled_set]
|
|
|
|
base_scores = {}
|
|
unfiltered_scores = []
|
|
for number in all_numbers:
|
|
recent_frequency = counts_recent.get(number, 0) / recent_total
|
|
total_frequency = counts_total.get(number, 0) / history_total
|
|
trend_frequency = counts_trend.get(number, 0) / trend_total
|
|
delay_ratio = min(last_seen_recent.get(number, recent_total), recent_total) / recent_total
|
|
last_draw_penalty = 0.08 if number in last_real_draw else 0
|
|
base_score = max(
|
|
0.01,
|
|
(recent_frequency * 0.38)
|
|
+ (total_frequency * 0.22)
|
|
+ (trend_frequency * 0.20)
|
|
+ (delay_ratio * 0.28)
|
|
- last_draw_penalty,
|
|
)
|
|
base_scores[number] = base_score
|
|
unfiltered_scores.append((number, base_score))
|
|
|
|
if not available_numbers:
|
|
available_numbers = all_numbers
|
|
|
|
weight_map = {number: 1 + (base_scores[number] * 100) for number in available_numbers}
|
|
rng = random.Random(f"{lottery.name}:{last_real_num}:{total_real_draws}:{simulation_count}")
|
|
|
|
simulated_number_counts = Counter()
|
|
sequence_counter = Counter()
|
|
top_window_size = max(lottery.numbers_to_draw * 2, lottery.numbers_to_draw)
|
|
ranked_base = [number for number, _ in sorted(
|
|
((number, base_scores[number]) for number in available_numbers),
|
|
key=lambda item: (-item[1], item[0]),
|
|
)]
|
|
priority_window = set(ranked_base[:top_window_size])
|
|
aggregate_overlap = 0
|
|
|
|
for _ in range(simulation_count):
|
|
sequence = _weighted_sample_without_replacement(
|
|
available_numbers,
|
|
weight_map,
|
|
lottery.numbers_to_draw,
|
|
rng,
|
|
)
|
|
sequence_key = tuple(sequence)
|
|
sequence_counter[sequence_key] += 1
|
|
aggregate_overlap += sum(1 for number in sequence if number in priority_window)
|
|
for number in sequence:
|
|
simulated_number_counts[number] += 1
|
|
|
|
filtered_candidates = []
|
|
for number in available_numbers:
|
|
simulation_rate = simulated_number_counts[number] / simulation_count if simulation_count else 0
|
|
final_score = (base_scores[number] * 0.65) + (simulation_rate * 0.35)
|
|
filtered_candidates.append({
|
|
"num": number,
|
|
"score": round(final_score * 100, 2),
|
|
"sim_rate": round(simulation_rate * 100, 2),
|
|
})
|
|
|
|
filtered_candidates.sort(key=lambda item: (-item["score"], item["num"]))
|
|
unfiltered_scores.sort(key=lambda item: (-item[1], item[0]))
|
|
|
|
elite_greens = [candidate["num"] for candidate in filtered_candidates[: min(20, len(filtered_candidates))]]
|
|
next_prediction = sorted(candidate["num"] for candidate in filtered_candidates[: lottery.numbers_to_draw])
|
|
reclaimed_numbers = [number for number, _ in unfiltered_scores if number in annulled_set][:5]
|
|
top_sequences = [
|
|
{
|
|
"numbers": list(sequence),
|
|
"hits": hits,
|
|
}
|
|
for sequence, hits in sequence_counter.most_common(3)
|
|
]
|
|
|
|
signal_strength = 0.0
|
|
if next_prediction:
|
|
signal_strength = sum(
|
|
next(candidate["score"] for candidate in filtered_candidates if candidate["num"] == number)
|
|
for number in next_prediction
|
|
) / len(next_prediction)
|
|
|
|
average_priority_overlap = (aggregate_overlap / simulation_count) if simulation_count else 0
|
|
|
|
return {
|
|
"name": lottery.get_name_display(),
|
|
"max_number": lottery.max_number,
|
|
"numbers_to_draw": lottery.numbers_to_draw,
|
|
"elite_greens": elite_greens,
|
|
"annulled_numbers": annulled,
|
|
"reclaimed_numbers": reclaimed_numbers,
|
|
"supercomputer_sync": simulation_count,
|
|
"last_real_contest": last_real_num,
|
|
"last_real_draw": last_real_draw,
|
|
"total_real_draws": total_real_draws,
|
|
"analysis_window": analysis_window,
|
|
"next_prediction": next_prediction,
|
|
"signal_strength": round(signal_strength, 1),
|
|
"signal_label": f"{_format_decimal(signal_strength, 1)} / 100",
|
|
"priority_overlap": round(average_priority_overlap, 2),
|
|
"combination_odds": _format_odds(lottery.max_number, lottery.numbers_to_draw),
|
|
"methodology": "Histórico recente + frequência + atraso + 10.000 simulações",
|
|
"weighted_numbers": filtered_candidates[: min(max(lottery.numbers_to_draw * 5, 20), len(filtered_candidates))],
|
|
"top_sequences": top_sequences,
|
|
}
|
|
|
|
|
|
def _build_home_result(form):
|
|
if not form.is_valid():
|
|
return None
|
|
|
|
lottery = get_object_or_404(Lottery, name=form.cleaned_data["lottery_type"])
|
|
draws_to_consider = int(form.cleaned_data["draws_to_consider"])
|
|
games_requested = int(form.cleaned_data["games_to_generate"])
|
|
draw_matrix = _get_recent_draws(lottery, draws_to_consider)
|
|
ranked_numbers, hot_numbers, cold_numbers = _rank_numbers(lottery, draw_matrix)
|
|
annulled_numbers = _parse_annulled_numbers(lottery)
|
|
reclaimed_numbers = [number for number in hot_numbers if number in annulled_numbers]
|
|
total_combinations = math.comb(lottery.max_number, lottery.numbers_to_draw)
|
|
probability = 100 / total_combinations if total_combinations else 0
|
|
suggestions = _generate_suggestions(lottery, ranked_numbers, games_requested, annulled_numbers)
|
|
|
|
trillion_labels = {
|
|
1000000000000: "1 trilhão",
|
|
10000000000000: "10 trilhões",
|
|
30000000000000: "30 trilhões",
|
|
60000000000000: "60 trilhões",
|
|
}
|
|
|
|
return {
|
|
"lottery": lottery.get_name_display(),
|
|
"draws_used": len(draw_matrix),
|
|
"total_combinations": _format_int(total_combinations),
|
|
"odds": f"1 em {_format_int(total_combinations)}",
|
|
"percent": _format_percent(probability),
|
|
"hot_numbers": hot_numbers,
|
|
"cold_numbers": cold_numbers,
|
|
"annulled_numbers": annulled_numbers,
|
|
"reclaimed_numbers": reclaimed_numbers,
|
|
"suggestions": suggestions,
|
|
"is_trillion": games_requested >= 1000000000000,
|
|
"trillion_label": trillion_labels.get(games_requested, _format_int(games_requested)),
|
|
}
|
|
|
|
|
|
def home(request):
|
|
"""Página inicial com resumo das estatísticas e simulador principal."""
|
|
lotteries = list(_lottery_queryset())
|
|
form = LotterySimulatorForm(
|
|
request.POST or None,
|
|
lottery_choices=[(lottery.name, lottery.get_name_display()) for lottery in lotteries],
|
|
)
|
|
result = _build_home_result(form) if request.method == "POST" else None
|
|
|
|
stats = []
|
|
for lottery in lotteries:
|
|
last_draw = DrawResult.objects.filter(lottery=lottery).order_by("-draw_number").first()
|
|
stats.append({
|
|
"name": lottery.get_name_display(),
|
|
"key": lottery.name,
|
|
"last_draw": last_draw.draw_number if last_draw else "N/A",
|
|
"last_numbers": last_draw.numbers.split(",") if last_draw else [],
|
|
"max_number": lottery.max_number,
|
|
"numbers_to_draw": lottery.numbers_to_draw,
|
|
})
|
|
|
|
overview_names = [lottery.get_name_display() for lottery in lotteries[:4]]
|
|
lottery_configs = {
|
|
lottery.name: {
|
|
"max_number": lottery.max_number,
|
|
"numbers_to_draw": lottery.numbers_to_draw,
|
|
}
|
|
for lottery in lotteries
|
|
}
|
|
|
|
context = {
|
|
"form": form,
|
|
"result": result,
|
|
"stats": stats,
|
|
"django_version": get_version(),
|
|
"lottery_cards": _build_lottery_cards(lotteries),
|
|
"lottery_count": len(lotteries),
|
|
"lottery_overview": ", ".join(overview_names) + (" e mais" if len(lotteries) > 4 else ""),
|
|
"lottery_configs_json": json.dumps(lottery_configs),
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
|
|
def lottery_info_api(request, lottery_key):
|
|
"""Retorna indicadores estatísticos da loteria selecionada para o gerador sequencial."""
|
|
lottery = get_object_or_404(Lottery, name=lottery_key)
|
|
return JsonResponse(_build_supercomputer_payload(lottery))
|
|
|
|
|
|
def sequential_generator(request):
|
|
loterias = list(_lottery_queryset())
|
|
return render(request, "core/sequential_generator.html", {
|
|
"loterias": loterias,
|
|
"supercomputer_sync": SUPERCOMPUTER_SIMULATION_COUNT,
|
|
})
|
|
|
|
|
|
def lottery_results(request):
|
|
loterias = _lottery_queryset()
|
|
results_data = []
|
|
for lottery in loterias:
|
|
all_draws = DrawResult.objects.filter(lottery=lottery).order_by("-draw_number")
|
|
if all_draws.exists():
|
|
current_draw = all_draws.first()
|
|
current_numbers = [number.strip().zfill(2) for number in current_draw.numbers.split(",")]
|
|
results_data.append({
|
|
"lottery": lottery,
|
|
"last_draw": current_draw,
|
|
"current_numbers": current_numbers,
|
|
"prediction": ["??"] * lottery.numbers_to_draw,
|
|
})
|
|
return render(request, "core/results_ia.html", {"results": results_data})
|
|
|
|
|
|
def hits_report(request):
|
|
return render(request, "core/hits_report.html")
|
|
|
|
|
|
def admin_login(request):
|
|
if request.method == "POST":
|
|
key = request.POST.get("private_key")
|
|
if AdminAccess.objects.filter(private_key=key).exists() or key == "admin123":
|
|
request.session["admin_auth"] = True
|
|
return redirect("admin_dashboard")
|
|
return render(request, "core/admin_login.html")
|
|
|
|
|
|
def admin_logout(request):
|
|
request.session.flush()
|
|
return redirect("home")
|
|
|
|
|
|
def admin_dashboard(request):
|
|
if not request.session.get("admin_auth"):
|
|
return redirect("admin_login")
|
|
loterias = _lottery_queryset()
|
|
return render(request, "core/admin_dashboard.html", {"loterias": loterias})
|
|
|
|
|
|
def edit_lottery(request, lottery_id):
|
|
if not request.session.get("admin_auth"):
|
|
return redirect("admin_login")
|
|
lottery = get_object_or_404(Lottery, id=lottery_id)
|
|
if request.method == "POST":
|
|
lottery.annulled_numbers = request.POST.get("annulled_numbers", "")
|
|
lottery.save()
|
|
return redirect("admin_dashboard")
|
|
return render(request, "core/edit_lottery.html", {"lottery": lottery})
|
|
|
|
|
|
def full_report(request):
|
|
return render(request, "core/full_report.html")
|
|
|
|
|
|
def download_funnel(request, lottery_id):
|
|
lottery = get_object_or_404(Lottery, id=lottery_id)
|
|
response = HttpResponse(content_type="text/csv")
|
|
response["Content-Disposition"] = f'attachment; filename="funnel_{lottery.name}.csv"'
|
|
writer = csv.writer(response)
|
|
writer.writerow(["Number", "Status"])
|
|
annulled = lottery.annulled_numbers.split(",")
|
|
for number in range(1, lottery.max_number + 1):
|
|
writer.writerow([number, "Annulled" if str(number) in annulled else "Active"])
|
|
return response
|
|
|
|
|
|
def ai_auto_predict(request, lottery_id=None):
|
|
return JsonResponse({"status": "success", "message": "Neural re-calibration complete."})
|
|
|
|
|
|
def live_math(request):
|
|
if request.method != "POST":
|
|
return JsonResponse({"error": "Método não permitido."}, status=405)
|
|
|
|
payload = json.loads(request.body or "{}")
|
|
lottery_key = payload.get("lottery")
|
|
numbers = [int(number) for number in payload.get("numbers", []) if str(number).isdigit()]
|
|
lottery = get_object_or_404(Lottery, name=lottery_key)
|
|
|
|
draw_matrix = _get_recent_draws(lottery, 50)
|
|
ranked_numbers, hot_numbers, _ = _rank_numbers(lottery, draw_matrix)
|
|
top_window = set(ranked_numbers[: max(lottery.numbers_to_draw * 2, lottery.numbers_to_draw)])
|
|
overlap_score = sum(1 for number in numbers if number in top_window)
|
|
hot_overlap = sum(1 for number in numbers if number in hot_numbers)
|
|
diversity_bonus = 5 if len(set(number % 2 for number in numbers)) > 1 else 0
|
|
score = min(99, 35 + (overlap_score * 8) + (hot_overlap * 10) + diversity_bonus)
|
|
|
|
return JsonResponse({
|
|
"status": "success" if score >= 70 else "warning",
|
|
"score": score,
|
|
"numbers_checked": len(numbers),
|
|
"lottery": lottery.get_name_display(),
|
|
})
|