**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 bugs found by comparing against KIS official GitHub examples:
1. FID_COND_SCR_DIV_CODE: "20001" → "20171" (volume-rank screen code)
2. FID_TRGT_EXLS_CLS_CODE: "000000" (6-digit) → "0000000000" (10-digit)
3. fluctuation ranking:
- TR_ID: "FHPST01710100" (invalid) → "FHPST01700000"
- path: /quotations/volume-rank → /ranking/fluctuation
- params: volume-rank params → lowercase fluctuation-specific params
(fid_rank_sort_cls_code, fid_input_cnt_1, fid_prc_cls_code,
fid_rsfl_rate1, fid_rsfl_rate2, etc.)
Note: VTS (paper trading) does not return data from ranking APIs regardless
of parameter correctness — this is a KIS policy restriction, not a code bug.
These fixes ensure correct behavior when switching to a live account.
Tests: TestFetchMarketRankings (3 tests) added to test_broker.py
Closes#155
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>
- Apply _PRICE_EXCHANGE_MAP in get_overseas_price() to send short codes
(NASD→NAS, NYSE→NYS, AMEX→AMS) required by HHDFS00000300 price API
- Add PAPER_OVERSEAS_CASH config setting (default $50,000) for simulated
USD balance when VTS overseas balance API returns 0 in paper mode
- Fall back to scan candidate price when live price API returns 0
- Both fixes together resolve "no affordable quantity (cash=0, price=0)"
which was preventing all overseas trade execution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The overseas ranking API was returning 404 for all exchanges because the
TR_IDs, API paths, and exchange codes were all incorrect. Updated to match
KIS official API documentation:
- TR_ID: HHDFS76290000 (updown-rate), HHDFS76270000 (volume-surge)
- Path: /uapi/overseas-stock/v1/ranking/{updown-rate,volume-surge}
- Exchange codes: NASD→NAS, NYSE→NYS, AMEX→AMS via ranking-specific mapping
Fixes#141
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause analysis revealed 3 critical issues causing EGW00201 errors:
1. **Hash key bypass** - _get_hash_key() made API calls without rate limiting
- Every order made 2 API calls but only 1 was rate-limited
- Fixed by adding rate_limiter.acquire() to _get_hash_key()
2. **Scanner concurrent burst** - scan_market() launched all stocks via asyncio.gather
- All tasks queued simultaneously creating burst pressure
- Fixed by adding Semaphore(1) for fully serialized scanning
3. **RPS too aggressive** - 5.0 RPS exceeded KIS API's real ~2 RPS limit
- Lowered to 2.0 RPS (500ms interval) for maximum safety
Changes:
- src/broker/kis_api.py: Add rate limiter to _get_hash_key()
- src/analysis/scanner.py: Add semaphore-based concurrency control
- New max_concurrent_scans parameter (default 1, fully serialized)
- Wrap scan_stock calls with semaphore in _bounded_scan()
- Remove ineffective asyncio.sleep(0.2) from scan_stock()
- src/config.py: Lower RATE_LIMIT_RPS from 5.0 to 2.0
- tests/test_broker.py: Add 2 tests for hash key rate limiting
- tests/test_volatility.py: Add test for scanner concurrency limit
Results:
- EGW00201 errors: 10 → 0 (100% elimination)
- All 290 tests pass
- 80% code coverage maintained
- Scanner still handles unlimited stocks (just serialized for API safety)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add asyncio.Lock to prevent multiple coroutines from simultaneously
refreshing the KIS access token, which hits the 1-per-minute rate
limit (EGW00133: "접근토큰 발급 잠시 후 다시 시도하세요").
Changes:
- Add self._token_lock in KISBroker.__init__
- Wrap token refresh in async with self._token_lock
- Re-check token validity after acquiring lock (double-check pattern)
- Add concurrent token refresh test (5 parallel requests → 1 API call)
The lock ensures that when multiple coroutines detect an expired token,
only the first one refreshes while others wait and reuse the result.
Fixes: #42
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implement comprehensive multi-market trading system with automatic
market selection based on timezone and trading hours.
## New Features
- Market schedule module with 10 global markets (KR, US, JP, HK, CN, VN)
- Overseas broker for KIS API international stock trading
- Automatic market detection based on current time and timezone
- Next market open waiting logic when all markets closed
- ConnectionError retry with exponential backoff (max 3 attempts)
## Architecture Changes
- Market-aware trading cycle with domestic/overseas broker routing
- Market context in AI prompts for better decision making
- Database schema extended with market and exchange_code columns
- Config setting ENABLED_MARKETS for market selection
## Testing
- 19 new tests for market schedule (timezone, DST, lunch breaks)
- All 54 tests passing
- Lint fixes with ruff
## Files Added
- src/markets/schedule.py - Market schedule and timezone logic
- src/broker/overseas.py - KIS overseas stock API client
- tests/test_market_schedule.py - Market schedule test suite
## Files Modified
- src/main.py - Multi-market main loop with retry logic
- src/config.py - ENABLED_MARKETS setting
- src/db.py - market/exchange_code columns with migration
- src/brain/gemini_client.py - Dynamic market context in prompts
Resolves#5
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The KIS virtual trading server (openapivts.koreainvestment.com) serves a
certificate with a hostname mismatch. Disable SSL hostname verification
only for the VTS endpoint via a custom TCPConnector; production endpoints
remain fully verified.
Closes#1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>