카테고리 없음

[시즌 3 - 2화] "재시작해도 기억을 잃지 않는 봇" SQLite 데이터베이스 설계와 트레이딩 로그 자동 기록 모듈 개발

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

안녕하세요! 지난 시간에 가상 서버인 AWS 라이트세일 환경에서 24시간 작동하는 리눅스 터미널 구동 환경을 성공적으로 개설했습니다. 이제 봇이 무한 구동 상태로 밤낮없이 돌아가기 시작했으니, 본격적으로 그 안에서 발생하는 데이터들을 영구적으로 보존할 장치가 필요합니다.

"어제 산 비트코인이 지금 봇 메모리에 들어있는데, 만약 서버가 재부팅되면 봇은 자기가 뭘 샀는지 기억할 수 있을까?"

이전 시즌처럼 시드 배분이나 매매 로그를 단순한 텍스트 파일(.txt)이나 .csv 파일에 한 줄씩 기록하는 방식은 한계가 명확합니다. 프로그램이 갑자기 정전이나 강제 종료를 맞았을 때, 혹은 동시에 여러 프로세스가 로그를 쓸 때 파일이 깨져버리거나(File Corruption) 엉키기 쉽기 때문입니다.

그래서 24시간 도는 실전 트레이딩 봇의 뇌 역할을 해줄 아주 가볍고 견고한 내장형 관계형 데이터베이스인 SQLite를 봇에 심어줄 시간입니다. 오늘 연재에서는 재시작 후에도 봇이 자기 자산과 포지션을 완벽히 복원해내도록 돕는 데이터베이스 스키마 설계와 자동 연동 모듈을 완성해 보겠습니다!


1. 봇을 위한 SQLite 경량 데이터베이스 테이블 스키마 설계

SQLite는 별도의 복잡한 데이터베이스 서버 설치 필요 없이, 파이썬 기본 라이브러리에 내장되어 있어 파일 하나(trade_bot.db)로 모든 것을 보관해주는 환상적인 크립토 특화 DB 엔진입니다.

우리가 설계할 트레이딩 테이블은 크게 2개입니다.

  1. trades 테이블: 매수/매도의 실제 거래 체결 내역 기록
  2. balance_history 테이블: 매일 또는 매 매매 시점의 계좌 자산 평가액 추이 보존 (수익률 그래프용)
-- 1. 거래 이력 보관 테이블
CREATE TABLE IF NOT EXISTS trades (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    symbol TEXT NOT NULL,
    side TEXT NOT NULL,       -- 'BUY' or 'SELL'
    price REAL NOT NULL,
    amount REAL NOT NULL,
    fee REAL NOT NULL,
    order_id TEXT UNIQUE      -- 거래소 고유 주문 ID
);

-- 2. 자산 변동성 모니터링 테이블
CREATE TABLE IF NOT EXISTS balance_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    total_balance REAL NOT NULL, -- 총 평가금액 (현금 + 보유코인 평가액)
    cash REAL NOT NULL           -- 보유 중인 순수 현금 자산
);

2. 파이썬 SQLite 데이터베이스 자동화 모듈 연동

이제 파이썬 코드 내부에서 위 테이블을 생성하고, 거래가 체결되거나 정산이 끝났을 때 데이터베이스에 실시간으로 로그를 주입(Insert)하는 제어용 데이터베이스 클래스를 조립해 봅니다.

import sqlite3
import os
from datetime import datetime

class TradeDatabase:
    def __init__(self, db_path="trade_bot.db"):
        self.db_path = db_path
        # 데이터베이스 잠금(Lock) 충돌 방지를 위한 timeout 설정 필수
        self.conn = sqlite3.connect(self.db_path, timeout=30.0)
        # [고급 성능 최적화] WAL(Write-Ahead Logging) 모드 활성화
        # 읽기/쓰기를 분리하여 동시성 성능을 높이고 database locked 오류 발생을 극소화합니다.
        self.conn.execute("PRAGMA journal_mode=WAL;")
        self.create_tables()

    def create_tables(self):
        """봇 구동에 필수적인 테이블 자동 생성"""
        cursor = self.conn.cursor()
        
        # 거래 기록 테이블 생성
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS trades (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                symbol TEXT NOT NULL,
                side TEXT NOT NULL,
                price REAL NOT NULL,
                amount REAL NOT NULL,
                fee REAL NOT NULL,
                order_id TEXT UNIQUE
            )
        """)
        
        # 자산 변동 리포트용 테이블 생성
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS balance_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                total_balance REAL NOT NULL,
                cash REAL NOT NULL
            )
        """)
        self.conn.commit()

    def record_trade(self, symbol, side, price, amount, fee, order_id):
        """매매 신호 체결 정보 데이터베이스 입력"""
        try:
            cursor = self.conn.cursor()
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            cursor.execute("""
                INSERT INTO trades (timestamp, symbol, side, price, amount, fee, order_id)
                VALUES (?, ?, ?, ?, ?, ?, ?)
            """, (now, symbol, side, price, amount, fee, order_id))
            self.conn.commit()
            print(f"[DB] {side} 기록 성공 - 주문 ID: {order_id}")
        except sqlite3.IntegrityError:
            print(f"[DB 경고] 이미 존재하는 주문 ID 입니다: {order_id}")
        except Exception as e:
            print(f"[DB 오류] 거래 기록 실패: {e}")

    def record_balance(self, total_balance, cash):
        """일일 총자산 규모 트래킹 데이터 입력"""
        try:
            cursor = self.conn.cursor()
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            cursor.execute("""
                INSERT INTO balance_history (timestamp, total_balance, cash)
                VALUES (?, ?, ?)
            """, (now, total_balance, cash))
            self.conn.commit()
            print(f"[DB] 자산 스냅샷 보존 완료 - 총액: {total_balance}원")
        except Exception as e:
            print(f"[DB 오류] 자산 스냅샷 실패: {e}")

    def close(self):
        self.conn.close()

