Compare commits

..

7 Commits

Author SHA1 Message Date
f58d42fdb0 Merge pull request 'fix: parse_response missing fields 시 raw 보존으로 플레이북 생성 복구 (#245)' (#246) from feature/issue-245-parse-response-preserve-raw into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #246
2026-02-25 01:33:34 +09:00
agentson
0b20251de0 fix: parse_response에서 missing fields 시 raw 텍스트 보존 (#245)
Some checks failed
CI / test (pull_request) Has been cancelled
pre_market_planner는 prompt_override로 Gemini에 플레이북 JSON을 요청한다.
Gemini가 플레이북 JSON을 반환해도 parse_response가 action/confidence/rationale 키가
없다는 이유로 rationale="Missing required fields"를 반환해 실제 응답이 버려졌다.

이로 인해 플레이북 생성이 항상 실패하고 RSI 기반 기본 폴백이 사용됐으며,
RSI가 없는 해외 시장 데이터와 매칭되지 않아 모든 결정이 HOLD(confidence=0)였다.

수정: missing fields 시 rationale=raw로 설정해 실제 Gemini 응답을 보존한다.
pre_market_planner가 decision.rationale에서 플레이북 JSON을 추출하여 정상 파싱 가능.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:31:54 +09:00
bffe6e9288 Merge pull request 'fix: Gemini compressed prompt 키 불일치 및 해외 스캐너 GUBN=0 수정 (#242, #243)' (#244) from feature/issue-242-243-gemini-key-fix-overseas-scanner into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #244
2026-02-25 01:18:41 +09:00
agentson
0146d1bf8a fix: Gemini compressed prompt 키 불일치 및 해외 스캐너 GUBN=0 수정 (#242, #243)
Some checks failed
CI / test (pull_request) Has been cancelled
- prompt_optimizer: build_compressed_prompt의 JSON 키를 act/conf/reason에서
  action/confidence/rationale로 수정 (parse_response와 일치시킴)
  → Gemini 응답 100% HOLD로 처리되던 버그 수정
- overseas: fetch_overseas_rankings의 GUBN 파라미터를 1(상승)에서 0(전체)으로 변경
  → 변동성 스캐너가 상승/하락 모두 대상으로 NASDAQ 후보 발견 가능
- test: GUBN==0 검증, build_compressed_prompt 키 이름 검증 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:16:51 +09:00
497564e75c Merge pull request 'fix: KR 등락률순위 API 파라미터 오류 수정 — 스캐너 미동작 해결 (#240)' (#241) from feature/issue-240-kr-scanner-rank-param-fix into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #241
2026-02-24 09:18:11 +09:00
agentson
988a56c07c fix: KR 등락률순위 API 파라미터 오류 수정 — 스캐너 미동작 해결 (#240)
Some checks failed
CI / test (pull_request) Has been cancelled
실전 API가 fid_rank_sort_cls_code='0000'(4자리)를 거부함.
'0'(1자리)으로 수정하고, 실전 응답의 종목코드 키가
mksc_shrn_iscd 대신 stck_shrn_iscd임을 반영하여 파싱 수정.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 09:15:40 +09:00
c9f1345e3c Merge pull request 'fix: 대시보드 mode 배지 os.getenv 대신 settings.MODE 사용 (#237)' (#239) from feature/issue-237-dashboard-mode-badge-fix into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #239
2026-02-24 06:52:29 +09:00
8 changed files with 49 additions and 8 deletions

View File

@@ -346,8 +346,10 @@ class GeminiClient:
# Validate required fields
if not all(k in data for k in ("action", "confidence", "rationale")):
logger.warning("Missing fields in Gemini response — defaulting to HOLD")
# Preserve raw text in rationale so prompt_override callers (e.g. pre_market_planner)
# can extract their own JSON format from decision.rationale (#245)
return TradeDecision(
action="HOLD", confidence=0, rationale="Missing required fields"
action="HOLD", confidence=0, rationale=raw
)
action = str(data["action"]).upper()

View File

@@ -179,8 +179,8 @@ class PromptOptimizer:
# Minimal instructions
prompt = (
f"{market_name} trader. Analyze:\n{data_str}\n\n"
'Return JSON: {"act":"BUY"|"SELL"|"HOLD","conf":<0-100>,"reason":"<text>"}\n'
"Rules: act=BUY/SELL/HOLD, conf=0-100, reason=concise. No markdown."
'Return JSON: {"action":"BUY"|"SELL"|"HOLD","confidence":<0-100>,"rationale":"<text>"}\n'
"Rules: action=BUY/SELL/HOLD, confidence=0-100, rationale=concise. No markdown."
)
else:
# Data only (for cached contexts where instructions are known)

View File

@@ -430,7 +430,7 @@ class KISBroker:
"fid_cond_mrkt_div_code": "J",
"fid_cond_scr_div_code": "20170",
"fid_input_iscd": "0000",
"fid_rank_sort_cls_code": "0000",
"fid_rank_sort_cls_code": "0",
"fid_input_cnt_1": str(limit),
"fid_prc_cls_code": "0",
"fid_input_price_1": "0",
@@ -466,7 +466,7 @@ class KISBroker:
rankings = []
for item in data.get("output", [])[:limit]:
rankings.append({
"stock_code": item.get("mksc_shrn_iscd", ""),
"stock_code": item.get("stck_shrn_iscd") or item.get("mksc_shrn_iscd", ""),
"name": item.get("hts_kor_isnm", ""),
"price": _safe_float(item.get("stck_prpr", "0")),
"volume": _safe_float(item.get("acml_vol", "0")),

View File

@@ -133,7 +133,7 @@ class OverseasBroker:
"AUTH": "",
"EXCD": ranking_excd,
"NDAY": "0",
"GUBN": "1",
"GUBN": "0", # 0=전체(상승+하락), 1=상승만 — 변동성 스캐너는 전체 필요
"VOL_RANG": "0",
}

View File

@@ -93,9 +93,21 @@ class TestMalformedJsonHandling:
def test_json_with_missing_fields_returns_hold(self, settings):
client = GeminiClient(settings)
decision = client.parse_response('{"action": "BUY"}')
raw = '{"action": "BUY"}'
decision = client.parse_response(raw)
assert decision.action == "HOLD"
assert decision.confidence == 0
# rationale preserves raw so prompt_override callers (e.g. pre_market_planner)
# can extract non-TradeDecision JSON from decision.rationale (#245)
assert decision.rationale == raw
def test_non_trade_decision_json_preserves_raw_in_rationale(self, settings):
"""Playbook JSON (no action/confidence/rationale) must be preserved for planner."""
client = GeminiClient(settings)
playbook_json = '{"market_outlook": "neutral", "stocks": []}'
decision = client.parse_response(playbook_json)
assert decision.action == "HOLD"
assert decision.rationale == playbook_json
def test_json_with_invalid_action_returns_hold(self, settings):
client = GeminiClient(settings)

View File

@@ -354,6 +354,8 @@ class TestFetchMarketRankings:
assert "ranking/fluctuation" in url
assert headers.get("tr_id") == "FHPST01700000"
assert params.get("fid_cond_scr_div_code") == "20170"
# 실전 API는 4자리("0000") 거부 — 1자리("0")여야 한다 (#240)
assert params.get("fid_rank_sort_cls_code") == "0"
@pytest.mark.asyncio
async def test_volume_returns_parsed_rows(self, broker: KISBroker) -> None:
@@ -376,6 +378,27 @@ class TestFetchMarketRankings:
assert result[0]["price"] == 75000.0
assert result[0]["change_rate"] == 2.5
@pytest.mark.asyncio
async def test_fluctuation_parses_stck_shrn_iscd(self, broker: KISBroker) -> None:
"""실전 API는 mksc_shrn_iscd 대신 stck_shrn_iscd를 반환한다 (#240)."""
items = [
{
"stck_shrn_iscd": "015260",
"hts_kor_isnm": "에이엔피",
"stck_prpr": "794",
"acml_vol": "4896196",
"prdy_ctrt": "29.74",
"vol_inrt": "0",
}
]
mock_resp = _make_ranking_mock(items)
with patch("aiohttp.ClientSession.get", return_value=mock_resp):
result = await broker.fetch_market_rankings(ranking_type="fluctuation")
assert len(result) == 1
assert result[0]["stock_code"] == "015260"
assert result[0]["change_rate"] == 29.74
# ---------------------------------------------------------------------------
# KRX tick unit / round-down helpers (issue #157)

View File

@@ -124,7 +124,7 @@ class TestFetchOverseasRankings:
assert "/uapi/overseas-stock/v1/ranking/updown-rate" in url
assert params["EXCD"] == "NAS"
assert params["NDAY"] == "0"
assert params["GUBN"] == "1"
assert params["GUBN"] == "0" # 0=전체(상승+하락), 변동성 스캐너에 필요
assert params["VOL_RANG"] == "0"
overseas_broker._broker._auth_headers.assert_called_with("HHDFS76290000")

View File

@@ -124,6 +124,10 @@ class TestPromptOptimizer:
assert len(prompt) < 300
assert "005930" in prompt
assert "75000" in prompt
# Keys must match parse_response expectations (#242)
assert '"action"' in prompt
assert '"confidence"' in prompt
assert '"rationale"' in prompt
def test_build_compressed_prompt_no_instructions(self):
"""Test compressed prompt without instructions."""