Skip to content

Commit 0435777

Browse files
authored
fix(parity): align local WFA rendering with cloud outputs (#13)
1 parent 9635461 commit 0435777

11 files changed

Lines changed: 984 additions & 56 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"test:coverage": "vitest run -c vitest.config.ts --coverage",
3737
"engine:check:boundary": "node scripts/check-engine-boundary.mjs",
3838
"engine:check:bundle": "node scripts/check-open-core-bundles.mjs",
39+
"bench:parity": "node scripts/bench-local-cloud-parity.mjs",
3940
"engine:validate": "npm run lint && npm run test && npm run engine:check:boundary && npm run engine:check:bundle",
4041
"engine:examples:generate-samples": "node docs/examples/scripts/generate-samples.mjs",
4142
"engine:examples:generate-monte-carlo-fixture": "node docs/examples/scripts/generate-monte-carlo-seed42.mjs",

packages/cli/src/commands/ui.ts

Lines changed: 321 additions & 9 deletions
Large diffs are not rendered by default.

packages/cli/web/src/legacy/wizard/KiploksWorkspacePanel.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,14 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
157157
setReportTitleTouched(true);
158158
}, [ctx.integration, ctx.selectedBacktestArtifactKey, ctx.backtestArtifacts, previousSelectedBacktestArtifactKey, reportTitleDraft]);
159159