3. 예외 상황 처리: 자산 상태 복구(State Restore) 패턴

이 모듈의 진정한 진가는 **시스템의 정상 복구 능력(Resiliency)**에 있습니다. 봇이 돌연 죽었다가 다시 살아날 때, 데이터베이스에서 가장 최근 기록된 보유 포지션 상태를 긁어오면 복잡한 외부 연동 없이도 즉각 자가 뇌이식이 가능합니다.

def restore_bot_state(db_path="trade_bot.db"):
    """
    가장 최근에 기록된 마지막 거래 정보를 가져와 현재 보유 중인 자산 유무 진단.
    가장 최신 거래가 'BUY'였다면 현재 봇은 포지션을 보유 중인 상태로 인식 복구.
    """
    if not os.path.exists(db_path):
        return {"position": "NONE", "last_price": 0.0}

    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    cursor.execute("SELECT side, price, amount FROM trades ORDER BY id DESC LIMIT 1")
    row = cursor.fetchone()
    conn.close()

    if row:
        last_side, last_price, last_amount = row
        if last_side == "BUY":
            return {"position": "LONG", "last_price": last_price, "amount": last_amount}
    
    return {"position": "NONE", "last_price": 0.0, "amount": 0.0}

⚠️ 리스크 경고: SQLite 동시 쓰기 잠금(Database Locked) 해결법

SQLite는 기본적으로 파일 기반의 가벼운 DB이기 때문에, 동시에 여러 작업이 쓰기(Write/Update)를 시도하면 sqlite3.OperationalError: database is locked 오류가 발생하며 봇의 기록이 멈추거나 튕길 수 있습니다.

  • 해결책 1 (강력 권장): WAL(Write-Ahead Logging) 모드 활성화
    • conn.execute("PRAGMA journal_mode=WAL;") 구문을 통해 WAL 모드를 켜면, 쓰기 작업이 진행 중이어도 읽기 작업을 대기 없이 즉시 수행할 수 있습니다. 대부분의 락(Lock) 충돌 문제를 소스 수준에서 예방할 수 있는 최고 효율의 옵션입니다.
  • 해결책 2: 데이터베이스 인스턴스를 커넥션 생성 시 sqlite3.connect('trade_bot.db', timeout=30.0)와 같이 timeout 대기 시간을 30초 이상 넉넉히 설정하여, 락(Lock)이 해제될 때까지 프로세스가 지치지 않고 기다리게 방어합니다.
  • 해결책 3: 멀티스레딩 환경에서 봇을 돌린다면 쓰기 스레드는 오직 단 하나만 담당하도록 큐(Queue) 패턴을 접목하거나, 데이터 쓰기 작업 직후 반드시 conn.commit()을 통해 트랜잭션을 짧게 가져가 락을 최대한 빠르게 반환하는 습관이 중요합니다.

에필로그: 다음 단계는 '거래소 테스트넷 모의투자!'

기억 상실에 빠지지 않는 불사조 같은 자동매매 봇의 튼튼한 메모리 보관함(DB)이 완성되었습니다.

  • 다음 편 예고 체크리스트:
    • [ ] 24시간 실전 모의 자금 세팅 및 계정 연결
    • [ ] 가상의 API 키를 활용한 테스트넷(Testnet) 연동
    • [ ] 실시간 비트코인 호가 창을 분석해 가상 자금으로 주문을 넣고 체결하는 실전 모의 트레이딩 코어 코딩

다음 편에서는 내 돈을 한 푼도 잃지 않고 실제 시장에서 거래가 어떻게 주문 체결되는지 실시간 테스트할 수 있는 **'거래소 테스트넷(Testnet) 모의투자 연동과 모의 체결 테스트'**를 설계해 보겠습니다.

오늘도 유익하셨다면 구독과 따뜻한 댓글 부탁드립니다. 감사합니다!

반응형