“비트코인 및 주요 코인 1시간 단위 시세 예측 및 실시간 UI 제공 시스템”
| 컬럼명 | 의미 | 비고 (추가 설명) |
|---|---|---|
market | 마켓 구분 | KRW-BTC → 원화(KRW)로 비트코인 거래 |
예: KRW-BTC, KRW-ETH 등 | ||
trade_date | 거래일자 (UTC 기준) | YYYYMMDD |
trade_time | 거래시간 (UTC 기준) | HHMMSS |
trade_date_kst | 거래일자 (KST, 한국시간) | YYYYMMDD |
trade_time_kst | 거래시간 (KST, 한국시간) | HHMMSS |
trade_timestamp | 거래 체결 시각 (UTC, ms 단위) | Unix timestamp (밀리초) |
opening_price | 당일 시가 | 하루 시작 가격 |
high_price | 당일 고가 | 하루 중 최고 거래 가격 |
low_price | 당일 저가 | 하루 중 최저 거래 가격 |
trade_price | 최근 거래 체결가 | |
| 해당 봉의 종가(Close price) | 현재/마지막 체결된 가격 | |
| 1시간 내 마지막 거래 가격 | ||
prev_closing_price | 전일 종가 | 어제 마지막 거래 가격 |
change | 전일 대비 등락 | RISE, FALL, EVEN |
change_price | 전일 대비 가격 차이 | (오늘가 - 전일 종가) |
change_rate | 전일 대비 등락률 | |
| 전 봉 대비 수익률(변동률) | 퍼센트(소수) | |
(현재 종가 / 이전 종가) - 1 | ||
signed_change_price | 부호 포함 가격 차이 | 양수(상승), 음수(하락) |
signed_change_rate | 부호 포함 등락률 | 양수(상승), 음수(하락) |
trade_volume | 최근 체결 거래량 | 해당 거래 건의 수량 (BTC 단위) |
acc_trade_price | 누적 거래대금 (당일) | 원화 기준 합계 |
acc_trade_price_24h | 24시간 누적 거래대금 | 직전 24시간 동안 원화 거래총액 |
acc_trade_volume | 누적 거래량 (당일) | BTC 기준 합계 |
| 단위: 해당 코인 기준, 예: BTC면 BTC 수량 | ||
acc_trade_volume_24h | 24시간 누적 거래량 | 직전 24시간 동안 BTC 거래총액 |
highest_52_week_price | 52주 최고가 | 최근 1년간 최고 가격 |
highest_52_week_date | 52주 최고가 기록일 | 날짜(YYYY-MM-DD) |
lowest_52_week_price | 52주 최저가 | 최근 1년간 최저 가격 |
lowest_52_week_date | 52주 최저가 기록일 | 날짜(YYYY-MM-DD) |
timestamp | 서버 응답 시각 (ms 단위) | API 호출 기준 시간 |
1시간봉 기준 시각, KST로 변환됨 (candle_date_time_kst) |