160-
const cloudTokenMissing =
161-
ctx.kiploksUi?.apiTarget === "cloud" && String(ctx.kiploksUi?.config?.api_token ?? "").trim() === "";
160+
const cloudTokenMissing = (ctx.kiploksUi?.apiTarget === "cloud" || ctx.kiploksUi?.apiTarget === "custom") &&
161+
String(ctx.kiploksUi?.config?.api_token ?? "").trim() === "";
162+
const customApiUrl = String(ctx.kiploksUi?.config?.api_url ?? "").trim();
163+
const customApiUrlInvalid = ctx.kiploksUi?.apiTarget === "custom" && !/^https?:\/\//i.test(customApiUrl);
162164
const cfg = ctx.kiploksUi?.config ?? {};
163165
const fieldValid = {
164-
apiToken: ctx.kiploksUi?.apiTarget === "cloud" ? hasText(cfg.api_token) : true,
166+
apiToken: ctx.kiploksUi?.apiTarget === "cloud" || ctx.kiploksUi?.apiTarget === "custom" ? hasText(cfg.api_token) : true,
167+
customApiUrl: ctx.kiploksUi?.apiTarget === "custom" ? /^https?:\/\//i.test(String(cfg.api_url ?? "").trim()) : true,
165168
topN: isPositiveInt(cfg.top_n),
166169
skipAlreadyUploaded: typeof cfg.skip_already_uploaded === "boolean",
167170
wfaPeriods: isPositiveInt(cfg.wfaPeriods),
@@ -336,7 +339,12 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
336339
) : null}
337340
{cloudTokenMissing ? (
338341
<p className="mt-1 text-xs leading-relaxed text-amber-300/90">
339-
Cloud target is selected, but api_token is empty. Fill API key in kiploks.json settings before Run Integration.
342+
Cloud or Custom target is selected, but api_token is empty. Fill API key in kiploks.json settings before Run Integration.
343+
</p>
344+
) : null}
345+
{customApiUrlInvalid ? (
346+
<p className="mt-1 text-xs leading-relaxed text-amber-300/90">
347+
Custom target is selected, but api_url must start with http:// or https://.
340348
</p>
341349
) : null}
342350
{(() => {
@@ -356,6 +364,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
356364
!ctx.canRunIntegration ||
357365
ctx.hasKiploksChanges ||
358366
cloudTokenMissing ||
367+
customApiUrlInvalid ||
359368
integrationSubmitting ||
360369
ctx.activeIntegrationJob?.status === "queued" ||
361370
ctx.activeIntegrationJob?.status === "running"
@@ -370,6 +379,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
370379
disabled={
371380
!ctx.hasPathForIntegration ||
372381
cloudTokenMissing ||
382+
customApiUrlInvalid ||
373383
integrationSubmitting ||
374384
ctx.activeIntegrationJob?.status === "queued" ||
375385
ctx.activeIntegrationJob?.status === "running"
@@ -385,6 +395,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
385395
disabled={
386396
!ctx.hasPathForIntegration ||
387397
cloudTokenMissing ||
398+
customApiUrlInvalid ||
388399
integrationSubmitting ||
389400
ctx.activeIntegrationJob?.status === "queued" ||
390401
ctx.activeIntegrationJob?.status === "running"
@@ -416,6 +427,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
416427
Local (UI {String(ctx.kiploksUi.localApiBaseUrl || "")} · Docker {String(ctx.kiploksUi.localApiDockerBaseUrl || "")})
417428
</option>
418429
<option value="cloud">https://kiploks.com/</option>
430+
<option value="custom">Custom URL</option>
419431
</select>
420432
{ctx.kiploksUi.apiTarget === "local" ? (
421433
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
@@ -442,6 +454,33 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
442454
</p>
443455
</>
444456
) : null}
457+
{ctx.kiploksUi.apiTarget === "custom" ? (
458+
<>
459+
<FieldLabelWithStatus label="custom_api_url" ok={fieldValid.customApiUrl} />
460+
<input
461+
className={oc.input}
462+
type="text"
463+
autoComplete="off"
464+
spellCheck={false}
465+
value={String(ctx.kiploksUi.config.api_url ?? "")}
466+
onChange={(e) => ctx.setKiploksField("api_url", e.target.value)}
467+
placeholder="http(s)://your-server.example"
468+
/>
469+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
470+
Save writes this value to kiploks.json as api_url and integration runs will upload to this server.
471+
</p>
472+
<FieldLabelWithStatus label="api_token" ok={fieldValid.apiToken} />
473+
<input
474+
className={oc.input}
475+
type="text"
476+
autoComplete="off"
477+
spellCheck={false}
478+
value={String(ctx.kiploksUi.config.api_token ?? "")}
479+
onChange={(e) => ctx.setKiploksField("api_token", e.target.value)}
480+
placeholder="Paste API key for custom Kiploks server"
481+
/>
482+
</>
483+
) : null}
445484

446485
{ctx.integration === "freqtrade" ? (
447486
<div className="mt-3 space-y-4">
@@ -645,7 +684,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
645684
}
646685
})();
647686
}}
648-
disabled={!ctx.kiploksUi || !ctx.hasKiploksChanges}
687+
disabled={!ctx.kiploksUi || !ctx.hasKiploksChanges || customApiUrlInvalid}
649688
>
650689
Save kiploks.json
651690
</button>

packages/cli/web/src/legacy/wizard/useOrchestratorApp.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ export function useOrchestratorApp() {
267267
if (!prev) return prev;
268268
const nextCfg = { ...prev.config };
269269
if (v === "local") nextCfg.api_token = "";
270+
if (v === "custom") nextCfg.api_url = "";
270271
return { ...prev, apiTarget: v, config: nextCfg };
271272
});
272273
};
@@ -286,7 +287,8 @@ export function useOrchestratorApp() {
286287
payload.hyperopt_loss = c.hyperopt_loss;
287288
payload.hyperopt_result_path = c.hyperopt_result_path;
288289
payload.keep_last_n_backtest_files = c.keep_last_n_backtest_files;
289-
if (kiploksUi.apiTarget === "cloud") payload.api_token = c.api_token || "";
290+
if (kiploksUi.apiTarget === "cloud" || kiploksUi.apiTarget === "custom") payload.api_token = c.api_token || "";
291+
if (kiploksUi.apiTarget === "custom") payload.custom_api_url = c.api_url || "";
290292
} else {
291293
payload.backtesting_path = c.backtesting_path;
292294
payload.top_n = c.top_n;
@@ -295,7 +297,8 @@ export function useOrchestratorApp() {
295297
payload.wfaISSize = c.wfaISSize;
296298
payload.wfaOOSSize = c.wfaOOSSize;
297299
payload.skip_already_uploaded = c.skip_already_uploaded;
298-
if (kiploksUi.apiTarget === "cloud") payload.api_token = c.api_token || "";
300+
if (kiploksUi.apiTarget === "cloud" || kiploksUi.apiTarget === "custom") payload.api_token = c.api_token || "";
301+
if (kiploksUi.apiTarget === "custom") payload.custom_api_url = c.api_url || "";
299302
}
300303
await api.post("/integrations/kiploks-config", payload);
301304
await loadKiploksConfig();

packages/cli/web/src/shell/report/ReportBlocksView.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
233233
const pro = asObj(lite.proBenchmarkMetrics);
234234
const wfa = asObj(lite.walkForwardAnalysis);
235235
const sens = asObj(lite.parameterSensitivity);
236+
const deploymentStatusGlobal = str(sens?.deploymentStatus);
237+
const deploymentRejectedGlobal = /REJECT|FAIL/i.test(deploymentStatusGlobal || "");
236238
const turnover = asObj(lite.turnoverAndCostDrag);
237239
const risk = asObj(lite.riskAnalysis);
238240
const actionPlan = asObj(lite.strategyActionPlan);
@@ -370,13 +372,16 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
370372
{robustRows.map((r) => {
371373
const v = typeof r.value === "number" ? Math.round(r.value) : null;
372374
const isBlocked = v != null && v <= 0;
375+
const isStabilityRow = r.key === "stability";
373376
const tone = scoreTone(v);
377+
const effectiveTone =
378+
isStabilityRow && deploymentRejectedGlobal && tone === "good" ? "warn" : tone;
374379
const weight =
375380
r.key === "validation" ? 40 : r.key === "risk" ? 30 : r.key === "stability" ? 20 : r.key === "execution" ? 10 : 0;
376381
const barClass =
377-
tone === "good"
382+
effectiveTone === "good"
378383
? "text-emerald-400"
379-
: tone === "warn"
384+
: effectiveTone === "warn"
380385
? "text-amber-300"
381386
: "text-rose-400";
382387
return (
@@ -389,6 +394,14 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
389394
>
390395
<p className="font-medium text-foreground">
391396
{r.label} <span className="text-muted-foreground">({weight}%)</span>
397+
{isStabilityRow ? (
398+
<span
399+
className="ml-1 cursor-help text-[10px] text-muted-foreground underline decoration-dotted"
400+
title="Advisory metric: Parameter Stability reflects local sensitivity behavior and does not override deployment gates (Performance Decay, Risk Class, hard blockers)."
401+
>
402+
[?]
403+
</span>
404+
) : null}
392405
{isBlocked ? <span className="font-semibold text-rose-400"> (blocking)</span> : null}
393406
</p>
394407
<div className="flex items-center gap-2">
@@ -398,7 +411,9 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
398411
<p className={"leading-tight " + (isBlocked ? "font-medium text-rose-400" : "text-muted-foreground")}>
399412
{isBlocked
400413
? "→ BLOCKED"
401-
: r.key === "stability"
414+
: isStabilityRow && deploymentRejectedGlobal
415+
? "→ Parameters stable in isolation, but deployment is blocked by audit gates"
416+
: r.key === "stability"
402417
? "→ Parameters stable across sensitivity tests"
403418
: "→ Within threshold"}
404419
</p>
@@ -748,16 +763,28 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
748763
const row = asObj(p);
749764
const sensitivity = num(row?.sensitivity);
750765
const status = str(row?.status) || (sensitivity != null && sensitivity >= 0.6 ? "Fragile" : "Stable");
766+
const topology =
767+
str(row?.topology) ||
768+
str(row?.displayLabel) ||
769+
(sensitivity == null
770+
? "n/a"
771+
: sensitivity >= 0.6
772+
? "Sharp peak"
773+
: sensitivity >= 0.4
774+
? "Moderate"
775+
: "Flat");
751776
const statusCls =
752777
/FRAGILE|HIGH/i.test(status) ? "text-rose-400" : /TUNING|MODERATE/i.test(status) ? "text-amber-300" : "text-emerald-400";
778+
const statusIcon =
779+
/FRAGILE|HIGH/i.test(status) ? "🔴" : /TUNING|MODERATE/i.test(status) ? "🟡" : "🟢";
753780
return (
754781
<div key={idx} className="space-y-1">
755782
<div className="grid grid-cols-[minmax(120px,1fr)_70px_64px_80px_100px] items-center gap-3">
756783
<div className="truncate">{str(row?.name) || `param_${idx + 1}`}</div>
757784
<div className="text-right">{str(row?.optimal) || num(row?.optimal) || "n/a"}</div>
758-
<div className="text-right text-muted-foreground">~</div>
785+
<div className="text-right text-muted-foreground">{topology}</div>
759786
<div className="text-right">{asNum(sensitivity, 2)}</div>
760-
<div className={"text-right font-semibold " + statusCls}>🟢 {status}</div>
787+
<div className={"text-right font-semibold " + statusCls}>{statusIcon} {status}</div>
761788
</div>
762789
<div className="text-xs text-muted-foreground">Suggested Mitigation: {str(row?.mitigation) || "Risk Neutral"}</div>
763790
</div>
@@ -794,21 +821,39 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
794821

795822
<div className="space-y-2 border-t border-dashed border-border pt-2 text-xs">
796823
<p className="font-semibold">AUDIT VERDICT</p>
824+
{(() => {
825+
const deploymentStatus = str(sens.deploymentStatus) || "APPROVED (no Decay check)";
826+
const deploymentCls = /REJECT|FAIL/i.test(deploymentStatus)
827+
? "text-rose-400"
828+
: /HOLD|WARN|CAUTION/i.test(deploymentStatus)
829+
? "text-amber-300"
830+
: "text-emerald-400";
831+
const riskClass = str(sens.riskClass) || "LOW";
832+
const riskClassCls = /HIGH|REJECT|FAIL/i.test(riskClass)
833+
? "text-rose-400"
834+
: /MODERATE|WARN|CAUTION/i.test(riskClass)
835+
? "text-amber-300"
836+
: "text-emerald-400";
837+
return (
838+
<>
797839
<p className="text-muted-foreground">
798840
Deployment Status:{" "}
799-
<span className="font-semibold text-emerald-400">{str(sens.deploymentStatus) || "APPROVED (no Decay check)"}</span>
841+
<span className={"font-semibold " + deploymentCls}>{deploymentStatus}</span>
800842
</p>
801843
<p className="text-amber-300">Performance Decay: {str(sens.performanceDecayNote) || "n/a (min 3 periods required for decay check)."}</p>
802844
<p className="text-muted-foreground">
803845
Risk Score:{" "}
804846
<span className="text-muted-foreground/90">
805847
{str(sens.riskScoreFormula) || `Base ${num(sens.baseScore) ?? DISPLAY_NA} - Penalty ${num(sens.penalty) ?? DISPLAY_NA} ->`}
806848
</span>{" "}
807-
<span className="font-semibold text-emerald-400">
808-
{str(sens.riskClass) || "LOW"} ({num(sens.riskScore) ?? DISPLAY_NA}/100)
849+
<span className={"font-semibold " + riskClassCls}>
850+
{riskClass} ({num(sens.riskScore) ?? DISPLAY_NA}/100)
809851
</span>
810852
</p>
811853
<p className="text-muted-foreground">Pro-Note: {str(sens.proNote) || "Highest sensitivity parameter shown in table."}</p>
854+
</>
855+
);
856+
})()}
812857
</div>
813858
</AnalysisBlockCardLite>
814859
) : null}

0 commit comments

Comments
 (0)