import re import ast import os import time from da...

Creado el: 14 de enero de 2026

Respondido usando GPT-5.2 Thinking por Chat01

Pregunta

nowgoal_20260114_082711.xlsx

import re
import ast
import os
import time
from dataclasses import dataclass
from typing import Optional, Dict, Any, Tuple
from datetime import datetime
from collections import deque
from pathlib import Path

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException

from openpyxl import Workbook, load_workbook
from openpyxl.chart import LineChart, Reference

===================== CONFIG =====================

URL = "https://www.nowgoal.com/basketball"

POLL_SECONDS = 5
STAKE = 1.0
HEADLESS = True

✅ NEW: wait after FIRST halftime appears (to stabilize odds/lines)

HALFTIME_WAIT_SECONDS = 15

DIFF_BANDS = [
(20.0, 30.0, "20-30"),
(30.0, 40.0, "30-40"),
(40.0, 50.0, "40-50"),
(50.0, None, "50+"),
]

SCROLL_EVERY_N_LOOPS = 12

EXCEL_PATH = os.getenv("EXCEL_PATH", "nowgoal_monitor.xlsx")

TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "8025557030:AAHgZX_pCOvQu7Gmi33X3iqe0oY7Y67yBMA")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "6667940236")

====== WINDOWS-EXCEL SAFE SAVE + SNAPSHOTS ======

SNAPSHOT_DIR = "snapshots" # сюда будут писаться файлы, которые можно открывать в Excel
SNAPSHOT_EVERY_SECONDS = 60 # снимок раз в минуту (можешь поставить 30)
AUTOSAVE_SUFFIX = "_AUTOSAVE.xlsx" # если основной файл открыт -> сохраняем сюда
SAVE_RETRIES = 3
SAVE_RETRY_SLEEP = 1.0

_last_snapshot_ts = 0.0
_excel_lock_notified = False

===================== TELEGRAM =====================

def tg_ready() -> bool:
return bool(TELEGRAM_TOKEN and TELEGRAM_CHAT_ID)

def tg_send(text: str) -> None:
"""Отправка сообщения в Telegram + лог в консоль."""
print(f"[TG] -> {text.replace('\n', ' | ')}", flush=True)
if not tg_ready():
print("[TG] Token/ChatId не заданы (пропускаю отправку).", flush=True)
return

text
try: r = requests.post( f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage", data={"chat_id": TELEGRAM_CHAT_ID, "text": text, "disable_web_page_preview": True}, timeout=10, ) if not r.ok: print(f"[TG] Ошибка отправки: {r.status_code} {r.text[:300]}", flush=True) except Exception as e: print(f"[TG] Exception: {e}", flush=True)

===================== MODELS =====================

@dataclass
class Team:
name: str
tag: Optional[str] = None

@dataclass
class LineOU:
total: Optional[float] = None
over_odds: Optional[float] = None # decimal
under_odds: Optional[float] = None # decimal

===================== HELPERS =====================

def now_ts() -> str:
return datetime.now().strftime("%H:%M:%S")

def now_dt() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def txt(el) -> str:
if el is None:
return ""
return (el.get_attribute("textContent") or "").strip()

def safe_float(x: Optional[str]) -> Optional[float]:
if not x:
return None
x = x.strip().replace(",", ".")
try:
return float(x)
except ValueError:
return None

def hk_to_decimal(odds: Optional[float]) -> Optional[float]:
# HK 0.83 -> Decimal 1.83
if odds is None:
return None
if odds >= 1.2:
return odds
return odds + 1.0

def read_odds_decimal(span) -> Optional[float]:
"""odds2 может быть текст 1.80, а может быть пусто, но есть data-o=0.80 (HK)."""
if span is None:
return None

text
t = (span.get_attribute("textContent") or "").strip() v = safe_float(t) if v is not None: return hk_to_decimal(v) v2 = safe_float(span.get_attribute("data-o")) if v2 is not None: return hk_to_decimal(v2) return None

-------- Excel safe save + snapshots (Windows) --------

def safe_save(wb: Workbook, path: str) -> bool:
"""
Пытаемся сохранить в основной файл.
Если Excel держит лок (Windows) -> не падаем, сохраняем в AUTOSAVE.
"""
global _excel_lock_notified

text
for i in range(1, SAVE_RETRIES + 1): try: wb.save(path) # если раньше предупреждали про лок — сбросим флаг, т.к. снова всё ок _excel_lock_notified = False return True except PermissionError: print(f"[{now_ts()}] Excel LOCKED: {path} (try {i}/{SAVE_RETRIES})", flush=True) time.sleep(SAVE_RETRY_SLEEP) except OSError as e: print(f"[{now_ts()}] Save OSError: {e} (try {i}/{SAVE_RETRIES})", flush=True) time.sleep(SAVE_RETRY_SLEEP) base, _ext = os.path.splitext(path) fallback = base + AUTOSAVE_SUFFIX try: wb.save(fallback) print(f"[{now_ts()}] Saved to AUTOSAVE: {fallback}", flush=True) # предупреждение в TG только один раз, чтобы не спамить if (not _excel_lock_notified) and tg_ready(): tg_send( "⚠️ Excel-файл открыт и заблокирован.\n" f"Не могу записать в: {os.path.abspath(path)}\n" f"Сохраняю во временный: {os.path.abspath(fallback)}\n" f"И делаю snapshots в папку: {os.path.abspath(SNAPSHOT_DIR)}" ) _excel_lock_notified = True return False except Exception as e: print(f"[{now_ts()}] AUTOSAVE failed: {e}", flush=True) if tg_ready(): tg_send(f"❌ Не могу сохранить Excel ни в основной, ни в AUTOSAVE: {e}") return False

def maybe_write_snapshot(wb: Workbook, base_path: str) -> None:
"""
Пишем снимок в отдельный файл с уникальным именем.
Такой файл можно держать открытым в Excel — бот не будет блокироваться.
"""
global _last_snapshot_ts
now = time.time()
if now - _last_snapshot_ts < SNAPSHOT_EVERY_SECONDS:
return
_last_snapshot_ts = now

text
try: Path(SNAPSHOT_DIR).mkdir(parents=True, exist_ok=True) stamp = datetime.now().strftime("%Y%m%d_%H%M%S") snap_path = os.path.join(SNAPSHOT_DIR, f"nowgoal_{stamp}.xlsx") wb.save(snap_path) print(f"[{now_ts()}] Snapshot saved: {snap_path}", flush=True) except Exception as e: print(f"[{now_ts()}] Snapshot save failed: {e}", flush=True)

OU_TOTAL_RE = re.compile(r"\b[OU]\s*([0-9]+(?:.[0-9]+)?)", re.IGNORECASE)

def read_total_from_cell(cell) -> Optional[float]:
"""Тотал берём из span.odds1, а если он пропал/пустой — выковыриваем из innerText."""
odds1 = cell.find_elements(By.CSS_SELECTOR, "span.odds1")
if odds1:
v = safe_float(txt(odds1[0]))
if v is not None:
return v

text
it = (cell.get_attribute("innerText") or "").strip() m = OU_TOTAL_RE.search(it) if m: return safe_float(m.group(1)) return None

def split_team_and_tag(s: str) -> Team:
s = (s or "").strip()
if s.endswith("]") and "[" in s:
i = s.rfind("[")
name = s[:i].strip()
tag = s[i + 1:-1].strip() or None
if name:
return Team(name=name, tag=tag)
return Team(name=s, tag=None)

def parse_analysis_onclick(onclick: str) -> Optional[Tuple[int, str, str, str]]:
if not onclick or "basketInPage.analysis" not in onclick:
return None
try:
args_str = onclick[onclick.find("(") + 1: onclick.rfind(")")]
args = ast.literal_eval("(" + args_str + ")")
return int(args[0]), str(args[1]), str(args[2]), str(args[3])
except Exception:
return None

def scroll_load_all(driver, max_rounds: int = 25, pause: float = 0.35):
last = 0
stable = 0
for _ in range(max_rounds):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(pause)
cur = len(driver.find_elements(By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]'))
if cur == last:
stable += 1
if stable >= 3:
break
else:
stable = 0
last = cur
driver.execute_script("window.scrollTo(0, 0);")
time.sleep(0.15)

def pick_band(abs_diff: float) -> Optional[str]:
for lo, hi, label in DIFF_BANDS:
if abs_diff >= lo and (hi is None or abs_diff < hi):
return label
return None

def line_complete(line: LineOU) -> bool:
return (line.total is not None and line.over_odds is not None and line.under_odds is not None)

===================== STATUS =====================

LIVE_PAT = re.compile(r"\b\d+(?:st|nd|rd|th)\s+Qtr\b|\bOT\b|\bOvertime\b|\bLive\b", re.IGNORECASE)
TIME_PAT = re.compile(r"^\s*\d{1,2}:\d{2}\s*(?:AM|PM)?\s*$", re.IGNORECASE)

def is_finished(status: Optional[str]) -> bool:
return (status or "").strip().lower() in {"finished", "final", "ft", "ended"}

def is_halftime(status: Optional[str]) -> bool:
return "halftime" in (status or "").strip().lower()

def is_live(status: Optional[str]) -> bool:
s = (status or "").strip()
if not s:
return False
if is_finished(s) or is_halftime(s):
return True
return bool(LIVE_PAT.search(s))

def is_pregame(status: Optional[str]) -> bool:
"""
pregame = всё что НЕ live/halftime/finished и НЕ отменено/перенесено.
Pend. считаем pregame (игра задерживается).
"""
s = (status or "").strip()
if not s:
return True
if TIME_PAT.match(s):
return True
sl = s.lower()
if is_live(s) or is_halftime(s) or is_finished(s):
return False
if any(k in sl for k in ["postp", "cancel", "abandon", "void"]):
return False
if "pend" in sl:
return True
return True

def status_bucket(status: Optional[str]) -> str:
if is_halftime(status):
return "halftime"
if is_finished(status):
return "finished"
if is_live(status):
return "live"
if is_pregame(status):
return "pregame"
return "other"

def pick_best_line(current: LineOU, st_rec: Dict[str, Any], status: Optional[str]) -> LineOU:
"""
Если текущая линия неполная — берём последнюю полную из истории (или ещё более раннюю).
Приоритет: тот же bucket статуса.
"""
if line_complete(current):
return current

text
bucket = status_bucket(status) hist = st_rec.get("line_hist") if not hist: return current for item in reversed(hist): if item["bucket"] == bucket and line_complete(item["line"]): return item["line"] for item in reversed(hist): if line_complete(item["line"]): return item["line"] return current

===================== STATS =====================

def empty_stats() -> Dict[str, Any]:
return {"bets": 0, "wins": 0, "losses": 0, "pushes": 0, "profit": 0.0}

def ensure_stats(d: Dict[str, Dict[str, Any]], key: str) -> Dict[str, Any]:
if key not in d:
d[key] = empty_stats()
return d[key]

def ensure_nested_stats(d: Dict[str, Dict[str, Dict[str, Any]]], k1: str, k2: str) -> Dict[str, Any]:
if k1 not in d:
d[k1] = {}
if k2 not in d[k1]:
d[k1][k2] = empty_stats()
return d[k1][k2]

def bet_profit(decimal_odds: float, stake: float, result: str) -> float:
if result == "push":
return 0.0
if result == "loss":
return -stake
return (decimal_odds - 1.0) * stake

def stats_line(s: Dict[str, Any]) -> str:
return f"Bets={s['bets']} W={s['wins']} L={s['losses']} P={s['pushes']} Profit={s['profit']:+.2f}"

def stats_block_extended(
stats_total: Dict[str, Any],
stats_by_band: Dict[str, Dict[str, Any]],
stats_by_league: Dict[str, Dict[str, Any]],
stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]],
focus_league: Optional[str] = None,
focus_band: Optional[str] = None,
) -> str:
lines = []
lines.append(f"TOTAL: {stats_line(stats_total)}")
for _, _, label in DIFF_BANDS:
lines.append(f"{label}: {stats_line(stats_by_band[label])}")

text
if focus_league: ls = ensure_stats(stats_by_league, focus_league) lines.append("") lines.append(f"LEAGUE: {focus_league}") lines.append(stats_line(ls)) if focus_band: lbs = ensure_nested_stats(stats_by_league_band, focus_league, focus_band) lines.append(f"LEAGUE×BAND ({focus_band}): {stats_line(lbs)}") return "\n".join(lines)

===================== EXCEL STORAGE =====================

BETS_HEADERS = [
"bet_uid", "match_id",
"league", "home", "away",
"band", "side",
"pre_total", "half_total", "diff", "abs_diff",
"odds", "stake",
"placed_at", "status_at_bet",
"final_total", "result", "profit", "settled_at",
]

def excel_open_or_create(path: str):
if os.path.exists(path):
wb = load_workbook(path)
else:
wb = Workbook()

text
# Bets sheet if "Bets" not in wb.sheetnames: ws = wb.active ws.title = "Bets" ws.append(BETS_HEADERS) else: ws = wb["Bets"] if ws.max_row < 1: ws.append(BETS_HEADERS) else: header = [ws.cell(row=1, column=c).value for c in range(1, ws.max_column + 1)] if header[: len(BETS_HEADERS)] != BETS_HEADERS: for i, h in enumerate(BETS_HEADERS, start=1): ws.cell(row=1, column=i).value = h # Ensure other sheets for sname in ["Stats_Total", "Stats_Band", "Stats_League", "Stats_League_Band", "Monthly"]: if sname not in wb.sheetnames: wb.create_sheet(sname) # mapping bet_uid -> row ws_bets = wb["Bets"] uid_to_row: Dict[str, int] = {} for r in range(2, ws_bets.max_row + 1): uid = ws_bets.cell(row=r, column=1).value if uid: uid_to_row[str(uid)] = r safe_save(wb, path) return wb, uid_to_row

def excel_append_bet(wb: Workbook, uid_to_row: Dict[str, int], path: str, row: Dict[str, Any]) -> None:
ws = wb["Bets"]
values = [row.get(h) for h in BETS_HEADERS]
ws.append(values)
uid_to_row[str(row["bet_uid"])] = ws.max_row
safe_save(wb, path)

def excel_update_settle(
wb: Workbook,
uid_to_row: Dict[str, int],
path: str,
bet_uid: str,
final_total: int,
result: str,
profit: float,
settled_at: str,
) -> None:
ws = wb["Bets"]
r = uid_to_row.get(str(bet_uid))
if not r:
return
ws.cell(row=r, column=16).value = int(final_total)
ws.cell(row=r, column=17).value = str(result)
ws.cell(row=r, column=18).value = float(profit)
ws.cell(row=r, column=19).value = str(settled_at)
safe_save(wb, path)

def _clear_sheet_keep_header(ws, headers: Optional[list] = None):
ws.delete_rows(1, ws.max_row)
if headers:
ws.append(headers)

def excel_write_stats(
wb: Workbook,
path: str,
stats_total: Dict[str, Any],
stats_by_band: Dict[str, Dict[str, Any]],
stats_by_league: Dict[str, Dict[str, Any]],
stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]],
):
ws = wb["Stats_Total"]
_clear_sheet_keep_header(ws, ["metric", "value"])
for k in ["bets", "wins", "losses", "pushes", "profit"]:
ws.append([k, stats_total[k]])

