146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
# TradingAgents/graph/signal_processing.py
|
|
|
|
import re
|
|
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
|
|
class SignalProcessor:
|
|
"""Processes trading signals to extract actionable decisions."""
|
|
|
|
def __init__(self, quick_thinking_llm: ChatOpenAI):
|
|
"""Initialize with an LLM for processing."""
|
|
self.quick_thinking_llm = quick_thinking_llm
|
|
|
|
def process_signal(self, full_signal: str) -> str:
|
|
"""
|
|
Process a full trading signal to extract the core decision.
|
|
|
|
Args:
|
|
full_signal: Complete trading signal text
|
|
|
|
Returns:
|
|
Extracted decision (BUY or PASS) - legacy mode
|
|
"""
|
|
messages = [
|
|
(
|
|
"system",
|
|
"너는 애널리스트 리포트에서 최종 투자 결론만 추출하는 파서다. 한국어/영어 혼합 문서도 처리한다. 우선순위는 (1) 마지막 `FINAL TRANSACTION PROPOSAL: **BUY/PASS**` 표기, (2) 명시적 최종 권고 문장이다. 출력은 반드시 BUY 또는 PASS 한 단어만 반환하고, 다른 문구는 절대 출력하지 마라.",
|
|
),
|
|
("human", full_signal),
|
|
]
|
|
|
|
return self.quick_thinking_llm.invoke(messages).content
|
|
|
|
def process_swing_signal(self, full_signal: str) -> dict:
|
|
"""
|
|
Process a swing trading signal to extract decision + order parameters.
|
|
|
|
Args:
|
|
full_signal: Complete trading signal text with SWING_ORDER block
|
|
|
|
Returns:
|
|
Dict with action, entry_price, stop_loss, take_profit,
|
|
position_size_pct, max_hold_days, rationale
|
|
"""
|
|
result = {
|
|
"action": "PASS",
|
|
"entry_price": None,
|
|
"stop_loss": None,
|
|
"take_profit": None,
|
|
"position_size_pct": None,
|
|
"max_hold_days": None,
|
|
"rationale": "",
|
|
}
|
|
|
|
# Try to extract SWING_ORDER block first
|
|
order_block = self._extract_swing_order(full_signal)
|
|
if order_block:
|
|
result.update(order_block)
|
|
else:
|
|
# Fallback: use LLM to extract
|
|
result = self._llm_extract_swing_signal(full_signal)
|
|
|
|
# Validate action
|
|
if result["action"] not in ("BUY", "SELL", "HOLD", "PASS"):
|
|
result["action"] = "PASS"
|
|
|
|
return result
|
|
|
|
def _extract_swing_order(self, text: str) -> dict | None:
|
|
"""Extract structured SWING_ORDER block from text using regex."""
|
|
pattern = r"SWING_ORDER:\s*\n(.*?)(?:\n```|\Z)"
|
|
match = re.search(pattern, text, re.DOTALL)
|
|
if not match:
|
|
# Try without code block
|
|
pattern = r"SWING_ORDER:\s*\n((?:\s+\w+:.*\n?)+)"
|
|
match = re.search(pattern, text, re.DOTALL)
|
|
if not match:
|
|
return None
|
|
|
|
block = match.group(1)
|
|
result = {}
|
|
|
|
field_map = {
|
|
"ACTION": ("action", str),
|
|
"ENTRY_PRICE": ("entry_price", float),
|
|
"STOP_LOSS": ("stop_loss", float),
|
|
"TAKE_PROFIT": ("take_profit", float),
|
|
"POSITION_SIZE_PCT": ("position_size_pct", float),
|
|
"MAX_HOLD_DAYS": ("max_hold_days", int),
|
|
"RATIONALE": ("rationale", str),
|
|
}
|
|
|
|
for key, (field_name, converter) in field_map.items():
|
|
field_pattern = rf"{key}:\s*(.+?)(?:\n|$)"
|
|
field_match = re.search(field_pattern, block)
|
|
if field_match:
|
|
raw_value = field_match.group(1).strip()
|
|
try:
|
|
if converter in (float, int):
|
|
# Remove commas and currency symbols
|
|
cleaned = re.sub(r"[^\d.\-]", "", raw_value)
|
|
if cleaned:
|
|
result[field_name] = converter(cleaned)
|
|
else:
|
|
result[field_name] = raw_value
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return result if "action" in result else None
|
|
|
|
def _llm_extract_swing_signal(self, full_signal: str) -> dict:
|
|
"""Fallback: use LLM to extract swing trading signal."""
|
|
messages = [
|
|
(
|
|
"system",
|
|
"""너는 스윙 트레이딩 리포트에서 최종 투자 결론과 주문 정보를 추출하는 파서다.
|
|
반드시 아래 JSON 형식만 출력하라. 다른 텍스트는 절대 출력하지 마라.
|
|
|
|
{"action": "BUY|SELL|HOLD|PASS", "entry_price": 숫자|null, "stop_loss": 숫자|null, "take_profit": 숫자|null, "position_size_pct": 숫자|null, "max_hold_days": 숫자|null, "rationale": "한 줄 요약"}""",
|
|
),
|
|
("human", full_signal),
|
|
]
|
|
|
|
import json
|
|
|
|
response = self.quick_thinking_llm.invoke(messages).content
|
|
try:
|
|
return json.loads(response)
|
|
except json.JSONDecodeError:
|
|
# Last resort: extract action only
|
|
action = "PASS"
|
|
for keyword in ("BUY", "SELL", "HOLD", "PASS"):
|
|
if keyword in response.upper():
|
|
action = keyword
|
|
break
|
|
return {
|
|
"action": action,
|
|
"entry_price": None,
|
|
"stop_loss": None,
|
|
"take_profit": None,
|
|
"position_size_pct": None,
|
|
"max_hold_days": None,
|
|
"rationale": "",
|
|
}
|