카테고리 없음

[시즌 3 - 6화] "계란을 나누고 비중을 맞춘다" 다중 자산(Multi-Asset) 분산 매매와 정적 켈리(Kelly) 공식 리밸런싱

myaistories 2026. 5. 27. 06:54
반응형

안녕하세요! 지난 연재에서는 외부 야외 활동 중에도 스마트폰으로 가상 서버의 봇을 간편하게 통제하고, 긴급 상황 시 포지션을 즉시 털어버릴 수 있는 '텔레그램 킬스위치 제어' 모듈을 개발했습니다.

원격 조종 장치가 생기니 한결 마음이 든든해졌지만, 여전히 우리의 투자 계좌는 커다란 구조적 위험에 직면해 있습니다. 만약 비트코인 단 한 가지만 거래하다가 갑자기 시장이 흔들리거나 예기치 못한 차트 노이즈에 휩쓸리면, 아무리 정교한 단일 전략이라도 며칠 만에 수차례 손절을 겪으며 시드가 크게 상해버리기 때문입니다.

"비트코인, 이더리움, 솔라나처럼 여러 코인에 나누어 매매를 돌리면 변동성이 낮아질 텐데, 각 코인마다 대체 내 시드의 몇 %씩 배정해서 베팅해야 하는 거지?"

이 자금 분배 문제를 직관이나 감에 의존하지 않고, 수학적으로 내 전략의 **승률(Win Rate)**과 **손익비(Win/Loss Ratio)**에 맞추어 최적의 배팅 비율을 산출해 주는 도구가 바로 **켈리 공식(Kelly Criterion)**입니다. 오늘 연재에서는 단일 자산 켈리를 넘어 여러 자산군에 안전하게 시드를 나누고 재조정하는 다중 자산 정적 켈리 배분 엔진을 개발해 보겠습니다!


1. 다중 자산 켈리 공식의 수학적 토대

켈리 공식은 매번 거래할 때마다 파산 확률을 0%로 통제하면서 내 자산의 기하급수적 성장률(Geometric Growth)을 극대화하는 투자 비중($f^*$)을 구하는 공식입니다.

$$f^* = \frac{p}{a} - \frac{q}{b} = \frac{bp - q}{b}$$

  • $p$: 과거 매매 승률 (예: 55% = 0.55)
  • $q$: 과거 매매 패율 ( $1 - p$ )
  • $b$: 순 손익비 (평균 수익금 / 평균 손실금)
  • $a$: 손실 시 손실률 (일반적으로 봇의 레버리지가 1배이고 전액 손실이 아니므로, 손절 비율 등 설정에 따름. 레버리지 1배 기준 청산 시 $a=1$)

⚠️ 실전 필터: Fractional Kelly (분할 켈리)

수학적 켈리 공식은 이론적으로 최고 효율을 내지만, 시장의 실제 확률 분포가 과거 데이터와 완벽히 같지 않기 때문에 켈리 공식이 계산해낸 값 그대로 베팅하면 계좌 변동성(Drawdown)이 극도로 커집니다. 따라서 실전 트레이딩에서는 계산된 켈리 값의 25%~50%만 보수적으로 베팅하는 '하프 켈리(Half-Kelly, 0.5)' 또는 '쿼터 켈리(Quarter-Kelly, 0.25)' 요율을 곱해 방어력을 높여야 합니다.


2. 파이썬 다중 자산 켈리 자금 분배 모듈 구현

여러 자산(BTC, ETH, SOL)에 대해 개별 백테스팅 데이터를 기반으로 켈리 베팅 비중을 산출하고, 전체 가용한 자산 범위 내로 비율을 정규화(Normalization)하는 파이썬 모듈입니다.