text
ws = wb["Stats_Band"] _clear_sheet_keep_header(ws, ["band", "bets", "wins", "losses", "pushes", "profit"]) for _, _, band in DIFF_BANDS: s = stats_by_band[band] ws.append([band, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) ws = wb["Stats_League"] _clear_sheet_keep_header(ws, ["league", "bets", "wins", "losses", "pushes", "profit"]) for league, s in sorted(stats_by_league.items(), key=lambda kv: (-kv[1]["bets"], kv[0])): ws.append([league, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) ws = wb["Stats_League_Band"] _clear_sheet_keep_header(ws, ["league", "band", "bets", "wins", "losses", "pushes", "profit"]) rows = [] for league, bands in stats_by_league_band.items(): for band, s in bands.items(): rows.append((league, band, s)) for league, band, s in sorted(rows, key=lambda x: (-x[2]["bets"], x[0], x[1])): ws.append([league, band, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) safe_save(wb, path)

def excel_write_monthly(wb: Workbook, path: str) -> None:
ws_bets = wb["Bets"]
ws_m = wb["Monthly"]

text
ws_m.delete_rows(1, ws_m.max_row) ws_m.append(["month", "bets_settled", "profit", "cum_profit"]) header = [ws_bets.cell(row=1, column=c).value for c in range(1, ws_bets.max_column + 1)] def col_idx(name: str) -> int: return header.index(name) + 1 try: c_profit = col_idx("profit") c_settled_at = col_idx("settled_at") except ValueError: safe_save(wb, path) return monthly_profit: Dict[str, float] = {} monthly_bets: Dict[str, int] = {} for r in range(2, ws_bets.max_row + 1): settled_at = ws_bets.cell(row=r, column=c_settled_at).value prof = ws_bets.cell(row=r, column=c_profit).value if not settled_at or prof is None: continue month = str(settled_at)[:7] monthly_profit[month] = monthly_profit.get(month, 0.0) + float(prof) monthly_bets[month] = monthly_bets.get(month, 0) + 1 cum = 0.0 for month in sorted(monthly_profit.keys()): p = float(monthly_profit[month]) b = int(monthly_bets.get(month, 0)) cum += p ws_m.append([month, b, p, cum]) ws_m._charts = [] if ws_m.max_row >= 2: chart = LineChart() chart.title = "Monthly Profit Trend" chart.y_axis.title = "Profit" chart.x_axis.title = "Month" data = Reference(ws_m, min_col=3, max_col=4, min_row=1, max_row=ws_m.max_row) cats = Reference(ws_m, min_col=1, min_row=2, max_row=ws_m.max_row) chart.add_data(data, titles_from_data=True) chart.set_categories(cats) ws_m.add_chart(chart, "F2") safe_save(wb, path)

===================== DRIVER =====================

def make_driver(headless: bool = True):
options = webdriver.ChromeOptions()
if headless:
options.add_argument("--headless=new")
options.add_argument("--window-size=1400,900")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
)
return webdriver.Chrome(options=options)

===================== SNAPSHOT PARSER =====================

def _parse_ou_for_mid(driver, mid: int) -> LineOU:
over_total = under_total = None
over_odds = under_odds = None

text
cells = [] ht = driver.find_elements(By.ID, f"ht_{mid}") gt = driver.find_elements(By.ID, f"gt_{mid}") if ht: cells.append(ht[0]) if gt: cells.append(gt[0]) if not cells: cells = driver.find_elements(By.CSS_SELECTOR, f'td.ouOdds[onclick*="oddscomp({mid}"]') for cell in cells: raw = (cell.get_attribute("innerText") or txt(cell)).strip().upper() odds2 = cell.find_elements(By.CSS_SELECTOR, "span.odds2") total_val = read_total_from_cell(cell) odds_val = read_odds_decimal(odds2[0]) if odds2 else None if raw.startswith("O"): over_total = total_val over_odds = odds_val elif raw.startswith("U"): under_total = total_val under_odds = odds_val line = LineOU(over_odds=over_odds, under_odds=under_odds) if over_total is not None and under_total is not None: if abs(over_total - under_total) < 1e-9: line.total = over_total else: line.total = (over_total + under_total) / 2.0 else: line.total = over_total if over_total is not None else under_total return line

def _get_snapshot_once(driver, do_scroll: bool) -> Dict[int, Dict[str, Any]]:
wait = WebDriverWait(driver, 30)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]')))
if do_scroll:
scroll_load_all(driver)

text
games: Dict[int, Dict[str, Any]] = {} analysis_tds = driver.find_elements(By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]') for td in analysis_tds: parsed = parse_analysis_onclick(td.get_attribute("onclick") or "") if not parsed: continue mid, home_s, away_s, league_s = parsed games[mid] = { "match_id": mid, "league": league_s, "home": split_team_and_tag(home_s), "away": split_team_and_tag(away_s), "status": None, "line": LineOU(), "home_score": None, "away_score": None, } for mid in list(games.keys()): status_els = driver.find_elements(By.ID, f"zt_{mid}") if status_els: games[mid]["status"] = txt(status_els[0]) or None games[mid]["line"] = _parse_ou_for_mid(driver, mid) h_els = driver.find_elements(By.ID, f"hzf_{mid}") g_els = driver.find_elements(By.ID, f"gzf_{mid}") if h_els and g_els: hs = safe_float(txt(h_els[0])) gs = safe_float(txt(g_els[0])) games[mid]["home_score"] = int(hs) if hs is not None else None games[mid]["away_score"] = int(gs) if gs is not None else None return games

def get_snapshot(driver, do_scroll: bool, retries: int = 3) -> Dict[int, Dict[str, Any]]:
for attempt in range(1, retries + 1):
try:
return _get_snapshot_once(driver, do_scroll=do_scroll)
except StaleElementReferenceException:
print(f"[{now_ts()}] snapshot stale -> retry {attempt}/{retries}", flush=True)
time.sleep(0.25)
return _get_snapshot_once(driver, do_scroll=do_scroll)

===================== MAIN LOOP =====================

def main():
print(f"[{now_ts()}] Starting monitor. poll={POLL_SECONDS}s stake={STAKE}", flush=True)
print(f"[{now_ts()}] Bands: " + ", ".join([lbl for _, _, lbl in DIFF_BANDS]), flush=True)
print(f"[{now_ts()}] Excel: {os.path.abspath(EXCEL_PATH)}", flush=True)
print(f"[{now_ts()}] Snapshots dir: {os.path.abspath(SNAPSHOT_DIR)} every {SNAPSHOT_EVERY_SECONDS}s", flush=True)
print(f"[{now_ts()}] Halftime wait: {HALFTIME_WAIT_SECONDS}s (first halftime only)", flush=True)

text
wb, uid_to_row = excel_open_or_create(EXCEL_PATH) tg_send( "🟢 NowGoal monitor STARTED\n" f"poll={POLL_SECONDS}s | stake={STAKE}\n" f"bands: " + ", ".join([lbl for _, _, lbl in DIFF_BANDS]) + "\n" f"halftime_wait={HALFTIME_WAIT_SECONDS}s (first halftime only)\n" f"time={now_dt()}\n" f"excel(main)={os.path.abspath(EXCEL_PATH)}\n" f"snapshots={os.path.abspath(SNAPSHOT_DIR)}" ) driver = make_driver(headless=HEADLESS) state: Dict[int, Dict[str, Any]] = {} # stats (начинаются с нуля — как ты и сказал) stats_total = empty_stats() stats_by_band = {label: empty_stats() for _, _, label in DIFF_BANDS} stats_by_league: Dict[str, Dict[str, Any]] = {} stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]] = {} loop_n = 0 try: driver.get(URL) WebDriverWait(driver, 30).until( EC.presence_of_element_located((By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]')) ) while True: loop_n += 1 t0 = time.time() do_scroll = (loop_n == 1) or (loop_n % SCROLL_EVERY_N_LOOPS == 0) try: driver.refresh() time.sleep(0.35) except Exception as e: print(f"[{now_ts()}] refresh error: {e} -> reopen", flush=True) driver.get(URL) try: snap = get_snapshot(driver, do_scroll=do_scroll, retries=3) except Exception as e: print(f"[{now_ts()}] snapshot error: {e}", flush=True) time.sleep(POLL_SECONDS) continue # summary counts cnt_total = len(snap) cnt_live = cnt_half = cnt_finished = 0 for g in snap.values(): st = g.get("status") if is_finished(st): cnt_finished += 1 elif is_halftime(st): cnt_half += 1 elif is_live(st): cnt_live += 1 print( f"[{now_ts()}] loop={loop_n} games={cnt_total} live={cnt_live} halftime={cnt_half} finished={cnt_finished} " f"| TOTAL profit={stats_total['profit']:+.2f} bets={stats_total['bets']}", flush=True ) for mid, g in snap.items(): st_rec = state.setdefault(mid, { "league": g["league"], "home": g["home"], "away": g["away"], "pregame_total_last": None, "pregame_total_frozen": None, "bet": None, "bet_uid": None, "settled": False, "last_status": None, "line_hist": deque(maxlen=10), # ✅ NEW: halftime tracking "halftime_count": 0, # сколько раз вошли в halftime (переходом) "halftime_first_ts": None, # time.time() первого появления halftime }) status = g.get("status") line_raw: LineOU = g.get("line", LineOU()) line = pick_best_line(line_raw, st_rec, status) if line_complete(line): st_rec["line_hist"].append({ "ts": now_dt(), "bucket": status_bucket(status), "line": LineOU(line.total, line.over_odds, line.under_odds), }) # keep previous status for transitions prev_status = st_rec["last_status"] if status != prev_status: st_rec["last_status"] = status if status: parts = [] if line.total is not None: parts.append(f"OU={line.total:g}") if line.over_odds is not None: parts.append(f"O@{line.over_odds:.2f}") if line.under_odds is not None: parts.append(f"U@{line.under_odds:.2f}") line_str = " | " + (" ".join(parts) if parts else "OU=-") print( f" [{now_ts()}] status id={mid} | {st_rec['home'].name} vs {st_rec['away'].name} | {status}{line_str}", flush=True ) # ✅ NEW: detect FIRST entry into halftime (transition into halftime) if is_halftime(status) and not is_halftime(prev_status): st_rec["halftime_count"] += 1 if st_rec["halftime_count"] == 1: st_rec["halftime_first_ts"] = time.time() print( f" [{now_ts()}] FIRST HALFTIME detected id={mid} -> waiting {HALFTIME_WAIT_SECONDS}s for odds to stabilize", flush=True ) else: # second+ halftime (often OT) -> ignore for betting logic print( f" [{now_ts()}] HALFTIME повторно (#{st_rec['halftime_count']}) id={mid} -> IGNORE (OT/extra break)", flush=True ) # 1) pregame -> сохраняем последний pregame total if is_pregame(status): if line.total is not None: st_rec["pregame_total_last"] = float(line.total) # 2) first live -> freeze pregame total if is_live(status) and st_rec["pregame_total_frozen"] is None and st_rec["pregame_total_last"] is not None: st_rec["pregame_total_frozen"] = float(st_rec["pregame_total_last"]) print(f" [{now_ts()}] pregame frozen id={mid} pre_total={st_rec['pregame_total_frozen']:g}", flush=True) # 3) halftime -> bet (✅ only on FIRST halftime + ✅ wait 15s) if ( is_halftime(status) and st_rec["bet"] is None and st_rec["pregame_total_frozen"] is not None and st_rec["halftime_count"] == 1 # ✅ only first halftime ): # ✅ wait after first halftime appears ht_ts = st_rec.get("halftime_first_ts") if ht_ts is None: st_rec["halftime_first_ts"] = time.time() ht_ts = st_rec["halftime_first_ts"] if (time.time() - float(ht_ts)) < HALFTIME_WAIT_SECONDS: continue pre_total = float(st_rec["pregame_total_frozen"]) half_total = line.total if half_total is None: continue diff = float(half_total) - pre_total abs_diff = abs(diff) band = pick_band(abs_diff) if band is None: continue if diff > 0: side = "UNDER" odds = line.under_odds else: side = "OVER" odds = line.over_odds if odds is None: continue bet_uid = f"{mid}-{int(time.time()*1000)}" st_rec["bet_uid"] = bet_uid st_rec["bet"] = { "band": band, "side": side, "line": float(half_total), "odds": float(odds), "stake": float(STAKE), "pre_total": float(pre_total), "half_total": float(half_total), "diff": float(diff), "abs_diff": float(abs_diff), "placed_at": now_dt(), "status_at_bet": status, } league = st_rec["league"] stats_total["bets"] += 1 stats_by_band[band]["bets"] += 1 ensure_stats(stats_by_league, league)["bets"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["bets"] += 1 excel_append_bet( wb, uid_to_row, EXCEL_PATH, { "bet_uid": bet_uid, "match_id": mid, "league": league, "home": st_rec["home"].name, "away": st_rec["away"].name, "band": band, "side": side, "pre_total": float(pre_total), "half_total": float(half_total), "diff": float(diff), "abs_diff": float(abs_diff), "odds": float(odds), "stake": float(STAKE), "placed_at": st_rec["bet"]["placed_at"], "status_at_bet": status, "final_total": None, "result": None, "profit": None, "settled_at": None, } ) excel_write_stats(wb, EXCEL_PATH, stats_total, stats_by_band, stats_by_league, stats_by_league_band) excel_write_monthly(wb, EXCEL_PATH) print( f" [{now_ts()}] BET PLACED [{band}] id={mid} {side} {half_total:g} @ {odds:.2f} " f"(pre {pre_total:g}, diff {diff:+g})", flush=True ) block = stats_block_extended( stats_total, stats_by_band, stats_by_league, stats_by_league_band, focus_league=league, focus_band=band ) tg_send( f"✅ BET PLACED (TOTAL #{stats_total['bets']}) [{band}]\n" f"{league}\n" f"{st_rec['home'].name} vs {st_rec['away'].name}\n" f"Status: {status}\n" f"(waited {HALFTIME_WAIT_SECONDS}s after first halftime)\n" f"Pre total: {pre_total:g}\n" f"Halftime line: {half_total:g} (diff {diff:+g})\n" f"BET: {side} {half_total:g} @ {float(odds):.2f}\n" f"Stake: {STAKE}\n" f"\n📊 STATS\n{block}\n" f"id={mid}\n" f"bet_uid={bet_uid}" ) # 4) finished -> settle bet if is_finished(status) and st_rec["bet"] is not None and not st_rec["settled"]: hs = g.get("home_score") gs = g.get("away_score") if hs is None or gs is None: continue final_total = int(hs) + int(gs) band = st_rec["bet"]["band"] league = st_rec["league"] side = st_rec["bet"]["side"] line_bet = float(st_rec["bet"]["line"]) odds = float(st_rec["bet"]["odds"]) stake = float(st_rec["bet"]["stake"]) if abs(final_total - line_bet) < 1e-9: result = "push" stats_total["pushes"] += 1 stats_by_band[band]["pushes"] += 1 ensure_stats(stats_by_league, league)["pushes"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["pushes"] += 1 else: win = (final_total < line_bet) if side == "UNDER" else (final_total > line_bet) if win: result = "win" stats_total["wins"] += 1 stats_by_band[band]["wins"] += 1 ensure_stats(stats_by_league, league)["wins"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["wins"] += 1 else: result = "loss" stats_total["losses"] += 1 stats_by_band[band]["losses"] += 1 ensure_stats(stats_by_league, league)["losses"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["losses"] += 1 prof = bet_profit(odds, stake, result) stats_total["profit"] += prof stats_by_band[band]["profit"] += prof ensure_stats(stats_by_league, league)["profit"] += prof ensure_nested_stats(stats_by_league_band, league, band)["profit"] += prof st_rec["settled"] = True bet_uid = st_rec.get("bet_uid") if bet_uid: excel_update_settle( wb, uid_to_row, EXCEL_PATH, bet_uid, final_total, result.upper(), float(prof), now_dt() ) excel_write_stats(wb, EXCEL_PATH, stats_total, stats_by_band, stats_by_league, stats_by_league_band) excel_write_monthly(wb, EXCEL_PATH) block = stats_block_extended( stats_total, stats_by_band, stats_by_league, stats_by_league_band, focus_league=league, focus_band=band ) print( f" [{now_ts()}] BET SETTLED [{band}] id={mid} {result.upper()} final_total={final_total} " f"bet={side} {line_bet:g} @ {odds:.2f} P/L={prof:+.2f}\n{block}", flush=True ) tg_send( f"🏁 BET RESULT: {result.upper()} [{band}]\n" f"{league}\n" f"{st_rec['home'].name} {hs} : {gs} {st_rec['away'].name}\n" f"Final total: {final_total}\n" f"Bet: {side} {line_bet:g} @ {odds:.2f} | stake {stake}\n" f"P/L: {prof:+.2f}\n\n" f"📊 STATS\n{block}\n" f"id={mid}\n" f"bet_uid={bet_uid}" ) # делаем snapshot раз в SNAPSHOT_EVERY_SECONDS (можно смотреть в Excel без блокировок) maybe_write_snapshot(wb, EXCEL_PATH) elapsed = time.time() - t0 time.sleep(max(0.2, POLL_SECONDS - elapsed)) finally: driver.quit() print(f"[{now_ts()}] Driver closed.", flush=True)

if name == "main":
main() по работе этого кода у меня такой вот результат, excel файл, нудно изучить этот файл, найти проблемные зоны в ставках где я минусую, найти хорошие зоны где есть профит, и переделать код, так чтобы он делал ставки только по плюсовой стратегии

Pensamiento

Creating summary and providing a download link

I'll provide the user with a summarized report from their Excel, highlighting key insights like "bad zones" and the recommended "plus" rule. Also, I’ll showcase the code modifications, including the new STRATEGY_RULES and strategy_allows functions. I’ll make sure the code handles bet statistics correctly and suggest cleaning up state data after a game finishes.

Refining betting strategy and stats handling

The user wants to apply a "plus" strategy and focus on bets where it's profitable, particularly for the "Under" strategy and avoiding certain bins like 20-30 and 22-26. I’ll also explain that they can optionally load stats from a file to continue previous data, although they prefer starting from zero. This new filter should be more profitable, with a predicted +10.66.

Implementing strategy rules and clarifications

I’m setting up the strategy rules to limit "UNDER" bets in the 20-30 range to specific abs_diff values. To avoid confusion, we'll keep 30 exclusive in bands. I'll also insert a check to ensure the strategy works as intended. Lastly, I need to create a code patch for the user but ensure no sensitive data leaks.

Proposing security update and configuration options

It’s important to handle the token security issue — we can recommend removing the fallback, requiring an environment variable instead. However, this could break functionality without the variable. I’ll mention it in the notes. Also considering adding a “Min bets filter” with both fixed choices and rule selection as config.

Thought for 6m 25s

Respuesta

Я открыл твой Excel /mnt/data/nowgoal_20260114_082711.xlsx (лист Bets, 251 ставка — все рассчитаны/settled) и посчитал, где именно ты «льёшь», а где есть плюс.

Download the analysis report

1) Что по результату в целом

  • 251 ставок
  • Profit: −16.12
  • ROI: −6.42%
  • Win/Loss: 128 / 123 (push = 0)

2) Проблемные зоны (где основной минус)

По твоим текущим “зонам” (band × side)

BandSideBetsProfitROI
20-30OVER91−15.67−17.22%
30-40UNDER26−4.10−15.77%
40-50UNDER6−0.45−7.50%
40-50OVER2−0.17−8.50%

