들어가며 — "어떤 종목을 살까?"가 모든 문제의 시작이다
자동매매를 처음 만들면 누구나 똑같은 함정에 빠진다. 매매 로직, 손절선, 익절 비율을 먼저 고민하기 시작한다. 그런데 정작 "어떤 종목 풀에 이 로직을 적용할 것인가"를 빼먹는다.
종목 풀이 잘못되면 어떤 전략도 못 살린다. KOSPI/KOSDAQ 합쳐 2,500개가 넘는 종목 중에서 무작위로 10개를 골라 백테스트하면, 같은 vwap_ema 전략으로도 결과가 천차만별이다. 어떤 종목은 Sharpe 3.0, 어떤 종목은 -1.0. 종목 선정이 사실상 알파의 60~70%를 결정한다.
지난 6주 동안 한국 주식 자동매매 시스템을 만들면서 가장 많이 다듬은 부분도 결국 종목 선정 파이프라인이었다. 오늘은 이 4단 필터링이 어떻게 동작하는지, 그리고 각 단계가 왜 필요한지를 공유한다.
전체 파이프라인 — 2,500개에서 10개까지
KOSPI/KOSDAQ 전종목 (~2,500개)
│
▼ Filter 1: 재무 점수 (≥ 70점, 5-metric × 5분기)
│
재무 우량 종목 (~200개)
│
▼ Filter 2: 테마 분류 (Naver 테마 + YouTube AI)
│
테마 매칭 종목 (~50개)
│
▼ Filter 3: 매수 신호 4조건 + signal_score
│
신호 확정 종목 (~10개)
│
▼ ticker_agent: 차트 5지표 + 적정가 7지수 + 거래주체
│
운영 종목 (3~7개, anchor 합 100%)
각 단계는 앞 단계의 노이즈를 제거하는 역할을 한다. 한 단계라도 빼면 다음 단계가 노이즈에 휘말려 의미 없는 결과를 낸다. 거꾸로, 4단계를 모두 통과한 종목은 재무도 우량하고, 테마도 살아 있고, 차트 신호도 떴고, AI도 강세로 판단한 종목이다.
① Filter 1 — 재무 점수 (core/screening/scorer.py)
왜 재무 점수가 첫 번째인가
전략·신호·테마 다 좋아도 재무가 부실한 종목은 일주일에 30% 빠진다. 단기 트레이딩이라도 이 위험은 피해야 한다. 그래서 재무가 가장 첫 게이트다.
채점 방식 — 5 metric × 5분기
# core/screening/scorer.py 핵심
THRESHOLD_FINANCE_SCORE = 70 # config.yaml
METRICS = [
"ROE", # 자기자본이익률
"Operating Margin", # 영업이익률
"Net Profit Margin", # 순이익률
"PBR", # 주가순자산비율
"PER", # 주가수익비율
]
# 분기별 비교: Q-4 ~ Q0 (현재) 5개 분기
# 각 metric × 각 분기 = 25 cell
# cell 점수: 임계값 만족 → 4점, 보통 → 2점, 미달 → 0점
# Total: 0 ~ 100점
각 분기를 별도로 채점하는 이유는 한 분기만 잠깐 좋아진 종목을 거르기 위함이다. 5분기 연속 우량이어야 70점을 넘는다. 추세가 살아 있는 종목만 잡힌다.
실제 작동 — sysTrade 이식
이 채점 로직은 sysTrade 원본을 거의 그대로 이식했다. config.yaml에 임계값을 넣어 운영 중 튜닝 가능하게 만들었다.
# config.yaml
financial_score:
threshold: 70 # 70점 이상만 통과
metrics:
ROE: { good: 15, normal: 5 } # 단위 %
operating_margin: { good: 10, normal: 3 }
net_profit_margin: { good: 8, normal: 2 }
pbr: { good: 1.5, normal: 3.0 } # 낮을수록 good
per: { good: 15, normal: 25 }
run_finance_report.sh 실행 → output/reports/finance-report.html 생성 → 통과 200여 종목 추출.
② Filter 2 — 테마 분류 (core/screening/theme_filter.py)
왜 테마가 두 번째인가
한국 시장은 테마장이 강하다. 같은 재무 우량 종목이라도 시장 관심이 없는 섹터는 몇 달간 횡보한다. 반대로 테마에 편입된 종목은 짧게는 1주일, 길게는 한 달 안에 +20~50% 움직인다.
문제는 "테마"가 빠르게 변한다는 점이다. 작년에는 2차전지, 올해는 원전·AI 인프라. 한 달 전 인기 테마가 지금은 시들 수 있다. 그래서 실시간 테마 검출이 필요하다.
5가지 모드의 테마 필터
# core/screening/theme_filter.py
MODES = [
"etf_momentum", # ETF 가중합 모멘텀 (1M×40% + 3M×30% + 6M×20% + 1Y×10%)
"kis_mst", # KIS Master 데이터 + 업종 키워드 매칭
"youtube_ai", # YouTube 5채널 AI 분석 (Apify + GPT)
"naver", # Naver 공식 테마 (≈450개, 신설 2026-04-20)
"all", # 위 4개 합집합
]
Naver 테마 크롤러 — de-facto 표준 (2026-04-20 신설)
처음에는 LLM이 자유 텍스트로 sector를 생성하게 했다. "원전 인프라", "AI 에이전트" 등. 그런데 LLM이 같은 종목을 다른 이름으로 분류하거나, 한국 시장에서 통용되지 않는 용어를 만들어내는 문제가 발생했다.
해결: Naver 금융의 공식 테마(약 450개)를 단일 진실원으로 채택. 매일 08:45에 크롤링해서 naver_theme_cache + naver_theme_tickers 두 테이블에 저장. LLM은 이 whitelist 안에서만 분류하도록 prompt 제약.
# core/screening/naver_theme_crawler.py
def crawl_naver_themes():
"""매일 08:45 cron + 주 1회 일요일 20:00"""
themes = parse("https://finance.naver.com/sise/theme.naver")
for t in themes:
tickers = parse(f".../theme_detail?themeId={t.id}")
save_to_db(t, tickers)
diff = compare_with_yesterday()
if diff.new_themes:
send_telegram_alert(f"신규 테마 감지: {diff.new_themes}")
YouTube AI 분석 — 5개 채널 시황 자동 요약
screening/youtube_analyzer.py가 매일 5개 시황 채널의 최근 7일 영상을 자동 분석한다.
1. Apify로 transcript 추출 (karamelo~youtube-transcripts)
2. GPT-5.4 / Claude Sonnet으로 섹터·종목·stance 추출
3. SQLite 3일 TTL 캐시
4. 5채널 결과 집계 → 다수결로 strong sector 도출
실제 결과 (2026-04-17): Focus Now 채널 9개 섹터 도출, Watch List 채널 38개 섹터. 5채널 합집합 → 신뢰도 높은 8~10개 strong sector 추출.
③ Filter 3 — 매수 신호 (core/screening/signals.py)
4조건 + signal_score (0~4점)
재무 우량 + 테마 매칭까지 통과한 ~50개 중에서, 지금 매수해도 되는 종목만 추출하는 단계다.
# core/screening/signals.py
def compute_buy_signal(ticker: str) -> SignalScore:
score = 0
reasons = []
# 조건 1: 재무 우량 (이미 통과했지만 score에 반영)
if finance_score(ticker) >= 70:
score += 1
reasons.append("재무우량")
# 조건 2: 평가절하 (현재가 < 적정가 composite)
if current_price < fair_value_composite(ticker):
score += 1
reasons.append("평가절하")
# 조건 3: 거래량 급증 (당일 거래량 / 20일 평균 ≥ 2.0)
if volume_today / volume_20d_avg >= 2.0:
score += 1
reasons.append("거래량급증")
# 조건 4: 양봉 + MA 정배열 (close > MA5 > MA20 > MA60)
if is_bullish_alignment(ticker):
score += 1
reasons.append("양봉정배열")
return SignalScore(score=score, reasons=reasons)
signal_score ≥ 3 종목만 다음 단계로 넘어간다. 4점이면 강신호, 3점이면 보통 신호.
확장 — 프로그램 매수 서지 (2026-04-20 등록)
기존 4조건에 5번째를 추가하는 작업이 등록되어 있다.
# 조건 5: 프로그램 순매수 서지 (당일 / 20일 평균 ≥ 2.0)
if program_net_buy_today / program_net_buy_20d_avg >= 2.0:
score += 1
reasons.append("프로그램서지")
외인·기관의 프로그램 매매가 갑자기 늘어나는 종목은 펀더멘털 정보가 빠르게 반영되고 있다는 신호. 데이터는 KIS API program-trade-by-stock 또는 pykrx + KRX 공시.
④ ticker_agent의 차트 5지표 + 적정가 7지수 + 거래주체
Filter 1~3을 통과한 종목은 이제 ticker_agent(Phase B, 2026-05-15 완성)가 봉별로 평가한다.
차트 5지표 (core/agents/ticker_agent.py)
ticker_agent는 단일 종목의 차트만 본다. 포트폴리오 비중, 현금, max_position_pct 등은 일부러 입력에서 제거했다. 이유: ticker_agent가 다른 종목의 영향을 받아 판단이 흔들리는 것을 막기 위해.
입력 5지표:
① 적정주가 — 현재가 vs composite fair value
② 매집도 — OBV 추세 + 누적 거래대금
③ ADX — 추세 강도 (DI+ / DI- / ADX)
④ 상대 거래량 — 당일 / 20일 평균
⑤ 매물대 공백 — Volume Profile 빈 구간
출력 (BUY):
{ "decision": "BUY"|"HOLD",
"signal_strength": 0.0~1.0,
"reasoning": "한 문장",
"confidence": 0.0~1.0 }
적정 가격 지수 7개 (core/indicators/fair_value.py)
단일 metric으로는 평가가 어렵다. 7개를 composite로 묶었다.
#
지수
계산
1
PER 업종 비교
종목 PER vs 업종 평균 (할인율)
2
PBR 업종 비교
종목 PBR vs 업종 평균
3
단순 DCF
FCF × 5년 + 잔존가치 / WACC
4
기술적 지지·저항
최근 60일 거래대금 가중 매물대
5
수급 (flow)
외인·기관 5일 누적 순매수
6
옵션 IV
내재 변동성 → 시장의 예상 변동폭
7
업종 분위
같은 업종 종목들 중 상대 순위
+
뉴스 sentiment
scout_signals의 24h 평균 sentiment
7개를 가중 합산 → composite fair value 산출. ticker_agent prompt에 [C-1] 적정가 7지수 + composite 블록으로 주입.
거래주체 추적 (core/indicators/flow.py)
# 15:35 cron — 매일 종가 직후
def daily_flow_update():
for ticker in active_picks:
flow = pykrx.get_market_trading_value_by_investor(ticker)
# 외인 / 기관 / 개인 일일 net
save_to_db("flow_data", flow)
# ticker_agent input 에 [C-2] 블록으로 주입:
# 외인 5일 누적: +12.3억 (매수 우세)
# 기관 5일 누적: +5.7억
# 개인 5일 누적: -18.0억 (개미 매도)
# dominant: 외인+기관 / trend: 매집
외인·기관이 5일 연속 매수하고 개인이 매도하는 패턴은 펀더멘털 호재가 시장에 빠르게 반영되고 있다는 신호. ticker_agent가 이걸 보고 signal_strength를 1.0으로 올린다.
⑤ ticker_prompts — 종목별 매매 철학 주입
같은 차트 신호라도 종목마다 다르게 해석해야 한다. 반도체 장비주는 짧은 모멘텀, 원전 인프라는 긴 추세. 이걸 코드에 박지 않고 종목별 system prompt로 분리했다.
-- ticker_prompts 테이블
CREATE TABLE ticker_prompts (
id INTEGER PRIMARY KEY,
ticker TEXT,
version INTEGER,
prompt_text TEXT,
is_active INTEGER,
note TEXT,
created_at TEXT
);
UI: StepTelegram → "프롬프트 튜닝" 탭 → 종목 선택 → 편집 → 새 버전 저장 → 활성화. 다음 cron부터 신규 프롬프트로 평가.
예시:
[종목 010120 — LS ELECTRIC]
당신은 원전·전력 인프라 장기 추세 추종 매매를 지향합니다.
9EMA 되돌림에서 적극 매수, 매물대 공백이 큰 경우 FULL 진입.
단기 -3% 출렁임은 무시. 진정한 위험은 ADX < 15 추세 소실.
이 프롬프트가 ticker_agent prompt의 [D] 사용자 매매 철학 블록으로 자동 주입된다.
운영 예시 — 2026-05-04 후보 6종
지금 시스템이 운영 중인 후보 종목들이다.
코드
종목명
테마 (Naver)
finance
signal_score
010120
LS ELECTRIC
전력 인프라
85
4
033100
제룡전기
전력 인프라
72
3
052690
한전기술
원전
78
4
084370
유진테크
반도체 장비
81
4
095610
테스
반도체 장비
75
3
403870
HPSP
반도체 장비
(미정)
(미정)
테마는 원전·전력 인프라 + 반도체 장비 2개 섹터로 자연스럽게 분산됐다. 4단계 필터링이 의도적인 분산까지 만들어낸다.
결론 — 종목 선정은 데이터 파이프라인 문제다
"좋은 종목을 고른다" = 직관
"4단 필터링을 통과시킨다" = 데이터 파이프라인
직관은 흔들리지만 파이프라인은 흔들리지 않는다. 매일 같은 룰로 2,500개에서 10개를 추출한다. 결과가 마음에 안 들면 룰을 고친다 — 종목을 직접 고르지 않는다.
다음 주(2주차)에는 이렇게 추출된 후보 종목들을 3개의 AI 에이전트(scout/ticker/strategy)가 어떻게 협업해서 실제 매매하는지를 다룬다. 종목 선정은 출발선일 뿐, 진짜 알파는 그 뒤에 있다.