veteroner commited on
Commit
93e16c3
·
1 Parent(s): f1dfac9

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 { readFile } from 'fs/promises'
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')