Ключевое: OVER почти полностью убивает итог (−13.50 по всем OVER), UNDER около нуля (−2.62).

Самая “ядовитая” подзона внутри 20–30 (по abs_diff)

Если разбить abs_diff 20–30 на поддиапазоны по 2 пункта:

  • 24–26: 36 ставок, −12.22 (очень плохо)
  • 22–24: 51 ставка, −5.11
  • 20–22: 69 ставок, +2.67
  • 28–30: 20 ставок, +2.12 (26–28 около нуля суммарно, но там OVER тянет вниз)

👉 То есть у тебя не просто “band 20-30”, а внутри него есть дыра 22–26, которая даёт основной минус.

3) Хорошие зоны (где реально есть плюс)

Самое “стабильное” по объёму

  • 20-30 + UNDER: 110 ставок, +1.93 (ROI +1.75%) — небольшой, но плюс.

Самая плюсовая (по этому файлу) — с фильтром по abs_diff

Если делать только UNDER и убрать abs_diff 22–26, оставив:

  • abs_diff ∈ [20,22) ИЛИ [26,30)

то на твоей истории получается:

  • 61 ставка, +10.66 (ROI +17.5%)

⚠️ Это бэктест по тем же данным → есть риск переобучения, но по цифрам видно, что дырка 22–26 статистически токсичная, и её логично вырезать уже сейчас.


4) Правка кода: ставить только по плюсовой стратегии

Ниже минимальная и безопасная переделка: добавляем правила стратегии и проверку перед размещением ставки.

A) Добавь в CONFIG (вверху файла)

python
# ===================== STRATEGY FILTER ===================== USE_STRATEGY_FILTER = True # Плюсовая стратегия по твоему Excel: # ставим ТОЛЬКО UNDER в band 20-30 и только если abs_diff в [20,22) или [26,30) STRATEGY_RULES = [ {"band": "20-30", "side": "UNDER", "abs_min": 20.0, "abs_max": 22.0}, {"band": "20-30", "side": "UNDER", "abs_min": 26.0, "abs_max": 30.0}, ]

B) Добавь функцию-проверку (где-нибудь в HELPERS, после pick_band например)

python
def strategy_allows(league: str, band: str, side: str, abs_diff: float) -> bool: """ True = ставку можно делать, False = пропускаем. abs_max считается как верхняя граница EXCLUSIVE (abs_diff < abs_max). """ if not USE_STRATEGY_FILTER: return True for r in STRATEGY_RULES: if r.get("band") and band != r["band"]: continue if r.get("side") and side != r["side"]: continue # опционально, если захочешь правила под конкретные лиги: # if r.get("league_re") and not re.search(r["league_re"], league or "", re.I): # continue abs_min = r.get("abs_min") abs_max = r.get("abs_max") if abs_min is not None and abs_diff < float(abs_min): continue if abs_max is not None and abs_diff >= float(abs_max): continue return True return False

C) Вставь фильтр прямо перед созданием bet (в блоке # 3) halftime -> bet)

Найди место после вычисления band, side, odds — и добавь:

python
league = st_rec["league"] # ✅ Strategy filter: ставим только если правило разрешает if not strategy_allows(league, band, side, abs_diff): # можно залогировать, но без TG (чтобы не спамить) print( f" [{now_ts()}] SKIP (strategy) id={mid} {league} [{band}] {side} abs_diff={abs_diff:g}", flush=True ) continue

После этого бот перестанет ставить:

  • все OVER
  • весь 30-40 UNDER
  • и самое важное: UNDER в abs_diff 22–26, который у тебя даёт основной провал

Важное (без морали, чисто практика)

  • Ты засветил TELEGRAM_TOKEN в коде (он прямо в тексте). Лучше сразу перевыпустить токен у BotFather и оставить только env-переменные (без дефолта в коде), иначе его могут утащить.

Если хочешь, я могу сделать ещё более “устойчиво”:

  • авто-обучение whitelist правил из Excel при старте (с минимумом ставок на правило, чтобы не ловить шум),
  • и/или “rolling window” только по последним N ставкам, чтобы стратегия адаптировалась.

Pregunta

а почему по лигам не разделил? может какие-то лиги тоже стоит убрать или нет?

Python

python
import pandas as pd, openpyxl, os, math, numpy as np, re, statistics path="/mnt/data/nowgoal_20260114_082711.xlsx" wb=openpyxl.load_workbook(path, data_only=True) wb.sheetnames ['Bets', 'Stats_Total', 'Stats_Band', 'Stats_League', 'Stats_League_Band', 'Monthly']

Python

python
ws=wb["Bets"] # read to dataframe data=[] headers=[cell.value for cell in ws[1]] for row in ws.iter_rows(min_row=2, values_only=True): if all(v is None for v in row): continue data.append(row[:len(headers)]) df=pd.DataFrame(data, columns=headers) df.head(), df.shape, df.columns ( bet_uid match_id league \ 0 663973-1767501590274 663973 National Basketball Association 1 659463-1767506023947 659463 National Basketball League(Japan) 2 658877-1767506082278 658877 Korea Basketball League 3 658308-1767506504688 658308 Basketball Japan League 4 658311-1767506647375 658311 Basketball Japan League home away band side pre_total \ 0 Los Angeles Clippers Boston Celtics 20-30 UNDER 220.0 1 Kumamoto Volters Iwate Big Bulls 20-30 UNDER 147.5 2 Busan KTF Magicwings Changwon LG Sakers 20-30 OVER 151.5 3 Saga Ballooners Gunma Crane Thunders 20-30 OVER 160.5 4 Hitachi Sunrockers Nagoya Fighting Eagles 20-30 UNDER 159.5 half_total diff abs_diff odds stake placed_at status_at_bet \ 0 241.5 21.5 21.5 1.83 1 2026-01-04 13:39:50 Halftime 1 167.5 20.0 20.0 1.83 1 2026-01-04 14:53:43 Halftime 2 131.5 -20.0 20.0 1.83 1 2026-01-04 14:54:42 Halftime 3 137.5 -23.0 23.0 1.86 1 2026-01-04 15:01:44 Halftime 4 184.5 25.0 25.0 1.80 1 2026-01-04 15:04:07 Halftime final_total result profit settled_at 0 261 LOSS -1.00 2026-01-04 14:47:06 1 153 WIN 0.83 2026-01-04 16:07:08 2 151 WIN 0.83 2026-01-04 16:04:30 3 132 LOSS -1.00 2026-01-04 16:02:45 4 175 WIN 0.80 2026-01-04 16:16:43 , (251, 19), Index(['bet_uid', 'match_id', 'league', 'home', 'away', 'band', 'side', 'pre_total', 'half_total', 'diff', 'abs_diff', 'odds', 'stake', 'placed_at', 'status_at_bet', 'final_total', 'result', 'profit', 'settled_at'], dtype='object'))

Python

python
df['profit']=pd.to_numeric(df['profit'], errors='coerce') df['stake']=pd.to_numeric(df['stake'], errors='coerce').fillna(1.0) overall_bets=len(df) overall_profit=df['profit'].sum() overall_stake=df['stake'].sum() roi=overall_profit/overall_stake overall_bets, overall_profit, roi (251, -16.12, -0.06422310756972112)

Python

python
def agg_table(d): out=[] for league, g in d.groupby('league'): bets=len(g) prof=g['profit'].sum() stake=g['stake'].sum() roi=prof/stake if stake else np.nan wins=(g['result'].astype(str).str.upper()=='WIN').sum() losses=(g['result'].astype(str).str.upper()=='LOSS').sum() pushes=(g['result'].astype(str).str.upper()=='PUSH').sum() winrate=wins/(wins+losses) if (wins+losses)>0 else np.nan out.append((league,bets,prof,roi,wins,losses,pushes,winrate)) tab=pd.DataFrame(out, columns=['league','bets','profit','roi','wins','losses','pushes','winrate']) tab=tab.sort_values(['bets','profit'], ascending=[False,True]) return tab league_tab=agg_table(df) league_tab.head(20) league bets profit roi \ 76 National Committee Association America 33 -3.07 -0.093030 77 National Committee Association America W 11 -3.74 -0.340000 70 National Basketball Association 9 -1.52 -0.168889 74 National Basketball League(China) 5 -1.37 -0.274000 52 Italy Serie B 5 -1.34 -0.268000 5 Argentina La Liga 5 -1.31 -0.262000 3 Angola Campeonato Nacional 5 2.32 0.464000 92 Romania Liga 1 4 -2.17 -0.542500 22 Chinese Taiwan Professional Basketball League 4 -0.34 -0.085000 69 Macau NBL 4 -0.34 -0.085000 66 Ligue Feminine de Basketball 4 1.43 0.357500 20 Chinese Basketball Association 4 1.49 0.372500 96 Slovak Extraliga Basketball 4 1.49 0.372500 35 Finland Division 1A 3 -3.00 -1.000000 87 Portugal Nacional 1A 3 -3.00 -1.000000 71 National Basketball League (Bulgaria) 3 -1.20 -0.400000 49 Israeli Liga Leumit 3 -1.17 -0.390000 30 El Salvador Fesabal U21 3 -1.14 -0.380000 88 Portugal Nacional 2A 3 -1.14 -0.380000 13 Basketball Japan League 3 0.63 0.210000 wins losses pushes winrate 76 16 17 0 0.484848 77 4 7 0 0.363636 70 4 5 0 0.444444 74 2 3 0 0.400000 52 2 3 0 0.400000 5 2 3 0 0.400000 3 4 1 0 0.800000 92 1 3 0 0.250000 22 2 2 0 0.500000 69 2 2 0 0.500000 66 3 1 0 0.750000 20 3 1 0 0.750000 96 3 1 0 0.750000 35 0 3 0 0.000000 87 0 3 0 0.000000 71 1 2 0 0.333333 49 1 2 0 0.333333 30 1 2 0 0.333333 88 1 2 0 0.333333 13 2 1 0 0.666667

Python

python
top_leagues=league_tab.sort_values('bets',ascending=False).head(20) top_leagues league bets profit roi \ 76 National Committee Association America 33 -3.07 -0.093030 77 National Committee Association America W 11 -3.74 -0.340000 70 National Basketball Association 9 -1.52 -0.168889 74 National Basketball League(China) 5 -1.37 -0.274000 52 Italy Serie B 5 -1.34 -0.268000 5 Argentina La Liga 5 -1.31 -0.262000 3 Angola Campeonato Nacional 5 2.32 0.464000 66 Ligue Feminine de Basketball 4 1.43 0.357500 96 Slovak Extraliga Basketball 4 1.49 0.372500 20 Chinese Basketball Association 4 1.49 0.372500 69 Macau NBL 4 -0.34 -0.085000 22 Chinese Taiwan Professional Basketball League 4 -0.34 -0.085000 92 Romania Liga 1 4 -2.17 -0.542500 13 Basketball Japan League 3 0.63 0.210000 102 Tunisia League 3 2.49 0.830000 93 Saudi Arabia Premier League 3 2.49 0.830000 82 Philippine Basketball Cup 3 2.49 0.830000 73 National Basketball League(Australia) 3 0.77 0.256667 34 Estonian–Latvian Basketball League 3 0.66 0.220000 14 Basketball League of Serbia 3 0.66 0.220000 wins losses pushes winrate 76 16 17 0 0.484848 77 4 7 0 0.363636 70 4 5 0 0.444444 74 2 3 0 0.400000 52 2 3 0 0.400000 5 2 3 0 0.400000 3 4 1 0 0.800000 66 3 1 0 0.750000 96 3 1 0 0.750000 20 3 1 0 0.750000 69 2 2 0 0.500000 22 2 2 0 0.500000 92 1 3 0 0.250000 13 2 1 0 0.666667 102 3 0 0 1.000000 93 3 0 0 1.000000 82 3 0 0 1.000000 73 2 1 0 0.666667 34 2 1 0 0.666667 14 2 1 0 0.666667

Python

python
league10=league_tab[league_tab['bets']>=10].sort_values('roi') league10 league bets profit roi wins \ 77 National Committee Association America W 11 -3.74 -0.34000 4 76 National Committee Association America 33 -3.07 -0.09303 16 losses pushes winrate 77 7 0 0.363636 76 17 0 0.484848

Python

python
league5=league_tab[league_tab['bets']>=5].sort_values('roi') league5[['league','bets','profit','roi','winrate']] league bets profit roi winrate 77 National Committee Association America W 11 -3.74 -0.340000 0.363636 74 National Basketball League(China) 5 -1.37 -0.274000 0.400000 52 Italy Serie B 5 -1.34 -0.268000 0.400000 5 Argentina La Liga 5 -1.31 -0.262000 0.400000 70 National Basketball Association 9 -1.52 -0.168889 0.444444 76 National Committee Association America 33 -3.07 -0.093030 0.484848 3 Angola Campeonato Nacional 5 2.32 0.464000 0.800000

Python

python
# Proposed strategy filter mask = (df['band']=='20-30') & (df['side']=='UNDER') & ( ((df['abs_diff']>=20.0) & (df['abs_diff']<22.0)) | ((df['abs_diff']>=26.0) & (df['abs_diff']<30.0)) ) df_strat=df[mask].copy() len(df_strat), df_strat['profit'].sum(), df_strat['profit'].sum()/df_strat['stake'].sum() (61, 10.660000000000002, 0.17475409836065578)

Python

python
league_strat=agg_table(df_strat) league_strat.sort_values('bets',ascending=False).head(20) league bets profit roi wins \ 30 National Committee Association America 10 5.00 0.500 8 28 National Basketball League(China) 3 0.63 0.210 2 10 Finland Division 1A 2 -2.00 -1.000 0 36 Romania Liga 1 2 -2.00 -1.000 0 1 Argentina La Liga 2 -0.17 -0.085 1 21 Liga Nacional de Bosquetbol 2 -0.17 -0.085 1 31 National Committee Association America W 2 -0.17 -0.085 1 3 British Championship Basketball 2 1.69 0.845 2 24 Ligue Nationale de Basket Pro B 1 0.83 0.830 1 9 Estonian–Latvian Basketball League 1 0.83 0.830 1 15 Iceland Cup Women 1 0.83 0.830 1 16 Israel Premier League Women 1 0.83 0.830 1 17 Israeli Basketball Premier League 1 0.83 0.830 1 20 Lebanon FLB 1 0.83 0.830 1 23 Ligue Feminine de Basketball 1 0.83 0.830 1 37 Saudi Arabia Premier League 1 0.83 0.830 1 29 National Basketball League(Japan) 1 0.83 0.830 1 32 Norway Ligaen Women 1 0.83 0.830 1 33 Philippine Basketball Cup 1 0.83 0.830 1 5 Chinese Basketball Association 1 0.83 0.830 1 losses pushes winrate 30 2 0 0.800000 28 1 0 0.666667 10 2 0 0.000000 36 2 0 0.000000 1 1 0 0.500000 21 1 0 0.500000 31 1 0 0.500000 3 0 0 1.000000 24 0 0 1.000000 9 0 0 1.000000 15 0 0 1.000000 16 0 0 1.000000 17 0 0 1.000000 20 0 0 1.000000 23 0 0 1.000000 37 0 0 1.000000 29 0 0 1.000000 32 0 0 1.000000 33 0 0 1.000000 5 0 0 1.000000

Python

python
def league_side_band(df_in, league): g=df_in[df_in['league']==league] tab=g.groupby(['band','side']).agg(bets=('bet_uid','count'), profit=('profit','sum'), stake=('stake','sum')).reset_index() tab['roi']=tab['profit']/tab['stake'] return tab.sort_values('bets', ascending=False) league_side_band(df, "National Committee Association America W") band side bets profit stake roi 0 20-30 OVER 5 -1.37 5 -0.274 1 20-30 UNDER 2 -0.17 2 -0.085 2 30-40 OVER 2 -0.20 2 -0.100 3 30-40 UNDER 2 -2.00 2 -1.000

Python

python
league_side_band(df, "National Committee Association America") band side bets profit stake roi 1 20-30 UNDER 15 3.76 15 0.250667 0 20-30 OVER 14 -4.73 14 -0.337857 2 30-40 OVER 2 -0.10 2 -0.050000 3 30-40 UNDER 2 -2.00 2 -1.000000

Python

python
league_side_band(df, "National Basketball Association") band side bets profit stake roi 0 20-30 OVER 8 -0.52 8 -0.065 1 20-30 UNDER 1 -1.00 1 -1.000

Python

python
league3=league_tab[league_tab['bets']>=3].copy() pos=league3.sort_values('profit',ascending=False).head(15)[['league','bets','profit','roi','winrate']] neg=league3.sort_values('profit',ascending=True).head(15)[['league','bets','profit','roi','winrate']] pos, neg ( league bets profit roi \ 102 Tunisia League 3 2.49 0.830000 82 Philippine Basketball Cup 3 2.49 0.830000 93 Saudi Arabia Premier League 3 2.49 0.830000 3 Angola Campeonato Nacional 5 2.32 0.464000 96 Slovak Extraliga Basketball 4 1.49 0.372500 20 Chinese Basketball Association 4 1.49 0.372500 66 Ligue Feminine de Basketball 4 1.43 0.357500 73 National Basketball League(Australia) 3 0.77 0.256667 14 Basketball League of Serbia 3 0.66 0.220000 34 Estonian–Latvian Basketball League 3 0.66 0.220000 13 Basketball Japan League 3 0.63 0.210000 22 Chinese Taiwan Professional Basketball League 4 -0.34 -0.085000 69 Macau NBL 4 -0.34 -0.085000 30 El Salvador Fesabal U21 3 -1.14 -0.380000 88 Portugal Nacional 2A 3 -1.14 -0.380000 winrate 102 1.000000 82 1.000000 93 1.000000 3 0.800000 96 0.750000 20 0.750000 66 0.750000 73 0.666667 14 0.666667 34 0.666667 13 0.666667 22 0.500000 69 0.500000 30 0.333333 88 0.333333 , league bets profit roi \ 77 National Committee Association America W 11 -3.74 -0.340000 76 National Committee Association America 33 -3.07 -0.093030 87 Portugal Nacional 1A 3 -3.00 -1.000000 35 Finland Division 1A 3 -3.00 -1.000000 92 Romania Liga 1 4 -2.17 -0.542500 70 National Basketball Association 9 -1.52 -0.168889 74 National Basketball League(China) 5 -1.37 -0.274000 52 Italy Serie B 5 -1.34 -0.268000 5 Argentina La Liga 5 -1.31 -0.262000 71 National Basketball League (Bulgaria) 3 -1.20 -0.400000 49 Israeli Liga Leumit 3 -1.17 -0.390000 30 El Salvador Fesabal U21 3 -1.14 -0.380000 88 Portugal Nacional 2A 3 -1.14 -0.380000 69 Macau NBL 4 -0.34 -0.085000 22 Chinese Taiwan Professional Basketball League 4 -0.34 -0.085000 winrate 77 0.363636 76 0.484848 87 0.000000 35 0.000000 92 0.250000 70 0.444444 74 0.400000 52 0.400000 5 0.400000 71 0.333333 49 0.333333 30 0.333333 88 0.333333 69 0.500000 22 0.500000 )

Python

python
neg league bets profit roi \ 77 National Committee Association America W 11 -3.74 -0.340000 76 National Committee Association America 33 -3.07 -0.093030 87 Portugal Nacional 1A 3 -3.00 -1.000000 35 Finland Division 1A 3 -3.00 -1.000000 92 Romania Liga 1 4 -2.17 -0.542500 70 National Basketball Association 9 -1.52 -0.168889 74 National Basketball League(China) 5 -1.37 -0.274000 52 Italy Serie B 5 -1.34 -0.268000 5 Argentina La Liga 5 -1.31 -0.262000 71 National Basketball League (Bulgaria) 3 -1.20 -0.400000 49 Israeli Liga Leumit 3 -1.17 -0.390000 30 El Salvador Fesabal U21 3 -1.14 -0.380000 88 Portugal Nacional 2A 3 -1.14 -0.380000 69 Macau NBL 4 -0.34 -0.085000 22 Chinese Taiwan Professional Basketball League 4 -0.34 -0.085000 winrate 77 0.363636 76 0.484848 87 0.000000 35 0.000000 92 0.250000 70 0.444444 74 0.400000 52 0.400000 5 0.400000 71 0.333333 49 0.333333 30 0.333333 88 0.333333 69 0.500000 22 0.500000

Python

python
df_over2030=df[(df['band']=='20-30') & (df['side']=='OVER')].copy() len(df_over2030), df_over2030['profit'].sum() (91, -15.67)

Python

python
over_league=agg_table(df_over2030).sort_values('profit') over_league.head(15), over_league.sort_values('bets',ascending=False).head(10) ( league bets profit roi wins \ 38 National Committee Association America 14 -4.73 -0.337857 5 6 Basketball Bundesliga 2 -2.00 -1.000000 0 20 France Nationale 1 2 -2.00 -1.000000 0 56 West Asia Super League 2 -2.00 -1.000000 0 39 National Committee Association America W 5 -1.37 -0.274000 2 18 Finland Korisliiga Women 1 -1.00 -1.000000 0 24 Italy Cup Women 1 -1.00 -1.000000 0 25 Japan Basketball Mikado Cup 1 -1.00 -1.000000 0 29 Liga Femenina de Baloncesto 1 -1.00 -1.000000 0 30 Liga Nationala (Women´´s basketball) 1 -1.00 -1.000000 0 31 Liga Portuguesa de Basquetebol 1 -1.00 -1.000000 0 45 Romania Liga 1 1 -1.00 -1.000000 0 41 Osterreichische Basketball Bundesliga 1 -1.00 -1.000000 0 17 Finland Division 1A 1 -1.00 -1.000000 0 53 Turkey TB2L 1 -1.00 -1.000000 0 losses pushes winrate 38 9 0 0.357143 6 2 0 0.000000 20 2 0 0.000000 56 2 0 0.000000 39 3 0 0.400000 18 1 0 0.000000 24 1 0 0.000000 25 1 0 0.000000 29 1 0 0.000000 30 1 0 0.000000 31 1 0 0.000000 45 1 0 0.000000 41 1 0 0.000000 17 1 0 0.000000 53 1 0 0.000000 , league bets profit roi wins \ 38 National Committee Association America 14 -4.73 -0.337857 5 34 National Basketball Association 8 -0.52 -0.065000 4 39 National Committee Association America W 5 -1.37 -0.274000 2 20 France Nationale 1 2 -2.00 -1.000000 0 6 Basketball Bundesliga 2 -2.00 -1.000000 0 47 Serie A2 Gold Basket 2 1.66 0.830000 2 12 Chinese Basketball Association 2 -0.17 -0.085000 1 28 Lietuvos krepsinio lyga 2 -0.17 -0.085000 1 44 Portugal Nacional Liga Women 2 -0.17 -0.085000 1 56 West Asia Super League 2 -2.00 -1.000000 0 losses pushes winrate 38 9 0 0.357143 34 4 0 0.500000 39 3 0 0.400000 20 2 0 0.000000 6 2 0 0.000000 47 0 0 1.000000 12 1 0 0.500000 28 1 0 0.500000 44 1 0 0.500000 56 2 0 0.000000 )

Python

python
df_under2030=df[(df['band']=='20-30') & (df['side']=='UNDER')].copy() df_under2030['profit'].sum(), len(df_under2030) (1.9300000000000015, 110)

Python

python
under_league=agg_table(df_under2030).sort_values('bets',ascending=False) under_league.head(15) league bets profit roi \ 47 National Committee Association America 15 3.76 0.250667 0 Angola Campeonato Nacional 4 1.49 0.372500 44 National Basketball League (Bulgaria) 3 -1.20 -0.400000 31 Italy Serie B 3 -1.17 -0.390000 58 Romania Liga 1 3 -1.17 -0.390000 45 National Basketball League(China) 3 0.63 0.210000 62 Tunisia League 3 2.49 0.830000 56 Portugal Nacional 2A 2 -0.14 -0.070000 8 British Championship Basketball 2 1.69 0.845000 59 Saudi Arabia Premier League 2 1.66 0.830000 51 Philippine Basketball Cup 2 1.66 0.830000 13 Chinese Taiwan Professional Basketball League 2 1.66 0.830000 11 Chinese Basketball Association 2 1.66 0.830000 4 Basketball Japan League 2 1.63 0.815000 5 Basketball League of Serbia 2 1.66 0.830000 wins losses pushes winrate 47 10 5 0 0.666667 0 3 1 0 0.750000 44 1 2 0 0.333333 31 1 2 0 0.333333 58 1 2 0 0.333333 45 2 1 0 0.666667 62 3 0 0 1.000000 56 1 1 0 0.500000 8 2 0 0 1.000000 59 2 0 0 1.000000 51 2 0 0 1.000000 13 2 0 0 1.000000 11 2 0 0 1.000000 4 2 0 0 1.000000 5 2 0 0 1.000000

Python

python
agg_table(df_bad).sort_values('bets',ascending=False).head(15) league bets profit roi wins \ 21 National Committee Association America 5 -1.24 -0.248 2 0 Angola Campeonato Nacional 3 0.69 0.230 2 16 Italy Serie B 3 -1.17 -0.390 1 27 Portugal Nacional 2A 2 -0.14 -0.070 1 30 Tunisia League 2 1.63 0.815 2 3 Basketball Japan League 2 1.63 0.815 2 9 Chinese Taiwan Professional Basketball League 2 1.66 0.830 2 1 Apollon Limassol BC 2 -0.20 -0.100 1 26 Portugal Nacional 1A 2 -2.00 -1.000 0 20 National Basketball League (Bulgaria) 2 -2.00 -1.000 0 14 Israeli Liga Leumit 2 -2.00 -1.000 0 24 Polska Liga Koszykówki Kobiet 1 -1.00 -1.000 0 29 Saudi Arabia Premier League 1 0.83 0.830 1 28 Romania Liga 1 1 0.83 0.830 1 23 Philippine Basketball Cup 1 0.83 0.830 1 losses pushes winrate 21 3 0 0.400000 0 1 0 0.666667 16 2 0 0.333333 27 1 0 0.500000 30 0 0 1.000000 3 0 0 1.000000 9 0 0 1.000000 1 1 0 0.500000 26 2 0 0.000000 20 2 0 0.000000 14 2 0 0.000000 24 1 0 0.000000 29 0 0 1.000000 28 0 0 1.000000 23 0 0 1.000000

Pensamiento

Refining the strategy and league blacklisting options

The bad absdiff zone has many small samples; for example, NCAA Men with 5 bets showing -1.24 and Portugal 1A with -2 for 2 bets. Now, we'll focus on the good zones under 20-30 (20-22 and 26-30). After computing per league for these zones, it turns out NCAA Women is the only clear loser at a decent sample size. Others are too small. We could propose optional league blacklisting and auto blacklisting based on existing bets history from Excel at startup.

Handling history and league exclusions

It seems the user prefers starting stats from zero, yet also wants to use history for decision-making without impacting reporting. I can adapt by reading the Bets sheet at startup, then calculating stats and storing them. For recommendations, I’d suggest excluding NCAA Women as a clear loser, while other leagues can be left optional. The code should include an option to exclude specific leagues based on the user's preferences.

Providing analysis with disclaimers and tables

I should present the analysis as requested with clear bullet points and a table of leagues with 5+ bets, which fall within our strategy. While doing so, I’ll include appropriate disclaimers regarding gambling – it’s legal to provide analysis, but we should avoid any illegal instructions. I'll provide concrete league tables, noting that initial league splits were excluded because only a few leagues had sufficient data in the user's Excel sheet.

League summary with numeric details

Here’s an overview of leagues with >=5 bets and their performance:

  • NCAA men: 33 bets, -3.07 ROI (-9.3%)
  • NCAA women: 11 bets, -3.74 ROI (-34%)
  • NBA: 9 bets, -1.52 ROI (-16.9%)
  • China NBL: 5 bets, -1.37 ROI (-27.4%)
  • Italy Serie B: 5 bets, -1.34 ROI (-26.8%)
  • Argentina La Liga: 5 bets, -1.31 ROI (-26.2%)
  • Angola: 5 bets, +2.32 ROI (+46.4%)

The data is still small, especially for NCAA men and women.

Filtered strategy and league recommendations

Here’s the filtered results based on the UNDER 20-30 absdiff strategy (20-22 or 26-30):

  • NCAA men: 10 bets, +5.00 ROI (+50%)
  • China NBL: 3 bets, +0.63 ROI (+21%)
  • British Championship Basketball: 2 bets, +1.69 ROI (+84.5%)
  • Finland Division 1A: 2 bets, -2.00 ROI (-100%)
  • Romania Liga 1: 2 bets, -2.00 ROI (-100%)
  • Argentina La Liga: 2 bets, -0.17 ROI (-8.5%)
  • Liga Nacional de Bosquetbol: 2 bets, -0.17 ROI (-8.5%)
  • NCAA women: 2 bets, -0.17 ROI (-8.5%)

Given many leagues only have 2 bets, I’d recommend avoiding filtering by league unless necessary. NCAA women seems consistently negative, so removing or stricter filtering is a good idea.

Thought for 3m 49s

Respuesta

Потому что в твоём файле почти все лиги имеют слишком маленькую выборку (1–3 ставки), и “по лигам” получается много шума: одна-две случайные победы/поражения резко переворачивают ROI. Поэтому я сначала резал по band/side/abs_diff — там выборка намного плотнее, и видно реальную “дыру” (22–26).

Но по лигам я всё равно разделил — вот что выходит на твоём Excel.

1) Разбивка по лигам (только где есть нормальный объём)

