-
Notifications
You must be signed in to change notification settings - Fork 0
Add reusable disclosure component, methodology page, and banned-phrase audit (DISCLOSURE-001) #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import { ConnectionStatus } from '../features/realtime/ConnectionStatus'; | |
| import { ErrorBoundary } from './ErrorBoundary'; | ||
| import { MobileNavDrawer } from './MobileNavDrawer'; | ||
| import { NAV_ITEMS } from '../routes/nav'; | ||
| import { Disclosure } from '../components/Disclosure'; | ||
|
|
||
| function truncateAddress(address: string): string { | ||
| return `${address.slice(0, 6)}…${address.slice(-4)}`; | ||
|
|
@@ -52,10 +53,11 @@ function SidebarContent({ onNavigate }: { onNavigate?: () => void }) { | |
| })} | ||
| </nav> | ||
|
|
||
| <div className="border-t border-border-subtle p-4"> | ||
| <div className="flex flex-col gap-3 border-t border-border-subtle p-4"> | ||
| <Badge variant="paper" className="w-full justify-center"> | ||
| Paper Trading | ||
| </Badge> | ||
| <Disclosure variant="compact" context="primary" /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On viewports below the Useful? React with 👍 / 👎. |
||
| </div> | ||
| </> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import { Router } from 'wouter'; | ||
| import { Disclosure } from './Disclosure'; | ||
|
|
||
| function renderWithRouter(ui: React.ReactElement) { | ||
| return render(<Router>{ui}</Router>); | ||
| } | ||
|
|
||
| describe('Disclosure', () => { | ||
| it('renders the compact primary copy with a link to the methodology page', () => { | ||
| renderWithRouter(<Disclosure variant="compact" context="primary" />); | ||
| expect(screen.getByText(/Paper trading only/i)).toBeInTheDocument(); | ||
| expect(screen.getByRole('link', { name: /learn more/i })).toHaveAttribute('href', '/methodology'); | ||
| }); | ||
|
|
||
| it('renders the detailed signals copy, distinct from the primary copy', () => { | ||
| renderWithRouter(<Disclosure variant="detailed" context="signals" />); | ||
| expect(screen.getByText(/Signal strength/)).toBeInTheDocument(); | ||
| expect(screen.getByText(/not a calibrated probability/i)).toBeInTheDocument(); | ||
| expect(screen.getByRole('link', { name: /read the full methodology/i })).toHaveAttribute('href', '/methodology'); | ||
| }); | ||
|
|
||
| it('renders distinct copy for every context, never sharing text across contexts', () => { | ||
| const contexts = ['primary', 'signals', 'backtesting', 'paper-fills'] as const; | ||
| const compactTexts = contexts.map((context) => { | ||
| const { container, unmount } = renderWithRouter(<Disclosure variant="compact" context={context} />); | ||
| const text = container.querySelector('p')?.textContent; | ||
| unmount(); | ||
| return text; | ||
| }); | ||
| expect(new Set(compactTexts).size).toBe(contexts.length); | ||
| }); | ||
|
|
||
| it('never uses banned phrases in any context/variant combination', () => { | ||
| const banned = [ | ||
| 'guaranteed', | ||
| 'safe profit', | ||
| 'high-confidence winner', | ||
| 'best trade', | ||
| 'cannot lose', | ||
| 'proven return', | ||
| 'buy now', | ||
| 'sell now', | ||
| ]; | ||
| const contexts = ['primary', 'signals', 'backtesting', 'paper-fills'] as const; | ||
| for (const context of contexts) { | ||
| for (const variant of ['compact', 'detailed'] as const) { | ||
| const { container, unmount } = renderWithRouter(<Disclosure variant={variant} context={context} />); | ||
| const text = container.textContent?.toLowerCase() ?? ''; | ||
| for (const phrase of banned) { | ||
| expect(text).not.toContain(phrase); | ||
| } | ||
| unmount(); | ||
| } | ||
| } | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { Info, TriangleAlert } from 'lucide-react'; | ||
| import { Link } from 'wouter'; | ||
| import { cn } from '../lib/utils'; | ||
|
|
||
| /** | ||
| * DISCLOSURE-001's single reusable disclosure component. Copy here must | ||
| * match docs/product/paper-trading-and-educational-scope.md exactly -- | ||
| * that document is the source of truth; this file implements it. | ||
| * | ||
| * `compact` is a persistent, low-visual-weight reminder (rendered once in | ||
| * the app shell sidebar, not duplicated per-page). `detailed` is a fuller | ||
| * explanation placed near the specific surface it contextualizes (Signals | ||
| * page, order ticket). Both use an icon *and* text, never color alone, to | ||
| * convey "this is informational/cautionary" -- the mission's explicit | ||
| * accessibility requirement for disclosure text. | ||
| */ | ||
|
|
||
| export type DisclosureContext = 'primary' | 'signals' | 'backtesting' | 'paper-fills'; | ||
|
|
||
| const COPY: Record<DisclosureContext, { compact: string; detailed: string }> = { | ||
| primary: { | ||
| compact: 'Paper trading only -- simulated fills, real market data, no real money.', | ||
| detailed: | ||
| 'LiquidAlpha is an educational paper-trading simulator. Prices are sourced live from Hyperliquid; every trade, fill, and result is simulated. Nothing here is financial advice, and no path in this product can place a real order on any exchange.', | ||
| }, | ||
| signals: { | ||
| compact: 'Signal strength reflects indicator agreement, not a probability of winning.', | ||
| detailed: | ||
| 'Signals are generated from real technical indicators (EMA, MACD, RSI, ADX, Fisher Transform, Keltner Channel), composed into a versioned, explainable 0-100 Signal strength score. This score measures how strongly the indicators agree with each other -- it is not a calibrated probability, a win-rate estimate, or investment advice. Past indicator agreement does not predict future price movement.', | ||
| }, | ||
| backtesting: { | ||
| compact: 'Backtest results are historical simulations, not a guarantee of future performance.', | ||
| detailed: | ||
| "Backtests replay historical Hyperliquid candles through this platform's real signal-generation logic, with documented, conservative assumptions about entry timing, slippage, fees, and funding. A strategy's historical simulated performance is not a guarantee, promise, or reliable predictor of how it would perform going forward -- markets change, and this engine cannot account for conditions it hasn't seen.", | ||
| }, | ||
| 'paper-fills': { | ||
| compact: 'Simulated fill -- not a real exchange execution.', | ||
| detailed: | ||
| 'Every paper fill is priced from a real Hyperliquid reference price, with documented simulated slippage and fees applied on top. No real order is ever sent to Hyperliquid or any other exchange.', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For every order confirmation, this says all fills have slippage and fees applied, but Useful? React with 👍 / 👎. |
||
| }, | ||
| }; | ||
|
|
||
| export interface DisclosureProps { | ||
| variant: 'compact' | 'detailed'; | ||
| context: DisclosureContext; | ||
| className?: string; | ||
| } | ||
|
|
||
| export function Disclosure({ variant, context, className }: DisclosureProps) { | ||
| const copy = COPY[context]; | ||
|
|
||
| if (variant === 'compact') { | ||
| return ( | ||
| <div | ||
| className={cn( | ||
| 'flex items-start gap-2 rounded-lg border border-border-subtle bg-bg-floating/60 px-3 py-2 text-xs leading-relaxed text-ink-muted', | ||
| className, | ||
| )} | ||
| > | ||
| <Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-gold-400" aria-hidden /> | ||
| <p> | ||
| {copy.compact}{' '} | ||
| <Link href="/methodology" className="underline hover:text-ink-secondary"> | ||
| Learn more | ||
| </Link> | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className={cn('rounded-lg border border-gold-500/30 bg-gold-500/10 p-4', className)}> | ||
| <div className="flex items-center gap-2 text-sm font-medium text-gold-400"> | ||
| <TriangleAlert className="h-4 w-4" aria-hidden /> | ||
| <span>Educational simulation</span> | ||
| </div> | ||
| <p className="mt-2 text-sm leading-relaxed text-ink-secondary">{copy.detailed}</p> | ||
| <Link href="/methodology" className="mt-2 inline-block text-xs font-medium text-gold-400 underline"> | ||
| Read the full methodology | ||
| </Link> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import { Link } from 'wouter'; | ||
| import { ArrowLeft } from 'lucide-react'; | ||
|
|
||
| /** | ||
| * DISCLOSURE-001's methodology page. Content here must stay in sync with | ||
| * docs/product/paper-trading-and-educational-scope.md -- that document is | ||
| * the source of truth; this page is its user-facing rendering. | ||
| */ | ||
|
|
||
| function Section({ title, children }: { title: string; children: React.ReactNode }) { | ||
| return ( | ||
| <section className="border-t border-border-subtle pt-6 first:border-t-0 first:pt-0"> | ||
| <h2 className="font-display text-lg font-medium tracking-tight text-ink-primary">{title}</h2> | ||
| <div className="mt-2 flex flex-col gap-2 text-sm leading-relaxed text-ink-secondary">{children}</div> | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
| export function MethodologyPage() { | ||
| return ( | ||
| <div className="mx-auto flex max-w-3xl flex-col gap-8"> | ||
| <div> | ||
| <Link href="/" className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-ink-secondary"> | ||
| <ArrowLeft className="h-3.5 w-3.5" aria-hidden /> | ||
| Back | ||
| </Link> | ||
| <h1 className="mt-3 font-display text-3xl font-medium tracking-tight text-ink-primary">Methodology</h1> | ||
| <p className="mt-2 text-sm text-ink-secondary"> | ||
| What LiquidAlpha is, what every number on this platform means, and exactly which assumptions produced it. | ||
| </p> | ||
| </div> | ||
|
|
||
| <Section title="What this is"> | ||
| <p> | ||
| An educational paper-trading simulator for Hyperliquid perpetuals. Every price is real -- sourced live from | ||
| Hyperliquid, with an explicitly-labeled CoinGecko fallback. Every trade, fill, fee, funding charge, and | ||
| backtest result is simulated. No real money, no real orders, no real exchange, ever -- this platform has no | ||
| code path that can sign or submit a real order. | ||
| </p> | ||
| <p>Not financial advice. Not a signal service. Not a broker, exchange, or custodian.</p> | ||
| </Section> | ||
|
|
||
| <Section title="Data sources"> | ||
| <p> | ||
| Hyperliquid is the primary source for prices, candles, and funding history. If Hyperliquid becomes | ||
| unreachable, market data falls back to CoinGecko, and every price/candle carries an explicit source label so | ||
| it's never presented as Hyperliquid-sourced when it isn't. The platform's own health status --{' '} | ||
|
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Hyperliquid candle retrieval fails, Useful? React with 👍 / 👎. |
||
| <code className="rounded bg-bg-floating px-1 py-0.5 text-xs">live</code>,{' '} | ||
| <code className="rounded bg-bg-floating px-1 py-0.5 text-xs">degraded</code>,{' '} | ||
| <code className="rounded bg-bg-floating px-1 py-0.5 text-xs">fallback</code>, or{' '} | ||
| <code className="rounded bg-bg-floating px-1 py-0.5 text-xs">unavailable</code> -- reflects which of these is | ||
| currently true. | ||
| </p> | ||
| </Section> | ||
|
|
||
| <Section title="Signal calculation"> | ||
| <p> | ||
| Signals are generated from real technical indicators -- EMA50/EMA200, MACD, RSI, ADX, Fisher Transform, and | ||
| Keltner Channel -- computed from historical price data. A signal only fires when the underlying trend (EMA) | ||
| and momentum (MACD) agree on direction; disagreement produces no signal at all, never a forced call. | ||
| </p> | ||
| </Section> | ||
|
|
||
| <Section title="What Signal strength means"> | ||
| <p> | ||
| Signal strength is a deterministic, versioned 0-100 score composed of six weighted components: trend | ||
| agreement, momentum agreement, trend-strength confirmation, volatility suitability, data freshness, and | ||
| indicator availability. It measures how strongly the available indicators agree with each other and with the | ||
| underlying trend -- it is not a probability of winning, a confidence level, or an expected return. Nothing on | ||
| this platform has been backtested to establish a real relationship between this score and actual trade | ||
| outcomes. | ||
| </p> | ||
| </Section> | ||
|
|
||
| <Section title="Backtesting assumptions"> | ||
| <p> | ||
| Backtests replay historical Hyperliquid candles through the exact same signal-generation logic that produces | ||
| live signals, with a strict no-lookahead guarantee: a signal decision at any point in history only ever sees | ||
| data that would genuinely have been available at that moment. A fired signal enters at the next candle's open | ||
| (never the signal candle's own close), with documented slippage, fees, and (optionally) funding applied. | ||
| Same-candle stop/target collisions resolve conservatively as a loss. Results below 10 trades show nothing; | ||
|
Comment on lines
+77
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The reviewed tree has no backtest engine or results implementation, and a repo-wide tracked-file search finds no referenced Useful? React with 👍 / 👎. |
||
| results below 30 show only basic figures; full statistics require 30+ trades -- the same sample-adequacy | ||
| discipline applied to live paper-trading performance metrics. | ||
| </p> | ||
| </Section> | ||
|
|
||
| <Section title="Paper-fill assumptions"> | ||
| <p> | ||
| Every paper fill is priced from a real Hyperliquid (or labeled-fallback) reference price, with documented | ||
| simulated slippage and a flat fee assumption applied on top. Open positions accrue real Hyperliquid funding | ||
| rates over time, pro-rated by how long the position has actually been open. A liquidation price shown for a | ||
| leveraged position is an estimate using a single flat maintenance-margin assumption -- not Hyperliquid's real, | ||
| per-asset, tiered margin schedule, and not accounting for funding already paid. | ||
|
Comment on lines
+90
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For any open leveraged position, this tells users that funding is accrued and a liquidation estimate is displayed, but Useful? React with 👍 / 👎. |
||
| </p> | ||
| </Section> | ||
|
|
||
| <Section title="Limitations"> | ||
| <ul className="list-disc pl-5"> | ||
| <li>Perpetuals only -- no spot-market simulation.</li> | ||
| <li>No reduce-only order behavior.</li> | ||
| <li>No cross-margin portfolio simulation -- each position is modeled independently.</li> | ||
| <li> | ||
| A single flat fee and a single flat maintenance-margin assumption, not Hyperliquid's real tiered schedules. | ||
| </li> | ||
| <li> | ||
| Backtests use one documented entry-timing/exit assumption set -- not every possible execution strategy. | ||
| </li> | ||
| </ul> | ||
| </Section> | ||
|
|
||
| <Section title="Versioning"> | ||
| <p> | ||
| Every number this platform shows is traceable to the exact model version that produced it: a signal-engine | ||
| rule version, a Signal-strength score-model version, a paper-fill model version, and a backtest engine | ||
| version. When any of these change, results computed under the old and new versions remain distinguishable from | ||
| each other. | ||
| </p> | ||
| </Section> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a signed-out user clicks either disclosure link on
ConnectScreen, the URL changes to/methodology, butAuthGatereturnsConnectScreenbefore reaching this authenticatedSwitch, soMethodologyPagenever renders. This breaks the only pre-login “Learn more” path; handle the methodology route outside the authenticated branch or allow this path through the gate.Useful? React with 👍 / 👎.