feat: focus fereastra TradeStation pe /ss, /resume și market_open
Captura e region-based (mss), deci dacă alt app acoperă TS, screenshot-urile manuale și cele scheduled după redeschiderea pieței erau inutile. Acum aducem TS în față înainte de captură. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,31 @@ def _cmd_dryrun(args) -> None:
|
||||
sys.exit(0 if result.acceptance_pass else 1)
|
||||
|
||||
|
||||
def _focus_window_by_title(needle: str) -> str | None:
|
||||
"""Adu în față cea mai mare fereastră al cărei titlu conține `needle`.
|
||||
|
||||
Returnează titlul găsit sau None (niciun match, Win32 a refuzat activate,
|
||||
sau `pygetwindow` lipsește — caz normal pe Linux/WSL unde rulează doar testele).
|
||||
"""
|
||||
try:
|
||||
import pygetwindow as gw # type: ignore[import-untyped]
|
||||
except ImportError:
|
||||
return None
|
||||
needle_lc = needle.lower()
|
||||
matches = [w for w in gw.getAllWindows() if w.title and needle_lc in w.title.lower()]
|
||||
matches.sort(key=lambda w: (w.width or 0) * (w.height or 0), reverse=True)
|
||||
if not matches:
|
||||
return None
|
||||
win = matches[0]
|
||||
try:
|
||||
if win.isMinimized:
|
||||
win.restore()
|
||||
win.activate()
|
||||
return win.title
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _cmd_run(args) -> None:
|
||||
cfg = Config.load_current(Path("configs"))
|
||||
cfg = _apply_operating_hours_cli_overrides(cfg, args)
|
||||
@@ -228,25 +253,11 @@ def _cmd_run(args) -> None:
|
||||
|
||||
# Auto-focus TradeStation by title substring so user doesn't need to alt-tab
|
||||
if not capture_stub and cfg.window_title:
|
||||
try:
|
||||
import pygetwindow as gw # type: ignore[import-untyped]
|
||||
needle = cfg.window_title.lower()
|
||||
matches = [w for w in gw.getAllWindows() if w.title and needle in w.title.lower()]
|
||||
# Prefer largest window (the main chart, not a tooltip or child)
|
||||
matches.sort(key=lambda w: (w.width or 0) * (w.height or 0), reverse=True)
|
||||
if matches:
|
||||
win = matches[0]
|
||||
try:
|
||||
if win.isMinimized:
|
||||
win.restore()
|
||||
win.activate()
|
||||
print(f"Focused window: {win.title!r}", flush=True)
|
||||
except Exception as exc:
|
||||
print(f"Could not focus {win.title!r}: {exc}", flush=True)
|
||||
else:
|
||||
print(f"WARN: no window contains {cfg.window_title!r} — bring TradeStation to front manually", flush=True)
|
||||
except ImportError:
|
||||
pass
|
||||
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, "startup_delay", 0.0)
|
||||
if delay > 0 and not capture_stub:
|
||||
@@ -777,9 +788,13 @@ def _maybe_log_transition(
|
||||
audit: _AuditLike,
|
||||
notifier: _NotifierLike,
|
||||
status_body: str = "",
|
||||
) -> None:
|
||||
) -> str | None:
|
||||
"""Log market_open / market_closed exactly once per transition.
|
||||
|
||||
Returns the event name emitted (``"market_open"`` / ``"market_closed"``)
|
||||
or ``None`` when no transition happened. Callers that need to react to
|
||||
the opening (e.g. bring TradeStation to the foreground) use this value.
|
||||
|
||||
Startup guard (R2): when last_window_state is None we just seed it; no
|
||||
alert/audit event is emitted for the initial evaluation. This prevents a
|
||||
spurious market_open alert when run_live_async starts in-window.
|
||||
@@ -790,14 +805,14 @@ def _maybe_log_transition(
|
||||
window_reason = "closed"
|
||||
else:
|
||||
# user_paused / drift_paused don't change market window state
|
||||
return
|
||||
return None
|
||||
|
||||
if window_reason == state.last_window_state:
|
||||
return
|
||||
return None
|
||||
|
||||
if state.last_window_state is None:
|
||||
state.last_window_state = window_reason
|
||||
return
|
||||
return None
|
||||
|
||||
event_name = "market_open" if window_reason == "open" else "market_closed"
|
||||
audit.log({"ts": now, "event": event_name, "reason": reason})
|
||||
@@ -813,6 +828,7 @@ def _maybe_log_transition(
|
||||
body=body,
|
||||
))
|
||||
state.last_window_state = window_reason
|
||||
return event_name
|
||||
|
||||
|
||||
def _sync_detection_tick(
|
||||
@@ -923,7 +939,13 @@ async def _run_tick(ctx: RunContext) -> _TickSyncResult:
|
||||
if ctx.lifecycle is not None:
|
||||
skip = _should_skip(now, ctx.lifecycle, ctx.cfg, ctx.canary)
|
||||
sb = _brief_status(ctx)
|
||||
_maybe_log_transition(skip, ctx.lifecycle, now, ctx.audit, ctx.notifier, status_body=sb)
|
||||
transition = _maybe_log_transition(
|
||||
skip, ctx.lifecycle, now, ctx.audit, ctx.notifier, status_body=sb,
|
||||
)
|
||||
if transition == "market_open" and ctx.cfg.window_title:
|
||||
title = await asyncio.to_thread(_focus_window_by_title, ctx.cfg.window_title)
|
||||
ctx.audit.log({"ts": now, "event": "window_focused", "command": "market_open", "title": title})
|
||||
await asyncio.sleep(0.15)
|
||||
if skip is not None:
|
||||
# No detection this tick. Empty result → _handle_fsm_result no-op.
|
||||
return _TickSyncResult()
|
||||
@@ -1026,6 +1048,10 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
|
||||
ctx.notifier.send(Alert(kind="status", title="ATM Status", body="\n".join(lines)))
|
||||
elif cmd.action == "ss":
|
||||
now_ss = time.time()
|
||||
if cfg.window_title:
|
||||
title = await asyncio.to_thread(_focus_window_by_title, cfg.window_title)
|
||||
ctx.audit.log({"ts": now_ss, "event": "window_focused", "command": "ss", "title": title})
|
||||
await asyncio.sleep(0.15)
|
||||
frame_ss = await asyncio.to_thread(ctx.capture)
|
||||
if frame_ss is None:
|
||||
ctx.notifier.send(Alert(
|
||||
@@ -1063,6 +1089,10 @@ async def _dispatch_command(ctx: RunContext, cmd) -> None:
|
||||
"ts": time.time(), "event": "user_resumed",
|
||||
"was_drift": was_drift, "was_user": was_user, "force": force,
|
||||
})
|
||||
if cfg.window_title:
|
||||
title = await asyncio.to_thread(_focus_window_by_title, cfg.window_title)
|
||||
ctx.audit.log({"ts": time.time(), "event": "window_focused", "command": "resume", "title": title})
|
||||
await asyncio.sleep(0.15)
|
||||
# Adaptive response
|
||||
if was_drift and not force:
|
||||
title = "Pauză user eliminată — dar Canary drift activ"
|
||||
|
||||
Reference in New Issue
Block a user