fix: /api/eligible 404 - add HF Space endpoint + proxy
Browse files- HuggingFace Space: added GET /api/eligible endpoint
- Next.js: proxy /api/eligible to HF Space on production
- Reads paper_trading/bist100_scan_results.json
- Returns eligible stocks sorted by Sharpe ratio
- Fixes auto-trading page 'Tek Döngü Çalıştır' button
huggingface-space/app.py
CHANGED
|
@@ -1051,6 +1051,91 @@ def trading_action(req: TradingActionRequest):
|
|
| 1051 |
raise HTTPException(status_code=400, detail=f"Unknown action: {req.action}")
|
| 1052 |
|
| 1053 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
# For Hugging Face Spaces
|
| 1055 |
if __name__ == "__main__":
|
| 1056 |
import uvicorn
|
|
|
|
| 1051 |
raise HTTPException(status_code=400, detail=f"Unknown action: {req.action}")
|
| 1052 |
|
| 1053 |
|
| 1054 |
+
@app.get("/api/eligible")
|
| 1055 |
+
def get_eligible_stocks():
|
| 1056 |
+
"""
|
| 1057 |
+
GET /api/eligible
|
| 1058 |
+
Returns list of eligible stocks from BIST100 scan results
|
| 1059 |
+
"""
|
| 1060 |
+
scan_file = Path("paper_trading/bist100_scan_results.json")
|
| 1061 |
+
|
| 1062 |
+
if not scan_file.exists():
|
| 1063 |
+
return {
|
| 1064 |
+
"ok": False,
|
| 1065 |
+
"error": "Scan results not found. Run BIST100 scan first.",
|
| 1066 |
+
"eligible": [],
|
| 1067 |
+
"excluded": [],
|
| 1068 |
+
"stage1Failures": [],
|
| 1069 |
+
"stage1PassedCount": 0,
|
| 1070 |
+
"summary": {
|
| 1071 |
+
"eligibleCount": 0,
|
| 1072 |
+
"excludedCount": 0,
|
| 1073 |
+
"stage1FailedCount": 0,
|
| 1074 |
+
"avgSharpe": 0,
|
| 1075 |
+
"avgReturn": 0,
|
| 1076 |
+
"avgHitRate": 0,
|
| 1077 |
+
},
|
| 1078 |
+
}
|
| 1079 |
+
|
| 1080 |
+
try:
|
| 1081 |
+
with open(scan_file, "r", encoding="utf-8") as f:
|
| 1082 |
+
data = json.load(f)
|
| 1083 |
+
|
| 1084 |
+
stage2 = data.get("stage2", {})
|
| 1085 |
+
eligible = []
|
| 1086 |
+
excluded = []
|
| 1087 |
+
|
| 1088 |
+
for symbol, info in stage2.items():
|
| 1089 |
+
if info.get("eligible"):
|
| 1090 |
+
eligible.append({
|
| 1091 |
+
"symbol": symbol,
|
| 1092 |
+
"eligible": True,
|
| 1093 |
+
"sharpe": info.get("sharpe", 0),
|
| 1094 |
+
"annual_return": info.get("annual_return", 0),
|
| 1095 |
+
"hit_rate": info.get("hit_rate", 0),
|
| 1096 |
+
"quality": info.get("quality", ""),
|
| 1097 |
+
})
|
| 1098 |
+
else:
|
| 1099 |
+
excluded.append({
|
| 1100 |
+
"symbol": symbol,
|
| 1101 |
+
"reason": info.get("reason", info.get("quality", "Yetersiz kalite")),
|
| 1102 |
+
"sharpe": info.get("sharpe"),
|
| 1103 |
+
})
|
| 1104 |
+
|
| 1105 |
+
# Sort eligible by sharpe descending
|
| 1106 |
+
eligible.sort(key=lambda x: x.get("sharpe", 0), reverse=True)
|
| 1107 |
+
excluded.sort(key=lambda x: x["symbol"])
|
| 1108 |
+
|
| 1109 |
+
stage1_failures = [
|
| 1110 |
+
{"symbol": sym, "reason": reason}
|
| 1111 |
+
for sym, reason in data.get("stage1", {}).get("failed", {}).items()
|
| 1112 |
+
]
|
| 1113 |
+
|
| 1114 |
+
avg_sharpe = sum(e.get("sharpe", 0) for e in eligible) / len(eligible) if eligible else 0
|
| 1115 |
+
avg_return = sum(e.get("annual_return", 0) for e in eligible) / len(eligible) if eligible else 0
|
| 1116 |
+
avg_hit_rate = sum(e.get("hit_rate", 0) for e in eligible) / len(eligible) if eligible else 0
|
| 1117 |
+
|
| 1118 |
+
return {
|
| 1119 |
+
"ok": True,
|
| 1120 |
+
"eligible": eligible,
|
| 1121 |
+
"excluded": excluded,
|
| 1122 |
+
"stage1Failures": stage1_failures,
|
| 1123 |
+
"stage1PassedCount": len(data.get("stage1", {}).get("passed", [])),
|
| 1124 |
+
"summary": {
|
| 1125 |
+
"eligibleCount": len(eligible),
|
| 1126 |
+
"excludedCount": len(excluded),
|
| 1127 |
+
"stage1FailedCount": len(stage1_failures),
|
| 1128 |
+
"avgSharpe": round(avg_sharpe, 3),
|
| 1129 |
+
"avgReturn": round(avg_return, 2),
|
| 1130 |
+
"avgHitRate": round(avg_hit_rate, 2),
|
| 1131 |
+
},
|
| 1132 |
+
"timestamp": data.get("timestamp"),
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
except Exception as e:
|
| 1136 |
+
return {"ok": False, "error": str(e)}
|
| 1137 |
+
|
| 1138 |
+
|
| 1139 |
# For Hugging Face Spaces
|
| 1140 |
if __name__ == "__main__":
|
| 1141 |
import uvicorn
|
nextjs-app/src/app/api/eligible/route.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
import { NextResponse } from 'next/server'
|
| 2 |
-
import {
|
| 3 |
-
import { join } from 'path'
|
| 4 |
import { requireAuth } from '@/lib/api-auth'
|
| 5 |
|
| 6 |
export const dynamic = 'force-dynamic'
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
interface ScanStage2 {
|
| 9 |
eligible: boolean
|
| 10 |
sharpe: number
|
|
@@ -23,18 +28,54 @@ interface ScanResults {
|
|
| 23 |
timestamp?: string
|
| 24 |
}
|
| 25 |
|
| 26 |
-
export async function GET() {
|
| 27 |
-
const auth = await requireAuth()
|
| 28 |
if (!auth.authenticated) return auth.response
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
const filePath = join(process.cwd(), '..', 'paper_trading', 'bist100_scan_results.json')
|
| 32 |
let raw: string
|
| 33 |
|
| 34 |
try {
|
| 35 |
raw = await readFile(filePath, 'utf-8')
|
| 36 |
} catch {
|
| 37 |
-
// Try alternative path
|
| 38 |
const altPath = join(process.cwd(), 'paper_trading', 'bist100_scan_results.json')
|
| 39 |
try {
|
| 40 |
raw = await readFile(altPath, 'utf-8')
|
|
|
|
| 1 |
import { NextResponse } from 'next/server'
|
| 2 |
+
import { apiUrl, API_BASE } from '@/lib/runtime-config'
|
|
|
|
| 3 |
import { requireAuth } from '@/lib/api-auth'
|
| 4 |
|
| 5 |
export const dynamic = 'force-dynamic'
|
| 6 |
|
| 7 |
+
/**
|
| 8 |
+
* On production (Netlify), proxy to HuggingFace Space.
|
| 9 |
+
* On local dev, fall back to filesystem-based logic.
|
| 10 |
+
*/
|
| 11 |
+
const isProduction = !!API_BASE
|
| 12 |
+
|
| 13 |
interface ScanStage2 {
|
| 14 |
eligible: boolean
|
| 15 |
sharpe: number
|
|
|
|
| 28 |
timestamp?: string
|
| 29 |
}
|
| 30 |
|
| 31 |
+
export async function GET(request: Request) {
|
| 32 |
+
const auth = await requireAuth(request)
|
| 33 |
if (!auth.authenticated) return auth.response
|
| 34 |
|
| 35 |
+
// Production: proxy to HuggingFace Space
|
| 36 |
+
if (isProduction) {
|
| 37 |
+
try {
|
| 38 |
+
const controller = new AbortController()
|
| 39 |
+
const timeout = setTimeout(() => controller.abort(), 30000)
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
const resp = await fetch(apiUrl('/api/eligible'), {
|
| 43 |
+
headers: { accept: 'application/json' },
|
| 44 |
+
signal: controller.signal,
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
if (!resp.ok) {
|
| 48 |
+
const text = await resp.text().catch(() => '')
|
| 49 |
+
return NextResponse.json(
|
| 50 |
+
{ ok: false, error: `Eligible backend error (HTTP ${resp.status}): ${text}` },
|
| 51 |
+
{ status: 502 }
|
| 52 |
+
)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const data = await resp.json()
|
| 56 |
+
return NextResponse.json(data)
|
| 57 |
+
} finally {
|
| 58 |
+
clearTimeout(timeout)
|
| 59 |
+
}
|
| 60 |
+
} catch (e: unknown) {
|
| 61 |
+
const msg = e instanceof Error ? e.message : 'Unknown'
|
| 62 |
+
return NextResponse.json({ ok: false, error: `Eligible proxy failed: ${msg}` }, { status: 502 })
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Local dev: read from filesystem
|
| 67 |
try {
|
| 68 |
+
/* eslint-disable */
|
| 69 |
+
const { readFile } = require('fs/promises')
|
| 70 |
+
const { join } = require('path')
|
| 71 |
+
/* eslint-enable */
|
| 72 |
+
|
| 73 |
const filePath = join(process.cwd(), '..', 'paper_trading', 'bist100_scan_results.json')
|
| 74 |
let raw: string
|
| 75 |
|
| 76 |
try {
|
| 77 |
raw = await readFile(filePath, 'utf-8')
|
| 78 |
} catch {
|
|
|
|
| 79 |
const altPath = join(process.cwd(), 'paper_trading', 'bist100_scan_results.json')
|
| 80 |
try {
|
| 81 |
raw = await readFile(altPath, 'utf-8')
|