SQLite와 Pandas로 주가 데이터 저장하기: 시계열 DB 설계 기초

Alchemist·2026년 10월 6일

KT AIVLE

목록 보기
56/130

주가 데이터를 매번 API에서 다시 받아오면 느리고 호출 제한에도 걸릴 수 있다.

따라서 알고리즘 트레이딩이나 데이터 분석 프로젝트에서는 과거 데이터를 로컬 데이터베이스에 저장해두고 재사용하는 구조가 유용하다.

작은 프로젝트라면 SQLite가 좋은 출발점이다.


SQLite의 특징

SQLite는 별도의 DB 서버를 설치하지 않아도 하나의 파일에 데이터를 저장할 수 있다.

market.db

장점:

  • 별도 서버가 필요 없음
  • Python 표준 라이브러리에서 지원
  • 프로젝트 단위 저장소로 간편함
  • SQL 학습 가능
  • Pandas와 쉽게 연동

대규모 동시 쓰기나 서버 애플리케이션으로 확장한다면 PostgreSQL 같은 DBMS를 고려할 수 있다.


Python에서 SQLite 연결

import sqlite3

connection = (
    sqlite3.connect(
        "market.db"
    )
)

작업 후 닫는다.

connection.close()

보통 with 문을 사용하면 편리하다.

import sqlite3

with sqlite3.connect(
    "market.db"
) as connection:
    ...

주가 테이블 설계

한 종목의 일봉을 저장한다고 하자.

CREATE TABLE IF NOT EXISTS daily_prices (
    symbol TEXT NOT NULL,
    date TEXT NOT NULL,
    open REAL NOT NULL,
    high REAL NOT NULL,
    low REAL NOT NULL,
    close REAL NOT NULL,
    volume INTEGER NOT NULL,
    PRIMARY KEY (symbol, date)
);

핵심은 (symbol, date) 조합을 기본키로 사용해 같은 날짜 데이터가 중복 저장되지 않도록 하는 것이다.


Python에서 테이블 생성

import sqlite3

create_sql = """
CREATE TABLE IF NOT EXISTS daily_prices (
    symbol TEXT NOT NULL,
    date TEXT NOT NULL,
    open REAL NOT NULL,
    high REAL NOT NULL,
    low REAL NOT NULL,
    close REAL NOT NULL,
    volume INTEGER NOT NULL,
    PRIMARY KEY (symbol, date)
)
"""

with sqlite3.connect(
    "market.db"
) as connection:
    connection.execute(
        create_sql
    )

한 행 저장

SQL에 문자열을 직접 연결하지 않는다.

나쁜 방식:

sql = (
    "INSERT INTO daily_prices "
    f"VALUES ('{symbol}', ...)"
)

SQL Injection이나 따옴표 처리 문제가 발생할 수 있다.

파라미터 바인딩을 사용한다.

insert_sql = """
INSERT INTO daily_prices (
    symbol,
    date,
    open,
    high,
    low,
    close,
    volume
)
VALUES (?, ?, ?, ?, ?, ?, ?)
"""
row = (
    "AAA",
    "2026-10-06",
    100.0,
    105.0,
    99.0,
    103.0,
    150000,
)

with sqlite3.connect(
    "market.db"
) as connection:
    connection.execute(
        insert_sql,
        row,
    )

여러 행 한 번에 저장

rows = [
    (
        "AAA",
        "2026-10-01",
        100,
        103,
        98,
        102,
        120000,
    ),
    (
        "AAA",
        "2026-10-02",
        102,
        105,
        101,
        104,
        160000,
    ),
]
with sqlite3.connect(
    "market.db"
) as connection:
    connection.executemany(
        insert_sql,
        rows,
    )

반복해서 execute()를 호출하는 것보다 효율적이다.


중복 데이터 처리: UPSERT

API에서 같은 날짜 데이터를 다시 받아올 수 있다.

SQLite에서는 충돌 시 업데이트하는 방식으로 처리할 수 있다.

INSERT INTO daily_prices (
    symbol,
    date,
    open,
    high,
    low,
    close,
    volume
)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(symbol, date)
DO UPDATE SET
    open = excluded.open,
    high = excluded.high,
    low = excluded.low,
    close = excluded.close,
    volume = excluded.volume;

최근 데이터가 정정될 가능성이 있는 경우 유용하다.


데이터 조회

select_sql = """
SELECT
    symbol,
    date,
    open,
    high,
    low,
    close,
    volume
FROM daily_prices
WHERE symbol = ?
ORDER BY date
"""
with sqlite3.connect(
    "market.db"
) as connection:
    cursor = (
        connection.execute(
            select_sql,
            ("AAA",),
        )
    )

    rows = cursor.fetchall()

Pandas와 SQLite 연결

import pandas as pd
import sqlite3

with sqlite3.connect(
    "market.db"
) as connection:
    df = pd.read_sql_query(
        """
        SELECT *
        FROM daily_prices
        WHERE symbol = ?
        ORDER BY date
        """,
        connection,
        params=("AAA",),
        parse_dates=[
            "date",
        ],
    )

이제 DataFrame에서 바로 분석할 수 있다.

df[
    "ma20"
] = (
    df["close"]
    .rolling(20)
    .mean()
)

DataFrame을 DB에 저장하기

prices = pd.DataFrame(
    {
        "symbol": [
            "AAA",
            "AAA",
        ],
        "date": [
            "2026-10-01",
            "2026-10-02",
        ],
        "close": [
            102,
            104,
        ],
    }
)
with sqlite3.connect(
    "market.db"
) as connection:
    prices.to_sql(
        "temporary_prices",
        connection,
        if_exists="append",
        index=False,
    )

