slice 3 COMPLETE: onboarding wizard (complexity/risk/drawdown + starter watchlist + demand subscribe)
starter.ts by ornith (9 symbols w/ ticker-kind + ADR-0007 reasons, defaults, 12 tests). onboarding.complete + onboarding.starter + me().onboarded by orchestrator; schema +drawdown_tolerance. Wizard UI (complexity picker, starter list, disclaimer, finish). onboarding subscribes starter symbols to the demand set (adapter fetches them). 87/87 backend + 2/2 lint green. Slice 3 done; slices 1-3 complete.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, it } from "node:test";
|
||||
import * as assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
STARTER_WATCHLIST,
|
||||
ONBOARDING_DISCLAIMER,
|
||||
defaultDrawdownTolerancePct,
|
||||
defaultRiskTolerance,
|
||||
type StarterSymbol,
|
||||
} from "../starter.ts";
|
||||
|
||||
describe("onboarding/starter", () => {
|
||||
describe("STARTER_WATCHLIST", () => {
|
||||
it("has exactly 9 entries", () => {
|
||||
assert.equal(STARTER_WATCHLIST.length, 9);
|
||||
});
|
||||
|
||||
it("matches the prescribed symbol+tickerKind order", () => {
|
||||
const expected: Array<{ symbol: string; tickerKind: string }> = [
|
||||
{ symbol: "IREN", tickerKind: "equity" },
|
||||
{ symbol: "CIFR", tickerKind: "equity" },
|
||||
{ symbol: "ASST", tickerKind: "equity" },
|
||||
{ symbol: "SLNH", tickerKind: "equity" },
|
||||
{ symbol: "BKKT", tickerKind: "equity" },
|
||||
{ symbol: "NUAI", tickerKind: "equity" },
|
||||
{ symbol: "NVDA", tickerKind: "equity" },
|
||||
{ symbol: "BTC", tickerKind: "crypto" },
|
||||
{ symbol: "SATA", tickerKind: "equity" },
|
||||
];
|
||||
assert.equal(STARTER_WATCHLIST.length, expected.length);
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
const got: StarterSymbol = STARTER_WATCHLIST[i];
|
||||
assert.equal(got.symbol, expected[i].symbol, `item #${i} symbol`);
|
||||
assert.equal(got.tickerKind, expected[i].tickerKind, `item #${i} tickerKind`);
|
||||
}
|
||||
});
|
||||
|
||||
it("BTC has tickerKind 'crypto' while all others are 'equity'", () => {
|
||||
for (const item of STARTER_WATCHLIST) {
|
||||
if (item.symbol === "BTC") {
|
||||
assert.equal(item.tickerKind, "crypto");
|
||||
} else {
|
||||
assert.equal(item.tickerKind, "equity");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("every entry has a non-empty reason string", () => {
|
||||
for (const item of STARTER_WATCHLIST) {
|
||||
assert.ok(item.reason.length > 0, `reason for ${item.symbol} is empty`);
|
||||
}
|
||||
});
|
||||
|
||||
it("reasons contain no imperative trade verbs (ADR-0007 / P6)", () => {
|
||||
const forbidden = [
|
||||
/buy\b/i,
|
||||
/sell\b/i,
|
||||
/you should\b/i,
|
||||
/add to your\b/i,
|
||||
/rotate into\b/i,
|
||||
/action needed\b/i,
|
||||
];
|
||||
for (const item of STARTER_WATCHLIST) {
|
||||
const hit = forbidden.find((rx) => rx.test(item.reason));
|
||||
assert.equal(hit, undefined, `forbidden verb in reason for ${item.symbol}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultDrawdownTolerancePct", () => {
|
||||
it("returns -20 for beginner", () => {
|
||||
assert.equal(defaultDrawdownTolerancePct("beginner"), -20);
|
||||
});
|
||||
|
||||
it("returns -30 for intermediate", () => {
|
||||
assert.equal(defaultDrawdownTolerancePct("intermediate"), -30);
|
||||
});
|
||||
|
||||
it("returns -40 for advanced", () => {
|
||||
assert.equal(defaultDrawdownTolerancePct("advanced"), -40);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultRiskTolerance", () => {
|
||||
it("returns 'moderate' for beginner", () => {
|
||||
assert.equal(defaultRiskTolerance("beginner"), "moderate");
|
||||
});
|
||||
|
||||
it("returns 'moderate' for intermediate", () => {
|
||||
assert.equal(defaultRiskTolerance("intermediate"), "moderate");
|
||||
});
|
||||
|
||||
it("returns 'aggressive' for advanced", () => {
|
||||
assert.equal(defaultRiskTolerance("advanced"), "aggressive");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ONBOARDING_DISCLAIMER", () => {
|
||||
it("contains the required keywords", () => {
|
||||
assert.ok(
|
||||
ONBOARDING_DISCLAIMER.toLowerCase().includes("educational"),
|
||||
"missing 'educational'",
|
||||
);
|
||||
assert.ok(
|
||||
ONBOARDING_DISCLAIMER.toLowerCase().includes("not financial advice"),
|
||||
"missing 'not financial advice'",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
// ADR-0007: Zero imperative trade verbs anywhere in this module.
|
||||
// Pure data + helpers for the first-login onboarding wizard — no deps.
|
||||
|
||||
export type Complexity = "beginner" | "intermediate" | "advanced";
|
||||
|
||||
/**
|
||||
* One entry for the starter watchlist shown to a new user during onboarding.
|
||||
* The reason field is plain-English and purely educational/descriptive.
|
||||
*/
|
||||
export interface StarterSymbol {
|
||||
symbol: string;
|
||||
tickerKind: "equity" | "crypto" | "etf" | "index";
|
||||
/** One-line plain-English description — factual, no trade verbs (P6 / ADR-0007). */
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The starter watchlist presented to every new user.
|
||||
* Exactly 9 entries, fixed order.
|
||||
*/
|
||||
export const STARTER_WATCHLIST: StarterSymbol[] = [
|
||||
{
|
||||
symbol: "IREN",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"Iris Energy — a Bitcoin-mining and energy-infrastructure company focused on sustainable operations.",
|
||||
},
|
||||
{
|
||||
symbol: "CIFR",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"Cipher Mining — a digital-asset mining operator with large-scale energy infrastructure.",
|
||||
},
|
||||
{
|
||||
symbol: "ASST",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"Asseco Systems — an enterprise technology and blockchain-services provider.",
|
||||
},
|
||||
{
|
||||
symbol: "SLNH",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"Solena AI — a data-center infrastructure company supporting AI and compute workloads.",
|
||||
},
|
||||
{
|
||||
symbol: "BKKT",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"Bakkt — a regulated digital-asset exchange built on institutional-grade infrastructure.",
|
||||
},
|
||||
{
|
||||
symbol: "NUAI",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"NuAerospace — an aerospace-and-defense company pursuing advanced space systems.",
|
||||
},
|
||||
{
|
||||
symbol: "NVDA",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"NVIDIA — a semiconductor leader in GPUs and AI-accelerator hardware.",
|
||||
},
|
||||
{
|
||||
symbol: "BTC",
|
||||
tickerKind: "crypto",
|
||||
reason:
|
||||
"Bitcoin — the original cryptocurrency; price and sentiment tracked, SEC disclosure modules do not apply to on-chain assets.",
|
||||
},
|
||||
{
|
||||
symbol: "SATA",
|
||||
tickerKind: "equity",
|
||||
reason:
|
||||
"Solid Alpha Technology Acquisition — a SPAC holding a technology-focused portfolio.",
|
||||
},
|
||||
];
|
||||
|
||||
/** Default drawdown tolerance (percent) per complexity level. */
|
||||
export function defaultDrawdownTolerancePct(complexity: Complexity): number {
|
||||
switch (complexity) {
|
||||
case "beginner":
|
||||
return -20;
|
||||
case "intermediate":
|
||||
return -30;
|
||||
case "advanced":
|
||||
return -40;
|
||||
}
|
||||
}
|
||||
|
||||
/** Default risk tolerance per complexity level. */
|
||||
export function defaultRiskTolerance(
|
||||
complexity: Complexity,
|
||||
): "conservative" | "moderate" | "aggressive" {
|
||||
switch (complexity) {
|
||||
case "beginner":
|
||||
return "moderate";
|
||||
case "intermediate":
|
||||
return "moderate";
|
||||
case "advanced":
|
||||
return "aggressive";
|
||||
}
|
||||
}
|
||||
|
||||
/** Legal disclaimer shown during onboarding. */
|
||||
export const ONBOARDING_DISCLAIMER =
|
||||
"This is an educational tool, not financial advice.";
|
||||
Reference in New Issue
Block a user