first vibe coding
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
monitor/dashboard.py - Streamlit 대시보드
|
||||
실행: streamlit run monitor/dashboard.py --server.port 8501
|
||||
"""
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
DB_PATH = os.getenv("DB_PATH", "data/stockbot.db")
|
||||
AI_CTX = os.getenv("AI_CONTEXT_PATH", "data/daily_context.json")
|
||||
|
||||
st.set_page_config(page_title="단타봇 대시보드", layout="wide")
|
||||
st.title("📈 단타 자동매매 대시보드")
|
||||
st.caption(f"마지막 갱신: {datetime.now().strftime('%H:%M:%S')}")
|
||||
|
||||
# ── AI 판단 현황 ──
|
||||
st.subheader("🤖 오늘의 AI 판단")
|
||||
try:
|
||||
with open(AI_CTX, encoding="utf-8") as f:
|
||||
ctx = json.load(f)
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
col1.metric("시장 분위기", ctx.get("market_sentiment", "-"))
|
||||
col2.metric("감성 점수", f"{ctx.get('sentiment_score', 0)}점")
|
||||
col3.metric("리스크 레벨", ctx.get("risk_level", "-"))
|
||||
col4.metric("거래 허용", "✅" if ctx.get("trade_allowed") else "❌")
|
||||
st.info(f"💬 {ctx.get('reason', '')}")
|
||||
col_l, col_r = st.columns(2)
|
||||
col_l.write(f"**주목 섹터:** {', '.join(ctx.get('hot_sectors', []))}")
|
||||
col_r.write(f"**회피 섹터:** {', '.join(ctx.get('avoid_sectors', []))}")
|
||||
except:
|
||||
st.warning("AI 판단 데이터 없음 (08:00 이후 생성)")
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── 오늘 매매 현황 ──
|
||||
st.subheader("📊 오늘 매매 현황")
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
df = pd.read_sql(
|
||||
"SELECT * FROM trades WHERE date=?", conn, params=(today,)
|
||||
)
|
||||
conn.close()
|
||||
|
||||
if df.empty:
|
||||
st.info("오늘 매매 없음")
|
||||
else:
|
||||
total = len(df)
|
||||
wins = (df["pnl"] > 0).sum()
|
||||
net = df["pnl"].sum()
|
||||
win_rt = wins / total * 100 if total else 0
|
||||
|
||||
c1, c2, c3, c4 = st.columns(4)
|
||||
c1.metric("총 매매", f"{total}회")
|
||||
c2.metric("승률", f"{win_rt:.0f}%")
|
||||
c3.metric("순손익", f"{net:+,.0f}원")
|
||||
c4.metric("승/패", f"{wins}/{total-wins}")
|
||||
st.dataframe(df[["entry_time","ticker","name","entry_price",
|
||||
"exit_price","pnl","exit_reason"]].fillna("-"),
|
||||
use_container_width=True)
|
||||
except Exception as e:
|
||||
st.error(f"DB 연결 실패: {e}")
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── 주간 손익 ──
|
||||
st.subheader("📅 최근 7일 손익")
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||
df_w = pd.read_sql(
|
||||
"SELECT date, SUM(pnl) as net_pnl, COUNT(*) as trades "
|
||||
"FROM trades WHERE date >= ? GROUP BY date ORDER BY date",
|
||||
conn, params=(week_ago,)
|
||||
)
|
||||
conn.close()
|
||||
if not df_w.empty:
|
||||
st.bar_chart(df_w.set_index("date")["net_pnl"])
|
||||
st.dataframe(df_w, use_container_width=True)
|
||||
except:
|
||||
st.info("주간 데이터 없음")
|
||||
|
||||
st.button("🔄 새로고침", on_click=st.rerun)
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
monitor/notifier.py
|
||||
디스코드 Webhook 알림 (단방향)
|
||||
aiohttp만 사용 - 별도 라이브러리 없음
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL", "")
|
||||
|
||||
async def send(message: str) -> None:
|
||||
"""디스코드 Webhook 메시지 전송"""
|
||||
if not WEBHOOK_URL:
|
||||
logger.warning(f"[Discord 미설정] {message}")
|
||||
return
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.post(
|
||||
WEBHOOK_URL,
|
||||
json={"content": message},
|
||||
timeout=aiohttp.ClientTimeout(total=5)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Discord 알림 실패: {e}")
|
||||
|
||||
# ── 이벤트별 메시지 ──
|
||||
|
||||
async def notify_ai_result(sentiment: str, score: int,
|
||||
hot: list, avoid: list, reason: str):
|
||||
hot_str = ",".join(hot) if hot else "없음"
|
||||
avoid_str = ",".join(avoid) if avoid else "없음"
|
||||
await send(
|
||||
f"[AI분석] 시장: {sentiment}({score}점) | "
|
||||
f"주목: {hot_str} | 회피: {avoid_str}\n💬 {reason}"
|
||||
)
|
||||
|
||||
async def notify_ai_blocked(ticker: str, name: str, reason: str):
|
||||
await send(f"[AI차단] {name}({ticker}) - {reason}")
|
||||
|
||||
async def notify_buy(ticker: str, name: str, price: int,
|
||||
target: int, stop: int, boosted: bool = False):
|
||||
star = "★ " if boosted else ""
|
||||
await send(
|
||||
f"[매수{star}] {name}({ticker}) {price:,}원 | "
|
||||
f"목표 {target:,} | 손절 {stop:,}"
|
||||
)
|
||||
|
||||
async def notify_tp1(ticker: str, name: str, pct: float):
|
||||
await send(f"[익절1] {name}({ticker}) +{pct:.1f}% / 잔여 50%")
|
||||
|
||||
async def notify_tp2(ticker: str, name: str, pct: float):
|
||||
await send(f"[익절2] {name}({ticker}) +{pct:.1f}% / 전량 청산")
|
||||
|
||||
async def notify_sl(ticker: str, name: str, pct: float):
|
||||
await send(f"[손절] {name}({ticker}) {pct:.1f}% / 즉시 청산")
|
||||
|
||||
async def notify_force_exit():
|
||||
await send("[14:50 강제청산] 전 포지션 청산 완료")
|
||||
|
||||
async def notify_risk(level: str, message: str):
|
||||
await send(f"[경고-{level}] {message}")
|
||||
|
||||
async def notify_daily_summary(trades: int, wins: int,
|
||||
losses: int, net_pnl: float):
|
||||
win_rate = wins / trades * 100 if trades else 0
|
||||
await send(
|
||||
f"[결산] 매매 {trades}회 / 승 {wins} 패 {losses} "
|
||||
f"({win_rate:.0f}%) / 순손익 {net_pnl:+,.0f}원"
|
||||
)
|
||||
|
||||
async def notify_error(message: str):
|
||||
await send(f"[긴급] {message}")
|
||||
|
||||
async def notify_ai_fallback():
|
||||
await send("[경고] AI 판단 실패 → 기본값 적용 (비중 80%)")
|
||||
Reference in New Issue
Block a user