to_sql()은 편리하지만 복합 기본키, 세밀한 UPSERT, 타입 제어가 필요한 핵심 테이블에서는 직접 SQL을 작성하는 편이 명확할 수 있다.


날짜를 어떻게 저장할까?

SQLite에는 PostgreSQL처럼 강력한 날짜 타입 시스템이 없다.

일봉 날짜는 ISO 형식 문자열로 저장하면 정렬과 비교가 편하다.

2026-01-03
2026-02-01
2026-10-06

ISO 형식:

YYYY-MM-DD

시간까지 필요하다면:

2026-10-06T09:30:00+09:00

시간대까지 명확하게 관리하는 것이 좋다.


종목 마스터 테이블

가격 외에 종목 정보를 따로 관리한다.

CREATE TABLE IF NOT EXISTS symbols (
    symbol TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    market TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

가격 테이블에 종목명을 매일 중복 저장할 필요가 없다.


주문 로그 테이블

CREATE TABLE IF NOT EXISTS orders (
    order_id TEXT PRIMARY KEY,
    client_order_id TEXT,
    symbol TEXT NOT NULL,
    side TEXT NOT NULL,
    quantity INTEGER NOT NULL,
    price REAL,
    status TEXT NOT NULL,
    created_at TEXT NOT NULL
);

체결 테이블

부분 체결을 고려하면 주문과 체결을 분리하는 편이 좋다.

CREATE TABLE IF NOT EXISTS executions (
    execution_id TEXT PRIMARY KEY,
    order_id TEXT NOT NULL,
    quantity INTEGER NOT NULL,
    price REAL NOT NULL,
    executed_at TEXT NOT NULL
);
1개의 주문
→ 여러 체결

관계로 표현할 수 있다.


인덱스

자주 검색하는 조건에는 인덱스가 도움이 된다.

CREATE INDEX IF NOT EXISTS
idx_daily_prices_date
ON daily_prices(date);

다만 인덱스가 많으면 쓰기 비용이 늘어나므로 실제 조회 패턴에 맞게 만든다.


트랜잭션

여러 작업이 하나의 논리적 단위라면 함께 성공하거나 함께 실패해야 할 수 있다.

with sqlite3.connect(
    "market.db"
) as connection:
    connection.execute(
        sql_a,
        params_a,
    )

    connection.execute(
        sql_b,
        params_b,
    )

with 블록에서 예외가 발생하면 rollback되고 정상 종료되면 commit되는 형태로 사용할 수 있다.


데이터 수집 파이프라인

Broker API
    ↓
응답 검증
    ↓
DataFrame 변환
    ↓
중복 제거
    ↓
SQLite UPSERT
    ↓
분석

마지막 저장 날짜 이후만 가져오기

DB에서 최근 날짜를 확인한다.

def get_last_date(
    connection,
    symbol,
):
    row = connection.execute(
        """
        SELECT MAX(date)
        FROM daily_prices
        WHERE symbol = ?
        """,
        (symbol,),
    ).fetchone()

    return row[0]

예:

DB 마지막 날짜
2026-09-30

→ API 요청 시작일
2026-10-01

전체 데이터를 매번 다시 받을 필요가 없다.


저장 전에 데이터 검증

def validate_prices(
    df,
):
    required = {
        "symbol",
        "date",
        "open",
        "high",
        "low",
        "close",
        "volume",
    }

    missing = (
        required
        - set(df.columns)
    )

    if missing:
        raise ValueError(
            f"필수 컬럼 누락: "
            f"{missing}"
        )

가격 관계도 검사할 수 있다.

invalid = df[
    (df["high"] < df["low"])
    | (df["volume"] < 0)
]

DB 백업

SQLite는 하나의 파일이므로 백업하기 쉽지만 실행 중 파일을 단순 복사하는 것보다 SQLite의 backup API를 사용할 수 있다.

import sqlite3

source = sqlite3.connect(
    "market.db"
)

target = sqlite3.connect(
    "market-backup.db"
)

with target:
    source.backup(
        target
    )

source.close()
target.close()

SQLite에서 PostgreSQL로 넘어갈 시점

다음 요구가 커지면 서버 DB를 고려할 수 있다.

  • 여러 프로세스가 동시에 쓰기
  • 여러 사용자 접속
  • 원격 서버 운영
  • 대용량 데이터
  • 강한 권한 관리
  • 백업과 복제
  • 복잡한 분석 쿼리

학습과 로컬 전략 연구에는 SQLite가 충분히 유용하다.


핵심 정리

  • SQLite는 별도 서버 없이 파일 하나로 사용할 수 있어 개인 데이터 분석과 트레이딩 프로젝트에 적합하다.
  • 주가 데이터는 (symbol, date)처럼 자연스러운 복합 키를 사용해 중복을 막을 수 있다.
  • SQL 문자열에 값을 직접 합치지 말고 파라미터 바인딩을 사용한다.
  • 이미 존재하는 시세를 갱신하려면 SQLite의 UPSERT를 활용할 수 있다.
  • Pandas의 read_sql_query()를 사용하면 DB 데이터를 바로 DataFrame으로 불러올 수 있다.
  • 주문과 체결은 1:N 관계가 될 수 있으므로 별도 테이블로 관리하는 것이 좋다.
  • 마지막 저장 날짜를 조회해 이후 데이터만 수집하면 API 호출량을 줄일 수 있다.
  • 데이터 규모와 동시성이 커지면 PostgreSQL 같은 서버형 DBMS로 확장할 수 있다.
profile
html_programming_language

0개의 댓글