포스트

감정 태그 × 수익률 — 투자 습관을 데이터로 기록하기

투자와 감정

행동재무학(Behavioral Finance)의 핵심 통찰이 있다. 투자자는 합리적 계산이 아니라 감정으로 결정을 내리는 경우가 많다.

  • 공포: 주가가 떨어질 때 손절하지 못하거나, 반대로 패닉 셀링
  • FOMO: 급등 뉴스에 추격 매수
  • 확증 편향: 자신의 매수 결정을 지지하는 정보만 수집

문제는 이런 패턴을 본인이 인식하기 어렵다는 점이다. 기억은 결과에 따라 왜곡된다. 수익이 나면 “나는 분석했다”, 손실이 나면 “운이 나빴다”고 기억한다.

monticker의 감정 태그는 주문 시점에 감정을 기록하게 한다. 나중에 수익률과 교차 분석하면 “나는 어떤 감정일 때 더 잘 수익을 냈는가”를 객관적으로 볼 수 있다.


감정 태그 설계

1
2
3
4
5
6
7
8
9
10
enum class EmotionTag {
    FOMO,           // 놓칠 것 같아서 (Fear of Missing Out)
    FEAR,           // 손실이 두려워서
    GREEDY,         // 더 벌고 싶어서
    CONFIDENT,      // 분석에 확신이 있어서
    UNCERTAIN,      // 잘 모르겠지만 일단
    PLANNED,        // 계획된 전략대로
    PANIC,          // 패닉 상태에서
    HOPEFUL,        // 반등을 기대하며
}

주문 완료 직후 사용자에게 “지금 어떤 감정이었나요?”를 묻고 기록한다. 강제가 아니라 선택이지만, 기록이 쌓이면 패턴을 볼 수 있다.


order_emotion_tags 테이블

1
2
3
4
5
6
7
8
CREATE TABLE order_emotion_tags (
    id              BIGSERIAL    PRIMARY KEY,
    user_id         BIGINT       NOT NULL REFERENCES users(id),
    paper_order_id  BIGINT       NOT NULL REFERENCES paper_orders(id),
    emotion_tag     VARCHAR(20)  NOT NULL,
    note            TEXT,                    -- 선택적 메모
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT now()
);

EmotionTagService — 감정 태그 저장 및 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
class EmotionTagService(private val jdbcTemplate: JdbcTemplate) {

    fun saveTag(userId: Long, orderId: Long, tag: EmotionTag, note: String?) {
        jdbcTemplate.update("""
            INSERT INTO order_emotion_tags (user_id, paper_order_id, emotion_tag, note)
            VALUES (?, ?, ?, ?)
        """, userId, orderId, tag.name, note)
    }

    fun analyzeEmotionReturn(userId: Long): List<EmotionReturnStat> {
        return jdbcTemplate.query("""
            SELECT
                oet.emotion_tag,
                COUNT(*)                        AS order_count,
                AVG(po.realized_pnl_pct)        AS avg_return_pct,
                SUM(CASE WHEN po.realized_pnl > 0 THEN 1 ELSE 0 END)::float
                    / COUNT(*)                  AS win_rate
            FROM order_emotion_tags oet
            JOIN paper_orders po ON po.id = oet.paper_order_id
            WHERE oet.user_id = ?
              AND po.status = 'FILLED'
            GROUP BY oet.emotion_tag
            ORDER BY avg_return_pct DESC
        """, { rs, _ ->
            EmotionReturnStat(
                tag         = EmotionTag.valueOf(rs.getString("emotion_tag")),
                orderCount  = rs.getInt("order_count"),
                avgReturnPct = rs.getDouble("avg_return_pct"),
                winRate     = rs.getDouble("win_rate"),
            )
        }, userId)
    }
}

분석 결과 예시:

1
2
3
4
5
6
7
감정 태그별 수익률 분석 (내 투자 기록):

PLANNED    → 평균 수익률 +2.3%,  승률 67%  ← 가장 좋은 결과
CONFIDENT  → 평균 수익률 +1.1%,  승률 58%
UNCERTAIN  → 평균 수익률 -0.2%,  승률 45%
FOMO       → 평균 수익률 -1.8%,  승률 31%  ← 가장 나쁜 결과
PANIC      → 평균 수익률 -2.5%,  승률 22%

이 데이터를 보고 사용자는 깨달을 수 있다: “나는 FOMO 상태일 때 일관되게 손실을 낸다.”


투자 행동 점수 (Behavior Score)

감정 태그와 함께 주문 패턴을 분석해 투자 행동 점수를 계산한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
class BehaviorScoreService(private val jdbcTemplate: JdbcTemplate) {

    fun calculateBehaviorScore(userId: Long): BehaviorScore {
        var score = 50  // 기본 50점

        // ✅ 좋은 습관 가점
        val splitBuyRatio = getSplitBuyRatio(userId)
        if (splitBuyRatio > 0.5) score += 20  // 분할 매수 50% 이상

        val stoplossKeptRatio = getStoplossKeptRatio(userId)
        if (stoplossKeptRatio > 0.8) score += 15  // 손절가 준수율 80% 이상

        val chaseAfterSpikeRatio = getChaseBuyAfterSpikeRatio(userId)
        if (chaseAfterSpikeRatio < 0.1) score += 15  // 급등 후 추격매수 10% 미만

        // ❌ 나쁜 습관 감점
        if (chaseAfterSpikeRatio > 0.3) score -= 20  // 급등 추격매수 30% 이상
        if (splitBuyRatio < 0.2) score -= 15  // 몰빵 매수

        val sameStockSameDayCount = getSameStockSameDayAvg(userId)
        if (sameStockSameDayCount > 3) score -= 15  // 당일 동일 종목 3회 이상

        return BehaviorScore(
            score       = score.coerceIn(0, 100),
            breakdown   = buildBreakdown(splitBuyRatio, stoplossKeptRatio, chaseAfterSpikeRatio),
            analyzedAt  = Instant.now(),
        )
    }
}

