fix: forțează maximize TradeStation la focus + atm debug --canary

Canary drift imediat după /ss, /resume, market_open sau startup când
TradeStation nu era maximizat — chart_window_region e absolute, deci orice
dimensiune sub full-screen capturează alt conținut și phash-ul diverge.
SW_RESTORE pe fereastră zoomed o de-maximiza; acum IsIconic-unminimize +
SW_SHOWMAXIMIZED necondiționat.

Adaugă atm debug --canary: salvează crop-ul canary ROI + distance vs baseline
pentru diagnostic rapid când drift-ul apare în teren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 16:48:45 +03:00
parent 2c1dae14fc
commit a796e91e90

View File

@@ -123,6 +123,10 @@ def main(argv=None) -> None:
"--delay", type=float, default=3.0, metavar="SEC",
help="Seconds to wait before capture (bring TradeStation to front first). Default 3.",
)
p_debug.add_argument(
"--canary", action="store_true",
help="Diagnostic-only: capture canary ROI, compute phash distance vs baseline, save crop.",
)
# report
p_report = sub.add_parser("report", help="Print weekly performance report")
@@ -208,6 +212,10 @@ def _focus_window_by_title(needle: str) -> str | None:
(protecția anti focus-stealing introdusă în Win2000). Trucul standard:
trimiți un ALT press/release ca să resetezi lock-ul, apoi restore +
SetForegroundWindow prin ctypes pe hwnd direct.
Forțează maximize dacă fereastra nu e deja zoomed — `chart_window_region`
din calibrare presupune fereastra fullscreen, orice altă dimensiune
produce canary drift imediat.
"""
try:
import pygetwindow as gw # type: ignore[import-untyped]
@@ -232,7 +240,12 @@ def _focus_window_by_title(needle: str) -> str | None:
# VK_MENU=0x12 (ALT), KEYEVENTF_KEYUP=0x02 — unlocks focus protection.
user32.keybd_event(0x12, 0, 0, 0)
user32.keybd_event(0x12, 0, 0x02, 0)
user32.ShowWindow(hwnd, 9) # SW_RESTORE
# Dacă e minimizată o scoatem; apoi forțăm maximize necondiționat.
# SW_RESTORE pe o fereastră maximizată o de-maximizează — niciodată nu-l
# folosim aici (chart_window_region presupune fullscreen).
if user32.IsIconic(hwnd):
user32.ShowWindow(hwnd, 9) # SW_RESTORE (doar unminimize)
user32.ShowWindow(hwnd, 3) # SW_SHOWMAXIMIZED
user32.BringWindowToTop(hwnd)
user32.SetForegroundWindow(hwnd)
return win.title
@@ -370,9 +383,16 @@ def _cmd_debug(args) -> None:
cfg = Config.load_current(Path("configs"))
capture = _build_capture(cfg)
if cfg.window_title:
title = _focus_window_by_title(cfg.window_title)
if title:
print(f"Focused window: {title!r}", flush=True)
else:
print(f"WARN: no window contains {cfg.window_title!r} — bring TradeStation to front manually", flush=True)
delay = getattr(args, "delay", 0.0)
if delay > 0:
print(f"Bring TradeStation to front. Capturing in {delay:.0f}s...", flush=True)
print(f"Capturing in {delay:.0f}s...", flush=True)
for i in range(int(delay), 0, -1):
print(f" {i}...", flush=True)
time.sleep(1.0)
@@ -391,6 +411,27 @@ def _cmd_debug(args) -> None:
cv2.imwrite(str(full_path), frame)
print(f"captured frame: {frame.shape}{full_path}")
if getattr(args, "canary", False):
from atm.vision import phash, hamming_hex
try:
canary_crop = crop_roi(frame, cfg.canary.roi)
except Exception as exc:
sys.exit(f"canary ROI crop failed: {exc}")
canary_path = out_dir / f"debug_canary_{ts}.png"
cv2.imwrite(str(canary_path), canary_crop)
live_hash = phash(canary_crop)
distance = hamming_hex(live_hash, cfg.canary.baseline_phash)
threshold = cfg.canary.drift_threshold
verdict = "DRIFT" if distance > threshold else "ok"
roi = cfg.canary.roi
print("---")
print(f"canary ROI: x={roi.x} y={roi.y} w={roi.w} h={roi.h}")
print(f"crop: {canary_crop.shape}{canary_path}")
print(f"baseline: {cfg.canary.baseline_phash}")
print(f"live phash: {live_hash}")
print(f"distance: {distance}/{threshold}{verdict}")
return
# crop dot_roi + run detector
try:
dot_crop = crop_roi(frame, cfg.dot_roi)