XGBoost
데이터 개수가 적은 상황에서 XGBoost를 성능이 가장 좋을 것 같았습니다. 왜냐하면 2가지 이유가 있었습니다.
max_depth, min_child_weight → 트리 복잡도 제한subsample, colsample_bytree → 데이터 샘플링gamma → 최소 손실 감소 기준LSTM
시계열 데이터에 특화된 순환 신경망 모델로써 기존의 RNN의 기울기 소실 문제를 해결한 모델로써 시계열 데이터를 학습하는데 유리한 것 같아서 선정했습니다.
Transfromer
LSTM은 RNN 계열로 순차 데이터 처리에 강점이 있었지만, 병렬 처리가 어렵고, 긴 거리 의존성 문제가 여전히 있었고, 훈련 시간이 증가되는 문제들이 있었습니다. 이러한 문제를 보완하기 위해서 나온 모델입니다.
→ 선정 (X) / 1. 시간적으로 여유가 없었습니다. 2. 이번 프로젝트는 모델의 성능이 중요한게 아니라 MLOps의 흐름을 알아가는 프로젝트기 때문에 사용하기에는 너무 딥했습니다.
일단 위의 모델들 설명을 보면 알 수 있드시, 선택할 수 있었던 모델은 XGBoost와 LSTM 정도였습니다. 위 두 모델 중에서 성능이 높았던 LSTM을 선택을 했고, 또한 DL을 간단하게 구현해 보는게 좋은 기회지 않을까 싶어서 LSTM으로 선정을 했습니다. 또한 시계열 특성을 잘 살릴 수 있었던 것이 LSTM 모델이기 때문도 있었습니다.
모델링은 Many to one 모델링을 사용하기로 했습니다. 즉 24 to 1의 형태로 현재 기준으로 24시간 전의 데이터를 바탕으로 1시간 후의 데이터를 예측하는 형태로 구현하기로 했습니다.
시간별로 학습 및 예측을 구현한 이유
: 초당 변화로 하게 되면 데이터의 변동성이 매우 클 수 있습니다. 초 단위로 예측을 진행하게 되면 단기 패턴 예측을 사용하는게 적합합니다. 따라서 초 단위가 아니라 분이나 시간 단위로 예측을 진행하기로 했습니다. 여기서 모델이 학습 및 예측을 하는 시간까지 고려하면 시간 단위로 나누는게 더 적절해 보였습니다.
1시간을 예측한 이유
: 일단 24시간을 통으로 예측을 한다고 하면, 168:24인데, 이렇게 되면 시퀀스 길이가 길이지기 때문에 기울기 소실 문제가 발생할 수 도 있습니다. 또한 모델이 과적합 될 위험도 생기며, 예측 간격이 길어지므로 오차가 누적될 수 도 있습니다.
import pandas as pd
import numpy as np
import tensorflow as tf
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
from tensorflow.keras import layers, Model
btc = pd.read_csv('./data/KRW-BTC_historical.csv')
features = ["trade_price", "change_rate", "acc_trade_volume"]
scaler = MinMaxScaler()
data_scaled = scaler.fit_transform(btc[features])
price_scaler = MinMaxScaler()
data_price = btc[["trade_price"]].values
price_scaler.fit(data_price)
def create_sequences(data, seq_len=24, pred_len=1):
X, y = [], []
for i in range(len(data) - seq_len - pred_len + 1):
X.append(data[i:i+seq_len]) # 입력: 모든 feature
y.append(data[i+seq_len:i+seq_len+pred_len, 0]) # 출력: trade_price만
return np.array(X), np.array(y)
X, y = create_sequences(data_scaled, seq_len=24, pred_len=1)
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
X_train = tf.convert_to_tensor(X_train, dtype=tf.float32)
y_train = tf.convert_to_tensor(y_train, dtype=tf.float32)
X_test = tf.convert_to_tensor(X_test, dtype=tf.float32)
y_test = tf.convert_to_tensor(y_test, dtype=tf.float32)
class LSTMModel(tf.keras.Model):
def __init__(self, input_size, hidden_size=256, output_size=24, num_layers=2, dropout=0.2):
super(LSTMModel, self).__init__()
# LSTM 층 여러 개 쌓기
self.lstm_layers = []
for i in range(num_layers - 1):
self.lstm_layers.append(
layers.LSTM(hidden_size, return_sequences=True, dropout=dropout)
)
# 마지막 LSTM 층 (return_sequences=False → 마지막 hidden state만 반환)
self.lstm_layers.append(
layers.LSTM(hidden_size, return_sequences=False, dropout=dropout)
)
# Fully connected (Dense) layer
self.fc = layers.Dense(output_size)
def call(self, x):
out = x
for lstm in self.lstm_layers:
out = lstm(out)
out = self.fc(out)
return out
input_size = X_train.shape[2]
hidden_size = 256
output_size = 1
model = LSTMModel(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
seq_len = X_train.shape[1]
model.build(input_shape=(None, seq_len, input_size))
model.compile(
loss=tf.keras.losses.MeanSquaredError(),
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
metrics=[tf.keras.metrics.MeanAbsoluteError()]
)
model.summary()
history = model.fit(
X_train, y_train,
epochs=100,
batch_size=32,
validation_data=(X_test, y_test),
verbose=1
)
test_loss, test_mae = model.evaluate(X_test, y_test, verbose=0)
print(f"Test Loss: {test_loss:.6f}, Test MAE: {test_mae:.6f}")
y_pred = model.predict(X_test)
y_test_scaled = np.expm1(y_test)
y_pred_scaled = np.expm1(y_pred)
y_test_scaled = price_scaler.inverse_transform(y_test.numpy())
y_pred_scaled = price_scaler.inverse_transform(y_pred.numpy())
from sklearn.metrics import mean_squared_error
import numpy as np
rmse = np.sqrt(mean_squared_error(y_test_scaled, y_pred_scaled))
print(f"Validation RMSE: {rmse:.4f}")
streamlit, pandas, numpy, plotly, boto3, httpx, asyncio 사용.env 파일로부터 AWS 환경 변수(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, BUCKET_NAME) 로드(1) S3에서 시간 단위 데이터 로드
upbit_ticker/{날짜}/파일30시간 데이터를 로드event_timestamp = timestamp(ms) + 9시간(KST) 변환event_hour 단위로 그룹화 → 코인별 시계열 데이터 생성(2) 증분 데이터 로드 (Incremental)
last_load_hour 기준으로 새로운 데이터만 추가 로드drop_duplicates)(3) 더미 데이터 생성
numpy로 가격 시뮬레이션 생성(1) 비동기 호출
httpx.AsyncClient → FastAPI 서버로 요청/predict/lstm 엔드포인트 호출(2) 동기 호출 Wrapper
fetch_prediction() → 내부적으로 asyncio.run() 실행(3) Fallback 예측 (더미/통계 기반)
(1) 사이드바 옵션
KRW-BTC, KRW-ETH, KRW-DOGE, KRW-XRP)LSTM, GRU, Prophet, LightGBM)(2) 데이터 로드 로직
@st.cache_data 사용 → 1시간 캐싱(3) 예측 실행
@st.cache_data (5분 캐싱) → 같은 데이터/시간대는 API 재호출 방지(1) 메트릭 카드
(2) 신뢰도 Progress Bar
AI 활성 vs 통계 모드)(3) Plotly 차트
(4) 데이터 테이블
event_hour, trade_price, trade_volume)(5) 모델 성능 지표
(6) 시스템 상태
(7) 자동 새로고침 옵션
st.rerun()GitHub Actions은(는) 단순한 DevOps 수준을 넘어 리포지토리에서 다른 이벤트가 발생할 때 워크플로를 실행할 수 있도록 합니다. 예를 들어 누군가가 리포지토리에서 새 이슈를 만들 때마다 워크플로를 실행하여 적절한 레이블을 자동으로 추가할 수 있습니다.
GitHub에서 워크플로를 실행할 Linux, Windows, macOS 가상 머신을 제공하거나, 사용자 고유의 데이터 센터 또는 클라우드 인프라에서 자체 호스트형 실행기를 호스트할 수 있습니다.
<깃허브 공식 문서>
내용이 꽤나 많아서 이해하길 힘들 것 같기는 한데, 간단하게 이해를 하면 Github를 이용한 ci/cd를 구현이 가능한 기능입니다. 또한 깃허브 Github Action에는 몇가지 구성 요소가 있습니다. 아래에서 설명을 자세히 하겠습니다!
워크플로는 하나 이상의 작업을 실행할 구성 가능한 자동화된 프로세스입니다. 워크플로는 리포지토리에 체크 인된 YAML 파일에서 정의되며, 리포지토리의 이벤트로 트리거될 때 실행되거나 수동으로 또는 정의된 일정에 따라 트리거될 수 있습니다.
워크플로는 리포지토리의
.github/workflows디렉터리에 정의됩니다. 리포지토리에 다음과 같은 각각의 다른 작업 집합을 수행하는 여러 워크플로가 있을 수 있습니다.
- 끌어오기 요청을 빌드하고 테스트합니다.
- 릴리스가 생성될 때마다 애플리케이션을 배포합니다.
- 새 문제가 보고될 때마다 레이블을 추가합니다.
이벤트는 워크플로 실행을 트리거하는 리포지토리의 특정 활동입니다. 예를 들어 누군가가 끌어오기 요청을 만들거나, 이슈를 열거나, 리포지토리에 커밋을 푸시할 때 GitHub에서 활동이 시작될 수 있습니다.
작업(Job)은 워크플로에서 동일한 러너(Runner)에서 실행되는 일련의 단계(Step) 집합입니다. 각 단계는 실행될 셸 스크립트이거나 실행될 액션(Action)일 수 있습니다. 단계는 순서대로 실행되며 서로 의존 관계가 있습니다. 모든 단계가 동일한 러너에서 실행되므로 한 단계에서 다른 단계로 데이터를 공유할 수 있습니다. 예를 들어, 애플리케이션을 빌드하는 단계 다음에 빌드된 애플리케이션을 테스트하는 단계를 둘 수 있습니다.
작업은 다른 작업과의 의존성을 설정할 수 있습니다. 기본적으로 작업에는 의존성이 없으며 병렬로 실행됩니다. 작업이 다른 작업에 의존하도록 설정되면, 해당 작업이 완료될 때까지 대기한 후 실행됩니다.
또한 매트릭스(Matrix)를 사용하여 동일한 작업을 여러 번 실행할 수 있으며, 각 실행마다 운영체제나 언어 버전과 같은 다른 변수 조합을 적용할 수 있습니다.
액션(Action)은 워크플로 내에서 특정 작업을 수행하는 미리 정의된 재사용 가능한 작업(Job) 또는 코드 집합으로, 워크플로 파일에서 반복되는 코드를 줄여줍니다. 액션이 수행할 수 있는 작업의 예시는 다음과 같습니다.
- GitHub에서 Git 리포지토리 가져오기(Pull)
- 빌드 환경에 맞는 도구 체인(toolchain) 설정
- 클라우드 제공자(cloud provider) 인증 설정
직접 액션을 작성할 수도 있고, GitHub 마켓플레이스(GitHub Marketplace)에서 워크플로에 사용할 액션을 찾아 사용할 수도 있습니다.
러너(Runner)는 워크플로가 트리거될 때 해당 워크플로를 실행하는 서버입니다. 각 러너는 한 번에 하나의 작업(Job)만 실행할 수 있습니다. GitHub는 워크플로 실행을 위해 Ubuntu Linux, Microsoft Windows, macOS 러너를 제공합니다. 각 워크플로 실행은 새로 프로비저닝된 가상 머신(VM)에서 실행됩니다.
name: Streamlit CI/CD
on:
push:
paths:
- 'streamlit/app.py'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# 1️⃣ 레포지토리 코드 가져오기
- name: Checkout repository
uses: actions/checkout@v3
# 2️⃣ Docker Buildx 설치
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# 3️⃣ Docker 이미지 빌드
- name: Build Docker image
run: |
docker build -t bitcoin-ui ./streamlit
# 4️⃣ 기존 컨테이너 종료/제거 후 새 컨테이너 실행
- name: Run Docker container
run: |
docker stop bitcoin-ui || true
docker rm bitcoin-ui || true
docker run -d -p 8501:8501 --name bitcoin-ui bitcoin-ui
Github Action이 작동하는 방법이 2가지 있습니다. 설명은 아래를 읽어보시면 될 것 같습니다. 여튼 위 두 방법중에 Action Runner를 쓰게 되면 Action Runner 역시 관리를 해줘야하기 때문에 관리 해야할 task가 늘어났습니다. 그래서 ssh로 원격 서버에 연결하는 방식을 사용하기로 했습니다. 이 부분에서 AWS의 보안그룹 문제가 발생했습니다.
1️⃣ Action Runner를 통해 실행하는 방법
.github/workflows/*.yml)를 러너(Runner)가 읽어서 실행jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: python main.py
2️⃣ SSH로 원격 서버에 연결해 스크립트를 실행하는 방법
- name: Deploy via SSH
uses: appleboy/ssh-action@v0.1.9
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_KEY }}
script: |
cd /app
git pull
docker-compose up -d
🔹 핵심 차이
| 방식 | 설명 | 특징 |
|---|---|---|
| Action Runner | 워크플로를 직접 Runner가 실행 | GitHub 이벤트 기반, 자동화 중심 |
| SSH 연결 | 워크플로에서 원격 서버 접속 후 스크립트 실행 | 원격 환경 활용, 배포/관리 중심 |
AWS 보안 그룹이 무슨 이슈가 있냐고 하면 결국에 ssh로 연결을 해서 스크립트를 실행하는 방식이기 때문에 들어오는 호스트를 특정 할 수 있어야 합니다. 하지만 깃허브 actions의 경우에는 IP가 계속해서 바뀌는 문제가 있었습니다. 아래의 2가지 상황에서 할 수 있는 방법들이 있습니다.
야생과 같은 인터넷 세상에서 모든 호스트에 대해서 ssh를 연결하는 경우에는 AWS 비용으로 유사 유럽 풀패키지 여행을 다녀올 수 있습니다. 그렇다면 왜 모든 호스트 연결이란 방법이 있을까요?
바로 AWS에서 자체적으로 발급해주는 보안키입니다. 해당 보안키를 유출하지 않는 이상 위와 같이 유사 유럽 풀패키지 여행을 다녀올 수는 없을 겁니다. 하지만 여기서 보안키를 유출하지 않는다는 가정이 있는데, 이 사바나와 같은 인터넷 세상에서 키를 잃어버리는 것은 어려운 일은 아니죠..따라서 아래 방법을 사용하기로 했습니다.
나의 IP만 ssh 연결을 하게 되면 깃허브 action의 IP는 계속 바뀌기 때문에 내 IP를 하면 어떻게 연결을 하냐 싶지만, 연결할 수 없습니다. 그래서 뭘 하겠냐는 말이냐면, 아래 해결방법 부분에서 자세히 설명해 드리겠습니다.

github action이 작동을 하게되면 해당 요청에 대한 IP를 저장하고 해당 IP를 저장해뒀다가 나중에 AWS의 보안 그룹에 임시로 추가를 해두고 스크립트 작업 이후에는 보안 그룹을 삭제하는 식으로 구현을 했습니다. 이러한 방법으로 github action ci/cd를 구축했습니다.
배스천 호스트..를 쓰는게 현업에서 쓰는거라고 합니다..
즉, IAM 인증을 사용해서 ssh를 접근하게 한다!
main.py (FastAPI 앱 설정)app = FastAPI(
title="Bitcoin Prediction API",
description="비트코인 가격 예측을 위한 FastAPI 서버",
version="1.0.0"
)for router, prefix, tags in routers:
app.include_router(router)if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)core/config.py (환경 설정 관리).env 기반 환경 변수 관리 (pydantic_settings.BaseSettings)ML_FLOW_URL → MLflow 서버 주소MODEL_NAME → 모델 이름MODEL_VERSION_ALIAS → 모델 버전 aliasMODEL_URL → models:/모델명/버전 형태 URI 생성core/utils.py (모델 로딩 및 전처리)🔹 scale_model_load()
mlflow.keras.load_modelscaler_X.joblib, scaler_y.joblib 다운로드 후 로드🔹 data_preprocess(df)
timestamp 변환 및 인덱스 설정resample("1H"))MA_3, MA_6) 생성lag_1) 생성(X_input, num_features)🔹 model_predict()
y_pred)api/__init__.py (라우터 자동 등록)api 디렉토리 내 routes_*.py 파일을 자동으로 탐색router를 FastAPI 앱에 등록routes_xxx.py/xxx"Xxx"api/routes_predict.py (예측 API 라우터)🔹 엔드포인트 정의
PredictRequest){
"coin": "BTC",
"model": "lstm",
"recent_data": [
{"event_timestamp": "...", "trade_price": ..., "acc_trade_volume": ...},
...
]
}🔹 처리 흐름
recent_data → DataFrame 변환
scale_model_load() → 모델 + 스케일러 불러오기
data_preprocess(df) → 입력 데이터 전처리
model_predict() → 모델 예측 수행
예측 결과 반환
{
"coin": "BTC",
"model": "lstm",
"predicted_price": 48000000.0,
"metrics": {
"rmse": 25000,
"mae": 15000,
"r2": 0.92
}
}
/predict/lstm POST, 최근 데이터 전송)scale_model_load)data_preprocess)model_predict)