AssetPilot OrangePi 5 Pluse Server-First Commit

This commit is contained in:
Wind
2026-02-10 12:23:22 +09:00
commit bf87175f51
45 changed files with 4060 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
from typing import Dict, Optional
class Calculator:
"""손익 계산 클래스"""
@staticmethod
def calc_pnl(
gold_buy_price: float,
gold_quantity: float,
btc_buy_price: float,
btc_quantity: float,
current_gold: Optional[float],
current_btc: Optional[float]
) -> Dict:
"""
금과 BTC의 손익 계산
Returns:
{
"금손익": float,
"금손익%": float,
"BTC손익": float,
"BTC손익%": float,
"총손익": float,
"총손익%": float
}
"""
result = {
"금손익": 0.0,
"금손익%": 0.0,
"BTC손익": 0.0,
"BTC손익%": 0.0,
"총손익": 0.0,
"총손익%": 0.0
}
# 금 손익 계산
if current_gold:
cost_gold = gold_buy_price * gold_quantity
pnl_gold = gold_quantity * (float(current_gold) - gold_buy_price)
result["금손익"] = round(pnl_gold, 0)
if cost_gold > 0:
result["금손익%"] = round((pnl_gold / cost_gold * 100), 2)
# BTC 손익 계산
if current_btc:
cost_btc = btc_buy_price * btc_quantity
pnl_btc = btc_quantity * (float(current_btc) - btc_buy_price)
result["BTC손익"] = round(pnl_btc, 0)
if cost_btc > 0:
result["BTC손익%"] = round((pnl_btc / cost_btc * 100), 2)
# 총 손익 계산
result["총손익"] = result["금손익"] + result["BTC손익"]
total_cost = (gold_buy_price * gold_quantity) + (btc_buy_price * btc_quantity)
if total_cost > 0:
result["총손익%"] = round((result["총손익"] / total_cost * 100), 2)
return result

View File

@@ -0,0 +1,29 @@
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv
load_dotenv()
# 데이터베이스 URL 가져오기
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://asset_user:password@localhost/asset_pilot")
# SQLAlchemy 엔진 생성
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20,
pool_pre_ping=True, # 연결 유효성 자동 확인
echo=False # SQL 쿼리 로그 (디버깅 시 True로 변경)
)
# 세션 팩토리 생성
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
"""데이터베이스 세션 의존성"""
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,113 @@
import requests
import re
from typing import Dict, Optional
import time
class DataFetcher:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
})
def fetch_investing_com(self, asset_code: str) -> Optional[float]:
"""인베스팅닷컴 (윈도우 앱 방식 정규식 적용)"""
try:
url = f"https://www.investing.com/currencies/{asset_code.lower().replace('/', '-')}"
if asset_code == "USD/DXY":
url = "https://www.investing.com/indices/usdollar"
# allow_redirects를 True로 하여 주소 변경에 대응
response = self.session.get(url, timeout=10, allow_redirects=True)
html = response.text
# 윈도우에서 가장 잘 되던 패턴 순서대로 시도
patterns = [
r'data-test="instrument-price-last">([\d,.]+)<',
r'last_last">([\d,.]+)<',
r'instrument-price-last">([\d,.]+)<'
]
for pattern in patterns:
p = re.search(pattern, html)
if p:
return float(p.group(1).replace(',', ''))
except Exception as e:
print(f"⚠️ Investing 수집 실패 ({asset_code}): {e}")
return None
def fetch_binance(self) -> Optional[float]:
"""바이낸스 BTC/USDT (보내주신 윈도우 코드 로직)"""
url = "https://api.binance.com/api/v3/ticker/price"
try:
response = requests.get(url, params={"symbol": "BTCUSDT"}, timeout=5)
response.raise_for_status()
return float(response.json()["price"])
except Exception as e:
print(f"❌ Binance API 실패: {e}")
return None
def fetch_upbit(self) -> Optional[float]:
"""업비트 BTC/KRW (보내주신 윈도우 코드 로직)"""
url = "https://api.upbit.com/v1/ticker"
try:
response = requests.get(url, params={"markets": "KRW-BTC"}, timeout=5)
response.raise_for_status()
data = response.json()
return float(data[0]["trade_price"]) if data else None
except Exception as e:
print(f"❌ Upbit API 실패: {e}")
return None
def fetch_usd_krw(self) -> Optional[float]:
"""USD/KRW 환율 (DNS 에러 방지 이중화)"""
# 방법 1: 두나무 CDN (원래 주소)
try:
url = "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD"
res = requests.get(url, timeout=3)
if res.status_code == 200:
return float(res.json()[0]["basePrice"])
except:
pass # 실패하면 바로 인베스팅닷컴으로 전환
# 방법 2: 인베스팅닷컴에서 환율 가져오기 (가장 확실한 백업)
return self.fetch_investing_com("USD/KRW")
def fetch_krx_gold(self) -> Optional[float]:
"""금 시세 (네이버 금융 모바일)"""
try:
url = "https://m.stock.naver.com/marketindex/metals/M04020000"
res = requests.get(url, timeout=5)
m = re.search(r'\"closePrice\":\"([\d,]+)\"', res.text)
return float(m.group(1).replace(",", "")) if m else None
except:
return None
def fetch_all(self) -> Dict[str, Dict]:
print(f"📊 [{time.strftime('%H:%M:%S')}] 수집 시작...")
# 1. 환율 먼저 수집 (계산의 핵심)
usd_krw = self.fetch_usd_krw()
# 2. 나머지 자산 수집
results = {
"XAU/USD": {"가격": self.fetch_investing_com("XAU/USD"), "단위": "USD/oz"},
"XAU/CNY": {"가격": self.fetch_investing_com("XAU/CNY"), "단위": "CNY/oz"},
"XAU/GBP": {"가격": self.fetch_investing_com("XAU/GBP"), "단위": "GBP/oz"},
"USD/DXY": {"가격": self.fetch_investing_com("USD/DXY"), "단위": "Index"},
"USD/KRW": {"가격": usd_krw, "단위": "KRW"},
"BTC/USD": {"가격": self.fetch_binance(), "단위": "USDT"},
"BTC/KRW": {"가격": self.fetch_upbit(), "단위": "KRW"},
"KRX/GLD": {"가격": self.fetch_krx_gold(), "단위": "KRW/g"},
}
# 3. XAU/KRW 계산
xau_krw = None
if results["XAU/USD"]["가격"] and usd_krw:
xau_krw = round((results["XAU/USD"]["가격"] / 31.1034768) * usd_krw, 0)
results["XAU/KRW"] = {"가격": xau_krw, "단위": "KRW/g"}
success_count = sum(1 for v in results.values() if v['가격'] is not None)
print(f"✅ 수집 완료 (성공: {success_count}/9)")
return results
fetcher = DataFetcher()

