Very Good till Now !
This commit is contained in:
303
.TemporaryDocument/main.py.good
Normal file
303
.TemporaryDocument/main.py.good
Normal file
@@ -0,0 +1,303 @@
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app.database import get_db, engine
|
||||
from app.models import Base, Asset, UserAsset, AlertSetting
|
||||
from app.fetcher import fetcher
|
||||
from app.calculator import Calculator
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# 데이터베이스 테이블 생성
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI(title="Asset Pilot - Orange Pi Edition", version="1.0.0")
|
||||
|
||||
# 1. 스케줄러 설정 (Asia/Seoul 타임존)
|
||||
scheduler = BackgroundScheduler(timezone="Asia/Seoul")
|
||||
|
||||
# 정적 파일 및 템플릿 설정
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# 전역 변수: 현재 가격 캐시 및 연결 상태 관리
|
||||
current_prices: Dict = {}
|
||||
connected_clients = 0
|
||||
clients_lock = asyncio.Lock()
|
||||
|
||||
# 텔레그램 알림용 전역 변수 (메모리상에서 중복 알림 방지)
|
||||
last_alert_time = {}
|
||||
|
||||
# ==================== Pydantic 모델 ====================
|
||||
|
||||
class UserAssetUpdate(BaseModel):
|
||||
symbol: str
|
||||
previous_close: float
|
||||
average_price: float
|
||||
quantity: float
|
||||
|
||||
class AlertSettingUpdate(BaseModel):
|
||||
settings: Dict
|
||||
|
||||
# ==================== 유틸리티 함수 ====================
|
||||
|
||||
def send_telegram_msg(text: str):
|
||||
""".env에 설정된 토큰과 ID를 사용하여 텔레그램 메시지 전송"""
|
||||
token = os.getenv("TELEGRAM_TOKEN")
|
||||
chat_id = os.getenv("TELEGRAM_CHAT_ID")
|
||||
|
||||
if not token or not chat_id:
|
||||
print("⚠️ 텔레그램 설정이 .env에 없습니다.")
|
||||
return
|
||||
|
||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||
payload = {
|
||||
"chat_id": chat_id,
|
||||
"text": text,
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, json=payload, timeout=5)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ 텔레그램 전송 실패: {response.text}")
|
||||
else:
|
||||
print(f"✅ 텔레그램 알림 발송 성공: {text[:20]}...")
|
||||
except Exception as e:
|
||||
print(f"❌ 텔레그램 연결 오류: {e}")
|
||||
|
||||
# ==================== 데이터베이스 초기화 로직 ====================
|
||||
|
||||
def init_assets(db: Session):
|
||||
assets_data = [
|
||||
("XAU/USD", "금/달러", "귀금속"),
|
||||
("XAU/CNY", "금/위안", "귀금속"),
|
||||
("XAU/GBP", "금/파운드", "귀금속"),
|
||||
("USD/DXY", "달러인덱스", "환율"),
|
||||
("USD/KRW", "달러/원", "환율"),
|
||||
("BTC/USD", "비트코인/달러", "암호화폐"),
|
||||
("BTC/KRW", "비트코인/원", "암호화폐"),
|
||||
("KRX/GLD", "금 현물", "귀금속"),
|
||||
("XAU/KRW", "금/원", "귀금속"),
|
||||
]
|
||||
for symbol, name, category in assets_data:
|
||||
existing = db.query(Asset).filter(Asset.symbol == symbol).first()
|
||||
if not existing:
|
||||
asset = Asset(symbol=symbol, name=name, category=category)
|
||||
db.add(asset)
|
||||
db.commit()
|
||||
print("✅ 자산 마스터 데이터 초기화 완료")
|
||||
|
||||
def init_user_assets(db: Session):
|
||||
assets = db.query(Asset).all()
|
||||
for asset in assets:
|
||||
existing = db.query(UserAsset).filter(UserAsset.asset_id == asset.id).first()
|
||||
if not existing:
|
||||
user_asset = UserAsset(asset_id=asset.id, previous_close=0, average_price=0, quantity=0)
|
||||
db.add(user_asset)
|
||||
db.commit()
|
||||
print("✅ 사용자 자산 데이터 초기화 완료")
|
||||
|
||||
def init_alert_settings(db: Session):
|
||||
default_settings = {
|
||||
"급등락_감지": False,
|
||||
"급등락_임계값": 3.0,
|
||||
"목표수익률_감지": False,
|
||||
"목표수익률": 10.0,
|
||||
"특정가격_감지": False,
|
||||
"금_목표가격": 100000,
|
||||
"BTC_목표가격": 100000000,
|
||||
}
|
||||
for key, value in default_settings.items():
|
||||
existing = db.query(AlertSetting).filter(AlertSetting.setting_key == key).first()
|
||||
if not existing:
|
||||
setting = AlertSetting(setting_key=key, setting_value=json.dumps(value))
|
||||
db.add(setting)
|
||||
db.commit()
|
||||
print("✅ 알림 설정 초기화 완료")
|
||||
|
||||
# ==================== 앱 시작/종료 이벤트 ====================
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
db = next(get_db())
|
||||
try:
|
||||
init_assets(db)
|
||||
init_user_assets(db)
|
||||
init_alert_settings(db)
|
||||
print("🚀 데이터베이스 초기화 완료")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
scheduler.add_job(fetcher.update_closing_prices, 'cron', hour=7, minute=10, id='daily_snapshot')
|
||||
scheduler.start()
|
||||
|
||||
if not hasattr(fetcher, 'daily_closing_prices') or not fetcher.daily_closing_prices:
|
||||
fetcher.update_closing_prices()
|
||||
|
||||
asyncio.create_task(background_fetch())
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def stop_scheduler():
|
||||
scheduler.shutdown()
|
||||
print("🛑 스케줄러 정지")
|
||||
|
||||
# ==================== 핵심 가변 수집 및 알림 로직 (에러 수정됨) ====================
|
||||
|
||||
async def background_fetch():
|
||||
global current_prices
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with clients_lock:
|
||||
current_count = connected_clients
|
||||
|
||||
interval = 5 if current_count > 0 else 10
|
||||
current_prices = fetcher.fetch_all()
|
||||
|
||||
if current_prices:
|
||||
db = next(get_db())
|
||||
try:
|
||||
settings_raw = db.query(AlertSetting).all()
|
||||
sets = {s.setting_key: json.loads(s.setting_value) for s in settings_raw}
|
||||
user_assets = db.query(Asset, UserAsset).join(UserAsset).all()
|
||||
|
||||
for asset, ua in user_assets:
|
||||
symbol = asset.symbol
|
||||
if symbol not in current_prices: continue
|
||||
|
||||
# [중요] 수집된 데이터가 None이거나 에러인 경우 건너뜀 (NoneType 에러 방지)
|
||||
price_data = current_prices[symbol].get("가격")
|
||||
if price_data is None: continue
|
||||
|
||||
curr_p = float(price_data)
|
||||
prev_c = float(ua.previous_close) if ua.previous_close else 0
|
||||
avg_p = float(ua.average_price) if ua.average_price else 0
|
||||
now_ts = datetime.now().timestamp()
|
||||
|
||||
# 1. 급등락 체크
|
||||
if sets.get("급등락_감지") and prev_c > 0:
|
||||
change = ((curr_p - prev_c) / prev_c) * 100
|
||||
if abs(change) >= float(sets.get("급등락_임계값", 3.0)):
|
||||
if now_ts - last_alert_time.get(f"{symbol}_vol", 0) > 3600:
|
||||
icon = "🚀 급등" if change > 0 else "📉 급락"
|
||||
send_telegram_msg(f"<b>[{icon}] {symbol}</b>\n현재가: {curr_p:,.2f}\n변동률: {change:+.2f}%")
|
||||
last_alert_time[f"{symbol}_vol"] = now_ts
|
||||
|
||||
# 2. 목표수익률 체크
|
||||
if sets.get("목표수익률_감지") and avg_p > 0:
|
||||
profit = ((curr_p - avg_p) / avg_p) * 100
|
||||
if profit >= float(sets.get("목표수익률", 10.0)):
|
||||
if now_ts - last_alert_time.get(f"{symbol}_profit", 0) > 86400:
|
||||
send_telegram_msg(f"<b>💰 목표수익 달성! ({symbol})</b>\n수익률: {profit:+.2f}%\n현재가: {curr_p:,.2f}")
|
||||
last_alert_time[f"{symbol}_profit"] = now_ts
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
# 예기치 못한 에러 발생 시 로그를 찍고 다음 루프로 진행
|
||||
print(f"❌ 백그라운드 작업 중 에러 발생: {e}")
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
# ==================== API 엔드포인트 ====================
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def read_root(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@app.get("/api/prices")
|
||||
async def get_prices():
|
||||
return current_prices
|
||||
|
||||
@app.get("/api/assets")
|
||||
async def get_assets(db: Session = Depends(get_db)):
|
||||
assets = db.query(Asset, UserAsset).join(UserAsset, Asset.id == UserAsset.asset_id).all()
|
||||
result = []
|
||||
for asset, user_asset in assets:
|
||||
result.append({
|
||||
"symbol": asset.symbol,
|
||||
"name": asset.name,
|
||||
"category": asset.category,
|
||||
"previous_close": float(user_asset.previous_close),
|
||||
"average_price": float(user_asset.average_price),
|
||||
"quantity": float(user_asset.quantity),
|
||||
})
|
||||
return result
|
||||
|
||||
@app.post("/api/assets")
|
||||
async def update_asset(data: UserAssetUpdate, db: Session = Depends(get_db)):
|
||||
asset = db.query(Asset).filter(Asset.symbol == data.symbol).first()
|
||||
if not asset: raise HTTPException(status_code=404, detail="Asset not found")
|
||||
user_asset = db.query(UserAsset).filter(UserAsset.asset_id == asset.id).first()
|
||||
if user_asset:
|
||||
user_asset.previous_close = data.previous_close
|
||||
user_asset.average_price = data.average_price
|
||||
user_asset.quantity = data.quantity
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
raise HTTPException(status_code=404, detail="UserAsset not found")
|
||||
|
||||
@app.get("/api/pnl")
|
||||
async def get_pnl(db: Session = Depends(get_db)):
|
||||
krx_asset = db.query(Asset).filter(Asset.symbol == "KRX/GLD").first()
|
||||
krx_user = db.query(UserAsset).filter(UserAsset.asset_id == krx_asset.id).first() if krx_asset else None
|
||||
btc_asset = db.query(Asset).filter(Asset.symbol == "BTC/KRW").first()
|
||||
btc_user = db.query(UserAsset).filter(UserAsset.asset_id == btc_asset.id).first() if btc_asset else None
|
||||
|
||||
pnl = Calculator.calc_pnl(
|
||||
float(krx_user.average_price) if krx_user else 0, float(krx_user.quantity) if krx_user else 0,
|
||||
float(btc_user.average_price) if btc_user else 0, float(btc_user.quantity) if btc_user else 0,
|
||||
current_prices.get("KRX/GLD", {}).get("가격"), current_prices.get("BTC/KRW", {}).get("가격")
|
||||
)
|
||||
return pnl
|
||||
|
||||
@app.get("/api/alerts/settings")
|
||||
async def get_alert_settings(db: Session = Depends(get_db)):
|
||||
settings = db.query(AlertSetting).all()
|
||||
return {s.setting_key: json.loads(s.setting_value) for s in settings}
|
||||
|
||||
@app.post("/api/alerts/settings")
|
||||
async def update_alert_settings(data: AlertSettingUpdate, db: Session = Depends(get_db)):
|
||||
for key, value in data.settings.items():
|
||||
setting = db.query(AlertSetting).filter(AlertSetting.setting_key == key).first()
|
||||
if setting: setting.setting_value = json.dumps(value)
|
||||
else: db.add(AlertSetting(setting_key=key, setting_value=json.dumps(value)))
|
||||
db.commit()
|
||||
return {"status": "success"}
|
||||
|
||||
@app.get("/api/stream")
|
||||
async def stream_prices(request: Request):
|
||||
async def event_generator():
|
||||
global connected_clients
|
||||
async with clients_lock: connected_clients += 1
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected(): break
|
||||
if current_prices:
|
||||
yield f"data: {json.dumps(current_prices, ensure_ascii=False)}\n\n"
|
||||
await asyncio.sleep(5)
|
||||
finally:
|
||||
async with clients_lock: connected_clients = max(0, connected_clients - 1)
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
# 도커 Health Check용 경로 수정 (404 방지)
|
||||
@app.get("/health")
|
||||
@app.get("/health/")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run("main:app", host=os.getenv("APP_HOST", "0.0.0.0"), port=int(os.getenv("APP_PORT", 8000)), reload=False)
|
||||
Reference in New Issue
Block a user