class MultiAssetKellyAllocator:
    def __init__(self, assets_stats, fractional_multiplier=0.25):
        """
        - assets_stats: 각 자산의 백테스팅 승률과 손익비를 담은 딕셔너리
          예시: {
             'BTC/USDT': {'win_rate': 0.54, 'profit_loss_ratio': 1.4},
             'ETH/USDT': {'win_rate': 0.52, 'profit_loss_ratio': 1.5},
             'SOL/USDT': {'win_rate': 0.48, 'profit_loss_ratio': 1.8}
          }
        - fractional_multiplier: 변동성 제어를 위한 분할 켈리 배수 (기본 쿼터 켈리 0.25)
        """
        self.assets_stats = assets_stats
        self.multiplier = fractional_multiplier

    def calculate_kelly_fractions(self):
        """각 개별 자산의 raw 켈리 비율 계산"""
        raw_fractions = {}
        for symbol, stats in self.assets_stats.items():
            p = stats['win_rate']
            q = 1 - p
            b = stats['profit_loss_ratio']
            
            if b <= 0:
                raw_fractions[symbol] = 0.0
                continue
                
            # 켈리 공식 계산: f* = (bp - q) / b
            f_star = (b * p - q) / b
            
            # 켈리 값이 음수(기댓값이 마이너스인 불리한 전략)이면 베팅 금지
            raw_fractions[symbol] = max(0.0, f_star)
            
        return raw_fractions

    def allocate_weights(self, total_balance):
        """
        가용한 총잔고를 기준하여 각 자산군별 최종 진입 금액(USDT) 산출
        자산 간 합이 100%를 초과하는 레버리지 방지를 위한 정규화 수행
        """
        raw_fractions = self.calculate_kelly_fractions()
        
        # 1. 분할 켈리 배수 적용 (예: 계산값에 0.25를 곱해 안정화)
        adjusted_fractions = {k: v * self.multiplier for k, v in raw_fractions.items()}
        
        # 2. 총 배분 비율의 합이 가용 자산 한도(예: 현금의 90%)를 넘지 않도록 정규화
        total_fraction_sum = sum(adjusted_fractions.values())
        max_allowed_fraction = 0.90  # 계좌 안전 마진 10%를 남겨둠
        
        final_allocations = {}
        if total_fraction_sum > max_allowed_fraction:
            # 합이 한도를 넘어서면 각 비중을 비례적으로 축소(정규화)
            scale = max_allowed_fraction / total_fraction_sum
            for symbol, val in adjusted_fractions.items():
                final_allocations[symbol] = val * scale
        else:
            final_allocations = adjusted_fractions
            
        # 3. 비중을 기준으로 최종 할당 USDT 계산
        allocations_in_usdt = {}
        for symbol, weight in final_allocations.items():
            allocations_in_usdt[symbol] = round(total_balance * weight, 2)
            
        return final_allocations, allocations_in_usdt

# [고급 기능] 데이터베이스(DB) 기반 실시간 롤링 승률 및 손익비 산출
# 과거 정적 백테스트 값에 고이지 않고, 봇이 최근 체결한 실제 거래 데이터를 바탕으로
# 승률과 손익비를 실시간 재 계산하여 켈리 비중을 동적 리밸런싱합니다.
def get_rolling_stats_from_db(db_path="trade_bot.db", symbol="BTC/USDT", rolling_n=30):
    """
    SQLite 데이터베이스에서 최근 N개의 매매 내역을 조회하여 승률과 손익비를 계산합니다.
    """
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        
        # 최근 N개의 완료된 거래 이력 로드 (BUY -> SELL 쌍의 손익 확인용)
        cursor.execute("""
            SELECT side, price, amount FROM trades 
            WHERE symbol = ? 
            ORDER BY id DESC LIMIT ?
        """, (symbol, rolling_n * 2))
        rows = cursor.fetchall()
        conn.close()
        
        # 거래 기록이 부족할 때 사용할 디폴트 백테스트 기초 통계치 반환
        if len(rows) < 4:
            return {'win_rate': 0.53, 'profit_loss_ratio': 1.4}
            
        profits = []
        losses = []
        
        # 매수(BUY)와 매도(SELL) 주문 쌍 매칭 후 수익률 분석
        for i in range(0, len(rows) - 1, 2):
            trade_now = rows[i]   # 가장 최근
            trade_prev = rows[i+1] # 그 이전
            
            # 매수 진입 후 매도 청산 케이스
            if trade_now[0] == 'SELL' and trade_prev[0] == 'BUY':
                pnl = (trade_now[1] - trade_prev[1]) / trade_prev[1]
                if pnl > 0:
                    profits.append(pnl)
                else:
                    losses.append(abs(pnl))
                    
        total = len(profits) + len(losses)
        if total == 0:
            return {'win_rate': 0.53, 'profit_loss_ratio': 1.4}
            
        win_rate = len(profits) / total
        avg_profit = sum(profits) / len(profits) if profits else 0.0
        avg_loss = sum(losses) / len(losses) if losses else 0.001
        
        profit_loss_ratio = avg_profit / avg_loss if avg_loss > 0 else 1.0
        
        # 통계 아웃라이어 수치 제한 (클리핑)
        profit_loss_ratio = max(0.5, min(profit_loss_ratio, 3.0))
        
        return {'win_rate': round(win_rate, 2), 'profit_loss_ratio': round(profit_loss_ratio, 2)}
    except Exception as e:
        print(f"[동적 통계] 데이터 조회 에러 (기본값 폴백): {e}")
        return {'win_rate': 0.53, 'profit_loss_ratio': 1.4}