View File

@@ -0,0 +1,142 @@
import requests
from typing import Dict, Optional
from bs4 import BeautifulSoup
import time
class DataFetcher:
"""모든 자산 가격 수집 클래스"""
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
self.investing_cache = {}
self.cache_time = 0
def fetch_investing_com(self, asset_code: str) -> Optional[float]:
"""Investing.com에서 가격 수집"""
# 간단한 캐싱 (5초)
if time.time() - self.cache_time < 5 and asset_code in self.investing_cache:
return self.investing_cache[asset_code]
asset_map = {
"XAU/USD": "8830",
"XAU/CNY": "2186",
"XAU/GBP": "8500",
"USD/DXY": "8827"
}
asset_id = asset_map.get(asset_code)
if not asset_id:
return None
try:
url = f"https://www.investing.com/currencies/{asset_code.lower().replace('/', '-')}"
response = self.session.get(url, timeout=5)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'lxml')
price_elem = soup.select_one('[data-test="instrument-price-last"]')
if price_elem:
price_text = price_elem.text.strip().replace(',', '')
price = float(price_text)
self.investing_cache[asset_code] = price
return price
except Exception as e:
print(f"Investing.com 수집 실패 ({asset_code}): {e}")
return None
def fetch_binance(self) -> Optional[float]:
"""바이낸스 BTC/USDT 가격"""
try:
url = "https://api.binance.com/api/v3/ticker/price"
response = self.session.get(url, params={"symbol": "BTCUSDT"}, timeout=5)
response.raise_for_status()
data = response.json()
return float(data["price"]) if "price" in data else None
except Exception as e:
print(f"Binance API 실패: {e}")
return None
def fetch_upbit(self) -> Optional[float]:
"""업비트 BTC/KRW 가격"""
try:
url = "https://api.upbit.com/v1/ticker"
response = self.session.get(url, params={"markets": "KRW-BTC"}, timeout=5)
response.raise_for_status()
data = response.json()
return data[0]["trade_price"] if data and "trade_price" in data[0] else None
except Exception as e:
print(f"Upbit API 실패: {e}")
return None
def fetch_usd_krw(self) -> Optional[float]:
"""USD/KRW 환율"""
try:
url = "https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD"
response = self.session.get(url, timeout=5)
response.raise_for_status()
data = response.json()
return data[0]["basePrice"] if data else None
except Exception as e:
print(f"USD/KRW 수집 실패: {e}")
return None
def fetch_krx_gold(self) -> Optional[float]:
"""한국거래소 금 현물 가격"""
try:
url = "http://www.goldpr.co.kr/gms/default.asp"
response = self.session.get(url, timeout=5)
response.encoding = 'euc-kr'
soup = BeautifulSoup(response.text, 'lxml')
# 금 현물 가격 파싱 (사이트 구조에 따라 조정 필요)
price_elem = soup.select_one('table tr:nth-of-type(2) td:nth-of-type(2)')
if price_elem:
price_text = price_elem.text.strip().replace(',', '').replace('원', '')
return float(price_text)
except Exception as e:
print(f"KRX 금 가격 수집 실패: {e}")
return None
def fetch_all(self) -> Dict[str, Dict]:
"""모든 자산 가격 수집"""
print("📊 데이터 수집 시작...")
# 개별 자산 수집
xau_usd = self.fetch_investing_com("XAU/USD")
xau_cny = self.fetch_investing_com("XAU/CNY")
xau_gbp = self.fetch_investing_com("XAU/GBP")
usd_dxy = self.fetch_investing_com("USD/DXY")
usd_krw = self.fetch_usd_krw()
btc_usd = self.fetch_binance()
btc_krw = self.fetch_upbit()
krx_gold = self.fetch_krx_gold()
# XAU/KRW 계산 (트로이온스 -> 그램당 원화)
xau_krw = None
if xau_usd and usd_krw:
xau_krw = round((xau_usd / 31.1034768) * usd_krw, 0)
results = {
"XAU/USD": {"가격": xau_usd, "단위": "USD/oz"},
"XAU/CNY": {"가격": xau_cny, "단위": "CNY/oz"},
"XAU/GBP": {"가격": xau_gbp, "단위": "GBP/oz"},
"USD/DXY": {"가격": usd_dxy, "단위": "Index"},
"USD/KRW": {"가격": usd_krw, "단위": "KRW"},
"BTC/USD": {"가격": btc_usd, "단위": "USDT"},
"BTC/KRW": {"가격": btc_krw, "단위": "KRW"},
"KRX/GLD": {"가격": krx_gold, "단위": "KRW/g"},
"XAU/KRW": {"가격": xau_krw, "단위": "KRW/g"},
}
print(f"✅ 데이터 수집 완료 (성공: {sum(1 for v in results.values() if v['가격'])}/9)")
return results
# 전역 인스턴스
fetcher = DataFetcher()

