Spaces:
Sleeping
Sleeping
feat: optimize Hugging Face Space Docker build and zero-config deployment
Browse files- .dockerignore +14 -0
- Dockerfile +20 -5
- external/Quant.ai/backend/app/config.py +39 -2
- external/Quant.ai/backend/main_api.py +67 -4
- external/Quant.ai/backend/watchlist.json +10 -0
- inferroute/config.py +1 -1
- inferroute/main.py +18 -10
.dockerignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
**/node_modules
|
| 3 |
+
__pycache__
|
| 4 |
+
**/__pycache__
|
| 5 |
+
*.pyc
|
| 6 |
+
*.pyo
|
| 7 |
+
.venv
|
| 8 |
+
venv
|
| 9 |
+
.git
|
| 10 |
+
.pytest_cache
|
| 11 |
+
*.db
|
| 12 |
+
*.log
|
| 13 |
+
.DS_Store
|
| 14 |
+
Thumbs.db
|
Dockerfile
CHANGED
|
@@ -1,4 +1,12 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
FROM python:3.12-slim
|
| 3 |
|
| 4 |
# Set environment variables
|
|
@@ -14,27 +22,34 @@ ENV PYTHONUNBUFFERED=1 \
|
|
| 14 |
# Set working directory
|
| 15 |
WORKDIR /app
|
| 16 |
|
| 17 |
-
# Install system dependencies
|
| 18 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 19 |
build-essential \
|
|
|
|
| 20 |
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
|
| 22 |
# Copy requirements file first for caching
|
| 23 |
COPY requirements.txt .
|
| 24 |
|
| 25 |
-
# Install dependencies
|
| 26 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 27 |
|
| 28 |
-
# Copy
|
| 29 |
COPY inferroute/ ./inferroute/
|
| 30 |
COPY docs/ ./docs/
|
| 31 |
COPY benchmarks/ ./benchmarks/
|
| 32 |
COPY external/ ./external/
|
|
|
|
|
|
|
| 33 |
COPY *.html ./
|
| 34 |
COPY assets/ ./assets/
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
# Expose default port (7860 for Hugging Face Spaces)
|
| 37 |
EXPOSE 7860
|
| 38 |
|
| 39 |
-
#
|
| 40 |
CMD ["sh", "-c", "python -m uvicorn inferroute.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
|
|
|
|
| 1 |
+
# Stage 1: Build React frontend for Quant.ai
|
| 2 |
+
FROM node:18-alpine AS frontend-builder
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY external/Quant.ai/frontend/package*.json ./
|
| 5 |
+
RUN npm install --legacy-peer-deps
|
| 6 |
+
COPY external/Quant.ai/frontend/ ./
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Stage 2: Python 3.12 high-performance inference gateway
|
| 10 |
FROM python:3.12-slim
|
| 11 |
|
| 12 |
# Set environment variables
|
|
|
|
| 22 |
# Set working directory
|
| 23 |
WORKDIR /app
|
| 24 |
|
| 25 |
+
# Install system dependencies
|
| 26 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 27 |
build-essential \
|
| 28 |
+
curl \
|
| 29 |
&& rm -rf /var/lib/apt/lists/*
|
| 30 |
|
| 31 |
# Copy requirements file first for caching
|
| 32 |
COPY requirements.txt .
|
| 33 |
|
| 34 |
+
# Install Python dependencies
|
| 35 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 36 |
|
| 37 |
+
# Copy project source code
|
| 38 |
COPY inferroute/ ./inferroute/
|
| 39 |
COPY docs/ ./docs/
|
| 40 |
COPY benchmarks/ ./benchmarks/
|
| 41 |
COPY external/ ./external/
|
| 42 |
+
COPY platform/ ./platform/
|
| 43 |
+
COPY quant/ ./quant/
|
| 44 |
COPY *.html ./
|
| 45 |
COPY assets/ ./assets/
|
| 46 |
+
COPY *.svg ./
|
| 47 |
+
|
| 48 |
+
# Copy compiled React frontend distribution bundle from Stage 1
|
| 49 |
+
COPY --from=frontend-builder /app/frontend/dist ./external/Quant.ai/frontend/dist
|
| 50 |
|
| 51 |
# Expose default port (7860 for Hugging Face Spaces)
|
| 52 |
EXPOSE 7860
|
| 53 |
|
| 54 |
+
# Launch Uvicorn gateway
|
| 55 |
CMD ["sh", "-c", "python -m uvicorn inferroute.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
|
external/Quant.ai/backend/app/config.py
CHANGED
|
@@ -43,5 +43,42 @@ MARKET_OPEN_FOCUS_START = "09:30" # ๅผ็ๅผๅงๆถ้ด
|
|
| 43 |
MARKET_OPEN_FOCUS_END = "10:15" # ๅผ็็ปๆๆถ้ด (ๅ 45 ๅ้)
|
| 44 |
FORCE_LIQUIDATION_OPEN_FOCUS = "10:30" # ๅผ็็ชๅปๆจกๅผไธ๏ผ10:30 ๅผบๅถๆธ
ไปๅบๅบ๏ผ้ฒๅ็ๆจช็้่กๆ่
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
MARKET_OPEN_FOCUS_END = "10:15" # ๅผ็็ปๆๆถ้ด (ๅ 45 ๅ้)
|
| 44 |
FORCE_LIQUIDATION_OPEN_FOCUS = "10:30" # ๅผ็็ชๅปๆจกๅผไธ๏ผ10:30 ๅผบๅถๆธ
ไปๅบๅบ๏ผ้ฒๅ็ๆจช็้่กๆ่
|
| 45 |
|
| 46 |
+
# ็ญ้จๆฟๅๅ็ฑปๅบ (Hot Sectors Universe)
|
| 47 |
+
HOT_SECTORS = {
|
| 48 |
+
"AI_CHIPS": {
|
| 49 |
+
"name": "๐ฅ AI & ๅๅฏผไฝ็ฎๅ",
|
| 50 |
+
"description": "ไบบๅทฅๆบ่ฝ่ฏ็ใ็ฎๅๆๅกๅจไธๆถๅไปฃๅทฅ้พๅคด",
|
| 51 |
+
"tickers": ["NVDA", "AMD", "AVGO", "TSM", "SMCI"]
|
| 52 |
+
},
|
| 53 |
+
"MEMORY_STORAGE": {
|
| 54 |
+
"name": "๐พ ๅญๅจ่ฏ็ & AI ๅญๅจ",
|
| 55 |
+
"description": "DRAM/NAND/HBM ๅญๅจๅทจๅคดไธไผไธ็บง AI ๅญๅจๅบ็ก่ฎพๆฝ",
|
| 56 |
+
"tickers": ["MU", "WDC", "STX", "PSTG", "NTAP"]
|
| 57 |
+
},
|
| 58 |
+
"AI_POWER_NUCLEAR": {
|
| 59 |
+
"name": "โ๏ธ AI ๆ ธ่ฝ & ็ตๅๅบ็ก่ฎพๆฝ",
|
| 60 |
+
"description": "AIๆฐๆฎไธญๅฟๆ ธ็ตใๆธ
ๆด่ฝๆบไธ็ตๅๅบ็ก่ฎพๆฝ้พๅคด",
|
| 61 |
+
"tickers": ["OKLO", "SMR", "VST", "CEG", "TLN"]
|
| 62 |
+
},
|
| 63 |
+
"QUANTUM_FRONTIER": {
|
| 64 |
+
"name": "๐ฎ ้ๅญ่ฎก็ฎ & AI ่ฝฏไปถ",
|
| 65 |
+
"description": "้ๅญ่ฎก็ฎใๅคงๆฐๆฎๅๆไธ้ซ่ดๅกๅๆฒฟ็งๆ",
|
| 66 |
+
"tickers": ["PLTR", "IONQ", "RGTI"]
|
| 67 |
+
},
|
| 68 |
+
"BIG_TECH": {
|
| 69 |
+
"name": "๐๏ธ ็งๆๅทจๅคด / ๆ ธๅฟๆไป",
|
| 70 |
+
"description": "้ซๆตๅจๆงๅคง็็งๆ่กไธๆ ธๅฟ่ตไบง",
|
| 71 |
+
"tickers": ["TSLA", "AAPL", "MSFT", "META", "GOOGL"]
|
| 72 |
+
},
|
| 73 |
+
"MOMENTUM_CRYPTO": {
|
| 74 |
+
"name": "๐ ้ซๅจ่ฝ & ๅ ๅฏๆฆๅฟต",
|
| 75 |
+
"description": "ๆ้ซ่ดๅก็ณปๆฐใๅผบๆไบค้ไธๅ ๅฏ่ตไบงๆฆๅฟต่ก",
|
| 76 |
+
"tickers": ["MSTR", "COIN", "HOOD", "MARA", "TQQQ"]
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
# ้ป่ฎค็ๆง็็ฒพ็ฎๆ ธๅฟ่ก็ฅจๆฑ ๏ผๆนไพฟ็จๆท่ชไธปๆทปๅ ไธ่ชๅฎไน๏ผ
|
| 81 |
+
WATCHLIST = ["NVDA", "TSLA", "AAPL", "AMD", "MU", "PLTR", "MSTR"]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
external/Quant.ai/backend/main_api.py
CHANGED
|
@@ -32,7 +32,7 @@ def clean_float(val, default=0.0):
|
|
| 32 |
return default
|
| 33 |
|
| 34 |
|
| 35 |
-
from app.config import INITIAL_CASH, WATCHLIST, FORCE_LIQUIDATION_TIME
|
| 36 |
from app.data_manager import fetch_and_prepare_data, get_company_info, calculate_atr, INTERVAL_TO_PERIOD
|
| 37 |
from app.patterns import analyze_patterns
|
| 38 |
from app.simulator import run_backtest_sim
|
|
@@ -87,7 +87,7 @@ def load_persistent_watchlist() -> list:
|
|
| 87 |
return [str(t).upper() for t in data]
|
| 88 |
except Exception:
|
| 89 |
pass
|
| 90 |
-
return ["
|
| 91 |
|
| 92 |
def save_persistent_watchlist(watchlist: list):
|
| 93 |
try:
|
|
@@ -106,9 +106,72 @@ class WatchlistModifyRequest(BaseModel):
|
|
| 106 |
@app.get("/api/watchlist")
|
| 107 |
def get_watchlist_data():
|
| 108 |
"""
|
| 109 |
-
่ทๅ่ช้่กๆฑ ็ๅ่กจ๏ผๆฏๆๆไน
ๅ๏ผ
|
| 110 |
"""
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
@app.post("/api/watchlist/add")
|
| 114 |
def add_watchlist_ticker(req: WatchlistModifyRequest):
|
|
|
|
| 32 |
return default
|
| 33 |
|
| 34 |
|
| 35 |
+
from app.config import INITIAL_CASH, WATCHLIST, FORCE_LIQUIDATION_TIME, HOT_SECTORS
|
| 36 |
from app.data_manager import fetch_and_prepare_data, get_company_info, calculate_atr, INTERVAL_TO_PERIOD
|
| 37 |
from app.patterns import analyze_patterns
|
| 38 |
from app.simulator import run_backtest_sim
|
|
|
|
| 87 |
return [str(t).upper() for t in data]
|
| 88 |
except Exception:
|
| 89 |
pass
|
| 90 |
+
return ["NVDA", "TSLA", "AAPL", "AMD", "MU", "PLTR", "MSTR"]
|
| 91 |
|
| 92 |
def save_persistent_watchlist(watchlist: list):
|
| 93 |
try:
|
|
|
|
| 106 |
@app.get("/api/watchlist")
|
| 107 |
def get_watchlist_data():
|
| 108 |
"""
|
| 109 |
+
่ทๅ่ช้่กๆฑ ็ๅ่กจ๏ผๆฏๆๆไน
ๅ + ่ชๅจๆ ่ฎฐๆไป็ถๆไธ็ญ้จๆฟๅๅ
ๆฐๆฎ๏ผ
|
| 110 |
"""
|
| 111 |
+
open_positions = []
|
| 112 |
+
try:
|
| 113 |
+
positions_info = live_runner.alpaca.get_open_positions()
|
| 114 |
+
open_positions = [p["ticker"].upper() for p in positions_info]
|
| 115 |
+
except Exception:
|
| 116 |
+
pass
|
| 117 |
+
|
| 118 |
+
enriched_watchlist = []
|
| 119 |
+
for ticker in WATCHLIST:
|
| 120 |
+
# ๅคๆญๆๅฑ็ญ้จๆฟๅ
|
| 121 |
+
category = "ๅ
ถไป"
|
| 122 |
+
for sec_key, sec_data in HOT_SECTORS.items():
|
| 123 |
+
if ticker in sec_data["tickers"]:
|
| 124 |
+
category = sec_data["name"]
|
| 125 |
+
break
|
| 126 |
+
|
| 127 |
+
enriched_watchlist.append({
|
| 128 |
+
"ticker": ticker,
|
| 129 |
+
"is_holding": ticker in open_positions,
|
| 130 |
+
"category": category
|
| 131 |
+
})
|
| 132 |
+
|
| 133 |
+
return {
|
| 134 |
+
"watchlist": WATCHLIST,
|
| 135 |
+
"details": enriched_watchlist,
|
| 136 |
+
"total": len(WATCHLIST),
|
| 137 |
+
"holdings_count": len([item for item in enriched_watchlist if item["is_holding"]])
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
@app.post("/api/watchlist/sync_holdings")
|
| 141 |
+
def sync_holdings_to_watchlist():
|
| 142 |
+
"""
|
| 143 |
+
ไธ้ฎๅๆญฅๅทฒๆฅๆ็่ก็ฅจ๏ผๆไป๏ผๅฐ Watchlist
|
| 144 |
+
"""
|
| 145 |
+
try:
|
| 146 |
+
positions_info = live_runner.alpaca.get_open_positions()
|
| 147 |
+
added_count = 0
|
| 148 |
+
for p in positions_info:
|
| 149 |
+
t = p["ticker"].upper()
|
| 150 |
+
if t not in WATCHLIST:
|
| 151 |
+
WATCHLIST.append(t)
|
| 152 |
+
added_count += 1
|
| 153 |
+
if added_count > 0:
|
| 154 |
+
save_persistent_watchlist(WATCHLIST)
|
| 155 |
+
live_runner.active_tickers = WATCHLIST.copy()
|
| 156 |
+
return {
|
| 157 |
+
"success": True,
|
| 158 |
+
"added_count": added_count,
|
| 159 |
+
"watchlist": WATCHLIST,
|
| 160 |
+
"message": f"ๆๅไป็ฐๆๆไปๅๆญฅ {added_count} ๅช่ก็ฅจๅฐ Watchlist ็ๆงๆฑ ใ"
|
| 161 |
+
}
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return {"success": False, "error": f"ๅๆญฅๆไปๅคฑ่ดฅ: {str(e)}"}
|
| 164 |
+
|
| 165 |
+
@app.get("/api/watchlist/hot_sectors")
|
| 166 |
+
def get_hot_sectors_universe():
|
| 167 |
+
"""
|
| 168 |
+
่ทๅๆจ่็ญ้จๆฟๅๅบ (AI็ฎๅใ็งๆๅทจๅคดใๅ ๅฏๅจ่ฝ็ญ)
|
| 169 |
+
"""
|
| 170 |
+
return {
|
| 171 |
+
"success": True,
|
| 172 |
+
"hot_sectors": HOT_SECTORS,
|
| 173 |
+
"recommended_tickers": ["NVDA", "TSLA", "PLTR", "AMD", "AVGO", "MSTR", "MSFT", "AAPL", "COIN", "TQQQ"]
|
| 174 |
+
}
|
| 175 |
|
| 176 |
@app.post("/api/watchlist/add")
|
| 177 |
def add_watchlist_ticker(req: WatchlistModifyRequest):
|
external/Quant.ai/backend/watchlist.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
"NVDA",
|
| 3 |
+
"TSLA",
|
| 4 |
+
"AAPL",
|
| 5 |
+
"AMD",
|
| 6 |
+
"MU",
|
| 7 |
+
"PLTR",
|
| 8 |
+
"CRCL",
|
| 9 |
+
"MSTR"
|
| 10 |
+
]
|
inferroute/config.py
CHANGED
|
@@ -8,7 +8,7 @@ class Settings(BaseSettings):
|
|
| 8 |
|
| 9 |
# โโ Databases โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 10 |
DATABASE_URL: str = Field(
|
| 11 |
-
default="
|
| 12 |
validation_alias="DATABASE_URL"
|
| 13 |
)
|
| 14 |
REDIS_URL: str = Field(
|
|
|
|
| 8 |
|
| 9 |
# โโ Databases โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 10 |
DATABASE_URL: str = Field(
|
| 11 |
+
default="sqlite+aiosqlite:///./inferroute.db",
|
| 12 |
validation_alias="DATABASE_URL"
|
| 13 |
)
|
| 14 |
REDIS_URL: str = Field(
|
inferroute/main.py
CHANGED
|
@@ -76,21 +76,29 @@ concurrency_limiter = AdaptiveConcurrencyLimiter()
|
|
| 76 |
@asynccontextmanager
|
| 77 |
async def lifespan(app: FastAPI):
|
| 78 |
logger.info("Starting InferRoute gatewayโฆ")
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
auth.redis_client = redis_client
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
logger.info("InferRoute gateway ready.")
|
| 89 |
yield
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
|
| 96 |
# โโ App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 76 |
@asynccontextmanager
|
| 77 |
async def lifespan(app: FastAPI):
|
| 78 |
logger.info("Starting InferRoute gatewayโฆ")
|
| 79 |
+
try:
|
| 80 |
+
await init_db()
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.warning(f"Database init warning (running in degraded/mock mode): {e}")
|
|
|
|
| 83 |
|
| 84 |
+
try:
|
| 85 |
+
import inferroute.auth as auth
|
| 86 |
+
redis_client = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
|
| 87 |
+
auth.redis_client = redis_client
|
| 88 |
+
circuit_breaker.initialize_circuit_breakers(redis_client, list(ADAPTERS.keys()))
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.warning(f"Redis initialization skipped ({e}). Gateway running in standalone mode.")
|
| 91 |
|
| 92 |
logger.info("InferRoute gateway ready.")
|
| 93 |
yield
|
| 94 |
|
| 95 |
+
try:
|
| 96 |
+
import inferroute.auth as auth
|
| 97 |
+
if auth.redis_client:
|
| 98 |
+
await auth.redis_client.aclose()
|
| 99 |
+
logger.info("Redis connection closed.")
|
| 100 |
+
except Exception:
|
| 101 |
+
pass
|
| 102 |
|
| 103 |
|
| 104 |
# โโ App โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|