В этом файле только 2 лиги имеют ≥10 ставок:

  • National Committee Association America (NCAA Men) — 33 ставки, Profit −3.07, ROI −9.3%
  • National Committee Association America W (NCAA Women) — 11 ставок, Profit −3.74, ROI −34.0% ← это реально плохой сигнал

Если взять порог ≥5 ставок, добавляются ещё несколько лиг, но там всё равно выборки маленькие (5–9 ставок): NBA (9), China NBL (5), Italy Serie B (5), Argentina La Liga (5), Angola (5, плюс).

Что важно по смыслу

Даже в NCAA Men общий минус почти целиком создаётся твоей “плохой зоной” 20–30 + OVER:

  • NCAA Men, 20–30 OVER: 14 ставок, Profit −4.73
  • NCAA Men, 20–30 UNDER: 15 ставок, Profit +3.76

То есть лигой “лечить” тут не надо — лечится стороной (не брать OVER) и фильтром по abs_diff.

А вот NCAA Women действительно выглядит кандидатом на исключение: ROI −34% на 11 ставках — это уже не “случайность на 2 играх”.


2) Лиги внутри “плюсовой стратегии”, которую мы вырезали из твоих данных

Напомню стратегию, которую я предложил по твоему файлу:

Только UNDER, band 20–30, и abs_diff ∈ [20,22) или [26,30)
(убираем токсичную яму 22–26)

По этой стратегии у тебя: 61 ставка, +10.66, ROI +17.5%.

По лигам там картина такая:

  • NCAA Men: 10 ставок, +5.00, ROI +50% (лучший “доказательный” кусок, потому что объём есть)
  • Остальные лиги в этой стратегии в основном по 2–3 ставки — там я бы не делал выводов (например, Finland Division 1A −2.00 на 2 ставках — слишком мало, чтобы банить лигу навсегда).

Вывод: если добавлять лиговый фильтр прямо сейчас — то только один очевидный кандидат: NCAA Women.


3) Стоит ли убирать лиги?

Я бы сделал так (прагматично, без переобучения):

  1. Оставить фильтр по стратегии (UNDER + abs_diff зоны) — это главный источник улучшения.
  2. Добавить blacklist только для NCAA Women (потому что там 11 ставок и сильный минус).
  3. Остальные лиги не банить, пока не накопится хотя бы 8–12 ставок на лигу именно внутри твоей стратегии.

4) Как добавить фильтр по лигам в код (минимальная правка)

A) В CONFIG добавь:

python
# ===== LEAGUE FILTER ===== LEAGUE_BLACKLIST_RE = [ r"\bNational Committee Association America W\b", # NCAA Women (по твоему Excel) ] # если захочешь наоборот "белый список", можно включить: LEAGUE_WHITELIST_RE = [] # например: [r"NCAA", r"Japan", ...]

B) Добавь helper:

python
def league_allowed(league: str) -> bool: lg = league or "" # whitelist (если задан) — разрешаем только то, что матчится if LEAGUE_WHITELIST_RE: ok = any(re.search(p, lg, re.I) for p in LEAGUE_WHITELIST_RE) if not ok: return False # blacklist — запрещаем if any(re.search(p, lg, re.I) for p in LEAGUE_BLACKLIST_RE): return False return True

C) И прямо перед размещением ставки (там где ты уже вычислил league, band, side, abs_diff) вставь:

python
if not league_allowed(league): print(f" [{now_ts()}] SKIP (league blacklist) id={mid} league={league}", flush=True) continue

Если хочешь, я могу сделать вариант “умнее”: при старте читать твой Bets-лист и автоматически собирать blacklist для лиг, где (например) >=12 ставок и ROI < −5% в рамках выбранной стратегии. Это будет гораздо устойчивее, чем вручную банить лиги на 2–3 играх.

Pregunta

import re
import ast
import os
import time
from dataclasses import dataclass
from typing import Optional, Dict, Any, Tuple
from datetime import datetime
from collections import deque
from pathlib import Path

import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException

from openpyxl import Workbook, load_workbook
from openpyxl.chart import LineChart, Reference

===================== CONFIG =====================

URL = "https://www.nowgoal.com/basketball"

POLL_SECONDS = 5
STAKE = 1.0
HEADLESS = True

✅ NEW: wait after FIRST halftime appears (to stabilize odds/lines)

HALFTIME_WAIT_SECONDS = 15

DIFF_BANDS = [
(20.0, 30.0, "20-30"),
(30.0, 40.0, "30-40"),
(40.0, 50.0, "40-50"),
(50.0, None, "50+"),
]

SCROLL_EVERY_N_LOOPS = 12

EXCEL_PATH = os.getenv("EXCEL_PATH", "nowgoal_monitor.xlsx")

TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "8025557030:AAHgZX_pCOvQu7Gmi33X3iqe0oY7Y67yBMA")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "6667940236")

====== WINDOWS-EXCEL SAFE SAVE + SNAPSHOTS ======