View File

@@ -0,0 +1,55 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Text, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
Base = declarative_base()
class Asset(Base):
"""자산 마스터 테이블"""
__tablename__ = "assets"
id = Column(Integer, primary_key=True, index=True)
symbol = Column(String(20), unique=True, nullable=False, index=True)
name = Column(String(100), nullable=False)
category = Column(String(50)) # 귀금속, 암호화폐, 환율 등
created_at = Column(DateTime, default=datetime.utcnow)
# 관계
user_assets = relationship("UserAsset", back_populates="asset")
price_history = relationship("PriceHistory", back_populates="asset")
class UserAsset(Base):
"""사용자 자산 정보"""
__tablename__ = "user_assets"
id = Column(Integer, primary_key=True, index=True)
asset_id = Column(Integer, ForeignKey("assets.id"), nullable=False)
previous_close = Column(Float, default=0.0) # 전일종가
average_price = Column(Float, default=0.0) # 평균매입가
quantity = Column(Float, default=0.0) # 보유량
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 관계
asset = relationship("Asset", back_populates="user_assets")
class PriceHistory(Base):
"""가격 히스토리 (선택적)"""
__tablename__ = "price_history"
id = Column(Integer, primary_key=True, index=True)
asset_id = Column(Integer, ForeignKey("assets.id"), nullable=False)
price = Column(Float, nullable=False)
timestamp = Column(DateTime, default=datetime.utcnow, index=True)
# 관계
asset = relationship("Asset", back_populates="price_history")
class AlertSetting(Base):
"""알림 설정"""
__tablename__ = "alert_settings"
id = Column(Integer, primary_key=True, index=True)
setting_key = Column(String(100), unique=True, nullable=False)
setting_value = Column(Text) # JSON 형식으로 저장
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)