투자 생존 점수 (Survival Score)

생존 점수는 현재 포트폴리오 상태가 얼마나 위험한지를 측정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fun calculateSurvivalScore(userId: Long): SurvivalScore {
    var score = 100

    // 단일 종목 집중도
    val maxConcentration = getMaxConcentration(userId)
    if (maxConcentration > 0.7) score -= 30   // 70% 이상 한 종목
    else if (maxConcentration > 0.5) score -= 15

    // 현금 비중
    val cashRatio = getCashRatio(userId)
    if (cashRatio < 0.05) score -= 20  // 현금 5% 미만

    // 1시간 내 주문 빈도
    val recentOrderCount = getRecentOrderCount(userId, hours = 1)
    if (recentOrderCount > 5) score -= 15

    // 급등주(당일 +10% 이상) 보유 비중
    val hotStockRatio = getHotStockRatio(userId)
    if (hotStockRatio > 0.4) score -= 10

    return SurvivalScore(
        score       = score.coerceIn(0, 100),
        level       = when {
            score >= 80 -> "SAFE"
            score >= 60 -> "CAUTION"
            score >= 40 -> "WARNING"
            else        -> "DANGER"
        },
    )
}

투자 영수증

체결 직후 발행되는 투자 영수증은 재무적 사실 외에 맥락 정보를 포함한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "orderId": 12345,
  "symbol": "005930",
  "side": "BUY",
  "quantity": 10,
  "avgFillPrice": 73200.5,
  "totalCost": 732005,
  "commission": 1098,
  "emotionTag": "PLANNED",
  "priceChangeAtOrder": "+1.2%",
  "volumeRatioAtOrder": 2.3,
  "marketRegimeAtOrder": "BULL",
  "filledAt": "2025-07-11T10:07:23Z"
}

나중에 이 주문의 결과가 나왔을 때 영수증을 다시 보면 “그때 상황이 어땠는지”를 맥락과 함께 복기할 수 있다.


정리

  • 감정 태그는 주문 시점에 기록해야 한다. 결과 확인 후에는 기억이 왜곡된다.
  • emotion_tag × avg_return_pct 분석으로 어떤 심리 상태에서 더 좋은 결정을 하는지 객관적으로 볼 수 있다.
  • 행동 점수(좋은 습관 측정)와 생존 점수(현재 위험도 측정)를 분리해 다른 관점을 제공한다.

다음 시리즈(Series 5)에서는 코딩 없이 투자 전략을 만드는 Quant Lab의 룰 엔진을 다룬다.

  1. 1 가격이 아니라 이벤트를 팔자 — monticker 설계 철학
  2. 2 모듈식 모놀리스를 선택한 이유 — MSA의 유혹을 거부하기
  3. 3 TimescaleDB를 시계열 DB로 고른 이유 — Hypertable과 연속 집계
  4. 4 Go goroutine으로 202개 종목 동시 수집하기 — Market Gateway 설계
  5. 5 Kafka로 시세 파이프라인 분리하기 — 토픽 설계와 at-least-once
  6. 6 Netty로 수만 연결에 시세 브로드캐스트하기 — NioEventLoopGroup 리액터 패턴
  7. 7 EMA 기반 이상 탐지 — 가격 급등과 거래량 서지 실시간 감지
  8. 8 TreeMap으로 CLOB 호가창 구현하기 — 가격/시간 우선 매칭과 슬리피지
  9. 9 주문 전 동기 리스크 게이트 설계 — VaR, 집중도, 일일손실 5가지 규칙
  10. 10 잔고를 저장하지 말고 재구성하라 — 이벤트 소싱 원장 설계
  11. 11 감정 태그 × 수익률 — 투자 습관을 데이터로 기록하기
  12. 12 룰 엔진: RSI·MACD 조건식을 JSON DSL로 — Quant Lab 설계
  13. 13 백테스트 엔진: look-ahead 없는 시뮬레이션 — Sharpe·MDD·PF 계산
  14. 14 전략 지문(SHA-256)으로 룰셋 보호하기 — 서버 사이드 실행과 역공학 방어
  15. 15 Markowitz 최적화를 솔버 없이 구현하기 — 프로젝션 경사하강법
  16. 16 Kelly Criterion: 수학적 파산 방지 베팅 비율 — Half Kelly와 백테스트 연동
  17. 17 ZigZag + 패턴 템플릿 매칭으로 차트 패턴 감지 — 헤드앤숄더·이중바닥
  18. 18 ADX로 시장 국면 분류하기 — BULL·BEAR·SIDEWAYS·HIGH_VOL
  19. 19 손익통산으로 세금 줄이기 — Tax-Loss Harvesting 시뮬레이션
  20. 20 MockK로 JdbcTemplate 목킹하기 — 311개 테스트 작성 경험
  21. 21 OpenTelemetry + Jaeger로 분산 추적 — 시세 파이프라인 지연 측정
  22. 22 Circuit Breaker로 외부 API 장애 격리 — Resilience4j + KIS·Yahoo 폴백 체인
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다