# 실행 및 검증
if __name__ == "__main__":
    import sqlite3
    
    # 1. 대상 자산군 정의
    target_symbols = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT']
    
    # 2. 각 자산의 실시간 통계치를 DB로부터 롤링 집계
    db_path = "trade_bot.db"
    dynamic_assets_stats = {}
    
    for symbol in target_symbols:
        # DB에서 최근 30개 거래 기록 기반으로 실시간 승률/손익비 추출
        stats = get_rolling_stats_from_db(db_path=db_path, symbol=symbol, rolling_n=30)
        dynamic_assets_stats[symbol] = stats
        
    print("==== DB 수집 실시간 매매 통계 ====")
    for symbol, stats in dynamic_assets_stats.items():
        print(f"• {symbol}: 승률 {stats['win_rate']*100:.0f}% | 손익비 {stats['profit_loss_ratio']:.2f}")
    
    # 3. 쿼터 켈리(0.25) 적용 분배 엔진 인스턴스 생성 및 연산
    allocator = MultiAssetKellyAllocator(dynamic_assets_stats, fractional_multiplier=0.25)
    
    # 현재 내 지갑 총자산이 10,000 USDT라고 가정
    wallet_balance = 10000.0
    weights, amounts = allocator.allocate_weights(wallet_balance)
    
    print("\n==== 다중 자산 켈리 자금 배분 결과 ====")
    print(f"총 자산: {wallet_balance} USDT")
    for symbol in target_symbols:
        print(f"[{symbol}] 할당 비율: {weights[symbol]*100:.2f}% | 배팅 금액: {amounts[symbol]:.2f} USDT")
        
    print(f"남겨진 미배분 여유 현금: {wallet_balance - sum(amounts.values()):.2f} USDT")

3. 다중 자산 분산 배매 시 주의할 함정: 상관관계(Correlation)

수학적으로 깔끔하게 자금이 쪼개졌으나, 실제 암호화폐 시장에서 기계적으로 이를 실행할 때는 매우 중대한 **'자산 간 동조화 현상'**을 마주하게 됩니다.

  • 상관관계 1에 수렴하는 동반 폭락: 비트코인, 이더리움, 솔라나는 시장 급락 국면에서 거의 80%~95%의 유사한 상관관계(Correlation)를 보이며 동시에 떨어집니다. 즉, 세 코인의 백테스팅 데이터가 아무리 훌륭해도 시장 전체가 하락장으로 꺾이면 세 자산 모두 동시에 손절 신호를 뱉어내게 됩니다.
  • 포트폴리오의 실질 Drawdown 증폭: 개별 코인 켈리로 10%씩 안전하게 나눴다 하더라도, 3개 코인이 동시에 손절하면 내 전체 시드는 순식간에 30%에 해당하는 타격을 한 번에 입을 수 있습니다.
  • 해결 방안: 자산 간 상관관계 매트릭스를 추가로 검토하여 동조화가 강한 코인들끼리 묶여 있을 때는 분할 켈리 배수를 0.25에서 0.15 등으로 더 보수적으로 깎아야 합니다. 혹은 비가상화폐 자산(예: 골드, 달러 선물 등)을 포트폴리오에 섞어 상관관계를 억제하는 전략적 확장이 필요합니다.

에필로그: 다음 단계는 '시스템 모니터링 및 자가치유!'

다중 자산으로 전선을 넓히고, 자산을 안전하고 수학적인 비율로 배분하는 고도화된 트레이딩 시스템의 뼈대가 갖추어졌습니다. 이제 분산 투자를 통해 계좌 우상향의 안정성을 한 차원 끌어올릴 수 있습니다.

  • 다음 편 예고 체크리스트:
    • [ ] 클라우드 서버에서 봇이 소리 소문 없이 중단되는 현상(Memory Leak, API 에러) 분석
    • [ ] 봇이 죽었는지 주기적으로 감시하고 자동으로 살려내는 PM2 / systemd 환경 세팅
    • [ ] 단시간 과도한 API 호출로 인한 거래소 밴(IP Ban / Rate Limit)을 막는 Exponential Backoff(지수 백오프) 코딩

다음 편에서는 24시간 안정 구동 시스템의 최종 끝판왕인 '서버 자동 생존 관리: API Rate Limit 방어 및 봇 자가 치유(Self-Healing) 프로세스 구축' 편으로 찾아오겠습니다.

자금 관리를 모르는 트레이딩은 도박일 뿐입니다. 오늘 내용이 가치 있었다면 이웃 신청과 댓글로 힘을 보태주세요. 감사합니다!

반응형