주가 데이터를 매번 API에서 다시 받아오면 느리고 호출 제한에도 걸릴 수 있다.
따라서 알고리즘 트레이딩이나 데이터 분석 프로젝트에서는 과거 데이터를 로컬 데이터베이스에 저장해두고 재사용하는 구조가 유용하다.
작은 프로젝트라면 SQLite가 좋은 출발점이다.
SQLite는 별도의 DB 서버를 설치하지 않아도 하나의 파일에 데이터를 저장할 수 있다.
market.db
장점:
대규모 동시 쓰기나 서버 애플리케이션으로 확장한다면 PostgreSQL 같은 DBMS를 고려할 수 있다.
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) 조합을 기본키로 사용해 같은 날짜 데이터가 중복 저장되지 않도록 하는 것이다.
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()를 호출하는 것보다 효율적이다.
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()
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()
)
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)
]
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()
다음 요구가 커지면 서버 DB를 고려할 수 있다.
학습과 로컬 전략 연구에는 SQLite가 충분히 유용하다.
(symbol, date)처럼 자연스러운 복합 키를 사용해 중복을 막을 수 있다.read_sql_query()를 사용하면 DB 데이터를 바로 DataFrame으로 불러올 수 있다.