ci: fix lint baseline and stabilize failing main tests
Some checks failed
Gitea CI / test (push) Failing after 5s
Gitea CI / test (pull_request) Failing after 5s

This commit is contained in:
agentson
2026-03-01 20:17:13 +09:00
parent 6f047a6daf
commit 5730f0db2a
64 changed files with 1041 additions and 1380 deletions

View File

@@ -26,12 +26,12 @@ from src.context.aggregator import ContextAggregator
from src.context.layer import ContextLayer
from src.context.scheduler import ContextScheduler
from src.context.store import ContextStore
from src.core.criticality import CriticalityAssessor
from src.core.blackout_manager import (
BlackoutOrderManager,
QueuedOrderIntent,
parse_blackout_windows_kst,
)
from src.core.criticality import CriticalityAssessor
from src.core.kill_switch import KillSwitchOrchestrator
from src.core.order_policy import (
OrderPolicyRejected,
@@ -52,12 +52,16 @@ from src.evolution.optimizer import EvolutionOptimizer
from src.logging.decision_logger import DecisionLogger
from src.logging_config import setup_logging
from src.markets.schedule import MARKETS, MarketInfo, get_next_market_open, get_open_markets
from src.notifications.telegram_client import NotificationFilter, TelegramClient, TelegramCommandHandler
from src.strategy.models import DayPlaybook, MarketOutlook
from src.notifications.telegram_client import (
NotificationFilter,
TelegramClient,
TelegramCommandHandler,
)
from src.strategy.exit_rules import ExitRuleConfig, ExitRuleInput, evaluate_exit
from src.strategy.models import DayPlaybook, MarketOutlook
from src.strategy.playbook_store import PlaybookStore
from src.strategy.pre_market_planner import PreMarketPlanner
from src.strategy.position_state_machine import PositionState
from src.strategy.pre_market_planner import PreMarketPlanner
from src.strategy.scenario_engine import ScenarioEngine
logger = logging.getLogger(__name__)
@@ -350,9 +354,7 @@ async def _inject_staged_exit_features(
return
if "pred_down_prob" not in market_data:
market_data["pred_down_prob"] = _estimate_pred_down_prob_from_rsi(
market_data.get("rsi")
)
market_data["pred_down_prob"] = _estimate_pred_down_prob_from_rsi(market_data.get("rsi"))
existing_atr = safe_float(market_data.get("atr_value"), 0.0)
if existing_atr > 0:
@@ -389,7 +391,7 @@ async def _retry_connection(coro_factory: Any, *args: Any, label: str = "", **kw
return await coro_factory(*args, **kwargs)
except ConnectionError as exc:
if attempt < MAX_CONNECTION_RETRIES:
wait_secs = 2 ** attempt
wait_secs = 2**attempt
logger.warning(
"Connection error %s (attempt %d/%d), retrying in %ds: %s",
label,
@@ -413,7 +415,7 @@ async def sync_positions_from_broker(
broker: Any,
overseas_broker: Any,
db_conn: Any,
settings: "Settings",
settings: Settings,
) -> int:
"""Sync open positions from the live broker into the local DB at startup.
@@ -441,9 +443,7 @@ async def sync_positions_from_broker(
if market.exchange_code in seen_exchange_codes:
continue
seen_exchange_codes.add(market.exchange_code)
balance_data = await overseas_broker.get_overseas_balance(
market.exchange_code
)
balance_data = await overseas_broker.get_overseas_balance(market.exchange_code)
log_market = market_code # e.g. "US_NASDAQ"
except ConnectionError as exc:
logger.warning(
@@ -453,9 +453,7 @@ async def sync_positions_from_broker(
)
continue
held_codes = _extract_held_codes_from_balance(
balance_data, is_domestic=market.is_domestic
)
held_codes = _extract_held_codes_from_balance(balance_data, is_domestic=market.is_domestic)
for stock_code in held_codes:
if get_open_position(db_conn, stock_code, log_market):
continue # already tracked
@@ -487,9 +485,7 @@ async def sync_positions_from_broker(
synced += 1
if synced:
logger.info(
"Startup sync complete: %d position(s) synced from broker", synced
)
logger.info("Startup sync complete: %d position(s) synced from broker", synced)
else:
logger.info("Startup sync: no new positions to sync from broker")
return synced
@@ -859,15 +855,9 @@ def _apply_staged_exit_override_for_hold(
pnl_pct = (current_price - entry_price) / entry_price * 100.0
if exit_eval.reason == "hard_stop":
rationale = (
f"Stop-loss triggered ({pnl_pct:.2f}% <= "
f"{stop_loss_threshold:.2f}%)"
)
rationale = f"Stop-loss triggered ({pnl_pct:.2f}% <= {stop_loss_threshold:.2f}%)"
elif exit_eval.reason == "arm_take_profit":
rationale = (
f"Take-profit triggered ({pnl_pct:.2f}% >= "
f"{arm_pct:.2f}%)"
)
rationale = f"Take-profit triggered ({pnl_pct:.2f}% >= {arm_pct:.2f}%)"
elif exit_eval.reason == "atr_trailing_stop":
rationale = "ATR trailing-stop triggered"
elif exit_eval.reason == "be_lock_threat":
@@ -978,7 +968,10 @@ def _maybe_queue_order_intent(
)
if queued:
logger.warning(
"Blackout active: queued order intent %s %s (%s) qty=%d price=%.4f source=%s pending=%d",
(
"Blackout active: queued order intent %s %s (%s) "
"qty=%d price=%.4f source=%s pending=%d"
),
order_type,
stock_code,
market.code,
@@ -1071,7 +1064,10 @@ async def process_blackout_recovery_orders(
)
if queued_price <= 0 or current_price <= 0:
logger.info(
"Drop queued intent by price revalidation (invalid price): %s %s (%s) queued=%.4f current=%.4f",
(
"Drop queued intent by price revalidation (invalid price): "
"%s %s (%s) queued=%.4f current=%.4f"
),
intent.order_type,
intent.stock_code,
market.code,
@@ -1082,7 +1078,10 @@ async def process_blackout_recovery_orders(
drift_pct = abs(current_price - queued_price) / queued_price * 100.0
if drift_pct > max_drift_pct:
logger.info(
"Drop queued intent by price revalidation: %s %s (%s) queued=%.4f current=%.4f drift=%.2f%% max=%.2f%%",
(
"Drop queued intent by price revalidation: %s %s (%s) "
"queued=%.4f current=%.4f drift=%.2f%% max=%.2f%%"
),
intent.order_type,
intent.stock_code,
market.code,
@@ -1375,24 +1374,18 @@ async def trading_cycle(
# 1. Fetch market data
price_output: dict[str, Any] = {} # Populated for overseas markets; used for fallback metrics
if market.is_domestic:
current_price, price_change_pct, foreigner_net = await broker.get_current_price(
stock_code
)
current_price, price_change_pct, foreigner_net = await broker.get_current_price(stock_code)
balance_data = await broker.get_balance()
output2 = balance_data.get("output2", [{}])
total_eval = safe_float(output2[0].get("tot_evlu_amt", "0")) if output2 else 0
total_cash = safe_float(
balance_data.get("output2", [{}])[0].get("dnca_tot_amt", "0")
if output2
else "0"
balance_data.get("output2", [{}])[0].get("dnca_tot_amt", "0") if output2 else "0"
)
purchase_total = safe_float(output2[0].get("pchs_amt_smtl_amt", "0")) if output2 else 0
else:
# Overseas market
price_data = await overseas_broker.get_overseas_price(
market.exchange_code, stock_code
)
price_data = await overseas_broker.get_overseas_price(market.exchange_code, stock_code)
balance_data = await overseas_broker.get_overseas_balance(market.exchange_code)
output2 = balance_data.get("output2", [{}])
@@ -1459,11 +1452,7 @@ async def trading_cycle(
total_cash = settings.PAPER_OVERSEAS_CASH
# Calculate daily P&L %
pnl_pct = (
((total_eval - purchase_total) / purchase_total * 100)
if purchase_total > 0
else 0.0
)
pnl_pct = ((total_eval - purchase_total) / purchase_total * 100) if purchase_total > 0 else 0.0
market_data: dict[str, Any] = {
"stock_code": stock_code,
@@ -1491,11 +1480,13 @@ async def trading_cycle(
market_data["rsi"] = max(0.0, min(100.0, 50.0 + price_change_pct * 2.0))
if price_output and current_price > 0:
pr_high = safe_float(
price_output.get("high") or price_output.get("ovrs_hgpr")
price_output.get("high")
or price_output.get("ovrs_hgpr")
or price_output.get("stck_hgpr")
)
pr_low = safe_float(
price_output.get("low") or price_output.get("ovrs_lwpr")
price_output.get("low")
or price_output.get("ovrs_lwpr")
or price_output.get("stck_lwpr")
)
if pr_high > 0 and pr_low > 0 and pr_high >= pr_low:
@@ -1512,9 +1503,7 @@ async def trading_cycle(
if open_pos and current_price > 0:
entry_price = safe_float(open_pos.get("price"), 0.0)
if entry_price > 0:
market_data["unrealized_pnl_pct"] = (
(current_price - entry_price) / entry_price * 100
)
market_data["unrealized_pnl_pct"] = (current_price - entry_price) / entry_price * 100
entry_ts = open_pos.get("timestamp")
if entry_ts:
try:
@@ -1745,16 +1734,19 @@ async def trading_cycle(
stock_playbook=stock_playbook,
settings=settings,
)
if open_position and decision.action == "HOLD" and _should_force_exit_for_overnight(
if (
open_position
and decision.action == "HOLD"
and _should_force_exit_for_overnight(
market=market,
settings=settings,
)
):
decision = TradeDecision(
action="SELL",
confidence=max(decision.confidence, 85),
rationale=(
"Forced exit by overnight policy"
" (session close window / kill switch priority)"
"Forced exit by overnight policy (session close window / kill switch priority)"
),
)
logger.info(
@@ -1834,9 +1826,7 @@ async def trading_cycle(
return
broker_held_qty = (
_extract_held_qty_from_balance(
balance_data, stock_code, is_domestic=market.is_domestic
)
_extract_held_qty_from_balance(balance_data, stock_code, is_domestic=market.is_domestic)
if decision.action == "SELL"
else 0
)
@@ -1871,7 +1861,10 @@ async def trading_cycle(
)
if fx_blocked:
logger.warning(
"Skip BUY %s (%s): FX buffer guard (remaining=%.2f, required=%.2f, cash=%.2f, order=%.2f)",
(
"Skip BUY %s (%s): FX buffer guard "
"(remaining=%.2f, required=%.2f, cash=%.2f, order=%.2f)"
),
stock_code,
market.name,
remaining_cash,
@@ -2068,8 +2061,7 @@ async def trading_cycle(
action="SELL",
confidence=0,
rationale=(
"[ghost-close] Broker reported no balance;"
" position closed without fill"
"[ghost-close] Broker reported no balance; position closed without fill"
),
quantity=0,
price=0.0,
@@ -2275,17 +2267,13 @@ async def handle_domestic_pending_orders(
outcome="cancelled",
)
except Exception as notify_exc:
logger.warning(
"notify_unfilled_order failed: %s", notify_exc
)
logger.warning("notify_unfilled_order failed: %s", notify_exc)
else:
# First unfilled SELL → resubmit at last * 0.996 (-0.4%).
try:
last_price, _, _ = await broker.get_current_price(stock_code)
if last_price <= 0:
raise ValueError(
f"Invalid price ({last_price}) for {stock_code}"
)
raise ValueError(f"Invalid price ({last_price}) for {stock_code}")
new_price = kr_round_down(last_price * 0.996)
validate_order_policy(
market=MARKETS["KR"],
@@ -2298,9 +2286,7 @@ async def handle_domestic_pending_orders(
quantity=psbl_qty,
price=new_price,
)
sell_resubmit_counts[key] = (
sell_resubmit_counts.get(key, 0) + 1
)
sell_resubmit_counts[key] = sell_resubmit_counts.get(key, 0) + 1
try:
await telegram.notify_unfilled_order(
stock_code=stock_code,
@@ -2311,9 +2297,7 @@ async def handle_domestic_pending_orders(
new_price=float(new_price),
)
except Exception as notify_exc:
logger.warning(
"notify_unfilled_order failed: %s", notify_exc
)
logger.warning("notify_unfilled_order failed: %s", notify_exc)
except Exception as exc:
logger.error(
"SELL resubmit failed for KR %s: %s",
@@ -2381,9 +2365,7 @@ async def handle_overseas_pending_orders(
try:
orders = await overseas_broker.get_overseas_pending_orders(exchange_code)
except Exception as exc:
logger.warning(
"Failed to fetch pending orders for %s: %s", exchange_code, exc
)
logger.warning("Failed to fetch pending orders for %s: %s", exchange_code, exc)
continue
for order in orders:
@@ -2448,26 +2430,21 @@ async def handle_overseas_pending_orders(
outcome="cancelled",
)
except Exception as notify_exc:
logger.warning(
"notify_unfilled_order failed: %s", notify_exc
)
logger.warning("notify_unfilled_order failed: %s", notify_exc)
else:
# First unfilled SELL → resubmit at last * 0.996 (-0.4%).
try:
price_data = await overseas_broker.get_overseas_price(
order_exchange, stock_code
)
last_price = float(
price_data.get("output", {}).get("last", "0") or "0"
)
last_price = float(price_data.get("output", {}).get("last", "0") or "0")
if last_price <= 0:
raise ValueError(
f"Invalid price ({last_price}) for {stock_code}"
)
raise ValueError(f"Invalid price ({last_price}) for {stock_code}")
new_price = round(last_price * 0.996, 4)
market_info = next(
(
m for m in MARKETS.values()
m
for m in MARKETS.values()
if m.exchange_code == order_exchange and not m.is_domestic
),
None,
@@ -2485,9 +2462,7 @@ async def handle_overseas_pending_orders(
quantity=nccs_qty,
price=new_price,
)
sell_resubmit_counts[key] = (
sell_resubmit_counts.get(key, 0) + 1
)
sell_resubmit_counts[key] = sell_resubmit_counts.get(key, 0) + 1
try:
await telegram.notify_unfilled_order(
stock_code=stock_code,
@@ -2498,9 +2473,7 @@ async def handle_overseas_pending_orders(
new_price=new_price,
)
except Exception as notify_exc:
logger.warning(
"notify_unfilled_order failed: %s", notify_exc
)
logger.warning("notify_unfilled_order failed: %s", notify_exc)
except Exception as exc:
logger.error(
"SELL resubmit failed for %s %s: %s",
@@ -2659,13 +2632,16 @@ async def run_daily_session(
logger.warning("Playbook notification failed: %s", exc)
logger.info(
"Generated playbook for %s: %d stocks, %d scenarios",
market.code, playbook.stock_count, playbook.scenario_count,
market.code,
playbook.stock_count,
playbook.scenario_count,
)
except Exception as exc:
logger.error("Playbook generation failed for %s: %s", market.code, exc)
try:
await telegram.notify_playbook_failed(
market=market.code, reason=str(exc)[:200],
market=market.code,
reason=str(exc)[:200],
)
except Exception as notify_exc:
logger.warning("Playbook failed notification error: %s", notify_exc)
@@ -2676,12 +2652,10 @@ async def run_daily_session(
for stock_code in watchlist:
try:
if market.is_domestic:
current_price, price_change_pct, foreigner_net = (
await _retry_connection(
broker.get_current_price,
stock_code,
label=stock_code,
)
current_price, price_change_pct, foreigner_net = await _retry_connection(
broker.get_current_price,
stock_code,
label=stock_code,
)
else:
price_data = await _retry_connection(
@@ -2690,9 +2664,7 @@ async def run_daily_session(
stock_code,
label=f"{stock_code}@{market.exchange_code}",
)
current_price = safe_float(
price_data.get("output", {}).get("last", "0")
)
current_price = safe_float(price_data.get("output", {}).get("last", "0"))
# Fallback: if price API returns 0, use scanner candidate price
if current_price <= 0:
cand_lookup = candidate_map.get(stock_code)
@@ -2704,9 +2676,7 @@ async def run_daily_session(
)
current_price = cand_lookup.price
foreigner_net = 0.0
price_change_pct = safe_float(
price_data.get("output", {}).get("rate", "0")
)
price_change_pct = safe_float(price_data.get("output", {}).get("rate", "0"))
# Fall back to scanner candidate price if API returns 0.
if current_price <= 0:
cand_lookup = candidate_map.get(stock_code)
@@ -2769,15 +2739,9 @@ async def run_daily_session(
if market.is_domestic:
output2 = balance_data.get("output2", [{}])
total_eval = safe_float(
output2[0].get("tot_evlu_amt", "0")
) if output2 else 0
total_cash = safe_float(
output2[0].get("dnca_tot_amt", "0")
) if output2 else 0
purchase_total = safe_float(
output2[0].get("pchs_amt_smtl_amt", "0")
) if output2 else 0
total_eval = safe_float(output2[0].get("tot_evlu_amt", "0")) if output2 else 0
total_cash = safe_float(output2[0].get("dnca_tot_amt", "0")) if output2 else 0
purchase_total = safe_float(output2[0].get("pchs_amt_smtl_amt", "0")) if output2 else 0
else:
output2 = balance_data.get("output2", [{}])
if isinstance(output2, list) and output2:
@@ -2788,18 +2752,15 @@ async def run_daily_session(
balance_info = {}
total_eval = safe_float(balance_info.get("frcr_evlu_tota", "0") or "0")
purchase_total = safe_float(
balance_info.get("frcr_buy_amt_smtl", "0") or "0"
)
purchase_total = safe_float(balance_info.get("frcr_buy_amt_smtl", "0") or "0")
# Fetch available foreign currency cash via inquire-psamount (TTTS3007R/VTTS3007R).
# TTTS3012R output2 does not include a cash/deposit field — frcr_dncl_amt_2 does not exist.
# TTTS3012R output2 does not include a cash/deposit field.
# frcr_dncl_amt_2 does not exist.
# Use the first stock with a valid price as the reference for the buying power query.
# Source: 한국투자증권 오픈API 전체문서 (20260221) — '해외주식 매수가능금액조회' 시트
total_cash = 0.0
ref_stock = next(
(s for s in stocks_data if s.get("current_price", 0) > 0), None
)
ref_stock = next((s for s in stocks_data if s.get("current_price", 0) > 0), None)
if ref_stock:
try:
ps_data = await overseas_broker.get_overseas_buying_power(
@@ -2819,11 +2780,7 @@ async def run_daily_session(
# Paper mode fallback: VTS overseas balance API often fails for many accounts.
# Only activate in paper mode — live mode must use real balance from KIS.
if (
total_cash <= 0
and settings.MODE == "paper"
and settings.PAPER_OVERSEAS_CASH > 0
):
if total_cash <= 0 and settings.MODE == "paper" and settings.PAPER_OVERSEAS_CASH > 0:
total_cash = settings.PAPER_OVERSEAS_CASH
# Capture the day's opening portfolio value on the first market processed
@@ -2856,13 +2813,17 @@ async def run_daily_session(
# Evaluate scenarios for each stock (local, no API calls)
logger.info(
"Evaluating %d stocks against playbook for %s",
len(stocks_data), market.name,
len(stocks_data),
market.name,
)
for stock_data in stocks_data:
stock_code = stock_data["stock_code"]
stock_playbook = playbook.get_stock_playbook(stock_code)
match = scenario_engine.evaluate(
playbook, stock_code, stock_data, portfolio_data,
playbook,
stock_code,
stock_data,
portfolio_data,
)
decision = TradeDecision(
action=match.action.value,
@@ -2969,9 +2930,13 @@ async def run_daily_session(
stock_playbook=stock_playbook,
settings=settings,
)
if daily_open and decision.action == "HOLD" and _should_force_exit_for_overnight(
market=market,
settings=settings,
if (
daily_open
and decision.action == "HOLD"
and _should_force_exit_for_overnight(
market=market,
settings=settings,
)
):
decision = TradeDecision(
action="SELL",
@@ -3063,16 +3028,21 @@ async def run_daily_session(
)
continue
order_amount = stock_data["current_price"] * quantity
fx_blocked, remaining_cash, required_buffer = _should_block_overseas_buy_for_fx_buffer(
market=market,
action=decision.action,
total_cash=total_cash,
order_amount=order_amount,
settings=settings,
fx_blocked, remaining_cash, required_buffer = (
_should_block_overseas_buy_for_fx_buffer(
market=market,
action=decision.action,
total_cash=total_cash,
order_amount=order_amount,
settings=settings,
)
)
if fx_blocked:
logger.warning(
"Skip BUY %s (%s): FX buffer guard (remaining=%.2f, required=%.2f, cash=%.2f, order=%.2f)",
(
"Skip BUY %s (%s): FX buffer guard "
"(remaining=%.2f, required=%.2f, cash=%.2f, order=%.2f)"
),
stock_code,
market.name,
remaining_cash,
@@ -3090,7 +3060,10 @@ async def run_daily_session(
if now < daily_cooldown_until:
remaining = int(daily_cooldown_until - now)
logger.info(
"Skip BUY %s (%s): insufficient-balance cooldown active (%ds remaining)",
(
"Skip BUY %s (%s): insufficient-balance cooldown active "
"(%ds remaining)"
),
stock_code,
market.name,
remaining,
@@ -3149,13 +3122,9 @@ async def run_daily_session(
# Use limit orders (지정가) for domestic stocks.
# KRX tick rounding applied via kr_round_down.
if decision.action == "BUY":
order_price = kr_round_down(
stock_data["current_price"] * 1.002
)
order_price = kr_round_down(stock_data["current_price"] * 1.002)
else:
order_price = kr_round_down(
stock_data["current_price"] * 0.998
)
order_price = kr_round_down(stock_data["current_price"] * 0.998)
try:
validate_order_policy(
market=market,
@@ -3260,9 +3229,7 @@ async def run_daily_session(
except Exception as exc:
logger.warning("Telegram notification failed: %s", exc)
except Exception as exc:
logger.error(
"Order execution failed for %s: %s", stock_code, exc
)
logger.error("Order execution failed for %s: %s", stock_code, exc)
continue
if decision.action == "SELL" and order_succeeded:
@@ -3286,7 +3253,9 @@ async def run_daily_session(
accuracy=1 if trade_pnl > 0 else 0,
)
if trade_pnl < 0:
cooldown_key = _stoploss_cooldown_key(market=market, stock_code=stock_code)
cooldown_key = _stoploss_cooldown_key(
market=market, stock_code=stock_code
)
cooldown_minutes = _stoploss_cooldown_minutes(
settings,
market=market,
@@ -3369,7 +3338,8 @@ async def _handle_market_close(
def _run_context_scheduler(
scheduler: ContextScheduler, now: datetime | None = None,
scheduler: ContextScheduler,
now: datetime | None = None,
) -> None:
"""Run periodic context scheduler tasks and log when anything executes."""
result = scheduler.run_if_due(now=now)
@@ -3438,6 +3408,7 @@ def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
# reported synchronously (avoids the misleading "started" → "failed" log pair).
try:
import uvicorn # noqa: F401
from src.dashboard import create_dashboard_app # noqa: F401
except ImportError as exc:
logger.warning("Dashboard server unavailable (missing dependency): %s", exc)
@@ -3446,6 +3417,7 @@ def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
def _serve() -> None:
try:
import uvicorn
from src.dashboard import create_dashboard_app
app = create_dashboard_app(settings.DB_PATH, mode=settings.MODE)
@@ -3586,8 +3558,7 @@ async def run(settings: Settings) -> None:
pause_trading.set()
logger.info("Trading resumed via Telegram command")
await telegram.send_message(
"<b>▶️ Trading Resumed</b>\n\n"
"Trading operations have been restarted."
"<b>▶️ Trading Resumed</b>\n\nTrading operations have been restarted."
)
async def handle_status() -> None:
@@ -3630,9 +3601,7 @@ async def run(settings: Settings) -> None:
except Exception as exc:
logger.error("Error in /status handler: %s", exc)
await telegram.send_message(
"<b>⚠️ Error</b>\n\nFailed to retrieve trading status."
)
await telegram.send_message("<b>⚠️ Error</b>\n\nFailed to retrieve trading status.")
async def handle_positions() -> None:
"""Handle /positions command - show account summary."""
@@ -3643,8 +3612,7 @@ async def run(settings: Settings) -> None:
if not output2:
await telegram.send_message(
"<b>💼 Account Summary</b>\n\n"
"No balance information available."
"<b>💼 Account Summary</b>\n\nNo balance information available."
)
return
@@ -3673,9 +3641,7 @@ async def run(settings: Settings) -> None:
except Exception as exc:
logger.error("Error in /positions handler: %s", exc)
await telegram.send_message(
"<b>⚠️ Error</b>\n\nFailed to retrieve positions."
)
await telegram.send_message("<b>⚠️ Error</b>\n\nFailed to retrieve positions.")
async def handle_report() -> None:
"""Handle /report command - show daily summary metrics."""
@@ -3719,9 +3685,7 @@ async def run(settings: Settings) -> None:
)
except Exception as exc:
logger.error("Error in /report handler: %s", exc)
await telegram.send_message(
"<b>⚠️ Error</b>\n\nFailed to generate daily report."
)
await telegram.send_message("<b>⚠️ Error</b>\n\nFailed to generate daily report.")
async def handle_scenarios() -> None:
"""Handle /scenarios command - show today's playbook scenarios."""
@@ -3770,9 +3734,7 @@ async def run(settings: Settings) -> None:
await telegram.send_message("\n".join(lines).strip())
except Exception as exc:
logger.error("Error in /scenarios handler: %s", exc)
await telegram.send_message(
"<b>⚠️ Error</b>\n\nFailed to retrieve scenarios."
)
await telegram.send_message("<b>⚠️ Error</b>\n\nFailed to retrieve scenarios.")
async def handle_review() -> None:
"""Handle /review command - show recent scorecards."""
@@ -3788,9 +3750,7 @@ async def run(settings: Settings) -> None:
).fetchall()
if not rows:
await telegram.send_message(
"<b>📝 Recent Reviews</b>\n\nNo scorecards available."
)
await telegram.send_message("<b>📝 Recent Reviews</b>\n\nNo scorecards available.")
return
lines = ["<b>📝 Recent Reviews</b>", ""]
@@ -3808,9 +3768,7 @@ async def run(settings: Settings) -> None:
await telegram.send_message("\n".join(lines))
except Exception as exc:
logger.error("Error in /review handler: %s", exc)
await telegram.send_message(
"<b>⚠️ Error</b>\n\nFailed to retrieve reviews."
)
await telegram.send_message("<b>⚠️ Error</b>\n\nFailed to retrieve reviews.")
async def handle_notify(args: list[str]) -> None:
"""Handle /notify [key] [on|off] — query or change notification filters."""
@@ -3845,8 +3803,7 @@ async def run(settings: Settings) -> None:
else:
valid = ", ".join(list(status.keys()) + ["all"])
await telegram.send_message(
f"❌ 알 수 없는 키: <code>{key}</code>\n"
f"유효한 키: {valid}"
f"❌ 알 수 없는 키: <code>{key}</code>\n유효한 키: {valid}"
)
return
@@ -3858,30 +3815,22 @@ async def run(settings: Settings) -> None:
value = toggle == "on"
if telegram.set_notification(key, value):
icon = "" if value else ""
label = f"전체 알림" if key == "all" else f"<code>{key}</code> 알림"
label = "전체 알림" if key == "all" else f"<code>{key}</code> 알림"
state = "켜짐" if value else "꺼짐"
await telegram.send_message(f"{icon} {label}{state}")
logger.info("Notification filter changed via Telegram: %s=%s", key, value)
else:
valid = ", ".join(list(telegram.filter_status().keys()) + ["all"])
await telegram.send_message(
f"❌ 알 수 없는 키: <code>{key}</code>\n"
f"유효한 키: {valid}"
)
await telegram.send_message(f"❌ 알 수 없는 키: <code>{key}</code>\n유효한 키: {valid}")
async def handle_dashboard() -> None:
"""Handle /dashboard command - show dashboard URL if enabled."""
if not settings.DASHBOARD_ENABLED:
await telegram.send_message(
"<b>🖥️ Dashboard</b>\n\nDashboard is not enabled."
)
await telegram.send_message("<b>🖥️ Dashboard</b>\n\nDashboard is not enabled.")
return
url = f"http://{settings.DASHBOARD_HOST}:{settings.DASHBOARD_PORT}"
await telegram.send_message(
"<b>🖥️ Dashboard</b>\n\n"
f"<b>URL:</b> {url}"
)
await telegram.send_message(f"<b>🖥️ Dashboard</b>\n\n<b>URL:</b> {url}")
command_handler.register_command("help", handle_help)
command_handler.register_command("stop", handle_stop)
@@ -4182,9 +4131,7 @@ async def run(settings: Settings) -> None:
)
# Store candidates per market for selection context logging
scan_candidates[market.code] = {
c.stock_code: c for c in candidates
}
scan_candidates[market.code] = {c.stock_code: c for c in candidates}
logger.info(
"Smart Scanner: Found %d candidates for %s: %s",
@@ -4194,9 +4141,7 @@ async def run(settings: Settings) -> None:
)
# Get market-local date for playbook keying
market_today = datetime.now(
market.timezone
).date()
market_today = datetime.now(market.timezone).date()
# Load or generate playbook (1 Gemini call per market per day)
if market.code not in playbooks:
@@ -4234,7 +4179,8 @@ async def run(settings: Settings) -> None:
except Exception as exc:
logger.error(
"Playbook generation failed for %s: %s",
market.code, exc,
market.code,
exc,
)
try:
await telegram.notify_playbook_failed(
@@ -4279,7 +4225,8 @@ async def run(settings: Settings) -> None:
except Exception as exc:
logger.warning(
"Failed to fetch holdings for %s: %s — skipping holdings merge",
market.name, exc,
market.name,
exc,
)
held_codes = []
@@ -4288,7 +4235,8 @@ async def run(settings: Settings) -> None:
if extra_held:
logger.info(
"Holdings added to loop for %s (not in scanner): %s",
market.name, extra_held,
market.name,
extra_held,
)
if not stock_codes: