ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

๋ฐ˜์‘ํ˜•

๐Ÿ“ˆ Streamlit + Flask๋กœ ๋งŒ๋“œ๋Š” ์‹ค์‹œ๊ฐ„ ํ€€ํŠธ ํˆฌ์ž ๋Œ€์‹œ๋ณด๋“œ (์‹ค์ „ ์šด์˜ UI ์™„์„ฑํŽธ)

์ง€๊ธˆ๊นŒ์ง€ ์šฐ๋ฆฌ๋Š” ์™„๋ฒฝํžˆ ์ž๋™ํ™”๋œ ๋ฐฑ์—”๋“œ ํ€€ํŠธ ๋งค๋งค ์—”์ง„์„ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
๋ฐ์ดํ„ฐ ์ˆ˜์ง‘, ๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ, ๋งค๋งค, ๋ฆฌํฌํŠธ, ๋ฐฑ์—…—all done.
์ด์ œ ๋‚จ์€ ๊ฒƒ์€ ํ•˜๋‚˜, **์‚ฌ๋žŒ์ด ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค(UI)**์ž…๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” Streamlit + Flask๋ฅผ ์ด์šฉํ•ด
“์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆ˜์ต๋ฅ ·ํฌํŠธ ๊ตฌ์„ฑ·ํŒฉํ„ฐ ๋ณ€ํ™””๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๋Š”
ํ€€ํŠธ ์šด์šฉ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค.


๐ŸŽฏ ๋ชฉํ‘œ

  • PostgreSQL์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ๊ฐํ™”
  • ์ˆ˜์ต๋ฅ , ๋ณ€๋™์„ฑ, ๋ˆ„์  ์„ฑ๊ณผ๋ฅผ ์ฐจํŠธ๋กœ ํ‘œ์‹œ
  • Flask REST API → Streamlit ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™
  • Slack ์•Œ๋ฆผ๊ณผ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์›น์—์„œ ํ™•์ธ

๐Ÿงฑ 1๏ธโƒฃ ์ „์ฒด ๊ตฌ์กฐ

[ PostgreSQL ]
     โ–ฒ
     โ”‚ SQLAlchemy
     โ–ผ
[ Flask API ์„œ๋ฒ„ ]  ← /report, /performance
     โ–ฒ
     โ”‚ REST JSON
     โ–ผ
[ Streamlit Dashboard ]
     โ”œโ”€ ๋ˆ„์  ์ˆ˜์ต๋ฅ  ์ฐจํŠธ
     โ”œโ”€ ํ˜„์žฌ ํฌํŠธํด๋ฆฌ์˜ค
     โ””โ”€ ํŒฉํ„ฐ ๋ณ€ํ™” ํŠธ๋ Œ๋“œ

โš™๏ธ 2๏ธโƒฃ Flask API ์„œ๋ฒ„ (๋ฐฑ์—”๋“œ)

๋ฐ˜์‘ํ˜•
# api_server.py
from flask import Flask, jsonify
from sqlalchemy import create_engine
import pandas as pd, os

app = Flask(__name__)
engine = create_engine(os.getenv("DB_URL", "postgresql+psycopg2://quant_user:quant_pass@localhost:5432/quantdb"))

@app.route("/report")
def report():
    df = pd.read_sql("SELECT * FROM factors ORDER BY updated DESC LIMIT 100", engine)
    return jsonify(df.to_dict(orient="records"))

@app.route("/performance")
def performance():
    df = pd.read_sql("""
        SELECT date_trunc('day', updated) AS date,
               AVG(return_6m) AS avg_ret,
               AVG(vol_60) AS avg_vol
        FROM factors
        GROUP BY date
        ORDER BY date
    """, engine)
    return jsonify(df.to_dict(orient="records"))

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5050)

โœ… /report : ์ตœ๊ทผ ํŒฉํ„ฐ ๋ฐ์ดํ„ฐ 100๊ฐœ
โœ… /performance : ์ผ๋ณ„ ํ‰๊ท  ์ˆ˜์ต๋ฅ  & ๋ณ€๋™์„ฑ


๐Ÿ“Š 3๏ธโƒฃ Streamlit ํ”„๋ก ํŠธ์—”๋“œ

# dashboard.py
import streamlit as st
import pandas as pd
import requests, datetime as dt
import plotly.express as px

API_URL = "http://localhost:5050"

st.set_page_config(page_title="Quant Dashboard", layout="wide")

st.title("๐Ÿ“ˆ Quant Portfolio Live Dashboard")
st.markdown(f"๐Ÿ•’ Last update: {dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

# ์ตœ๊ทผ ํŒฉํ„ฐ ๋ฆฌํฌํŠธ
st.header("๐Ÿ“Š Latest Factor Data")
data = requests.get(f"{API_URL}/report").json()
df = pd.DataFrame(data)
st.dataframe(df)

# ์ผ๋ณ„ ์„ฑ๊ณผ
st.header("๐Ÿ“ˆ Performance Overview")
perf = pd.DataFrame(requests.get(f"{API_URL}/performance").json())

col1, col2 = st.columns(2)
with col1:
    fig1 = px.line(perf, x="date", y="avg_ret", title="Average 6M Return")
    st.plotly_chart(fig1, use_container_width=True)
with col2:
    fig2 = px.line(perf, x="date", y="avg_vol", title="Average Volatility (60D)")
    st.plotly_chart(fig2, use_container_width=True)

# ํฌํŠธํด๋ฆฌ์˜ค ์š”์•ฝ
st.header("๐Ÿ’ผ Portfolio Summary")
top_df = df.sort_values("return_6m", ascending=False).head(5)
st.bar_chart(top_df.set_index("ticker")["return_6m"])

๐Ÿ–ฅ๏ธ ์‹คํ–‰ ๋ฐฉ๋ฒ•

1. Flask ์„œ๋ฒ„ ์‹คํ–‰

python api_server.py

2. Streamlit ์‹คํ–‰

streamlit run dashboard.py

3. ์ ‘์†

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8501 →
๐Ÿ“Š ํ€€ํŠธ ํˆฌ์ž ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์˜คํ”ˆ!


๐Ÿ”„ 4๏ธโƒฃ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ

Streamlit์€ ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ(interval) ๊ธฐ๋Šฅ์ด ์—†์–ด
๊ฐ„๋‹จํžˆ ์•„๋ž˜ ์ฝ”๋“œ ์ถ”๊ฐ€๋กœ 1๋ถ„๋งˆ๋‹ค ๋ฆฌํ”„๋ ˆ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import time
st_autorefresh = st.experimental_rerun  # Streamlit 1.36+ ๋ฒ„์ „

st.button("๐Ÿ” ์ƒˆ๋กœ๊ณ ์นจ", on_click=lambda: st_autorefresh())

๋˜๋Š” Linux ์„œ๋ฒ„์—์„œ๋Š” watch curl http://127.0.0.1:5050/health
๋ช…๋ น์œผ๋กœ ๋ฐฑ์—”๋“œ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿงฉ 5๏ธโƒฃ ํ™•์žฅ ์•„์ด๋””์–ด

๊ธฐ๋Šฅ ์„ค๋ช…

๐Ÿ”” Slack Alert ์—ฐ๋™ ๋Œ€์‹œ๋ณด๋“œ ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ ์‹œ ์Šฌ๋ž™ ๋ฉ”์‹œ์ง€ ์ „์†ก
๐Ÿง  ํŒฉํ„ฐ๋ณ„ ์„ฑ๊ณผ Heatmap Plotly Heatmap์œผ๋กœ ํŒฉํ„ฐ๋ณ„ ์ƒ๋Œ€์„ฑ๊ณผ ์‹œ๊ฐํ™”
๐Ÿ“‰ Drawdown ๊ทธ๋ž˜ํ”„ MDD ๊ณ„์‚ฐ ํ›„ ๋ˆ„์  ์ˆ˜์ต๋ฅ  ๋Œ€๋น„ ํ•˜๋ฝ ๊ตฌ๊ฐ„ ํ‘œ์‹œ
๐Ÿ“ PDF Export Streamlit → Reportlab์œผ๋กœ ์›”๊ฐ„ ๋ฆฌํฌํŠธ ์ž๋™ ์ƒ์„ฑ
๐Ÿ“ฌ Email ๋ฐœ์†ก ๋งค์ฃผ ์›”์š”์ผ ์•„์นจ PDF + ์š”์•ฝ ๋ฆฌํฌํŠธ ์ „์†ก

๐Ÿงฐ 6๏ธโƒฃ Docker๋กœ ํ†ตํ•ฉ ๋ฐฐํฌ

version: "3.9"
services:
  flask-api:
    build: ./api
    ports:
      - "5050:5050"
    env_file: .env
  streamlit-dashboard:
    build: ./dashboard
    ports:
      - "8501:8501"
    depends_on:
      - flask-api
    restart: unless-stopped

๐Ÿ’ก ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑํ•˜๋ฉด Flask์™€ Streamlit์ด ํ•จ๊ป˜ ์‹คํ–‰๋˜์–ด
์ž๋™๋งค๋งค ๋ฐฑ์—”๋“œ + ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง UI๊ฐ€ ์™„์ „ํžˆ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค.


๐Ÿ“Œ ์ •๋ฆฌ

๊ตฌ์„ฑ ์š”์†Œ ์—ญํ• 

Flask REST API (๋ฐ์ดํ„ฐ ์ œ๊ณต)
PostgreSQL ํŒฉํ„ฐ ๋ฐ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์ €์žฅ
Streamlit ์‹ค์‹œ๊ฐ„ UI ๋Œ€์‹œ๋ณด๋“œ
Plotly ์ฐจํŠธ ์‹œ๊ฐํ™”
Docker ๋ฐฐํฌ ์ž๋™ํ™”

์ด์ œ ์„œ๋ฒ„๋Š” ์Šค์Šค๋กœ ํˆฌ์žํ•˜๊ณ ,
๋‹น์‹ ์€ ์›น์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ง„์งœ **‘ํ•˜๋ฃจ 24์‹œ๊ฐ„ ์ผํ•˜๋Š” ํ€€ํŠธ ๋งค๋‹ˆ์ €’**๊ฐ€ ์ƒ๊ธด ๊ฑฐ์ฃ .


๐Ÿ“˜ ๋‹ค์Œ ๊ธ€ ์˜ˆ๊ณ 

๋‹ค์Œ ํŽธ์—์„œ๋Š” **“AI ํŒฉํ„ฐ ์˜ˆ์ธก ๋ชจ๋ธ ๋„์ž… – Transformer๋กœ ๋‹ค์Œ ๋ถ„๊ธฐ ์ˆ˜์ต๋ฅ  ์˜ˆ์ธกํ•˜๊ธฐ”**๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.
์‹ค์ œ ํˆฌ์ž๋ฐ์ดํ„ฐ๋ฅผ ํ•™์Šตํ•œ ๋”ฅ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ํ€€ํŠธ ์˜ˆ์ธก ๋ชจ๋ธ(Transformer + PyTorch) ์„ ๊ตฌ์ถ•ํ•ด๋ณผ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.


 

Streamlit,Flask,ํ€€ํŠธ๋Œ€์‹œ๋ณด๋“œ,ํŒŒ์ด์ฌ์‹œ๊ฐํ™”,PostgreSQL,๋ฐ์ดํ„ฐํˆฌ์ž,์‹ค์‹œ๊ฐ„ํˆฌ์ž๋ชจ๋‹ˆํ„ฐ๋ง,Plotly,ํ€€ํŠธ์ž๋™ํ™”,ํŒŒ์ด์ฌ๋ฐฑ์—”๋“œ


 

โ€ป ์ด ํฌ์ŠคํŒ…์€ ์ฟ ํŒก ํŒŒํŠธ๋„ˆ์Šค ํ™œ๋™์˜ ์ผํ™˜์œผ๋กœ, ์ด์— ๋”ฐ๋ฅธ ์ผ์ •์•ก์˜ ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ์ œ๊ณต๋ฐ›์Šต๋‹ˆ๋‹ค.
๊ณต์ง€์‚ฌํ•ญ
์ตœ๊ทผ์— ์˜ฌ๋ผ์˜จ ๊ธ€
์ตœ๊ทผ์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€
Total
Today
Yesterday
๋งํฌ
ยซ   2026/02   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
๊ธ€ ๋ณด๊ด€ํ•จ
๋ฐ˜์‘ํ˜•