- Import MarketOutlook at module level in main.py
- After scenario evaluation, check market_outlook and apply BUY confidence
threshold: BEARISH→90, BULLISH→75, others→settings.CONFIDENCE_THRESHOLD
- BUY actions below the adjusted threshold are downgraded to HOLD with
a descriptive rationale including the outlook and threshold values
- Add 5 integration tests covering bearish suppression, bearish allow,
bullish allow, bullish suppression, and neutral default threshold
Closes#173
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add playbook_allocation_pct and scenario_confidence parameters to
_determine_order_quantity() with playbook-based sizing taking priority
over volatility-score fallback when provided
- Confidence scaling: confidence/80 multiplier (confidence 96 → 1.2x)
clipped to [POSITION_MIN_ALLOCATION_PCT, POSITION_MAX_ALLOCATION_PCT]
- Pass matched_scenario.allocation_pct and match.confidence from
trading_cycle so AI's allocation decisions reach order execution
- Add 4 new tests: playbook priority, confidence scaling, max clamp,
and fallback behavior
Closes#172
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DB의 주문 수량 기록은 실제 체결 수량과 다를 수 있음(부분 체결, 외부 수동 거래).
브로커 잔고 API(output1)를 source of truth로 사용하도록 수정.
## 변경 사항
### SELL 수량 (#164)
- _extract_held_qty_from_balance() 추가
- 국내: output1의 ord_psbl_qty (→ hldg_qty fallback)
- 해외: output1의 ovrs_cblc_qty (→ hldg_qty fallback)
- _determine_order_quantity()에 broker_held_qty 파라미터 추가
- SELL 시 broker_held_qty 반환 (0이면 주문 스킵)
- trading_cycle / run_daily_session 양쪽 호출 지점 수정
- 이미 fetch된 balance_data에서 수량 추출 (추가 API 호출 없음)
### 보유 종목 루프 (#165)
- _extract_held_codes_from_balance() 추가
- ord_psbl_qty > 0인 종목 코드 목록 반환
- 실시간 루프에서 스캔 시점에 get_balance() 호출해 보유 종목 병합
- 스캐너 후보 + 실제 보유 종목 union으로 trading_cycle 순회
- 실패 시 경고 로그 후 스캐너 후보만으로 계속 진행
### 테스트
- TestExtractHeldQtyFromBalance: 7개 (국내/해외/fallback/미보유)
- TestExtractHeldCodesFromBalance: 4개 (qty>0 포함, qty=0 제외 등)
- TestDetermineOrderQuantity: 5개 (SELL qty, BUY sizing)
- test_sell_order_uses_broker_balance_qty_not_db:
DB 10주 기록 vs 브로커 5주 확인 → 브로커 값(5) 사용 검증
- 기존 SELL/stop-loss/take-profit 테스트에 output1 mock 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HOLD 판정 후 보유 포지션에 대해 stop_loss와 함께 take_profit도 체크하도록 수정.
AI가 생성한 take_profit_pct가 실제 거래 로직에 반영되지 않던 구조적 결함 수정.
- HOLD 블록에서 loss_pct >= take_profit_threshold 조건 추가
- stop_loss와 상호 배타적으로 동작 (stop_loss 우선 체크)
- take_profit 기본값 3.0% (playbook 없는 경우 적용)
- 테스트 2개 추가:
- test_hold_overridden_to_sell_when_take_profit_triggered
- test_hold_not_overridden_when_between_stop_loss_and_take_profit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**Problem 1 — Current price always 0**
get_orderbook() used inquire-asking-price-exp-ccn which has no stck_prpr
in output1 (only askp/bidp data). This caused every domestic BUY to be
skipped with "no affordable quantity (cash=..., price=0.00)".
**Problem 2 — KRX tick unit error on limit orders**
Limit order prices were passed unrounded, triggering 호가단위 오류 in VTS.
Also ORD_DVSN was wrongly set to "01" (시장가) for limit orders.
**Fix**
- Add kr_tick_unit(price) and kr_round_down(price) module-level helpers
implementing KRX 7-tier price tick rules (1/5/10/50/100/500/1000원).
- Add get_current_price(stock_code) → (price, change_pct, foreigner_net)
using FHKST01010100 / inquire-price API (works in VTS, returns correct
stck_prpr, prdy_ctrt, frgn_ntby_qty).
- Fix send_order() ORD_DVSN: "00"=지정가, "01"=시장가 (was "01"/"06").
- Apply kr_round_down() to limit order price inside send_order().
- Replace both get_orderbook() calls in main.py with get_current_price().
- Update all 4 test_main.py mock sites to use get_current_price AsyncMock.
**Tests added** (25 new tests, all 646 pass)
- TestKrTickUnit: 13 parametrized boundary cases + 7 round-down cases
- TestGetCurrentPrice: correct fields, correct API path/TR_ID, HTTP error
- TestSendOrderTickRounding: tick rounding, ORD_DVSN 00/01
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes for overseas stock trading failures:
1. Price API exchange code mapping:
- get_overseas_price() now applies _PRICE_EXCHANGE_MAP (NASD→NAS, NYSE→NYS, AMEX→AMS)
- Price API HHDFS00000300 requires short exchange codes same as ranking API
2. rt_cd check in send_overseas_order():
- Log WARNING (not INFO) when rt_cd != "0" (e.g., "주문가능금액이 부족합니다")
- Caller (main.py) checks rt_cd == "0" before calling log_trade()
- Prevents DB from recording failed orders as successful trades
3. Limit order price premium for BUY:
- BUY limit price = current_price * 1.005 (0.5% premium)
- SELL limit price = current_price (no premium)
- Improves fill probability: KIS VTS only accepts limit orders,
and last price is typically at or below ask
4. PAPER_OVERSEAS_CASH fallback (config + main.py):
- New setting: PAPER_OVERSEAS_CASH = 50000.0 (USD)
- When VTS overseas balance API fails/returns 0, use this as simulated cash
- Applied in both trading_cycle() and run_daily_session()
5. Candidate price fallback:
- If price API returns 0, use scanner candidate price as fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KIS VTS (paper trading) rejects overseas market orders with:
"모의투자 주문처리가 안되었습니다(지정가만 가능한 상품입니다)"
Root cause: send_overseas_order() was called with price=0.0 (market order)
in both trading_cycle() and run_daily_session(), even though current_price
was already computed correctly by Fix#147 (exchange code mapping).
Fix: pass current_price as the limit order price in both call sites.
Domestic broker send_order() keeps price=0 (market orders are fine on KRX).
Adds regression test TestOverseasBalanceParsing::test_overseas_buy_order_uses_limit_price
verifying price=182.5 is passed, not 0.0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MARKET_SHORTHAND + expand_market_codes()로 config "US" → schedule "US_NASDAQ/NYSE/AMEX" 자동 확장
- /report, /scenarios, /review, /dashboard 텔레그램 명령 추가
- price_change_pct를 trading_cycle과 run_daily_session에 주입
- HOLD시 get_open_position 기반 손절 모니터링 및 자동 SELL 오버라이드
- 대시보드 /api/status 동적 market 조회로 변경
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add --dashboard CLI flag and DASHBOARD_ENABLED env var to start
FastAPI dashboard in a daemon thread alongside the trading loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap evolution notification in try/except so telegram failures don't
crash the evolution loop. Add integration tests for market close flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run EvolutionOptimizer.evolve() at US market close, skip for other
markets, and notify via Telegram when a strategy PR is generated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up periodic context rollups (weekly/monthly/quarterly/annual/legacy)
in both daily and realtime trading loops with dedup-safe scheduling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract _handle_market_close() helper that runs EOD aggregation,
generates scorecard with optional AI lessons, and sends Telegram summary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add decision_id column to trades table, capture log_decision() return
value, and update original BUY decision outcome on SELL execution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add L7_REALTIME writes in trading_cycle() for volatility, price, rsi, volume_ratio
- Normalize key format to {metric}_{market}_{stock_code} across scanner and main
- Fix existing key mismatch between scanner writes and main reads
- Remove unused MarketScanner dead code
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address PR #110 review findings:
1. High — Realtime mode now loads playbook from DB before calling Gemini,
preventing duplicate API calls on process restart (4/day budget).
2. Medium — Pass market-local date (via market.timezone) to
generate_playbook() and _empty_playbook() instead of date.today().
3. Medium — scan_candidates restructured from {stock_code: candidate}
to {market_code: {stock_code: candidate}} to prevent KR/US symbol
collision.
New test: test_scan_candidates_market_scoped verifies cross-market
isolation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unused imports (sys, ScenarioMatch, asyncio, StockPlaybook),
fix import ordering, and split long lines for ruff compliance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace brain.decide() with scenario_engine.evaluate() in trading_cycle
and brain.decide_batch() with per-stock scenario evaluation in
run_daily_session. Initialize PreMarketPlanner, ScenarioEngine, and
PlaybookStore in run(). Add pre-market playbook generation on market
open (1 Gemini call per market per day), market_data enrichment from
scanner metrics (rsi, volume_ratio), portfolio_data for global rules,
scenario match notifications, and playbook lifecycle management.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved conflict in src/main.py by using safe_float() from main
instead of float(...or '0') pattern.
Changes:
- src/main.py: Use safe_float() for consistent empty string handling
- All 16 tests pass including test_overseas_price_empty_string
Add safe_float() helper function to safely convert API response values
to float, handling empty strings, None, and invalid values that cause
ValueError: "could not convert string to float: ''".
Changes:
- Add safe_float() function in src/main.py with full docstring
- Replace all float() calls with safe_float() in trading_cycle()
- Domestic market: orderbook prices, balance amounts
- Overseas market: price data, balance info
- Add 6 comprehensive unit tests for safe_float()
The function handles:
- Empty strings ("") → default (0.0)
- None values → default (0.0)
- Invalid strings ("abc") → default (0.0)
- Valid strings ("123.45") → parsed float
- Float inputs (123.45) → pass through
This prevents crashes when KIS API returns empty strings during
market closed hours or data unavailability.
Fixes: #44
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add type checking for output2 response from get_overseas_balance API.
The API can return either list format [{}] or dict format {}, causing
KeyError when accessing output2[0].
Changes:
- Check isinstance before accessing output2[0]
- Handle list, dict, and empty cases
- Add safe fallback with "or" for empty strings
- Add 3 test cases for list/dict/empty formats
Fixes: #41
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Integrate Telegram notifications throughout the main trading loop to provide
real-time alerts for critical events and trading activities.
Changes:
- Add TelegramClient initialization in run() function
- Send system startup notification on agent start
- Send market open/close notifications when markets change state
- Send trade execution notifications for BUY/SELL orders
- Send fat finger rejection notifications when orders are blocked
- Send circuit breaker notifications when loss threshold is exceeded
- Pass telegram client to trading_cycle() function
- Add tests for all notification scenarios in test_main.py
All notifications wrapped in try/except to ensure trading continues even
if Telegram API fails. Notifications are non-blocking and do not affect
core trading logic.
Test coverage: 273 tests passed, overall coverage 79%
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>