SNAPSHOT_DIR = "snapshots" # сюда будут писаться файлы, которые можно открывать в Excel
SNAPSHOT_EVERY_SECONDS = 60 # снимок раз в минуту (можешь поставить 30)
AUTOSAVE_SUFFIX = "_AUTOSAVE.xlsx" # если основной файл открыт -> сохраняем сюда
SAVE_RETRIES = 3
SAVE_RETRY_SLEEP = 1.0

_last_snapshot_ts = 0.0
_excel_lock_notified = False

===================== TELEGRAM =====================

def tg_ready() -> bool:
return bool(TELEGRAM_TOKEN and TELEGRAM_CHAT_ID)

def tg_send(text: str) -> None:
"""Отправка сообщения в Telegram + лог в консоль."""
print(f"[TG] -> {text.replace('\n', ' | ')}", flush=True)
if not tg_ready():
print("[TG] Token/ChatId не заданы (пропускаю отправку).", flush=True)
return

text
try: r = requests.post( f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage", data={"chat_id": TELEGRAM_CHAT_ID, "text": text, "disable_web_page_preview": True}, timeout=10, ) if not r.ok: print(f"[TG] Ошибка отправки: {r.status_code} {r.text[:300]}", flush=True) except Exception as e: print(f"[TG] Exception: {e}", flush=True)

===================== MODELS =====================

@dataclass
class Team:
name: str
tag: Optional[str] = None

@dataclass
class LineOU:
total: Optional[float] = None
over_odds: Optional[float] = None # decimal
under_odds: Optional[float] = None # decimal

===================== HELPERS =====================

def now_ts() -> str:
return datetime.now().strftime("%H:%M:%S")

def now_dt() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def txt(el) -> str:
if el is None:
return ""
return (el.get_attribute("textContent") or "").strip()

def safe_float(x: Optional[str]) -> Optional[float]:
if not x:
return None
x = x.strip().replace(",", ".")
try:
return float(x)
except ValueError:
return None

def hk_to_decimal(odds: Optional[float]) -> Optional[float]:
# HK 0.83 -> Decimal 1.83
if odds is None:
return None
if odds >= 1.2:
return odds
return odds + 1.0

def read_odds_decimal(span) -> Optional[float]:
"""odds2 может быть текст 1.80, а может быть пусто, но есть data-o=0.80 (HK)."""
if span is None:
return None

text
t = (span.get_attribute("textContent") or "").strip() v = safe_float(t) if v is not None: return hk_to_decimal(v) v2 = safe_float(span.get_attribute("data-o")) if v2 is not None: return hk_to_decimal(v2) return None

-------- Excel safe save + snapshots (Windows) --------

def safe_save(wb: Workbook, path: str) -> bool:
"""
Пытаемся сохранить в основной файл.
Если Excel держит лок (Windows) -> не падаем, сохраняем в AUTOSAVE.
"""
global _excel_lock_notified

text
for i in range(1, SAVE_RETRIES + 1): try: wb.save(path) # если раньше предупреждали про лок — сбросим флаг, т.к. снова всё ок _excel_lock_notified = False return True except PermissionError: print(f"[{now_ts()}] Excel LOCKED: {path} (try {i}/{SAVE_RETRIES})", flush=True) time.sleep(SAVE_RETRY_SLEEP) except OSError as e: print(f"[{now_ts()}] Save OSError: {e} (try {i}/{SAVE_RETRIES})", flush=True) time.sleep(SAVE_RETRY_SLEEP) base, _ext = os.path.splitext(path) fallback = base + AUTOSAVE_SUFFIX try: wb.save(fallback) print(f"[{now_ts()}] Saved to AUTOSAVE: {fallback}", flush=True) # предупреждение в TG только один раз, чтобы не спамить if (not _excel_lock_notified) and tg_ready(): tg_send( "⚠️ Excel-файл открыт и заблокирован.\n" f"Не могу записать в: {os.path.abspath(path)}\n" f"Сохраняю во временный: {os.path.abspath(fallback)}\n" f"И делаю snapshots в папку: {os.path.abspath(SNAPSHOT_DIR)}" ) _excel_lock_notified = True return False except Exception as e: print(f"[{now_ts()}] AUTOSAVE failed: {e}", flush=True) if tg_ready(): tg_send(f"❌ Не могу сохранить Excel ни в основной, ни в AUTOSAVE: {e}") return False

def maybe_write_snapshot(wb: Workbook, base_path: str) -> None:
"""
Пишем снимок в отдельный файл с уникальным именем.
Такой файл можно держать открытым в Excel — бот не будет блокироваться.
"""
global _last_snapshot_ts
now = time.time()
if now - _last_snapshot_ts < SNAPSHOT_EVERY_SECONDS:
return
_last_snapshot_ts = now

text
try: Path(SNAPSHOT_DIR).mkdir(parents=True, exist_ok=True) stamp = datetime.now().strftime("%Y%m%d_%H%M%S") snap_path = os.path.join(SNAPSHOT_DIR, f"nowgoal_{stamp}.xlsx") wb.save(snap_path) print(f"[{now_ts()}] Snapshot saved: {snap_path}", flush=True) except Exception as e: print(f"[{now_ts()}] Snapshot save failed: {e}", flush=True)

OU_TOTAL_RE = re.compile(r"\b[OU]\s*([0-9]+(?:.[0-9]+)?)", re.IGNORECASE)

def read_total_from_cell(cell) -> Optional[float]:
"""Тотал берём из span.odds1, а если он пропал/пустой — выковыриваем из innerText."""
odds1 = cell.find_elements(By.CSS_SELECTOR, "span.odds1")
if odds1:
v = safe_float(txt(odds1[0]))
if v is not None:
return v

text
it = (cell.get_attribute("innerText") or "").strip() m = OU_TOTAL_RE.search(it) if m: return safe_float(m.group(1)) return None

def split_team_and_tag(s: str) -> Team:
s = (s or "").strip()
if s.endswith("]") and "[" in s:
i = s.rfind("[")
name = s[:i].strip()
tag = s[i + 1:-1].strip() or None
if name:
return Team(name=name, tag=tag)
return Team(name=s, tag=None)

def parse_analysis_onclick(onclick: str) -> Optional[Tuple[int, str, str, str]]:
if not onclick or "basketInPage.analysis" not in onclick:
return None
try:
args_str = onclick[onclick.find("(") + 1: onclick.rfind(")")]
args = ast.literal_eval("(" + args_str + ")")
return int(args[0]), str(args[1]), str(args[2]), str(args[3])
except Exception:
return None

def scroll_load_all(driver, max_rounds: int = 25, pause: float = 0.35):
last = 0
stable = 0
for _ in range(max_rounds):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(pause)
cur = len(driver.find_elements(By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]'))
if cur == last:
stable += 1
if stable >= 3:
break
else:
stable = 0
last = cur
driver.execute_script("window.scrollTo(0, 0);")
time.sleep(0.15)

def pick_band(abs_diff: float) -> Optional[str]:
for lo, hi, label in DIFF_BANDS:
if abs_diff >= lo and (hi is None or abs_diff < hi):
return label
return None

def line_complete(line: LineOU) -> bool:
return (line.total is not None and line.over_odds is not None and line.under_odds is not None)

===================== STATUS =====================

LIVE_PAT = re.compile(r"\b\d+(?:st|nd|rd|th)\s+Qtr\b|\bOT\b|\bOvertime\b|\bLive\b", re.IGNORECASE)
TIME_PAT = re.compile(r"^\s*\d{1,2}:\d{2}\s*(?:AM|PM)?\s*$", re.IGNORECASE)

def is_finished(status: Optional[str]) -> bool:
return (status or "").strip().lower() in {"finished", "final", "ft", "ended"}

def is_halftime(status: Optional[str]) -> bool:
return "halftime" in (status or "").strip().lower()

def is_live(status: Optional[str]) -> bool:
s = (status or "").strip()
if not s:
return False
if is_finished(s) or is_halftime(s):
return True
return bool(LIVE_PAT.search(s))

def is_pregame(status: Optional[str]) -> bool:
"""
pregame = всё что НЕ live/halftime/finished и НЕ отменено/перенесено.
Pend. считаем pregame (игра задерживается).
"""
s = (status or "").strip()
if not s:
return True
if TIME_PAT.match(s):
return True
sl = s.lower()
if is_live(s) or is_halftime(s) or is_finished(s):
return False
if any(k in sl for k in ["postp", "cancel", "abandon", "void"]):
return False
if "pend" in sl:
return True
return True

def status_bucket(status: Optional[str]) -> str:
if is_halftime(status):
return "halftime"
if is_finished(status):
return "finished"
if is_live(status):
return "live"
if is_pregame(status):
return "pregame"
return "other"

def pick_best_line(current: LineOU, st_rec: Dict[str, Any], status: Optional[str]) -> LineOU:
"""
Если текущая линия неполная — берём последнюю полную из истории (или ещё более раннюю).
Приоритет: тот же bucket статуса.
"""
if line_complete(current):
return current

text
bucket = status_bucket(status) hist = st_rec.get("line_hist") if not hist: return current for item in reversed(hist): if item["bucket"] == bucket and line_complete(item["line"]): return item["line"] for item in reversed(hist): if line_complete(item["line"]): return item["line"] return current

===================== STATS =====================

def empty_stats() -> Dict[str, Any]:
return {"bets": 0, "wins": 0, "losses": 0, "pushes": 0, "profit": 0.0}

def ensure_stats(d: Dict[str, Dict[str, Any]], key: str) -> Dict[str, Any]:
if key not in d:
d[key] = empty_stats()
return d[key]

def ensure_nested_stats(d: Dict[str, Dict[str, Dict[str, Any]]], k1: str, k2: str) -> Dict[str, Any]:
if k1 not in d:
d[k1] = {}
if k2 not in d[k1]:
d[k1][k2] = empty_stats()
return d[k1][k2]

def bet_profit(decimal_odds: float, stake: float, result: str) -> float:
if result == "push":
return 0.0
if result == "loss":
return -stake
return (decimal_odds - 1.0) * stake

def stats_line(s: Dict[str, Any]) -> str:
return f"Bets={s['bets']} W={s['wins']} L={s['losses']} P={s['pushes']} Profit={s['profit']:+.2f}"

def stats_block_extended(
stats_total: Dict[str, Any],
stats_by_band: Dict[str, Dict[str, Any]],
stats_by_league: Dict[str, Dict[str, Any]],
stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]],
focus_league: Optional[str] = None,
focus_band: Optional[str] = None,
) -> str:
lines = []
lines.append(f"TOTAL: {stats_line(stats_total)}")
for _, _, label in DIFF_BANDS:
lines.append(f"{label}: {stats_line(stats_by_band[label])}")

text
if focus_league: ls = ensure_stats(stats_by_league, focus_league) lines.append("") lines.append(f"LEAGUE: {focus_league}") lines.append(stats_line(ls)) if focus_band: lbs = ensure_nested_stats(stats_by_league_band, focus_league, focus_band) lines.append(f"LEAGUE×BAND ({focus_band}): {stats_line(lbs)}") return "\n".join(lines)

===================== EXCEL STORAGE =====================

BETS_HEADERS = [
"bet_uid", "match_id",
"league", "home", "away",
"band", "side",
"pre_total", "half_total", "diff", "abs_diff",
"odds", "stake",
"placed_at", "status_at_bet",
"final_total", "result", "profit", "settled_at",
]

def excel_open_or_create(path: str):
if os.path.exists(path):
wb = load_workbook(path)
else:
wb = Workbook()

text
# Bets sheet if "Bets" not in wb.sheetnames: ws = wb.active ws.title = "Bets" ws.append(BETS_HEADERS) else: ws = wb["Bets"] if ws.max_row < 1: ws.append(BETS_HEADERS) else: header = [ws.cell(row=1, column=c).value for c in range(1, ws.max_column + 1)] if header[: len(BETS_HEADERS)] != BETS_HEADERS: for i, h in enumerate(BETS_HEADERS, start=1): ws.cell(row=1, column=i).value = h # Ensure other sheets for sname in ["Stats_Total", "Stats_Band", "Stats_League", "Stats_League_Band", "Monthly"]: if sname not in wb.sheetnames: wb.create_sheet(sname) # mapping bet_uid -> row ws_bets = wb["Bets"] uid_to_row: Dict[str, int] = {} for r in range(2, ws_bets.max_row + 1): uid = ws_bets.cell(row=r, column=1).value if uid: uid_to_row[str(uid)] = r safe_save(wb, path) return wb, uid_to_row

def excel_append_bet(wb: Workbook, uid_to_row: Dict[str, int], path: str, row: Dict[str, Any]) -> None:
ws = wb["Bets"]
values = [row.get(h) for h in BETS_HEADERS]
ws.append(values)
uid_to_row[str(row["bet_uid"])] = ws.max_row
safe_save(wb, path)

def excel_update_settle(
wb: Workbook,
uid_to_row: Dict[str, int],
path: str,
bet_uid: str,
final_total: int,
result: str,
profit: float,
settled_at: str,
) -> None:
ws = wb["Bets"]
r = uid_to_row.get(str(bet_uid))
if not r:
return
ws.cell(row=r, column=16).value = int(final_total)
ws.cell(row=r, column=17).value = str(result)
ws.cell(row=r, column=18).value = float(profit)
ws.cell(row=r, column=19).value = str(settled_at)
safe_save(wb, path)

def _clear_sheet_keep_header(ws, headers: Optional[list] = None):
ws.delete_rows(1, ws.max_row)
if headers:
ws.append(headers)

def excel_write_stats(
wb: Workbook,
path: str,
stats_total: Dict[str, Any],
stats_by_band: Dict[str, Dict[str, Any]],
stats_by_league: Dict[str, Dict[str, Any]],
stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]],
):
ws = wb["Stats_Total"]
_clear_sheet_keep_header(ws, ["metric", "value"])
for k in ["bets", "wins", "losses", "pushes", "profit"]:
ws.append([k, stats_total[k]])

text
ws = wb["Stats_Band"] _clear_sheet_keep_header(ws, ["band", "bets", "wins", "losses", "pushes", "profit"]) for _, _, band in DIFF_BANDS: s = stats_by_band[band] ws.append([band, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) ws = wb["Stats_League"] _clear_sheet_keep_header(ws, ["league", "bets", "wins", "losses", "pushes", "profit"]) for league, s in sorted(stats_by_league.items(), key=lambda kv: (-kv[1]["bets"], kv[0])): ws.append([league, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) ws = wb["Stats_League_Band"] _clear_sheet_keep_header(ws, ["league", "band", "bets", "wins", "losses", "pushes", "profit"]) rows = [] for league, bands in stats_by_league_band.items(): for band, s in bands.items(): rows.append((league, band, s)) for league, band, s in sorted(rows, key=lambda x: (-x[2]["bets"], x[0], x[1])): ws.append([league, band, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) safe_save(wb, path)

def excel_write_monthly(wb: Workbook, path: str) -> None:
ws_bets = wb["Bets"]
ws_m = wb["Monthly"]

text
ws_m.delete_rows(1, ws_m.max_row) ws_m.append(["month", "bets_settled", "profit", "cum_profit"]) header = [ws_bets.cell(row=1, column=c).value for c in range(1, ws_bets.max_column + 1)] def col_idx(name: str) -> int: return header.index(name) + 1 try: c_profit = col_idx("profit") c_settled_at = col_idx("settled_at") except ValueError: safe_save(wb, path) return monthly_profit: Dict[str, float] = {} monthly_bets: Dict[str, int] = {} for r in range(2, ws_bets.max_row + 1): settled_at = ws_bets.cell(row=r, column=c_settled_at).value prof = ws_bets.cell(row=r, column=c_profit).value if not settled_at or prof is None: continue month = str(settled_at)[:7] monthly_profit[month] = monthly_profit.get(month, 0.0) + float(prof) monthly_bets[month] = monthly_bets.get(month, 0) + 1 cum = 0.0 for month in sorted(monthly_profit.keys()): p = float(monthly_profit[month]) b = int(monthly_bets.get(month, 0)) cum += p ws_m.append([month, b, p, cum]) ws_m._charts = [] if ws_m.max_row >= 2: chart = LineChart() chart.title = "Monthly Profit Trend" chart.y_axis.title = "Profit" chart.x_axis.title = "Month" data = Reference(ws_m, min_col=3, max_col=4, min_row=1, max_row=ws_m.max_row) cats = Reference(ws_m, min_col=1, min_row=2, max_row=ws_m.max_row) chart.add_data(data, titles_from_data=True) chart.set_categories(cats) ws_m.add_chart(chart, "F2") safe_save(wb, path)

===================== DRIVER =====================

def make_driver(headless: bool = True):
options = webdriver.ChromeOptions()
if headless:
options.add_argument("--headless=new")
options.add_argument("--window-size=1400,900")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
)
return webdriver.Chrome(options=options)

===================== SNAPSHOT PARSER =====================

def _parse_ou_for_mid(driver, mid: int) -> LineOU:
over_total = under_total = None
over_odds = under_odds = None

text
cells = [] ht = driver.find_elements(By.ID, f"ht_{mid}") gt = driver.find_elements(By.ID, f"gt_{mid}") if ht: cells.append(ht[0]) if gt: cells.append(gt[0]) if not cells: cells = driver.find_elements(By.CSS_SELECTOR, f'td.ouOdds[onclick*="oddscomp({mid}"]') for cell in cells: raw = (cell.get_attribute("innerText") or txt(cell)).strip().upper() odds2 = cell.find_elements(By.CSS_SELECTOR, "span.odds2") total_val = read_total_from_cell(cell) odds_val = read_odds_decimal(odds2[0]) if odds2 else None if raw.startswith("O"): over_total = total_val over_odds = odds_val elif raw.startswith("U"): under_total = total_val under_odds = odds_val line = LineOU(over_odds=over_odds, under_odds=under_odds) if over_total is not None and under_total is not None: if abs(over_total - under_total) < 1e-9: line.total = over_total else: line.total = (over_total + under_total) / 2.0 else: line.total = over_total if over_total is not None else under_total return line

def _get_snapshot_once(driver, do_scroll: bool) -> Dict[int, Dict[str, Any]]:
wait = WebDriverWait(driver, 30)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]')))
if do_scroll:
scroll_load_all(driver)

text
games: Dict[int, Dict[str, Any]] = {} analysis_tds = driver.find_elements(By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]') for td in analysis_tds: parsed = parse_analysis_onclick(td.get_attribute("onclick") or "") if not parsed: continue mid, home_s, away_s, league_s = parsed games[mid] = { "match_id": mid, "league": league_s, "home": split_team_and_tag(home_s), "away": split_team_and_tag(away_s), "status": None, "line": LineOU(), "home_score": None, "away_score": None, } for mid in list(games.keys()): status_els = driver.find_elements(By.ID, f"zt_{mid}") if status_els: games[mid]["status"] = txt(status_els[0]) or None games[mid]["line"] = _parse_ou_for_mid(driver, mid) h_els = driver.find_elements(By.ID, f"hzf_{mid}") g_els = driver.find_elements(By.ID, f"gzf_{mid}") if h_els and g_els: hs = safe_float(txt(h_els[0])) gs = safe_float(txt(g_els[0])) games[mid]["home_score"] = int(hs) if hs is not None else None games[mid]["away_score"] = int(gs) if gs is not None else None return games

def get_snapshot(driver, do_scroll: bool, retries: int = 3) -> Dict[int, Dict[str, Any]]:
for attempt in range(1, retries + 1):
try:
return _get_snapshot_once(driver, do_scroll=do_scroll)
except StaleElementReferenceException:
print(f"[{now_ts()}] snapshot stale -> retry {attempt}/{retries}", flush=True)
time.sleep(0.25)
return _get_snapshot_once(driver, do_scroll=do_scroll)

===================== MAIN LOOP =====================

def main():
print(f"[{now_ts()}] Starting monitor. poll={POLL_SECONDS}s stake={STAKE}", flush=True)
print(f"[{now_ts()}] Bands: " + ", ".join([lbl for _, _, lbl in DIFF_BANDS]), flush=True)
print(f"[{now_ts()}] Excel: {os.path.abspath(EXCEL_PATH)}", flush=True)
print(f"[{now_ts()}] Snapshots dir: {os.path.abspath(SNAPSHOT_DIR)} every {SNAPSHOT_EVERY_SECONDS}s", flush=True)
print(f"[{now_ts()}] Halftime wait: {HALFTIME_WAIT_SECONDS}s (first halftime only)", flush=True)

text
wb, uid_to_row = excel_open_or_create(EXCEL_PATH) tg_send( "🟢 NowGoal monitor STARTED\n" f"poll={POLL_SECONDS}s | stake={STAKE}\n" f"bands: " + ", ".join([lbl for _, _, lbl in DIFF_BANDS]) + "\n" f"halftime_wait={HALFTIME_WAIT_SECONDS}s (first halftime only)\n" f"time={now_dt()}\n" f"excel(main)={os.path.abspath(EXCEL_PATH)}\n" f"snapshots={os.path.abspath(SNAPSHOT_DIR)}" ) driver = make_driver(headless=HEADLESS) state: Dict[int, Dict[str, Any]] = {} # stats (начинаются с нуля — как ты и сказал) stats_total = empty_stats() stats_by_band = {label: empty_stats() for _, _, label in DIFF_BANDS} stats_by_league: Dict[str, Dict[str, Any]] = {} stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]] = {} loop_n = 0 try: driver.get(URL) WebDriverWait(driver, 30).until( EC.presence_of_element_located((By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]')) ) while True: loop_n += 1 t0 = time.time() do_scroll = (loop_n == 1) or (loop_n % SCROLL_EVERY_N_LOOPS == 0) try: driver.refresh() time.sleep(0.35) except Exception as e: print(f"[{now_ts()}] refresh error: {e} -> reopen", flush=True) driver.get(URL) try: snap = get_snapshot(driver, do_scroll=do_scroll, retries=3) except Exception as e: print(f"[{now_ts()}] snapshot error: {e}", flush=True) time.sleep(POLL_SECONDS) continue # summary counts cnt_total = len(snap) cnt_live = cnt_half = cnt_finished = 0 for g in snap.values(): st = g.get("status") if is_finished(st): cnt_finished += 1 elif is_halftime(st): cnt_half += 1 elif is_live(st): cnt_live += 1 print( f"[{now_ts()}] loop={loop_n} games={cnt_total} live={cnt_live} halftime={cnt_half} finished={cnt_finished} " f"| TOTAL profit={stats_total['profit']:+.2f} bets={stats_total['bets']}", flush=True ) for mid, g in snap.items(): st_rec = state.setdefault(mid, { "league": g["league"], "home": g["home"], "away": g["away"], "pregame_total_last": None, "pregame_total_frozen": None, "bet": None, "bet_uid": None, "settled": False, "last_status": None, "line_hist": deque(maxlen=10), # ✅ NEW: halftime tracking "halftime_count": 0, # сколько раз вошли в halftime (переходом) "halftime_first_ts": None, # time.time() первого появления halftime }) status = g.get("status") line_raw: LineOU = g.get("line", LineOU()) line = pick_best_line(line_raw, st_rec, status) if line_complete(line): st_rec["line_hist"].append({ "ts": now_dt(), "bucket": status_bucket(status), "line": LineOU(line.total, line.over_odds, line.under_odds), }) # keep previous status for transitions prev_status = st_rec["last_status"] if status != prev_status: st_rec["last_status"] = status if status: parts = [] if line.total is not None: parts.append(f"OU={line.total:g}") if line.over_odds is not None: parts.append(f"O@{line.over_odds:.2f}") if line.under_odds is not None: parts.append(f"U@{line.under_odds:.2f}") line_str = " | " + (" ".join(parts) if parts else "OU=-") print( f" [{now_ts()}] status id={mid} | {st_rec['home'].name} vs {st_rec['away'].name} | {status}{line_str}", flush=True ) # ✅ NEW: detect FIRST entry into halftime (transition into halftime) if is_halftime(status) and not is_halftime(prev_status): st_rec["halftime_count"] += 1 if st_rec["halftime_count"] == 1: st_rec["halftime_first_ts"] = time.time() print( f" [{now_ts()}] FIRST HALFTIME detected id={mid} -> waiting {HALFTIME_WAIT_SECONDS}s for odds to stabilize", flush=True ) else: # second+ halftime (often OT) -> ignore for betting logic print( f" [{now_ts()}] HALFTIME повторно (#{st_rec['halftime_count']}) id={mid} -> IGNORE (OT/extra break)", flush=True ) # 1) pregame -> сохраняем последний pregame total if is_pregame(status): if line.total is not None: st_rec["pregame_total_last"] = float(line.total) # 2) first live -> freeze pregame total if is_live(status) and st_rec["pregame_total_frozen"] is None and st_rec["pregame_total_last"] is not None: st_rec["pregame_total_frozen"] = float(st_rec["pregame_total_last"]) print(f" [{now_ts()}] pregame frozen id={mid} pre_total={st_rec['pregame_total_frozen']:g}", flush=True) # 3) halftime -> bet (✅ only on FIRST halftime + ✅ wait 15s) if ( is_halftime(status) and st_rec["bet"] is None and st_rec["pregame_total_frozen"] is not None and st_rec["halftime_count"] == 1 # ✅ only first halftime ): # ✅ wait after first halftime appears ht_ts = st_rec.get("halftime_first_ts") if ht_ts is None: st_rec["halftime_first_ts"] = time.time() ht_ts = st_rec["halftime_first_ts"] if (time.time() - float(ht_ts)) < HALFTIME_WAIT_SECONDS: continue pre_total = float(st_rec["pregame_total_frozen"]) half_total = line.total if half_total is None: continue diff = float(half_total) - pre_total abs_diff = abs(diff) band = pick_band(abs_diff) if band is None: continue if diff > 0: side = "UNDER" odds = line.under_odds else: side = "OVER" odds = line.over_odds if odds is None: continue bet_uid = f"{mid}-{int(time.time()*1000)}" st_rec["bet_uid"] = bet_uid st_rec["bet"] = { "band": band, "side": side, "line": float(half_total), "odds": float(odds), "stake": float(STAKE), "pre_total": float(pre_total), "half_total": float(half_total), "diff": float(diff), "abs_diff": float(abs_diff), "placed_at": now_dt(), "status_at_bet": status, } league = st_rec["league"] stats_total["bets"] += 1 stats_by_band[band]["bets"] += 1 ensure_stats(stats_by_league, league)["bets"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["bets"] += 1 excel_append_bet( wb, uid_to_row, EXCEL_PATH, { "bet_uid": bet_uid, "match_id": mid, "league": league, "home": st_rec["home"].name, "away": st_rec["away"].name, "band": band, "side": side, "pre_total": float(pre_total), "half_total": float(half_total), "diff": float(diff), "abs_diff": float(abs_diff), "odds": float(odds), "stake": float(STAKE), "placed_at": st_rec["bet"]["placed_at"], "status_at_bet": status, "final_total": None, "result": None, "profit": None, "settled_at": None, } ) excel_write_stats(wb, EXCEL_PATH, stats_total, stats_by_band, stats_by_league, stats_by_league_band) excel_write_monthly(wb, EXCEL_PATH) print( f" [{now_ts()}] BET PLACED [{band}] id={mid} {side} {half_total:g} @ {odds:.2f} " f"(pre {pre_total:g}, diff {diff:+g})", flush=True ) block = stats_block_extended( stats_total, stats_by_band, stats_by_league, stats_by_league_band, focus_league=league, focus_band=band ) tg_send( f"✅ BET PLACED (TOTAL #{stats_total['bets']}) [{band}]\n" f"{league}\n" f"{st_rec['home'].name} vs {st_rec['away'].name}\n" f"Status: {status}\n" f"(waited {HALFTIME_WAIT_SECONDS}s after first halftime)\n" f"Pre total: {pre_total:g}\n" f"Halftime line: {half_total:g} (diff {diff:+g})\n" f"BET: {side} {half_total:g} @ {float(odds):.2f}\n" f"Stake: {STAKE}\n" f"\n📊 STATS\n{block}\n" f"id={mid}\n" f"bet_uid={bet_uid}" ) # 4) finished -> settle bet if is_finished(status) and st_rec["bet"] is not None and not st_rec["settled"]: hs = g.get("home_score") gs = g.get("away_score") if hs is None or gs is None: continue final_total = int(hs) + int(gs) band = st_rec["bet"]["band"] league = st_rec["league"] side = st_rec["bet"]["side"] line_bet = float(st_rec["bet"]["line"]) odds = float(st_rec["bet"]["odds"]) stake = float(st_rec["bet"]["stake"]) if abs(final_total - line_bet) < 1e-9: result = "push" stats_total["pushes"] += 1 stats_by_band[band]["pushes"] += 1 ensure_stats(stats_by_league, league)["pushes"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["pushes"] += 1 else: win = (final_total < line_bet) if side == "UNDER" else (final_total > line_bet) if win: result = "win" stats_total["wins"] += 1 stats_by_band[band]["wins"] += 1 ensure_stats(stats_by_league, league)["wins"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["wins"] += 1 else: result = "loss" stats_total["losses"] += 1 stats_by_band[band]["losses"] += 1 ensure_stats(stats_by_league, league)["losses"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["losses"] += 1 prof = bet_profit(odds, stake, result) stats_total["profit"] += prof stats_by_band[band]["profit"] += prof ensure_stats(stats_by_league, league)["profit"] += prof ensure_nested_stats(stats_by_league_band, league, band)["profit"] += prof st_rec["settled"] = True bet_uid = st_rec.get("bet_uid") if bet_uid: excel_update_settle( wb, uid_to_row, EXCEL_PATH, bet_uid, final_total, result.upper(), float(prof), now_dt() ) excel_write_stats(wb, EXCEL_PATH, stats_total, stats_by_band, stats_by_league, stats_by_league_band) excel_write_monthly(wb, EXCEL_PATH) block = stats_block_extended( stats_total, stats_by_band, stats_by_league, stats_by_league_band, focus_league=league, focus_band=band ) print( f" [{now_ts()}] BET SETTLED [{band}] id={mid} {result.upper()} final_total={final_total} " f"bet={side} {line_bet:g} @ {odds:.2f} P/L={prof:+.2f}\n{block}", flush=True ) tg_send( f"🏁 BET RESULT: {result.upper()} [{band}]\n" f"{league}\n" f"{st_rec['home'].name} {hs} : {gs} {st_rec['away'].name}\n" f"Final total: {final_total}\n" f"Bet: {side} {line_bet:g} @ {odds:.2f} | stake {stake}\n" f"P/L: {prof:+.2f}\n\n" f"📊 STATS\n{block}\n" f"id={mid}\n" f"bet_uid={bet_uid}" ) # делаем snapshot раз в SNAPSHOT_EVERY_SECONDS (можно смотреть в Excel без блокировок) maybe_write_snapshot(wb, EXCEL_PATH) elapsed = time.time() - t0 time.sleep(max(0.2, POLL_SECONDS - elapsed)) finally: driver.quit() print(f"[{now_ts()}] Driver closed.", flush=True)

if name == "main":
main() тогда давай переделай этот код как сам знаешь, по плюсовой стратегии, и вышли мне полный вариант, и пусть файл excel будет создан уже другой, чтобы там в итоге потом смотреть статистику

Pregunta

почему после первого пуска не пришло сообщение втелеграм, переделай и вышли полный код

Pensamiento

Responding with updated code and considerations

I need to respond in Russian and provide a full code revision. The issues include ensuring messages are sent, logging errors, and possibly retrying. I’ll give a script that uses environment variables, not hardcoded tokens/chat IDs for security. It’s about gambling monitoring, not placing bets. I’ll mention token leakage, and a disclaimer on safety.

Implementing changes and fixing Telegram issue

I’ll implement the new strategy by focusing on the specified band and abs_diff range, adding an optional NCAA Women’s league blacklist. I’ll ensure a default Excel path and handle file creation with a timestamp if the environment isn’t set. For Telegram, I’ll add retry/backoff logic and verify the token at startup.

Improving error handling and Telegram token checks

I’ll add retries and better error logging, showing status codes and responses. Also, I’ll disable the web page preview parameter if needed. Checking the TELEGRAM_TOKEN could be an issue if it’s invalid or revoked, or if there's a chat ID mismatch. The script might have crashed before reaching tg_send, or the token might be improperly formatted.

Improving the Telegram bot script and logging

I’ll update the code to print the full response, even if it’s false, and add exception info. I’ll make sure the start message is sent before initializing heavy tasks like the webdriver. Also, I’ll add a "heartbeat" ping after driver launch and set up retries for failed tg_send calls.

Final code improvements and considerations

I'll implement the strategy filter and league blacklist configurations. For the Excel file path, I'll ensure that a timestamp is included, and the file can be created with a prefix. I'll also remove default token and chat ID values, relying on environment variables. We'll verify the Telegram bot setup with tg_self_test() and handle potential approval/mismatch issues, adding an optional debug mode. Plus, I'll send the start message before heavy operations and handle timeout and other states.

Polarizing strategy and bugs overview

The filter will focus on UNDER bets within a 20-30 band, checking the "abs_diff" and ensuring the allowed diff matches when half_total > pre_total. We'll maintain side computation and potentially track the "abs_diff_subrange." Keep extra columns optional for extra analysis. Fixing the "is_live" bug and ensuring the pregame freeze occurs only once. Addressing potential miscalculations in line_hist and considering pruning finished games. We'll also add a quick Telegram test snippet if it fails. Ready to integrate into the script.

Thought for 1m 4s

Respuesta

Причин, почему не пришло стартовое сообщение в Telegram у твоего скрипта, обычно 3:

  1. Неверный token/chat_id (или токен уже “сгорел”).
  2. Бот не имеет права писать в чат: в личке нужно открыть бота и нажать Start (/start); в группе — добавить бота и дать права.
  3. Запрос к Telegram падает (интернет/файрвол/timeout) — в твоей версии это могло пройти “тихо”, потому что ты смотришь только r.ok и печатаешь урезанный текст.

Ниже — полный переделанный код под плюсовую стратегию из твоего Excel + улучшенная отправка в Telegram (ретраи + подробный лог + ранний “startup ping”) + новый Excel-файл (по умолчанию отдельный, чтобы не смешивать со старым).

Что делает “плюсовая стратегия” в этом коде

Ставит только:

  • UNDER
  • band = 20–30
  • abs_diff ∈ [20,22) или [26,30)
    И дополнительно (по желанию) банит NCAA Women (как кандидат на исключение из твоих данных).

Полный код (готовый файл)

ВАЖНО: я не вставляю токен в код (он секретный). Задай переменные окружения TELEGRAM_TOKEN и TELEGRAM_CHAT_ID.
Пример (Windows PowerShell):
setx TELEGRAM_TOKEN "123:ABC"
setx TELEGRAM_CHAT_ID "6667940236"

python
import re import ast import os import time from dataclasses import dataclass from typing import Optional, Dict, Any, Tuple from datetime import datetime from collections import deque from pathlib import Path import requests from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import StaleElementReferenceException from openpyxl import Workbook, load_workbook from openpyxl.chart import LineChart, Reference # ===================== CONFIG ===================== URL = "https://www.nowgoal.com/basketball" POLL_SECONDS = 5 STAKE = 1.0 HEADLESS = True # Wait after FIRST halftime appears (to stabilize odds/lines) HALFTIME_WAIT_SECONDS = 15 DIFF_BANDS = [ (20.0, 30.0, "20-30"), (30.0, 40.0, "30-40"), (40.0, 50.0, "40-50"), (50.0, None, "50+"), ] SCROLL_EVERY_N_LOOPS = 12 # ✅ NEW: separate output file by default (so you don't mix with old runs) RUN_TAG = os.getenv("RUN_TAG", datetime.now().strftime("%Y%m%d")) EXCEL_PATH = os.getenv("EXCEL_PATH", f"nowgoal_monitor_PLUS_{RUN_TAG}.xlsx") # ✅ Telegram from env only (safer) TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "").strip() TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "").strip() TG_TIMEOUT = 15 TG_RETRIES = 3 TG_RETRY_SLEEP = 2.0 TG_DEBUG = True # show detailed Telegram errors in console # ====== WINDOWS-EXCEL SAFE SAVE + SNAPSHOTS ====== SNAPSHOT_DIR = "snapshots" SNAPSHOT_EVERY_SECONDS = 60 AUTOSAVE_SUFFIX = "_AUTOSAVE.xlsx" SAVE_RETRIES = 3 SAVE_RETRY_SLEEP = 1.0 _last_snapshot_ts = 0.0 _excel_lock_notified = False # ===================== STRATEGY (PLUS) ===================== # Based on your historical excel: # - only UNDER # - only band 20-30 # - abs_diff in [20,22) or [26,30) USE_STRATEGY_FILTER = True STRATEGY_RULES = [ {"band": "20-30", "side": "UNDER", "abs_min": 20.0, "abs_max": 22.0}, {"band": "20-30", "side": "UNDER", "abs_min": 26.0, "abs_max": 30.0}, ] # Optional: league blacklist (from your sample: NCAA Women looked bad) LEAGUE_BLACKLIST_RE = [ r"\bNational Committee Association America W\b", # NCAA Women ] LEAGUE_WHITELIST_RE = [] # keep empty unless you want strict allow-only # ===================== TELEGRAM ===================== def tg_ready() -> bool: return bool(TELEGRAM_TOKEN) and bool(TELEGRAM_CHAT_ID) def tg_send(text: str) -> bool: """ Telegram send with retries + detailed logging. Returns True if sent. """ print(f"[TG] -> {text.replace('\n', ' | ')}", flush=True) if not tg_ready(): print("[TG] SKIP: TELEGRAM_TOKEN / TELEGRAM_CHAT_ID not set.", flush=True) return False url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage" payload = { "chat_id": TELEGRAM_CHAT_ID, "text": text, "disable_web_page_preview": True, } last_err = None for attempt in range(1, TG_RETRIES + 1): try: r = requests.post(url, data=payload, timeout=TG_TIMEOUT) if r.ok: return True # Telegram returned error last_err = f"HTTP {r.status_code}: {r.text}" if TG_DEBUG: print(f"[TG] ERROR attempt {attempt}/{TG_RETRIES}: {last_err}", flush=True) except Exception as e: last_err = repr(e) if TG_DEBUG: print(f"[TG] EXCEPTION attempt {attempt}/{TG_RETRIES}: {last_err}", flush=True) time.sleep(TG_RETRY_SLEEP) print(f"[TG] FAILED after {TG_RETRIES} tries: {last_err}", flush=True) return False def tg_startup_ping() -> None: """ Send a minimal startup ping immediately, before webdriver/excel heavy ops. Helps detect token/chat_id issues early. """ tg_send(f"🟢 START ping | time={now_dt()} | excel={os.path.abspath(EXCEL_PATH)}") # ===================== MODELS ===================== @dataclass class Team: name: str tag: Optional[str] = None @dataclass class LineOU: total: Optional[float] = None over_odds: Optional[float] = None # decimal under_odds: Optional[float] = None # decimal # ===================== HELPERS ===================== def now_ts() -> str: return datetime.now().strftime("%H:%M:%S") def now_dt() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def txt(el) -> str: if el is None: return "" return (el.get_attribute("textContent") or "").strip() def safe_float(x: Optional[str]) -> Optional[float]: if not x: return None x = x.strip().replace(",", ".") try: return float(x) except ValueError: return None def hk_to_decimal(odds: Optional[float]) -> Optional[float]: # HK 0.83 -> Decimal 1.83 if odds is None: return None if odds >= 1.2: return odds return odds + 1.0 def read_odds_decimal(span) -> Optional[float]: """odds2 может быть текст 1.80, а может быть пусто, но есть data-o=0.80 (HK).""" if span is None: return None t = (span.get_attribute("textContent") or "").strip() v = safe_float(t) if v is not None: return hk_to_decimal(v) v2 = safe_float(span.get_attribute("data-o")) if v2 is not None: return hk_to_decimal(v2) return None # -------- Excel safe save + snapshots (Windows) -------- def safe_save(wb: Workbook, path: str) -> bool: """ Try to save to main file. If locked by Excel (Windows) -> save to AUTOSAVE and notify once in TG. """ global _excel_lock_notified for i in range(1, SAVE_RETRIES + 1): try: wb.save(path) _excel_lock_notified = False return True except PermissionError: print(f"[{now_ts()}] Excel LOCKED: {path} (try {i}/{SAVE_RETRIES})", flush=True) time.sleep(SAVE_RETRY_SLEEP) except OSError as e: print(f"[{now_ts()}] Save OSError: {e} (try {i}/{SAVE_RETRIES})", flush=True) time.sleep(SAVE_RETRY_SLEEP) base, _ext = os.path.splitext(path) fallback = base + AUTOSAVE_SUFFIX try: wb.save(fallback) print(f"[{now_ts()}] Saved to AUTOSAVE: {fallback}", flush=True) if (not _excel_lock_notified) and tg_ready(): tg_send( "⚠️ Excel-файл открыт и заблокирован.\n" f"Не могу записать в: {os.path.abspath(path)}\n" f"Сохраняю во временный: {os.path.abspath(fallback)}\n" f"И делаю snapshots в папку: {os.path.abspath(SNAPSHOT_DIR)}" ) _excel_lock_notified = True return False except Exception as e: print(f"[{now_ts()}] AUTOSAVE failed: {e}", flush=True) if tg_ready(): tg_send(f"❌ Не могу сохранить Excel ни в основной, ни в AUTOSAVE: {e}") return False def maybe_write_snapshot(wb: Workbook, base_path: str) -> None: global _last_snapshot_ts now = time.time() if now - _last_snapshot_ts < SNAPSHOT_EVERY_SECONDS: return _last_snapshot_ts = now try: Path(SNAPSHOT_DIR).mkdir(parents=True, exist_ok=True) stamp = datetime.now().strftime("%Y%m%d_%H%M%S") snap_path = os.path.join(SNAPSHOT_DIR, f"nowgoal_PLUS_{stamp}.xlsx") wb.save(snap_path) print(f"[{now_ts()}] Snapshot saved: {snap_path}", flush=True) except Exception as e: print(f"[{now_ts()}] Snapshot save failed: {e}", flush=True) OU_TOTAL_RE = re.compile(r"\b[OU]\s*([0-9]+(?:\.[0-9]+)?)", re.IGNORECASE) def read_total_from_cell(cell) -> Optional[float]: odds1 = cell.find_elements(By.CSS_SELECTOR, "span.odds1") if odds1: v = safe_float(txt(odds1[0])) if v is not None: return v it = (cell.get_attribute("innerText") or "").strip() m = OU_TOTAL_RE.search(it) if m: return safe_float(m.group(1)) return None def split_team_and_tag(s: str) -> Team: s = (s or "").strip() if s.endswith("]") and "[" in s: i = s.rfind("[") name = s[:i].strip() tag = s[i + 1:-1].strip() or None if name: return Team(name=name, tag=tag) return Team(name=s, tag=None) def parse_analysis_onclick(onclick: str) -> Optional[Tuple[int, str, str, str]]: if not onclick or "basketInPage.analysis" not in onclick: return None try: args_str = onclick[onclick.find("(") + 1: onclick.rfind(")")] args = ast.literal_eval("(" + args_str + ")") return int(args[0]), str(args[1]), str(args[2]), str(args[3]) except Exception: return None def scroll_load_all(driver, max_rounds: int = 25, pause: float = 0.35): last = 0 stable = 0 for _ in range(max_rounds): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(pause) cur = len(driver.find_elements(By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]')) if cur == last: stable += 1 if stable >= 3: break else: stable = 0 last = cur driver.execute_script("window.scrollTo(0, 0);") time.sleep(0.15) def pick_band(abs_diff: float) -> Optional[str]: for lo, hi, label in DIFF_BANDS: if abs_diff >= lo and (hi is None or abs_diff < hi): return label return None def line_complete(line: LineOU) -> bool: return (line.total is not None and line.over_odds is not None and line.under_odds is not None) # ===================== STATUS ===================== LIVE_PAT = re.compile(r"\b\d+(?:st|nd|rd|th)\s+Qtr\b|\bOT\b|\bOvertime\b|\bLive\b", re.IGNORECASE) TIME_PAT = re.compile(r"^\s*\d{1,2}:\d{2}\s*(?:AM|PM)?\s*$", re.IGNORECASE) def is_finished(status: Optional[str]) -> bool: return (status or "").strip().lower() in {"finished", "final", "ft", "ended"} def is_halftime(status: Optional[str]) -> bool: return "halftime" in (status or "").strip().lower() def is_live(status: Optional[str]) -> bool: s = (status or "").strip() if not s: return False if is_finished(s) or is_halftime(s): return True return bool(LIVE_PAT.search(s)) def is_pregame(status: Optional[str]) -> bool: s = (status or "").strip() if not s: return True if TIME_PAT.match(s): return True sl = s.lower() if is_live(s) or is_halftime(s) or is_finished(s): return False if any(k in sl for k in ["postp", "cancel", "abandon", "void"]): return False if "pend" in sl: return True return True def status_bucket(status: Optional[str]) -> str: if is_halftime(status): return "halftime" if is_finished(status): return "finished" if is_live(status): return "live" if is_pregame(status): return "pregame" return "other" def pick_best_line(current: LineOU, st_rec: Dict[str, Any], status: Optional[str]) -> LineOU: if line_complete(current): return current bucket = status_bucket(status) hist = st_rec.get("line_hist") if not hist: return current for item in reversed(hist): if item["bucket"] == bucket and line_complete(item["line"]): return item["line"] for item in reversed(hist): if line_complete(item["line"]): return item["line"] return current # ===================== STRATEGY FILTERS ===================== def league_allowed(league: str) -> bool: lg = league or "" if LEAGUE_WHITELIST_RE: ok = any(re.search(p, lg, re.I) for p in LEAGUE_WHITELIST_RE) if not ok: return False if any(re.search(p, lg, re.I) for p in LEAGUE_BLACKLIST_RE): return False return True def strategy_allows(league: str, band: str, side: str, abs_diff: float) -> bool: if not USE_STRATEGY_FILTER: return True # league gate first if not league_allowed(league): return False for r in STRATEGY_RULES: if r.get("band") and band != r["band"]: continue if r.get("side") and side != r["side"]: continue abs_min = r.get("abs_min") abs_max = r.get("abs_max") if abs_min is not None and abs_diff < float(abs_min): continue if abs_max is not None and abs_diff >= float(abs_max): continue return True return False # ===================== STATS ===================== def empty_stats() -> Dict[str, Any]: return {"bets": 0, "wins": 0, "losses": 0, "pushes": 0, "profit": 0.0} def ensure_stats(d: Dict[str, Dict[str, Any]], key: str) -> Dict[str, Any]: if key not in d: d[key] = empty_stats() return d[key] def ensure_nested_stats(d: Dict[str, Dict[str, Dict[str, Any]]], k1: str, k2: str) -> Dict[str, Any]: if k1 not in d: d[k1] = {} if k2 not in d[k1]: d[k1][k2] = empty_stats() return d[k1][k2] def bet_profit(decimal_odds: float, stake: float, result: str) -> float: if result == "push": return 0.0 if result == "loss": return -stake return (decimal_odds - 1.0) * stake def stats_line(s: Dict[str, Any]) -> str: return f"Bets={s['bets']} W={s['wins']} L={s['losses']} P={s['pushes']} Profit={s['profit']:+.2f}" def stats_block_extended( stats_total: Dict[str, Any], stats_by_band: Dict[str, Dict[str, Any]], stats_by_league: Dict[str, Dict[str, Any]], stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]], focus_league: Optional[str] = None, focus_band: Optional[str] = None, ) -> str: lines = [] lines.append(f"TOTAL: {stats_line(stats_total)}") for _, _, label in DIFF_BANDS: lines.append(f"{label}: {stats_line(stats_by_band[label])}") if focus_league: ls = ensure_stats(stats_by_league, focus_league) lines.append("") lines.append(f"LEAGUE: {focus_league}") lines.append(stats_line(ls)) if focus_band: lbs = ensure_nested_stats(stats_by_league_band, focus_league, focus_band) lines.append(f"LEAGUE×BAND ({focus_band}): {stats_line(lbs)}") return "\n".join(lines) # ===================== EXCEL STORAGE ===================== BETS_HEADERS = [ "bet_uid", "match_id", "league", "home", "away", "band", "side", "pre_total", "half_total", "diff", "abs_diff", "odds", "stake", "placed_at", "status_at_bet", "final_total", "result", "profit", "settled_at", ] def excel_open_or_create(path: str): if os.path.exists(path): wb = load_workbook(path) else: wb = Workbook() if "Bets" not in wb.sheetnames: ws = wb.active ws.title = "Bets" ws.append(BETS_HEADERS) else: ws = wb["Bets"] if ws.max_row < 1: ws.append(BETS_HEADERS) else: header = [ws.cell(row=1, column=c).value for c in range(1, ws.max_column + 1)] if header[: len(BETS_HEADERS)] != BETS_HEADERS: for i, h in enumerate(BETS_HEADERS, start=1): ws.cell(row=1, column=i).value = h for sname in ["Stats_Total", "Stats_Band", "Stats_League", "Stats_League_Band", "Monthly"]: if sname not in wb.sheetnames: wb.create_sheet(sname) ws_bets = wb["Bets"] uid_to_row: Dict[str, int] = {} for r in range(2, ws_bets.max_row + 1): uid = ws_bets.cell(row=r, column=1).value if uid: uid_to_row[str(uid)] = r safe_save(wb, path) return wb, uid_to_row def excel_append_bet(wb: Workbook, uid_to_row: Dict[str, int], path: str, row: Dict[str, Any]) -> None: ws = wb["Bets"] values = [row.get(h) for h in BETS_HEADERS] ws.append(values) uid_to_row[str(row["bet_uid"])] = ws.max_row safe_save(wb, path) def excel_update_settle( wb: Workbook, uid_to_row: Dict[str, int], path: str, bet_uid: str, final_total: int, result: str, profit: float, settled_at: str, ) -> None: ws = wb["Bets"] r = uid_to_row.get(str(bet_uid)) if not r: return ws.cell(row=r, column=16).value = int(final_total) ws.cell(row=r, column=17).value = str(result) ws.cell(row=r, column=18).value = float(profit) ws.cell(row=r, column=19).value = str(settled_at) safe_save(wb, path) def _clear_sheet_keep_header(ws, headers: Optional[list] = None): ws.delete_rows(1, ws.max_row) if headers: ws.append(headers) def excel_write_stats( wb: Workbook, path: str, stats_total: Dict[str, Any], stats_by_band: Dict[str, Dict[str, Any]], stats_by_league: Dict[str, Dict[str, Any]], stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]], ): ws = wb["Stats_Total"] _clear_sheet_keep_header(ws, ["metric", "value"]) for k in ["bets", "wins", "losses", "pushes", "profit"]: ws.append([k, stats_total[k]]) ws = wb["Stats_Band"] _clear_sheet_keep_header(ws, ["band", "bets", "wins", "losses", "pushes", "profit"]) for _, _, band in DIFF_BANDS: s = stats_by_band[band] ws.append([band, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) ws = wb["Stats_League"] _clear_sheet_keep_header(ws, ["league", "bets", "wins", "losses", "pushes", "profit"]) for league, s in sorted(stats_by_league.items(), key=lambda kv: (-kv[1]["bets"], kv[0])): ws.append([league, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) ws = wb["Stats_League_Band"] _clear_sheet_keep_header(ws, ["league", "band", "bets", "wins", "losses", "pushes", "profit"]) rows = [] for league, bands in stats_by_league_band.items(): for band, s in bands.items(): rows.append((league, band, s)) for league, band, s in sorted(rows, key=lambda x: (-x[2]["bets"], x[0], x[1])): ws.append([league, band, s["bets"], s["wins"], s["losses"], s["pushes"], float(s["profit"])]) safe_save(wb, path) def excel_write_monthly(wb: Workbook, path: str) -> None: ws_bets = wb["Bets"] ws_m = wb["Monthly"] ws_m.delete_rows(1, ws_m.max_row) ws_m.append(["month", "bets_settled", "profit", "cum_profit"]) header = [ws_bets.cell(row=1, column=c).value for c in range(1, ws_bets.max_column + 1)] def col_idx(name: str) -> int: return header.index(name) + 1 try: c_profit = col_idx("profit") c_settled_at = col_idx("settled_at") except ValueError: safe_save(wb, path) return monthly_profit: Dict[str, float] = {} monthly_bets: Dict[str, int] = {} for r in range(2, ws_bets.max_row + 1): settled_at = ws_bets.cell(row=r, column=c_settled_at).value prof = ws_bets.cell(row=r, column=c_profit).value if not settled_at or prof is None: continue month = str(settled_at)[:7] monthly_profit[month] = monthly_profit.get(month, 0.0) + float(prof) monthly_bets[month] = monthly_bets.get(month, 0) + 1 cum = 0.0 for month in sorted(monthly_profit.keys()): p = float(monthly_profit[month]) b = int(monthly_bets.get(month, 0)) cum += p ws_m.append([month, b, p, cum]) ws_m._charts = [] if ws_m.max_row >= 2: chart = LineChart() chart.title = "Monthly Profit Trend" chart.y_axis.title = "Profit" chart.x_axis.title = "Month" data = Reference(ws_m, min_col=3, max_col=4, min_row=1, max_row=ws_m.max_row) cats = Reference(ws_m, min_col=1, min_row=2, max_row=ws_m.max_row) chart.add_data(data, titles_from_data=True) chart.set_categories(cats) ws_m.add_chart(chart, "F2") safe_save(wb, path) # ===================== DRIVER ===================== def make_driver(headless: bool = True): options = webdriver.ChromeOptions() if headless: options.add_argument("--headless=new") options.add_argument("--window-size=1400,900") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument( "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/126.0 Safari/537.36" ) return webdriver.Chrome(options=options) # ===================== SNAPSHOT PARSER ===================== def _parse_ou_for_mid(driver, mid: int) -> LineOU: over_total = under_total = None over_odds = under_odds = None cells = [] ht = driver.find_elements(By.ID, f"ht_{mid}") gt = driver.find_elements(By.ID, f"gt_{mid}") if ht: cells.append(ht[0]) if gt: cells.append(gt[0]) if not cells: cells = driver.find_elements(By.CSS_SELECTOR, f'td.ouOdds[onclick*="oddscomp({mid}"]') for cell in cells: raw = (cell.get_attribute("innerText") or txt(cell)).strip().upper() odds2 = cell.find_elements(By.CSS_SELECTOR, "span.odds2") total_val = read_total_from_cell(cell) odds_val = read_odds_decimal(odds2[0]) if odds2 else None if raw.startswith("O"): over_total = total_val over_odds = odds_val elif raw.startswith("U"): under_total = total_val under_odds = odds_val line = LineOU(over_odds=over_odds, under_odds=under_odds) if over_total is not None and under_total is not None: if abs(over_total - under_total) < 1e-9: line.total = over_total else: line.total = (over_total + under_total) / 2.0 else: line.total = over_total if over_total is not None else under_total return line def _get_snapshot_once(driver, do_scroll: bool) -> Dict[int, Dict[str, Any]]: wait = WebDriverWait(driver, 30) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]'))) if do_scroll: scroll_load_all(driver) games: Dict[int, Dict[str, Any]] = {} analysis_tds = driver.find_elements(By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]') for td in analysis_tds: parsed = parse_analysis_onclick(td.get_attribute("onclick") or "") if not parsed: continue mid, home_s, away_s, league_s = parsed games[mid] = { "match_id": mid, "league": league_s, "home": split_team_and_tag(home_s), "away": split_team_and_tag(away_s), "status": None, "line": LineOU(), "home_score": None, "away_score": None, } for mid in list(games.keys()): status_els = driver.find_elements(By.ID, f"zt_{mid}") if status_els: games[mid]["status"] = txt(status_els[0]) or None games[mid]["line"] = _parse_ou_for_mid(driver, mid) h_els = driver.find_elements(By.ID, f"hzf_{mid}") g_els = driver.find_elements(By.ID, f"gzf_{mid}") if h_els and g_els: hs = safe_float(txt(h_els[0])) gs = safe_float(txt(g_els[0])) games[mid]["home_score"] = int(hs) if hs is not None else None games[mid]["away_score"] = int(gs) if gs is not None else None return games def get_snapshot(driver, do_scroll: bool, retries: int = 3) -> Dict[int, Dict[str, Any]]: for attempt in range(1, retries + 1): try: return _get_snapshot_once(driver, do_scroll=do_scroll) except StaleElementReferenceException: print(f"[{now_ts()}] snapshot stale -> retry {attempt}/{retries}", flush=True) time.sleep(0.25) return _get_snapshot_once(driver, do_scroll=do_scroll) # ===================== MAIN LOOP ===================== def main(): print(f"[{now_ts()}] Starting monitor PLUS. poll={POLL_SECONDS}s stake={STAKE}", flush=True) print(f"[{now_ts()}] Bands: " + ", ".join([lbl for _, _, lbl in DIFF_BANDS]), flush=True) print(f"[{now_ts()}] Excel: {os.path.abspath(EXCEL_PATH)}", flush=True) print(f"[{now_ts()}] Snapshots dir: {os.path.abspath(SNAPSHOT_DIR)} every {SNAPSHOT_EVERY_SECONDS}s", flush=True) print(f"[{now_ts()}] Halftime wait: {HALFTIME_WAIT_SECONDS}s (first halftime only)", flush=True) print(f"[{now_ts()}] Strategy: ONLY UNDER, band 20-30, abs_diff in [20,22) or [26,30)", flush=True) if LEAGUE_BLACKLIST_RE: print(f"[{now_ts()}] League blacklist enabled: {LEAGUE_BLACKLIST_RE}", flush=True) # ✅ Send ping immediately (so you know TG works) tg_startup_ping() wb, uid_to_row = excel_open_or_create(EXCEL_PATH) tg_send( "🟢 NowGoal monitor PLUS STARTED\n" f"poll={POLL_SECONDS}s | stake={STAKE}\n" f"strategy=UNDER only; band 20-30; abs_diff [20,22) or [26,30)\n" f"halftime_wait={HALFTIME_WAIT_SECONDS}s (first halftime only)\n" f"time={now_dt()}\n" f"excel(main)={os.path.abspath(EXCEL_PATH)}\n" f"snapshots={os.path.abspath(SNAPSHOT_DIR)}" ) driver = make_driver(headless=HEADLESS) state: Dict[int, Dict[str, Any]] = {} stats_total = empty_stats() stats_by_band = {label: empty_stats() for _, _, label in DIFF_BANDS} stats_by_league: Dict[str, Dict[str, Any]] = {} stats_by_league_band: Dict[str, Dict[str, Dict[str, Any]]] = {} loop_n = 0 try: driver.get(URL) WebDriverWait(driver, 30).until( EC.presence_of_element_located((By.CSS_SELECTOR, 'td[onclick*="basketInPage.analysis("]')) ) while True: loop_n += 1 t0 = time.time() do_scroll = (loop_n == 1) or (loop_n % SCROLL_EVERY_N_LOOPS == 0) try: driver.refresh() time.sleep(0.35) except Exception as e: print(f"[{now_ts()}] refresh error: {e} -> reopen", flush=True) driver.get(URL) try: snap = get_snapshot(driver, do_scroll=do_scroll, retries=3) except Exception as e: print(f"[{now_ts()}] snapshot error: {e}", flush=True) time.sleep(POLL_SECONDS) continue cnt_total = len(snap) cnt_live = cnt_half = cnt_finished = 0 for g in snap.values(): st = g.get("status") if is_finished(st): cnt_finished += 1 elif is_halftime(st): cnt_half += 1 elif is_live(st): cnt_live += 1 print( f"[{now_ts()}] loop={loop_n} games={cnt_total} live={cnt_live} halftime={cnt_half} finished={cnt_finished} " f"| TOTAL profit={stats_total['profit']:+.2f} bets={stats_total['bets']}", flush=True ) for mid, g in snap.items(): st_rec = state.setdefault(mid, { "league": g["league"], "home": g["home"], "away": g["away"], "pregame_total_last": None, "pregame_total_frozen": None, "bet": None, "bet_uid": None, "settled": False, "last_status": None, "line_hist": deque(maxlen=10), "halftime_count": 0, "halftime_first_ts": None, }) status = g.get("status") line_raw: LineOU = g.get("line", LineOU()) line = pick_best_line(line_raw, st_rec, status) if line_complete(line): st_rec["line_hist"].append({ "ts": now_dt(), "bucket": status_bucket(status), "line": LineOU(line.total, line.over_odds, line.under_odds), }) prev_status = st_rec["last_status"] if status != prev_status: st_rec["last_status"] = status if status: parts = [] if line.total is not None: parts.append(f"OU={line.total:g}") if line.over_odds is not None: parts.append(f"O@{line.over_odds:.2f}") if line.under_odds is not None: parts.append(f"U@{line.under_odds:.2f}") line_str = " | " + (" ".join(parts) if parts else "OU=-") print( f" [{now_ts()}] status id={mid} | {st_rec['home'].name} vs {st_rec['away'].name} | {status}{line_str}", flush=True ) # FIRST halftime transition if is_halftime(status) and not is_halftime(prev_status): st_rec["halftime_count"] += 1 if st_rec["halftime_count"] == 1: st_rec["halftime_first_ts"] = time.time() print( f" [{now_ts()}] FIRST HALFTIME detected id={mid} -> waiting {HALFTIME_WAIT_SECONDS}s", flush=True ) else: print( f" [{now_ts()}] HALFTIME повторно (#{st_rec['halftime_count']}) id={mid} -> IGNORE", flush=True ) # pregame -> remember last pregame total if is_pregame(status): if line.total is not None: st_rec["pregame_total_last"] = float(line.total) # first live -> freeze pregame total if is_live(status) and st_rec["pregame_total_frozen"] is None and st_rec["pregame_total_last"] is not None: st_rec["pregame_total_frozen"] = float(st_rec["pregame_total_last"]) print(f" [{now_ts()}] pregame frozen id={mid} pre_total={st_rec['pregame_total_frozen']:g}", flush=True) # halftime -> bet (only first halftime + wait + PLUS filters) if ( is_halftime(status) and st_rec["bet"] is None and st_rec["pregame_total_frozen"] is not None and st_rec["halftime_count"] == 1 ): ht_ts = st_rec.get("halftime_first_ts") if ht_ts is None: st_rec["halftime_first_ts"] = time.time() ht_ts = st_rec["halftime_first_ts"] if (time.time() - float(ht_ts)) < HALFTIME_WAIT_SECONDS: continue pre_total = float(st_rec["pregame_total_frozen"]) half_total = line.total if half_total is None: continue diff = float(half_total) - pre_total abs_diff = abs(diff) band = pick_band(abs_diff) if band is None: continue if diff > 0: side = "UNDER" odds = line.under_odds else: side = "OVER" odds = line.over_odds if odds is None: continue league = st_rec["league"] # ✅ PLUS STRATEGY GATE (main change) if not strategy_allows(league, band, side, abs_diff): print( f" [{now_ts()}] SKIP (PLUS strategy) id={mid} league={league} [{band}] {side} abs_diff={abs_diff:g}", flush=True ) continue bet_uid = f"{mid}-{int(time.time()*1000)}" st_rec["bet_uid"] = bet_uid st_rec["bet"] = { "band": band, "side": side, "line": float(half_total), "odds": float(odds), "stake": float(STAKE), "pre_total": float(pre_total), "half_total": float(half_total), "diff": float(diff), "abs_diff": float(abs_diff), "placed_at": now_dt(), "status_at_bet": status, } stats_total["bets"] += 1 stats_by_band[band]["bets"] += 1 ensure_stats(stats_by_league, league)["bets"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["bets"] += 1 excel_append_bet( wb, uid_to_row, EXCEL_PATH, { "bet_uid": bet_uid, "match_id": mid, "league": league, "home": st_rec["home"].name, "away": st_rec["away"].name, "band": band, "side": side, "pre_total": float(pre_total), "half_total": float(half_total), "diff": float(diff), "abs_diff": float(abs_diff), "odds": float(odds), "stake": float(STAKE), "placed_at": st_rec["bet"]["placed_at"], "status_at_bet": status, "final_total": None, "result": None, "profit": None, "settled_at": None, } ) excel_write_stats(wb, EXCEL_PATH, stats_total, stats_by_band, stats_by_league, stats_by_league_band) excel_write_monthly(wb, EXCEL_PATH) print( f" [{now_ts()}] BET PLACED PLUS [{band}] id={mid} {side} {half_total:g} @ {odds:.2f} " f"(pre {pre_total:g}, diff {diff:+g}, abs {abs_diff:g})", flush=True ) block = stats_block_extended( stats_total, stats_by_band, stats_by_league, stats_by_league_band, focus_league=league, focus_band=band ) tg_send( f"✅ BET PLACED (PLUS) (TOTAL #{stats_total['bets']}) [{band}]\n" f"{league}\n" f"{st_rec['home'].name} vs {st_rec['away'].name}\n" f"Status: {status}\n" f"(waited {HALFTIME_WAIT_SECONDS}s after first halftime)\n" f"Pre total: {pre_total:g}\n" f"Halftime line: {half_total:g} (diff {diff:+g}, abs {abs_diff:g})\n" f"BET: {side} {half_total:g} @ {float(odds):.2f}\n" f"Stake: {STAKE}\n" f"\n📊 STATS\n{block}\n" f"id={mid}\n" f"bet_uid={bet_uid}" ) # finished -> settle if is_finished(status) and st_rec["bet"] is not None and not st_rec["settled"]: hs = g.get("home_score") gs = g.get("away_score") if hs is None or gs is None: continue final_total = int(hs) + int(gs) band = st_rec["bet"]["band"] league = st_rec["league"] side = st_rec["bet"]["side"] line_bet = float(st_rec["bet"]["line"]) odds = float(st_rec["bet"]["odds"]) stake = float(st_rec["bet"]["stake"]) if abs(final_total - line_bet) < 1e-9: result = "push" stats_total["pushes"] += 1 stats_by_band[band]["pushes"] += 1 ensure_stats(stats_by_league, league)["pushes"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["pushes"] += 1 else: win = (final_total < line_bet) if side == "UNDER" else (final_total > line_bet) if win: result = "win" stats_total["wins"] += 1 stats_by_band[band]["wins"] += 1 ensure_stats(stats_by_league, league)["wins"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["wins"] += 1 else: result = "loss" stats_total["losses"] += 1 stats_by_band[band]["losses"] += 1 ensure_stats(stats_by_league, league)["losses"] += 1 ensure_nested_stats(stats_by_league_band, league, band)["losses"] += 1 prof = bet_profit(odds, stake, result) stats_total["profit"] += prof stats_by_band[band]["profit"] += prof ensure_stats(stats_by_league, league)["profit"] += prof ensure_nested_stats(stats_by_league_band, league, band)["profit"] += prof st_rec["settled"] = True bet_uid = st_rec.get("bet_uid") if bet_uid: excel_update_settle( wb, uid_to_row, EXCEL_PATH, bet_uid, final_total, result.upper(), float(prof), now_dt() ) excel_write_stats(wb, EXCEL_PATH, stats_total, stats_by_band, stats_by_league, stats_by_league_band) excel_write_monthly(wb, EXCEL_PATH) block = stats_block_extended( stats_total, stats_by_band, stats_by_league, stats_by_league_band, focus_league=league, focus_band=band ) print( f" [{now_ts()}] BET SETTLED PLUS [{band}] id={mid} {result.upper()} final_total={final_total} " f"bet={side} {line_bet:g} @ {odds:.2f} P/L={prof:+.2f}\n{block}", flush=True ) tg_send( f"🏁 BET RESULT (PLUS): {result.upper()} [{band}]\n" f"{league}\n" f"{st_rec['home'].name} {hs} : {gs} {st_rec['away'].name}\n" f"Final total: {final_total}\n" f"Bet: {side} {line_bet:g} @ {odds:.2f} | stake {stake}\n" f"P/L: {prof:+.2f}\n\n" f"📊 STATS\n{block}\n" f"id={mid}\n" f"bet_uid={bet_uid}" ) maybe_write_snapshot(wb, EXCEL_PATH) elapsed = time.time() - t0 time.sleep(max(0.2, POLL_SECONDS - elapsed)) finally: driver.quit() print(f"[{now_ts()}] Driver closed.", flush=True) if __name__ == "__main__": main()

Если после этого всё равно нет сообщений в TG

С этим кодом в консоль будет выведена реальная причина (например: 401 Unauthorized, 400 Bad Request: chat not found, bot was blocked by the user, etc.).

Самые частые фиксы:

  • В личке: открыть бота и нажать Start (или отправить /start).
  • Проверить TELEGRAM_CHAT_ID (в группе он обычно отрицательный).
  • Если видишь 401 Unauthorized — токен неверный/отозван → перевыпустить.

Если хочешь — скинь сюда ровно строку ошибки, которую выведет консоль ([TG] ERROR ...), и я точечно скажу что именно не так (chat_id vs токен vs права).

Compartir este Q&A