File size: 3,592 Bytes
9e89154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
 * Persists optimization snapshots in the browser so Reports can list past runs
 * (Executive Dashboard, Portfolio Lab, Quantum Engine) with stable IDs.
 * Server-side async lab runs live in SQLite via GET /api/runs — list those separately.
 */

import { mergeOptimizeResponse } from "@/lib/reportExport";

const STORAGE_KEY = "ql-optimization-runs-v1";
const MAX_RUNS = 100;

export type OptimizationRunSource =
  | "executive_dashboard"
  | "portfolio_lab"
  | "quantum_engine";

export interface StoredOptimizationRun {
  id: string;
  at: string;
  source: OptimizationRunSource;
  objective: string;
  tickers: string[];
  constraints: {
    weightMin: number;
    weightMax: number;
    kScreen?: string;
    kSelect?: string;
  };
  /** Raw API snapshot — may be `{ data, meta }`, stringified JSON, or async `{ result }` job output. */
  payload: unknown;
}

function safeParse(raw: string | null): StoredOptimizationRun[] {
  if (!raw) return [];
  try {
    const parsed = JSON.parse(raw) as unknown;
    if (!Array.isArray(parsed)) return [];
    return parsed.filter(
      (x): x is StoredOptimizationRun =>
        x != null &&
        typeof x === "object" &&
        typeof (x as StoredOptimizationRun).id === "string" &&
        typeof (x as StoredOptimizationRun).at === "string"
    );
  } catch {
    return [];
  }
}

export function getOptimizationRuns(): StoredOptimizationRun[] {
  if (typeof window === "undefined") return [];
  return safeParse(localStorage.getItem(STORAGE_KEY));
}

export function getOptimizationRunById(
  id: string
): StoredOptimizationRun | null {
  return getOptimizationRuns().find((r) => r.id === id) ?? null;
}

export function appendOptimizationRun(
  data: Omit<StoredOptimizationRun, "id" | "source">,
  source: OptimizationRunSource
): StoredOptimizationRun {
  const id =
    typeof crypto !== "undefined" && crypto.randomUUID
      ? crypto.randomUUID()
      : `run-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;

  const entry: StoredOptimizationRun = { ...data, id, source };

  if (typeof window === "undefined") return entry;

  const prev = safeParse(localStorage.getItem(STORAGE_KEY));
  const next = [entry, ...prev.filter((r) => r.id !== id)].slice(0, MAX_RUNS);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
  return entry;
}

export function clearOptimizationRuns(): void {
  if (typeof window === "undefined") return;
  localStorage.removeItem(STORAGE_KEY);
}

/** Human-readable source for tables */
export function formatOptimizationSource(s: OptimizationRunSource): string {
  switch (s) {
    case "executive_dashboard":
      return "Executive Dashboard";
    case "portfolio_lab":
      return "Portfolio Lab";
    case "quantum_engine":
      return "Quantum Engine";
    default:
      return s;
  }
}

/** Sharpe / return / vol from merged optimize payload */
export function summarizeStoredPayload(payload: unknown): {
  sharpe: number | null;
  retPct: number | null;
  volPct: number | null;
  nActive: number | null;
} {
  const merged = mergeOptimizeResponse(payload) as Record<string, unknown>;
  const sharpe = merged.sharpe_ratio;
  const ret = merged.expected_return;
  const vol = merged.volatility;
  const n = merged.n_active;
  return {
    sharpe: typeof sharpe === "number" && Number.isFinite(sharpe) ? sharpe : null,
    retPct:
      typeof ret === "number" && Number.isFinite(ret) ? ret * 100 : null,
    volPct:
      typeof vol === "number" && Number.isFinite(vol) ? vol * 100 : null,
    nActive: typeof n === "number" && Number.isFinite(n) ? n : null,
  };
}