Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"test:coverage": "vitest run -c vitest.config.ts --coverage",
"engine:check:boundary": "node scripts/check-engine-boundary.mjs",
"engine:check:bundle": "node scripts/check-open-core-bundles.mjs",
"bench:parity": "node scripts/bench-local-cloud-parity.mjs",
"engine:validate": "npm run lint && npm run test && npm run engine:check:boundary && npm run engine:check:bundle",
"engine:examples:generate-samples": "node docs/examples/scripts/generate-samples.mjs",
"engine:examples:generate-monte-carlo-fixture": "node docs/examples/scripts/generate-monte-carlo-seed42.mjs",
Expand Down
330 changes: 321 additions & 9 deletions packages/cli/src/commands/ui.ts

Large diffs are not rendered by default.

49 changes: 44 additions & 5 deletions packages/cli/web/src/legacy/wizard/KiploksWorkspacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,14 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
setReportTitleTouched(true);
}, [ctx.integration, ctx.selectedBacktestArtifactKey, ctx.backtestArtifacts, previousSelectedBacktestArtifactKey, reportTitleDraft]);

const cloudTokenMissing =
ctx.kiploksUi?.apiTarget === "cloud" && String(ctx.kiploksUi?.config?.api_token ?? "").trim() === "";
const cloudTokenMissing = (ctx.kiploksUi?.apiTarget === "cloud" || ctx.kiploksUi?.apiTarget === "custom") &&
String(ctx.kiploksUi?.config?.api_token ?? "").trim() === "";
const customApiUrl = String(ctx.kiploksUi?.config?.api_url ?? "").trim();
const customApiUrlInvalid = ctx.kiploksUi?.apiTarget === "custom" && !/^https?:\/\//i.test(customApiUrl);
const cfg = ctx.kiploksUi?.config ?? {};
const fieldValid = {
apiToken: ctx.kiploksUi?.apiTarget === "cloud" ? hasText(cfg.api_token) : true,
apiToken: ctx.kiploksUi?.apiTarget === "cloud" || ctx.kiploksUi?.apiTarget === "custom" ? hasText(cfg.api_token) : true,
customApiUrl: ctx.kiploksUi?.apiTarget === "custom" ? /^https?:\/\//i.test(String(cfg.api_url ?? "").trim()) : true,
topN: isPositiveInt(cfg.top_n),
skipAlreadyUploaded: typeof cfg.skip_already_uploaded === "boolean",
wfaPeriods: isPositiveInt(cfg.wfaPeriods),
Expand Down Expand Up @@ -336,7 +339,12 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
) : null}
{cloudTokenMissing ? (
<p className="mt-1 text-xs leading-relaxed text-amber-300/90">
Cloud target is selected, but api_token is empty. Fill API key in kiploks.json settings before Run Integration.
Cloud or Custom target is selected, but api_token is empty. Fill API key in kiploks.json settings before Run Integration.
</p>
) : null}
{customApiUrlInvalid ? (
<p className="mt-1 text-xs leading-relaxed text-amber-300/90">
Custom target is selected, but api_url must start with http:// or https://.
</p>
) : null}
{(() => {
Expand All @@ -356,6 +364,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
!ctx.canRunIntegration ||
ctx.hasKiploksChanges ||
cloudTokenMissing ||
customApiUrlInvalid ||
integrationSubmitting ||
ctx.activeIntegrationJob?.status === "queued" ||
ctx.activeIntegrationJob?.status === "running"
Expand All @@ -370,6 +379,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
disabled={
!ctx.hasPathForIntegration ||
cloudTokenMissing ||
customApiUrlInvalid ||
integrationSubmitting ||
ctx.activeIntegrationJob?.status === "queued" ||
ctx.activeIntegrationJob?.status === "running"
Expand All @@ -385,6 +395,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
disabled={
!ctx.hasPathForIntegration ||
cloudTokenMissing ||
customApiUrlInvalid ||
integrationSubmitting ||
ctx.activeIntegrationJob?.status === "queued" ||
ctx.activeIntegrationJob?.status === "running"
Expand Down Expand Up @@ -416,6 +427,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
Local (UI {String(ctx.kiploksUi.localApiBaseUrl || "")} · Docker {String(ctx.kiploksUi.localApiDockerBaseUrl || "")})
</option>
<option value="cloud">https://kiploks.com/</option>
<option value="custom">Custom URL</option>
</select>
{ctx.kiploksUi.apiTarget === "local" ? (
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
Expand All @@ -442,6 +454,33 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
</p>
</>
) : null}
{ctx.kiploksUi.apiTarget === "custom" ? (
<>
<FieldLabelWithStatus label="custom_api_url" ok={fieldValid.customApiUrl} />
<input
className={oc.input}
type="text"
autoComplete="off"
spellCheck={false}
value={String(ctx.kiploksUi.config.api_url ?? "")}
onChange={(e) => ctx.setKiploksField("api_url", e.target.value)}
placeholder="http(s)://your-server.example"
/>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
Save writes this value to kiploks.json as api_url and integration runs will upload to this server.
</p>
<FieldLabelWithStatus label="api_token" ok={fieldValid.apiToken} />
<input
className={oc.input}
type="text"
autoComplete="off"
spellCheck={false}
value={String(ctx.kiploksUi.config.api_token ?? "")}
onChange={(e) => ctx.setKiploksField("api_token", e.target.value)}
placeholder="Paste API key for custom Kiploks server"
/>
</>
) : null}

{ctx.integration === "freqtrade" ? (
<div className="mt-3 space-y-4">
Expand Down Expand Up @@ -645,7 +684,7 @@ export function KiploksWorkspacePanel({ ctx }: Props) {
}
})();
}}
disabled={!ctx.kiploksUi || !ctx.hasKiploksChanges}
disabled={!ctx.kiploksUi || !ctx.hasKiploksChanges || customApiUrlInvalid}
>
Save kiploks.json
</button>
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/web/src/legacy/wizard/useOrchestratorApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export function useOrchestratorApp() {
if (!prev) return prev;
const nextCfg = { ...prev.config };
if (v === "local") nextCfg.api_token = "";
if (v === "custom") nextCfg.api_url = "";
return { ...prev, apiTarget: v, config: nextCfg };
});
};
Expand All @@ -286,7 +287,8 @@ export function useOrchestratorApp() {
payload.hyperopt_loss = c.hyperopt_loss;
payload.hyperopt_result_path = c.hyperopt_result_path;
payload.keep_last_n_backtest_files = c.keep_last_n_backtest_files;
if (kiploksUi.apiTarget === "cloud") payload.api_token = c.api_token || "";
if (kiploksUi.apiTarget === "cloud" || kiploksUi.apiTarget === "custom") payload.api_token = c.api_token || "";
if (kiploksUi.apiTarget === "custom") payload.custom_api_url = c.api_url || "";
} else {
payload.backtesting_path = c.backtesting_path;
payload.top_n = c.top_n;
Expand All @@ -295,7 +297,8 @@ export function useOrchestratorApp() {
payload.wfaISSize = c.wfaISSize;
payload.wfaOOSSize = c.wfaOOSSize;
payload.skip_already_uploaded = c.skip_already_uploaded;
if (kiploksUi.apiTarget === "cloud") payload.api_token = c.api_token || "";
if (kiploksUi.apiTarget === "cloud" || kiploksUi.apiTarget === "custom") payload.api_token = c.api_token || "";
if (kiploksUi.apiTarget === "custom") payload.custom_api_url = c.api_url || "";
}
await api.post("/integrations/kiploks-config", payload);
await loadKiploksConfig();
Expand Down
61 changes: 53 additions & 8 deletions packages/cli/web/src/shell/report/ReportBlocksView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
const pro = asObj(lite.proBenchmarkMetrics);
const wfa = asObj(lite.walkForwardAnalysis);
const sens = asObj(lite.parameterSensitivity);
const deploymentStatusGlobal = str(sens?.deploymentStatus);
const deploymentRejectedGlobal = /REJECT|FAIL/i.test(deploymentStatusGlobal || "");
const turnover = asObj(lite.turnoverAndCostDrag);
const risk = asObj(lite.riskAnalysis);
const actionPlan = asObj(lite.strategyActionPlan);
Expand Down Expand Up @@ -370,13 +372,16 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
{robustRows.map((r) => {
const v = typeof r.value === "number" ? Math.round(r.value) : null;
const isBlocked = v != null && v <= 0;
const isStabilityRow = r.key === "stability";
const tone = scoreTone(v);
const effectiveTone =
isStabilityRow && deploymentRejectedGlobal && tone === "good" ? "warn" : tone;
const weight =
r.key === "validation" ? 40 : r.key === "risk" ? 30 : r.key === "stability" ? 20 : r.key === "execution" ? 10 : 0;
const barClass =
tone === "good"
effectiveTone === "good"
? "text-emerald-400"
: tone === "warn"
: effectiveTone === "warn"
? "text-amber-300"
: "text-rose-400";
return (
Expand All @@ -389,6 +394,14 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
>
<p className="font-medium text-foreground">
{r.label} <span className="text-muted-foreground">({weight}%)</span>
{isStabilityRow ? (
<span
className="ml-1 cursor-help text-[10px] text-muted-foreground underline decoration-dotted"
title="Advisory metric: Parameter Stability reflects local sensitivity behavior and does not override deployment gates (Performance Decay, Risk Class, hard blockers)."
>
[?]
</span>
) : null}
{isBlocked ? <span className="font-semibold text-rose-400"> (blocking)</span> : null}
</p>
<div className="flex items-center gap-2">
Expand All @@ -398,7 +411,9 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
<p className={"leading-tight " + (isBlocked ? "font-medium text-rose-400" : "text-muted-foreground")}>
{isBlocked
? "→ BLOCKED"
: r.key === "stability"
: isStabilityRow && deploymentRejectedGlobal
? "→ Parameters stable in isolation, but deployment is blocked by audit gates"
: r.key === "stability"
? "→ Parameters stable across sensitivity tests"
: "→ Within threshold"}
</p>
Expand Down Expand Up @@ -748,16 +763,28 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {
const row = asObj(p);
const sensitivity = num(row?.sensitivity);
const status = str(row?.status) || (sensitivity != null && sensitivity >= 0.6 ? "Fragile" : "Stable");
const topology =
str(row?.topology) ||
str(row?.displayLabel) ||
(sensitivity == null
? "n/a"
: sensitivity >= 0.6
? "Sharp peak"
: sensitivity >= 0.4
? "Moderate"
: "Flat");
const statusCls =
/FRAGILE|HIGH/i.test(status) ? "text-rose-400" : /TUNING|MODERATE/i.test(status) ? "text-amber-300" : "text-emerald-400";
const statusIcon =
/FRAGILE|HIGH/i.test(status) ? "🔴" : /TUNING|MODERATE/i.test(status) ? "🟡" : "🟢";
return (
<div key={idx} className="space-y-1">
<div className="grid grid-cols-[minmax(120px,1fr)_70px_64px_80px_100px] items-center gap-3">
<div className="truncate">{str(row?.name) || `param_${idx + 1}`}</div>
<div className="text-right">{str(row?.optimal) || num(row?.optimal) || "n/a"}</div>
<div className="text-right text-muted-foreground">~</div>
<div className="text-right text-muted-foreground">{topology}</div>
<div className="text-right">{asNum(sensitivity, 2)}</div>
<div className={"text-right font-semibold " + statusCls}>🟢 {status}</div>
<div className={"text-right font-semibold " + statusCls}>{statusIcon} {status}</div>
</div>
<div className="text-xs text-muted-foreground">Suggested Mitigation: {str(row?.mitigation) || "Risk Neutral"}</div>
</div>
Expand Down Expand Up @@ -794,21 +821,39 @@ export function ReportBlocksView({ lite }: { lite: TestResultDataLite }) {

<div className="space-y-2 border-t border-dashed border-border pt-2 text-xs">
<p className="font-semibold">AUDIT VERDICT</p>
{(() => {
const deploymentStatus = str(sens.deploymentStatus) || "APPROVED (no Decay check)";
const deploymentCls = /REJECT|FAIL/i.test(deploymentStatus)
? "text-rose-400"
: /HOLD|WARN|CAUTION/i.test(deploymentStatus)
? "text-amber-300"
: "text-emerald-400";
const riskClass = str(sens.riskClass) || "LOW";
const riskClassCls = /HIGH|REJECT|FAIL/i.test(riskClass)
? "text-rose-400"
: /MODERATE|WARN|CAUTION/i.test(riskClass)
? "text-amber-300"
: "text-emerald-400";
return (
<>
<p className="text-muted-foreground">
Deployment Status:{" "}
<span className="font-semibold text-emerald-400">{str(sens.deploymentStatus) || "APPROVED (no Decay check)"}</span>
<span className={"font-semibold " + deploymentCls}>{deploymentStatus}</span>
</p>
<p className="text-amber-300">Performance Decay: {str(sens.performanceDecayNote) || "n/a (min 3 periods required for decay check)."}</p>
<p className="text-muted-foreground">
Risk Score:{" "}
<span className="text-muted-foreground/90">
{str(sens.riskScoreFormula) || `Base ${num(sens.baseScore) ?? DISPLAY_NA} - Penalty ${num(sens.penalty) ?? DISPLAY_NA} ->`}
</span>{" "}
<span className="font-semibold text-emerald-400">
{str(sens.riskClass) || "LOW"} ({num(sens.riskScore) ?? DISPLAY_NA}/100)
<span className={"font-semibold " + riskClassCls}>
{riskClass} ({num(sens.riskScore) ?? DISPLAY_NA}/100)
</span>
</p>
<p className="text-muted-foreground">Pro-Note: {str(sens.proNote) || "Highest sensitivity parameter shown in table."}</p>
</>
);
})()}
</div>
</AnalysisBlockCardLite>
) : null}
Expand Down
Loading
Loading