chore: remove apps/web

Remove the unused Next.js web app along with its workspace declaration
and lockfile entries.
This commit is contained in:
Alex Yang
2026-05-16 15:13:45 -07:00
parent 0d20a3a185
commit c0198b2ec8
23 changed files with 132 additions and 6982 deletions
-41
View File
@@ -1,41 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
-5
View File
@@ -1,5 +0,0 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
-36
View File
@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
-18
View File
@@ -1,18 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
-5
View File
@@ -1,5 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;
-28
View File
@@ -1,28 +0,0 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"motion": "^12.38.0",
"next": "16.2.6",
"next-view-transitions": "^0.3.5",
"react": "19.2.6",
"react-dom": "19.2.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.0",
"@types/node": "^25.8.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^10.4.0",
"eslint-config-next": "16.2.6",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
}
}
-7
View File
@@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
-75
View File
@@ -1,75 +0,0 @@
@import "tailwindcss";
@theme inline {
--color-bg: #0a0a0a;
--color-bg-elevated: #141414;
--color-bg-hover: #1a1a1a;
--color-border: #262626;
--color-border-light: #1f1f1f;
--color-text: #ededed;
--color-text-secondary: #a1a1a1;
--color-text-muted: #666666;
--color-accent: #0070f3;
--color-accent-light: #0070f31a;
--color-green: #22c55e;
--color-yellow: #eab308;
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "Fira Code", ui-monospace, SFMono-Regular, monospace;
}
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
}
::selection {
background: var(--color-accent);
color: white;
}
/* View Transitions */
/* Persistent nav — stays static during transitions */
::view-transition-group(site-nav) {
animation: none;
z-index: 100;
}
/* Page content cross-fade with subtle slide */
@keyframes slide-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-4px); }
}
::view-transition-old(page-content) {
animation: 150ms ease-in both slide-out;
}
::view-transition-new(page-content) {
animation: 200ms ease-out 100ms both slide-in;
}
/* Caret blink for terminal animation */
@keyframes caret-blink {
0%, 70% { opacity: 1; }
71%, 100% { opacity: 0; }
}
.animate-caret-blink {
animation: caret-blink 1s step-end infinite;
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*),
::view-transition-group(*) {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
}
-39
View File
@@ -1,39 +0,0 @@
import type { Metadata } from "next";
import { Inter, Fira_Code } from "next/font/google";
import { ViewTransitions } from "next-view-transitions";
import { ScrollRestoration } from "./scroll-restoration";
import "./globals.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
const firaCode = Fira_Code({
variable: "--font-fira-code",
subsets: ["latin"],
});
export const metadata: Metadata = {
metadataBase: new URL("https://finance-skills.vercel.app"),
title: "Finance Skills",
description:
"Financial analysis skills for AI agents — earnings, market data, risk monitoring, and more.",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ViewTransitions>
<html lang="en" className={`${inter.variable} ${firaCode.variable} antialiased`} style={{ colorScheme: "dark" }}>
<body className="font-sans min-h-screen">
<ScrollRestoration />
{children}
</body>
</html>
</ViewTransitions>
);
}
-48
View File
@@ -1,48 +0,0 @@
import { ImageResponse } from "next/og";
export const alt = "Finance Skills";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default function Image() {
return new ImageResponse(
(
<div
style={{
background: "#0a0a0a",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
fontFamily: "Inter, sans-serif",
}}
>
<div
style={{
fontSize: 72,
fontWeight: 700,
color: "#ededed",
letterSpacing: "-0.02em",
}}
>
Finance Skills
</div>
<div
style={{
fontSize: 28,
color: "#888888",
marginTop: 16,
}}
>
Financial analysis skills for AI agents
</div>
</div>
),
{ ...size }
);
}
-93
View File
@@ -1,93 +0,0 @@
import { Suspense } from "react";
import dynamic from "next/dynamic";
import { skills } from "@/data/skills";
const SkillList = dynamic(() => import("./skill-list").then((mod) => mod.SkillList));
const TerminalAnimation = dynamic(
() => import("./terminal-animation").then((mod) => mod.TerminalAnimation)
);
async function getStarCount(): Promise<number | null> {
try {
const res = await fetch("https://api.github.com/repos/himself65/finance-skills", {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
const data = await res.json();
return data.stargazers_count ?? null;
} catch {
return null;
}
}
export default async function Home() {
const stars = await getStarCount();
return (
<div className="min-h-screen">
{/* Nav */}
<nav className="border-b border-border px-6 py-3" style={{ viewTransitionName: "site-nav" }}>
<div className="max-w-3xl mx-auto flex items-center gap-2 text-sm">
<span className="font-semibold">Finance Skills</span>
</div>
</nav>
<main className="max-w-3xl mx-auto px-6" style={{ viewTransitionName: "page-content" }}>
{/* Header */}
<div className="py-10">
<p className="text-sm text-text-muted mb-2">
<span>skills</span>
<span className="mx-1.5">/</span>
<span className="text-text-secondary">himself65</span>
<span className="mx-1.5">/</span>
<span className="text-text-secondary">finance-skills</span>
</p>
<h1 className="text-3xl font-bold tracking-tight text-balance">
himself65/finance-skills
</h1>
<p className="mt-3 text-text-secondary leading-relaxed max-w-xl">
Agent skills for financial analysis and trading. Earnings reports,
market data, options strategies, risk monitoring, and sentiment
analysis installable into Claude Code, Claude.ai, and other
AI agents.
</p>
<div className="flex items-center gap-4 mt-4 text-sm text-text-secondary">
<span>{skills.length} skills</span>
<a
href="https://github.com/himself65/finance-skills"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 hover:text-text transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded"
>
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
{stars !== null && (
<span>
<svg className="w-3.5 h-3.5 inline -mt-px" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z" />
</svg>
{" "}{stars.toLocaleString()}
</span>
)}
</a>
</div>
</div>
{/* Usage — terminal animation */}
<div className="pb-8 space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-wider text-text-muted">
Usage
</h2>
<TerminalAnimation />
</div>
{/* Skills by category with filter */}
<div className="border-t border-border pt-6">
<Suspense>
<SkillList skills={skills} />
</Suspense>
</div>
</main>
</div>
);
}
-36
View File
@@ -1,36 +0,0 @@
"use client";
import { usePathname } from "next/navigation";
import { useEffect, useRef } from "react";
const scrollPositions = new Map<string, number>();
export function ScrollRestoration() {
const pathname = usePathname();
const prevPathname = useRef(pathname);
// Save scroll position on scroll events
useEffect(() => {
const save = () => {
scrollPositions.set(pathname, window.scrollY);
};
window.addEventListener("scroll", save, { passive: true });
return () => window.removeEventListener("scroll", save);
}, [pathname]);
// Restore scroll position after navigation
useEffect(() => {
if (prevPathname.current !== pathname) {
const saved = scrollPositions.get(pathname);
if (saved !== undefined) {
// Wait for the view transition animation to finish (300ms total)
// before restoring, so the transition doesn't override scroll.
setTimeout(() => window.scrollTo({ top: saved, behavior: "smooth" }), 350);
}
prevPathname.current = pathname;
}
}, [pathname]);
return null;
}
-157
View File
@@ -1,157 +0,0 @@
"use client";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { Link } from "next-view-transitions";
import { motion, AnimatePresence, LayoutGroup } from "motion/react";
import type { Skill, PluginGroup } from "@/data/skills";
import { pluginGroupLabels, categoryLabels } from "@/data/skills";
const pluginOrder: PluginGroup[] = [
"market-analysis",
"social-readers",
"data-providers",
"startup-tools",
"ui-tools",
"skill-creator",
];
type PluginFilter = "all" | PluginGroup;
const pluginFilters: { value: PluginFilter; label: string }[] = [
{ value: "all", label: "All" },
...pluginOrder.map((p) => ({
value: p as PluginFilter,
label: pluginGroupLabels[p],
})),
];
function isValidPlugin(value: string | null): value is PluginGroup {
return value !== null && pluginOrder.includes(value as PluginGroup);
}
export function SkillList({ skills }: { skills: Skill[] }) {
const searchParams = useSearchParams();
const initialPlugin = searchParams.get("plugin");
const [activeFilter, setActiveFilter] = useState<PluginFilter>(
isValidPlugin(initialPlugin) ? initialPlugin : "all"
);
const filtered =
activeFilter === "all"
? skills
: skills.filter((s) => s.plugin === activeFilter);
const grouped = pluginOrder
.map((p) => ({
plugin: p,
label: pluginGroupLabels[p],
skills: filtered.filter((s) => s.plugin === p),
}))
.filter((g) => g.skills.length > 0);
return (
<div>
{/* Filter bar — sticky */}
<div className="sticky top-0 z-20 bg-bg/80 backdrop-blur-md flex items-center gap-3 py-3 -mx-6 px-6">
<LayoutGroup>
<div className="relative flex items-center gap-2 flex-wrap">
{pluginFilters.map((f) => (
<button
key={f.value}
onClick={() => setActiveFilter(f.value)}
className={`relative z-10 px-3 py-1.5 text-xs font-medium rounded-full transition-colors duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${
activeFilter === f.value
? "text-white"
: "bg-bg-elevated text-text-secondary border border-border hover:border-text-muted"
}`}
>
{activeFilter === f.value && (
<motion.div
layoutId="active-pill"
className="absolute inset-0 bg-accent rounded-full"
transition={{ type: "spring", duration: 0.4, bounce: 0.15 }}
/>
)}
<span className="relative z-10">{f.label}</span>
</button>
))}
</div>
</LayoutGroup>
<motion.span
key={filtered.length}
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
className="text-xs text-text-muted shrink-0"
>
{filtered.length} {filtered.length === 1 ? "skill" : "skills"}
</motion.span>
</div>
{/* Plugin sections */}
<div className="pb-16">
<AnimatePresence mode="popLayout" initial={false}>
{grouped.map((group) => (
<motion.section
key={group.plugin}
layout
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.25, ease: "easeOut" }}
className="mb-8"
>
<div className="flex items-center justify-between pb-3 border-b border-border">
<h2 className="text-xs font-semibold uppercase tracking-wider text-text-muted">
{group.label}
</h2>
</div>
<AnimatePresence mode="popLayout" initial={false}>
{group.skills.map((skill) => (
<motion.div
key={skill.name}
layout
initial={{ opacity: 0, x: -12 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 12 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="border-b border-border last:border-b-0"
>
<Link
href={`/skills/${skill.name}`}
className="flex items-center justify-between gap-4 py-4 hover:bg-bg-hover -mx-4 px-4 rounded transition-colors group"
>
<div className="min-w-0">
<span className="flex items-center gap-2">
<span className="font-medium text-sm group-hover:text-accent transition-colors">
{skill.name}
</span>
<span className="text-[10px] border border-border rounded px-1.5 py-0.5 text-text-muted">
{categoryLabels[skill.category]}
</span>
{skill.badge === "new" && (
<span className="text-[10px] font-semibold uppercase tracking-wider bg-accent/15 text-accent px-1.5 py-0.5 rounded">
New
</span>
)}
{skill.badge === "paid" && (
<span className="text-[10px] font-semibold uppercase tracking-wider bg-yellow/15 text-yellow px-1.5 py-0.5 rounded">
Paid
</span>
)}
</span>
<p className="text-xs text-text-muted mt-0.5 truncate">
{skill.description}
</p>
</div>
</Link>
</motion.div>
))}
</AnimatePresence>
</motion.section>
))}
</AnimatePresence>
</div>
</div>
);
}
@@ -1,71 +0,0 @@
import { ImageResponse } from "next/og";
import { skills, getSkill } from "@/data/skills";
export function generateStaticParams() {
return skills.map((s) => ({ name: s.name }));
}
export const alt = "Finance Skills";
export const size = {
width: 1200,
height: 630,
};
export const contentType = "image/png";
export default async function Image({
params,
}: {
params: Promise<{ name: string }>;
}) {
const { name } = await params;
const skill = getSkill(name);
const title = skill?.title ?? name;
return new ImageResponse(
(
<div
style={{
background: "#0a0a0a",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
fontFamily: "Inter, sans-serif",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
fontSize: 64,
fontWeight: 700,
color: "#ededed",
letterSpacing: "-0.02em",
}}
>
<span>Finance Skills</span>
<span style={{ color: "#888888" }}>/</span>
<span style={{ color: "#0070f3" }}>{title}</span>
</div>
{skill && (
<div
style={{
fontSize: 24,
color: "#888888",
marginTop: 16,
maxWidth: 800,
textAlign: "center",
lineHeight: 1.4,
}}
>
{skill.description}
</div>
)}
</div>
),
{ ...size }
);
}
-781
View File
@@ -1,781 +0,0 @@
import { skills, getSkill, categoryLabels, pluginGroupLabels } from "@/data/skills";
import type { Skill } from "@/data/skills";
import { notFound } from "next/navigation";
import { Link } from "next-view-transitions";
import { SepaStudyGuide } from "./sepa-study-guide";
import dynamic from "next/dynamic";
import type { TabContent, TerminalLine } from "../../terminal-animation";
const TerminalAnimation = dynamic(
() => import("../../terminal-animation").then((mod) => mod.TerminalAnimation)
);
export function generateStaticParams() {
return skills.map((s) => ({ name: s.name }));
}
export default async function SkillDetailPage({
params,
}: {
params: Promise<{ name: string }>;
}) {
const { name } = await params;
const skill = getSkill(name);
if (!skill) notFound();
return (
<div className="min-h-screen">
{/* Nav */}
<nav className="border-b border-border px-6 py-3" style={{ viewTransitionName: "site-nav" }}>
<div className="max-w-5xl mx-auto flex items-center gap-2 text-sm">
<Link href="/" scroll={false} className="font-semibold hover:text-accent transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded">
Finance Skills
</Link>
</div>
</nav>
<main className="max-w-5xl mx-auto px-6 py-8" style={{ viewTransitionName: "page-content" }}>
{/* Breadcrumb */}
<p className="text-sm text-text-muted mb-4">
<Link href="/" scroll={false} className="hover:text-text-secondary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded">
skills
</Link>
<span className="mx-1.5">/</span>
<Link href="/" scroll={false} className="hover:text-text-secondary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded">
himself65
</Link>
<span className="mx-1.5">/</span>
<Link href="/" scroll={false} className="hover:text-text-secondary transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded">
finance-skills
</Link>
<span className="mx-1.5">/</span>
<span className="text-text-secondary">{skill.name}</span>
</p>
{/* Title */}
<div className="flex items-center gap-3 mb-3">
<h1 className="text-3xl font-bold tracking-tight text-balance">
{skill.name}
</h1>
{skill.badge === "new" && (
<span className="text-xs font-semibold uppercase tracking-wider bg-accent/15 text-accent px-2 py-1 rounded">
New
</span>
)}
{skill.badge === "paid" && (
<span className="text-xs font-semibold uppercase tracking-wider bg-yellow/15 text-yellow px-2 py-1 rounded">
Paid
</span>
)}
</div>
<div className="flex gap-10 flex-col lg:flex-row">
{/* Content */}
<div className="flex-1 min-w-0">
<div className="border border-border rounded-lg overflow-hidden">
<div className="border-b border-border px-4 py-2.5 bg-bg-elevated flex items-center gap-2 text-xs text-text-muted">
<svg
className="w-4 h-4"
viewBox="0 0 16 16"
fill="currentColor"
aria-hidden="true"
>
<path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914Z" />
</svg>
SKILL.md
</div>
<div className="p-6 space-y-4">
<h2 className="text-xl font-bold">{skill.title}</h2>
<p className="text-text-secondary leading-relaxed">
{skill.description}
</p>
<div className="flex gap-2 flex-wrap">
<Link
href={`/?plugin=${skill.plugin}`}
scroll={false}
className="text-xs border border-border rounded px-2 py-0.5 text-text-secondary hover:border-accent hover:text-accent transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
>
{pluginGroupLabels[skill.plugin]}
</Link>
<span className="text-xs border border-border rounded px-2 py-0.5 text-text-secondary">
{categoryLabels[skill.category]}
</span>
{skill.tags.map((tag) => (
<span
key={tag}
className="text-xs border border-border rounded px-2 py-0.5 text-text-secondary"
>
{tag}
</span>
))}
</div>
</div>
{/* Terminal — example usage */}
<div className="border-t border-border">
<div className="px-4 py-2.5 bg-bg-elevated flex items-center gap-2 text-xs text-text-muted">
<svg className="w-4 h-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm.5 3.5a.75.75 0 0 1 .55-.72l.2-.03a.75.75 0 0 1 .53.22l2 2a.75.75 0 0 1 0 1.06l-2 2a.75.75 0 0 1-1.06-1.06L3.94 8 2.47 6.53a.75.75 0 0 1-.22-.53ZM7 10.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z" />
</svg>
Example
</div>
<TerminalAnimation
tabs={buildSkillTabs(skill)}
variant="card"
minHeight="auto"
/>
</div>
</div>
{/* Skill-specific study guide */}
{skill.name === "sepa-strategy" && <SepaStudyGuide />}
</div>
{/* Sidebar */}
<div className="lg:w-56 shrink-0 space-y-6">
<div>
<p className="text-xs uppercase tracking-wider text-text-muted mb-1.5">
Plugin
</p>
<Link
href={`/?plugin=${skill.plugin}`}
scroll={false}
className="text-sm text-accent hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded"
>
{pluginGroupLabels[skill.plugin]}
</Link>
</div>
<div>
<p className="text-xs uppercase tracking-wider text-text-muted mb-1.5">
Repository
</p>
<a
href="https://github.com/himself65/finance-skills"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-accent hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent rounded"
>
himself65/finance-skills
</a>
</div>
<div>
<p className="text-xs uppercase tracking-wider text-text-muted mb-1.5">
Install
</p>
<div className="space-y-1.5 text-xs text-text-secondary font-mono">
<p>npx plugins add himself65/finance-skills --plugin finance-{skill.plugin}</p>
</div>
</div>
</div>
</div>
</main>
</div>
);
}
// ---------------------------------------------------------------------------
// Helpers — line builders for mock Claude Code output
// ---------------------------------------------------------------------------
const c = {
muted: "var(--color-text-muted)",
secondary: "var(--color-text-secondary)",
accent: "var(--color-accent)",
green: "var(--color-green)",
yellow: "var(--color-yellow)",
} as const;
/** Claude "thinking" line */
function thinking(text: string, delay = 300): TerminalLine {
return { text: `${text}`, color: c.accent, delay };
}
/** Tool call header */
function tool(name: string, arg: string, delay = 200): TerminalLine {
return { text: `${name} ${arg}`, color: c.secondary, delay };
}
/** Indented output */
function out(text: string, delay = 80): TerminalLine {
return { text: ` ${text}`, color: c.muted, delay };
}
/** Blank spacer */
function gap(delay = 60): TerminalLine {
return { text: "", delay };
}
/** Green success line */
function ok(text: string, delay = 150): TerminalLine {
return { text: `${text}`, color: c.green, delay };
}
/** Yellow warning line */
function warn(text: string, delay = 150): TerminalLine {
return { text: `${text}`, color: c.yellow, delay };
}
/** Plain response text from Claude */
function reply(text: string, delay = 40): TerminalLine {
return { text: ` ${text}`, color: c.secondary, delay };
}
// ---------------------------------------------------------------------------
// Per-skill mock sessions
// ---------------------------------------------------------------------------
type MockSession = { prompt: string; lines: TerminalLine[] };
const mockSessions: Record<string, MockSession> = {
"earnings-preview": {
prompt: "earnings preview for NVDA",
lines: [
gap(),
thinking("Reading skill: earnings-preview"),
gap(),
tool("Bash", "pip install yfinance --quiet", 300),
ok("yfinance installed"),
gap(),
tool("Bash", "python3 fetch_earnings.py NVDA", 400),
out("Fetching consensus estimates from Yahoo Finance..."),
gap(),
reply("## NVDA — Earnings Preview (Q4 FY2026)"),
gap(),
reply("Report Date: May 28, 2026 (after close)"),
reply("Consensus EPS: $0.89 (12 analysts)"),
reply("Consensus Rev: $44.2B (15 analysts)"),
gap(),
reply("Beat/Miss History (last 4 quarters):"),
{ text: " Q3 +12.4% Q2 +8.7% Q1 +15.2% Q4 +11.8%", color: c.green, delay: 80 },
gap(),
reply("Analyst Sentiment: Bullish — 82% Buy, 14% Hold, 4% Sell"),
reply("Options IV (earnings week): 8.2% vs 4.1% 30-day avg"),
],
},
"earnings-recap": {
prompt: "how did AAPL earnings go",
lines: [
gap(),
thinking("Reading skill: earnings-recap"),
gap(),
tool("Bash", "python3 fetch_recap.py AAPL", 400),
out("Fetching Q1 FY2026 actuals vs estimates..."),
gap(),
reply("## AAPL — Earnings Recap (Q1 FY2026)"),
gap(),
reply("EPS: $2.18 actual vs $2.10 est → +3.8% surprise"),
reply("Rev: $124.3B actual vs $121.1B est → +2.6% beat"),
gap(),
reply("Stock Reaction: +4.2% after-hours"),
reply("iPhone revenue: $71.4B (+6% YoY) — above expectations"),
reply("Services: $26.3B (+14% YoY) — new record"),
gap(),
reply("Management raised Q2 guidance above consensus."),
],
},
"estimate-analysis": {
prompt: "estimate revisions for TSLA",
lines: [
gap(),
thinking("Reading skill: estimate-analysis"),
gap(),
tool("Bash", "python3 fetch_estimates.py TSLA", 400),
out("Loading estimate revision data across 4 periods..."),
gap(),
reply("## TSLA — Estimate Revisions"),
gap(),
reply(" Current 7d ago 30d ago 90d ago"),
reply("FY2026 EPS $3.42 $3.38 $3.21 $2.98"),
reply("FY2026 Rev $128.5B $127.1B $124.8B $119.2B"),
reply("FY2027 EPS $4.81 $4.75 $4.52 $4.11"),
gap(),
reply("Trend: Consistent upward revisions across all periods."),
reply("Revisions (30d): 18 up / 3 down / 2 unchanged"),
],
},
"etf-premium": {
prompt: "is BITO trading at a premium to NAV?",
lines: [
gap(),
thinking("Reading skill: etf-premium"),
gap(),
tool("Bash", "python3 etf_premium.py BITO", 400),
out("Fetching market price and NAV from Yahoo Finance..."),
gap(),
reply("## BITO — Premium/Discount Analysis"),
gap(),
reply("Market Price: $10.06"),
reply("NAV: $9.90"),
reply("Premium: +1.57% ($0.16/share)"),
reply("Bid-Ask Spread: 0.20% → premium is real, not noise"),
gap(),
reply("Peer Comparison (Digital Assets category):"),
reply(" BITO +1.57% ◄ lowest premium"),
reply(" GBTC +1.64%"),
reply(" ARKB +1.75%"),
reply(" FBTC +1.76%"),
reply(" IBIT +1.84%"),
reply(" ETHA +1.99%"),
gap(),
reply("All crypto ETFs at premium today — category-wide pattern."),
reply("Normal range for crypto ETFs: 0.5% to 3.0%."),
],
},
"yfinance-data": {
prompt: "get MSFT financials",
lines: [
gap(),
thinking("Reading skill: yfinance-data"),
gap(),
tool("Bash", "python3 -c \"import yfinance; ...\"", 400),
out("Downloading MSFT financial statements..."),
gap(),
reply("## MSFT — Financial Summary"),
gap(),
reply("Market Cap: $3.18T"),
reply("Revenue (TTM): $254.2B (+14.8% YoY)"),
reply("Net Income: $89.4B (+18.2% YoY)"),
reply("Free Cash Flow: $74.1B"),
gap(),
reply("Margins: Gross 69.8% | Operating 44.6% | Net 35.2%"),
reply("P/E: 35.4x | Fwd P/E: 30.1x | EV/EBITDA: 26.8x"),
],
},
prompt: "NVDA options flow",
lines: [
gap(),
gap(),
gap(),
reply("## NVDA — Options Flow (today)"),
gap(),
reply("Total premium: $842M (calls 64% / puts 36%)"),
reply("Largest block: NVDA Jul 140C × 12,000 $18.4M ask-side"),
reply("GEX flip: $128.50"),
reply("Put wall: $120.00 (28k OI)"),
reply("Call wall: $150.00 (41k OI)"),
gap(),
reply("Net delta: +$1.2B — bullish positioning"),
],
},
"options-payoff": {
prompt: "show payoff for AAPL 200/210 call spread",
lines: [
gap(),
thinking("Reading skill: options-payoff"),
gap(),
tool("Bash", "python3 black_scholes.py AAPL 200 210 call", 400),
out("Computing Black-Scholes pricing..."),
gap(),
reply("## AAPL — Bull Call Spread 200/210"),
gap(),
reply("Long 200C: $8.42 Delta 0.58 IV 24.1%"),
reply("Short 210C: $4.18 Delta 0.38 IV 23.6%"),
reply("Net debit: $4.24"),
gap(),
reply("Max profit: $5.76 (+135.8%) at $210+"),
reply("Max loss: $4.24 (-100%) at $200"),
reply("Breakeven: $204.24"),
gap(),
reply("P(profit) at expiry: ~52%"),
],
},
"stock-correlation": {
prompt: "what correlates with NVDA",
lines: [
gap(),
thinking("Reading skill: stock-correlation"),
gap(),
tool("Bash", "python3 correlation.py NVDA --period 90d", 400),
out("Computing 90-day rolling correlations..."),
gap(),
reply("## NVDA — Correlated Stocks (90d)"),
gap(),
reply(" AMD 0.91 ████████████████████▏"),
reply(" AVGO 0.87 ███████████████████▎ "),
reply(" MRVL 0.84 ██████████████████▋ "),
reply(" TSM 0.82 ██████████████████▏ "),
reply(" MU 0.79 █████████████████▌ "),
gap(),
reply("Inverse: TLT 0.41 | XLU 0.38"),
],
},
"hormuz-strait": {
prompt: "Hormuz strait status",
lines: [
gap(),
thinking("Reading skill: hormuz-strait"),
gap(),
tool("WebFetch", "tankertracking.com/api/hormuz", 400),
tool("WebFetch", "oilprice.com/api/brent", 200),
out("Fetching real-time strait transit data..."),
gap(),
reply("## Strait of Hormuz — Live Status"),
gap(),
reply("Transits (24h): 38 vessels (avg: 41)"),
reply("Oil throughput: 18.2M bbl/d"),
reply("Brent crude: $82.14 (+0.8%)"),
gap(),
reply("Risk level: ELEVATED"),
warn("2 tankers delayed — Iranian naval exercise reported"),
gap(),
reply("Insurance premium: +15bps WoW"),
],
},
"generative-ui": {
prompt: "visualize AAPL revenue trend",
lines: [
gap(),
thinking("Reading skill: generative-ui"),
gap(),
tool("show_widget", "interactive SVG chart", 400),
out("Rendering inline revenue chart..."),
gap(),
reply("## AAPL Revenue — Quarterly Trend"),
gap(),
reply(" Q1 ██████████████████████████ $124.3B"),
reply(" Q4 █████████████████████████ $119.6B"),
reply(" Q3 ████████████████████████ $116.2B"),
reply(" Q2 ███████████████████████ $108.4B"),
gap(),
reply("↑ Interactive chart rendered in conversation above."),
],
},
"twitter-reader": {
prompt: "search Twitter for NVDA sentiment",
lines: [
gap(),
thinking("Reading skill: twitter-reader"),
gap(),
tool("Bash", "opencli x search 'NVDA OR $NVDA' --limit 50", 400),
out("Searching recent tweets..."),
gap(),
reply("## NVDA — Twitter Sentiment (last 24h)"),
gap(),
reply("Tweets analyzed: 1,284"),
reply("Sentiment: 72% bullish / 18% neutral / 10% bearish"),
gap(),
reply("Top themes:"),
reply(" • Data center demand surge (382 mentions)"),
reply(" • Blackwell GPU ramp (241 mentions)"),
reply(" • Valuation concerns (128 mentions)"),
],
},
"telegram-reader": {
prompt: "read my crypto Telegram channels",
lines: [
gap(),
thinking("Reading skill: telegram-reader"),
gap(),
tool("Bash", "tdl chat ls --filter subscribed", 300),
out("Listing subscribed channels..."),
tool("Bash", "tdl chat export -c 'Crypto Signals' --limit 20", 400),
gap(),
reply("## Telegram — Recent Crypto Intel"),
gap(),
reply("Channel: Crypto Signals (12.4k members)"),
reply(" • BTC breaking $72k resistance — watch $74.5k"),
reply(" • ETH/BTC ratio at 3-month low"),
gap(),
reply("Channel: DeFi Alpha"),
reply(" • New Eigenlayer restaking vault — 8.4% APY"),
reply(" • Aave v4 governance vote passing"),
],
},
"discord-reader": {
prompt: "search Discord for AAPL discussion",
lines: [
gap(),
thinking("Reading skill: discord-reader"),
gap(),
tool("Bash", "discordcli search --query 'AAPL' --channels trading", 400),
out("Searching connected trading servers..."),
gap(),
reply("## Discord — AAPL Discussion (48h)"),
gap(),
reply("Messages found: 89 across 3 servers"),
gap(),
reply("#stocks-general — WallStreet Hub"),
reply(" \"AAPL broke out of the cup and handle\" ↑12 reactions"),
reply(" \"Services revenue is the real story\" ↑8 reactions"),
gap(),
reply("#earnings — Trading Floor"),
reply(" \"Guidance was the key — bullish into Q2\" ↑15 reactions"),
],
},
"linkedin-reader": {
prompt: "what are analysts saying on LinkedIn about NVDA",
lines: [
gap(),
thinking("Reading skill: linkedin-reader"),
gap(),
tool("Bash", "opencli linkedin search 'NVDA analyst' --type posts", 400),
out("Reading LinkedIn analyst posts..."),
gap(),
reply("## LinkedIn — NVDA Analyst Commentary"),
gap(),
reply("Dan Ives (Wedbull) — 2h ago"),
reply(" \"AI spending is not slowing. NVDA remains our top pick.\""),
reply(" → 1.2k reactions"),
gap(),
reply("Lisa Su (AMD CEO) — 6h ago"),
reply(" \"The AI infrastructure buildout is a decade-long trend.\""),
reply(" → 8.4k reactions"),
],
},
"opencli-reader": {
prompt: "pull Eastmoney hot rank and HackerNews top stories",
lines: [
gap(),
thinking("Reading skill: opencli-reader"),
gap(),
tool("Bash", "opencli list -f json | grep -E 'eastmoney|hackernews'", 300),
out("Discovering available adapters..."),
gap(),
tool("Bash", "opencli eastmoney hot-rank -f json", 400),
out("Fetching Eastmoney hot-rank (PUBLIC strategy)..."),
gap(),
reply("## Eastmoney — Hot Rank (retail flow)"),
reply(" 1. 贵州茅台 (SH600519) +2.4% ¥1,782"),
reply(" 2. 比亚迪 (SZ002594) +3.1% ¥248"),
reply(" 3. 宁德时代 (SZ300750) +1.8% ¥215"),
gap(),
tool("Bash", "opencli hackernews top --limit 5 -f json", 300),
out("Fetching HN top (PUBLIC strategy)..."),
gap(),
reply("## HackerNews — Top 5"),
reply(" • Show HN: Fast LLM inference on CPU 842 pts"),
reply(" • The economics of running a side project 614 pts"),
reply(" • Why we switched from Postgres to DuckDB 489 pts"),
gap(),
ok("Fallback successful — both sources pulled without a dedicated skill"),
],
},
"yc-reader": {
prompt: "show me YC fintech companies that are hiring",
lines: [
gap(),
thinking("Reading skill: yc-reader"),
gap(),
tool("Bash", "curl -s 'api.ycombinator.com/v0.1/companies?tags=fintech'", 400),
out("Fetching YC fintech companies..."),
gap(),
reply("## YC Fintech — Hiring Now"),
gap(),
reply(" Mercury (W19) Banking for startups 42 roles"),
reply(" Brex (W17) Corporate cards & spend 38 roles"),
reply(" Ramp (W19) Expense management 35 roles"),
reply(" Stripe (S09) Payments infrastructure 124 roles"),
reply(" Plaid (W13) Banking APIs 28 roles"),
gap(),
reply("Total: 23 fintech companies actively hiring (267 roles)"),
],
},
"startup-analysis": {
prompt: "analyze LangChain as a potential investment",
lines: [
gap(),
thinking("Reading skill: startup-analysis"),
gap(),
tool("WebSearch", "LangChain funding valuation 2026", 400),
tool("WebFetch", "crunchbase.com/organization/langchain", 300),
out("Gathering funding data, team info, market position..."),
gap(),
reply("## LangChain — 3-Perspective Analysis"),
gap(),
reply("VC Investor View:"),
reply(" Valuation: $3B (Series B, Oct 2025)"),
reply(" ARR est: $45M → ~66x revenue multiple"),
reply(" Risk: High burn, crowded LLM tooling market"),
gap(),
reply("Job Applicant View:"),
reply(" Team: 142 employees, strong eng culture"),
reply(" Glassdoor: 4.2/5 | Runway: ~24 months"),
gap(),
reply("CEO/Founder View:"),
reply(" Moat: developer ecosystem + LangSmith lock-in"),
],
},
"company-valuation": {
prompt: "what is AAPL worth — DCF + peer comps",
lines: [
gap(),
thinking("Reading skill: company-valuation"),
gap(),
tool("Bash", "python3 -c 'import yfinance' && echo OK", 300),
ok("yfinance, numpy, pandas ready"),
tool("Bash", "python3 -c \"yf.Ticker('^TNX').fast_info.last_price\"", 250),
out("RF_10Y=0.0432 — using live risk-free rate"),
gap(),
tool("Bash", "python3 valuation.py AAPL", 500),
out("Projecting 5-yr FCFF, computing WACC, building peer set..."),
gap(),
reply("## AAPL — Blended Fair Value"),
gap(),
reply("Method Implied Weight vs Current"),
reply("DCF (base) $228.40 50% +15.4%"),
reply("Relative (peers) $201.80 50% +2.0%"),
reply("Blended $215.10 +8.7%"),
gap(),
reply("Current price: $197.82"),
gap(),
reply("DCF assumptions:"),
reply(" Revenue growth path: 6.8% → 3.5% (5-yr fade)"),
reply(" EBIT margin: 30.2% (3y median)"),
reply(" WACC: 8.9% | Terminal g: 2.5% | TV/EV: 68%"),
gap(),
reply("Peers (fwd P/E median 28.4x):"),
reply(" MSFT 30.1x GOOGL 22.4x META 24.8x ORCL 26.1x"),
gap(),
reply("Sensitivity (WACC × g): range $178 to $267"),
reply("Bull / Base / Bear: $268 / $215 / $172"),
gap(),
warn("Services segment may justify SOTP — not run here"),
],
},
"saas-valuation-compression": {
prompt: "analyze Vercel valuation from Series D to E",
lines: [
gap(),
thinking("Reading skill: saas-valuation-compression"),
gap(),
tool("WebSearch", "Vercel Series D E valuation ARR", 400),
out("Searching funding rounds and ARR estimates..."),
gap(),
reply("## Vercel — Valuation Compression Analysis"),
gap(),
reply("Series D (Nov 2021): $2.5B at ~150x ARR"),
reply("Series E (May 2024): $3.2B at ~40x ARR"),
reply("Compression: 73% multiple reduction"),
gap(),
reply("ARR growth: $17M → $80M (+370%)"),
reply("But multiple shrunk faster than ARR grew."),
gap(),
reply("Macro attribution: ~60% rate environment"),
reply("AI premium: +12x vs non-AI SaaS peers"),
],
},
"sepa-strategy": {
prompt: "analyze NVDA using SEPA — is it a good setup?",
lines: [
gap(),
thinking("Reading skill: sepa-strategy"),
gap(),
tool("Bash", "python3 sepa_analysis.py NVDA", 500),
out("Running Minervini SEPA checklist..."),
gap(),
reply("## NVDA — SEPA Strategy Analysis"),
gap(),
reply("Stage: 2 (Advancing) ✓"),
gap(),
reply("Trend Template (8 conditions):"),
ok("Price > 50-day MA ($132.40)"),
ok("Price > 200-day MA ($118.60)"),
ok("200-day MA trending up 4+ months"),
ok("50-day MA > 200-day MA"),
ok("Price > 52-week low by ≥30%"),
ok("Price within 25% of 52-week high"),
ok("RS rating ≥ 70 (current: 94)"),
ok("Current price above 50-day MA"),
gap(),
reply("VCP: Base #3 forming — 28% → 14% → 6% contraction"),
reply("Pivot: $142.80 on volume > 48M"),
reply("Stop: $134.20 (6.0%) | R/R: 3.2:1"),
],
},
"stock-liquidity": {
prompt: "analyze TSLA liquidity profile",
lines: [
gap(),
thinking("Reading skill: stock-liquidity"),
gap(),
tool("Bash", "python3 liquidity.py TSLA", 400),
out("Analyzing bid-ask spreads, volume, and depth..."),
gap(),
reply("## TSLA — Liquidity Profile"),
gap(),
reply("Avg Daily Volume: 128.4M shares"),
reply("Bid-Ask Spread: $0.01 (0.004%)"),
reply("Amihud Illiquidity: 0.0021 (highly liquid)"),
gap(),
reply("Turnover Ratio: 4.1% daily"),
reply("Market Impact (1M shares): ~$0.12 (0.05%)"),
gap(),
reply("Verdict: Extremely liquid — minimal slippage risk."),
],
},
"finance-sentiment": {
prompt: "cross-source sentiment for AAPL",
lines: [
gap(),
thinking("Reading skill: finance-sentiment"),
gap(),
tool("Bash", "curl -s 'api.adanos.ai/v1/sentiment?symbol=AAPL'", 400),
out("Fetching cross-source sentiment via Adanos API..."),
gap(),
reply("## AAPL — Multi-Source Sentiment"),
gap(),
reply("Source Buzz Bullish% Mentions Trend"),
reply("Reddit High 74% 2,841 ↑"),
reply("X.com Med 68% 1,422 →"),
reply("News High 81% 384 ↑"),
reply("Polymarket — 72% — ↑"),
gap(),
reply("Composite score: 73% bullish (above 30d avg of 65%)"),
],
},
};
// ---------------------------------------------------------------------------
// Build terminal tabs for a skill
// ---------------------------------------------------------------------------
function buildSkillTabs(skill: Skill): TabContent[] {
const session = mockSessions[skill.name];
if (session) {
return [
{
label: "example",
command: `claude "${session.prompt}"`,
lines: session.lines,
},
];
}
return [
{
label: "example",
command: `claude "use ${skill.name}"`,
lines: [
gap(),
thinking(`Reading skill: ${skill.name}`),
gap(),
reply(skill.description),
gap(),
{ text: " ✓ Done", color: c.green, delay: 300 },
],
},
];
}
@@ -1,768 +0,0 @@
"use client";
import { useState } from "react";
type Chapter = {
id: string;
num: string;
title: string;
content: React.ReactNode;
};
function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
className={`w-3.5 h-3.5 shrink-0 text-text-muted transition-transform duration-200 ${open ? "rotate-90" : ""}`}
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z" />
</svg>
);
}
function Note({ children }: { children: React.ReactNode }) {
return (
<div className="border border-border rounded-lg px-4 py-3 text-sm text-text-secondary leading-relaxed bg-bg-elevated">
{children}
</div>
);
}
function Label({ children, color }: { children: React.ReactNode; color?: "green" | "red" | "yellow" }) {
const colorClass = color === "green" ? "text-green" : color === "red" ? "text-red-400" : color === "yellow" ? "text-yellow" : "text-text-muted";
return (
<span className={`text-[10px] font-semibold uppercase tracking-wider inline-block px-1.5 py-0.5 rounded bg-bg-hover border border-border shrink-0 ${colorClass}`}>
{children}
</span>
);
}
function RuleItem({
label,
labelColor,
title,
desc,
}: {
label: string;
labelColor?: "green" | "red" | "yellow";
title: string;
desc: string;
}) {
return (
<div className="flex items-start gap-3 border border-border rounded-lg p-3 bg-bg">
<Label color={labelColor}>{label}</Label>
<div>
<p className="font-medium text-sm text-text">{title}</p>
<p className="text-xs text-text-muted mt-0.5 leading-relaxed">{desc}</p>
</div>
</div>
);
}
function StageBox({
num,
title,
accent,
children,
}: {
num: string;
title: string;
accent?: "green" | "yellow" | "red";
children: React.ReactNode;
}) {
const accentBorder = accent === "green" ? "border-green/40" : accent === "yellow" ? "border-yellow/40" : accent === "red" ? "border-red-400/40" : "border-border";
const numColor = accent === "green" ? "text-green" : accent === "yellow" ? "text-yellow" : accent === "red" ? "text-red-400" : "text-text-muted";
return (
<div className={`border rounded-lg p-4 bg-bg ${accentBorder}`}>
<div className={`text-lg font-bold font-mono ${numColor}`}>{num}</div>
<div className="font-semibold text-sm mt-1 text-text-secondary">{title}</div>
<div className="text-xs text-text-muted mt-2 leading-relaxed">{children}</div>
</div>
);
}
function CompareColumn({
title,
items,
type,
}: {
title: string;
items: string[];
type: "positive" | "negative";
}) {
const symbol = type === "positive" ? "+" : "";
const symbolColor = type === "positive" ? "text-green" : "text-red-400";
return (
<div className="border border-border rounded-lg p-4 bg-bg">
<p className="font-semibold text-sm mb-2 text-text">{title}</p>
<ul className="space-y-1.5">
{items.map((item, i) => (
<li key={i} className="text-xs text-text-muted flex gap-2 leading-relaxed">
<span className={`${symbolColor} shrink-0`}>{symbol}</span>
{item}
</li>
))}
</ul>
</div>
);
}
function FormulaBox({ children }: { children: React.ReactNode }) {
return (
<div className="border border-border rounded-lg px-4 py-3 text-center font-mono text-sm text-text-secondary bg-bg-elevated">
{children}
</div>
);
}
function CheckItem({ children }: { children: React.ReactNode }) {
return (
<li className="flex items-start gap-2 text-xs text-text-muted bg-bg border border-border rounded px-3 py-2">
<span className="text-text-muted shrink-0 mt-px">&#9744;</span>
<span className="leading-relaxed">{children}</span>
</li>
);
}
function StatBox({ label, value, sub, valueColor }: { label: string; value: string; sub: string; valueColor?: "green" | "red" | "yellow" }) {
const vc = valueColor === "green" ? "text-green" : valueColor === "red" ? "text-red-400" : valueColor === "yellow" ? "text-yellow" : "text-text";
return (
<div className="border border-border rounded-lg p-3 text-center bg-bg">
<p className="text-[10px] text-text-muted uppercase tracking-wider">{label}</p>
<p className={`text-lg font-bold mt-0.5 font-mono ${vc}`}>{value}</p>
<p className="text-[10px] text-text-muted mt-0.5">{sub}</p>
</div>
);
}
// ─── Chapter Content ────────────────────────────────────────────
const chapters: Chapter[] = [
{
id: "overview",
num: "1",
title: "What Is SEPA?",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
<strong className="text-text">SEPA</strong> (Specific Entry Point Analysis) is a complete trading system developed by Mark Minervini over 37 years of active trading. His audited track record includes +220% average annual return over 5 years, +155% in the 1997 U.S. Investing Championship, and +334.8% in the 2021 Championship.
</p>
<Note>
<strong className="text-text">One-sentence summary:</strong> Buy the right stock, in the right stage, at a precise entry point, with strict risk management.
</Note>
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mt-6 mb-3">Five Pillars</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{[
["1. Stage 2 Uptrend", "Only buy stocks in their primary uptrend phase — skip basing and declining stocks entirely."],
["2. Trend Template", "8 technical conditions must all pass before considering entry. Fail one → skip the stock."],
["3. Fundamental Support", "Accelerating EPS, revenue growth, institutional buying. Real leaders have real earnings behind them."],
["4. Chart Patterns (VCP)", "Wait for volatility to contract to the breaking point, then enter at the precise moment supply is exhausted."],
].map(([title, desc]) => (
<div key={title} className="border border-border rounded-lg p-3 bg-bg">
<p className="font-semibold text-sm text-text">{title}</p>
<p className="text-xs text-text-muted mt-1 leading-relaxed">{desc}</p>
</div>
))}
</div>
<div className="border border-border rounded-lg p-3 bg-bg-elevated">
<p className="font-semibold text-sm text-text">5. Strict Risk Control <span className="text-text-muted font-normal">(Most Important)</span></p>
<p className="text-xs text-text-muted mt-1 leading-relaxed">Risk 1-2% per trade, stop loss set before entry, R:R 2:1. Win rate is only ~50% profitability comes from losing small and winning big.</p>
</div>
<Note>
<strong className="text-text">The casino analogy:</strong> A casino doesn&apos;t win every hand it wins through mathematical edge over thousands of hands. SEPA works the same way. With 50% win rate: 5 wins × 15% 5 losses × 6% = <strong className="text-green">+45% net</strong>. A trader with 55% win rate but no stop discipline: 5.5 × 5% 4.5 × 12% = <strong className="text-red-400">26.5% net</strong>.
</Note>
</div>
),
},
{
id: "stages",
num: "2",
title: "The 4 Stages of Every Stock",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
Every stock cycles through four stages repeatedly (Weinstein, 1988). <strong className="text-text">Identifying the current stage is the starting point for all decisions.</strong>
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<StageBox num="01" title="Basing">
Price near 200MA, MAs flat/tangled, low volume. Institutions quietly accumulate.
<p className="mt-2 font-medium text-text-secondary"> Do nothing. Wait.</p>
</StageBox>
<StageBox num="02" title="Advancing" accent="green">
Higher highs/lows, bullish MA alignment, volume expands on up days. VCP patterns appear.
<p className="mt-2 font-medium text-green"> The ONLY stage to buy.</p>
</StageBox>
<StageBox num="03" title="Topping" accent="yellow">
Wide swings at highs, false breakouts, heavy volume without progress. Media hype peaks.
<p className="mt-2 font-medium text-yellow"> Reduce. No new buys.</p>
</StageBox>
<StageBox num="04" title="Declining" accent="red">
Below all MAs, bearish alignment. &quot;Down 60%&quot; can still go to zero.
<p className="mt-2 font-medium text-red-400"> Full cash. Stay away.</p>
</StageBox>
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mt-4 mb-2">Base Count How Far Along Is Stage 2?</p>
<div className="overflow-x-auto">
<table className="w-full text-xs border border-border rounded-lg overflow-hidden">
<thead>
<tr>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Base #</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Safety</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Advice</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-border"><td className="px-3 py-2">12</td><td className="px-3 py-2 text-green">Highest</td><td className="px-3 py-2 text-text-muted">Full position early Stage 2, max upside</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2">34</td><td className="px-3 py-2 text-yellow">Moderate</td><td className="px-3 py-2 text-text-muted">Slightly smaller position</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2">56</td><td className="px-3 py-2 text-red-400">Low</td><td className="px-3 py-2 text-text-muted">Half position max late stage</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2">7+</td><td className="px-3 py-2 text-red-400">Dangerous</td><td className="px-3 py-2 text-text-muted">Avoid likely transitioning to Stage 3</td></tr>
</tbody>
</table>
</div>
<Note>
<strong className="text-text">Stage 12 transition signals:</strong> (1) 200MA shifts from declining flat upward (2) Price breaks above consolidation with volume (3) 50MA crosses above 150MA/200MA (golden cross)
</Note>
</div>
),
},
{
id: "trend-template",
num: "3",
title: "Trend Template — 8 Conditions",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
The trend template is a pre-entry qualification. <strong className="text-text">All 8 must pass simultaneously.</strong> If any fails, skip the stock entirely.
</p>
<div className="space-y-2">
<RuleItem label="Must" labelColor="green" title="1. Price > 150MA AND Price > 200MA" desc="Stock is above long-term moving averages — confirmed uptrend." />
<RuleItem label="Must" labelColor="green" title="2. 150MA > 200MA" desc="Intermediate MA leads the long-term MA — bullish hierarchy." />
<RuleItem label="Must" labelColor="green" title="3. 200MA trending up ≥ 1 month (ideally 4-5 months)" desc="Long-term trend is healthy and sustained, not a temporary bounce." />
<RuleItem label="Must" labelColor="green" title="4. 50MA > 150MA AND 50MA > 200MA" desc="Short-term MA leads all others — strong recent momentum." />
<RuleItem label="Must" labelColor="green" title="5. Price > 50MA" desc="Near-term trend confirmed positive." />
<RuleItem label="Must" labelColor="green" title="6. Price ≥ 30% above 52-week low" desc="Stock has truly left its bottom — not a minor bounce." />
<RuleItem label="Must" labelColor="green" title="7. Price within 25% of 52-week high" desc="Trading near highs, not far off a peak. Closer = stronger." />
<RuleItem label="Key" labelColor="yellow" title="8. Relative Strength > 70th percentile (prefer 85-90+)" desc="Only trade real market leaders. RS = how this stock's 12-month return ranks vs the entire market." />
</div>
<Note>
<strong className="text-text">Memory aid 3 sentences:</strong><br />
Conditions 1-5 = <strong className="text-text">MA Staircase</strong> (Price {`>`} 50MA {`>`} 150MA {`>`} 200MA, 200MA rising)<br />
Conditions 6-7 = <strong className="text-text">Price Position</strong> (far from low 30%, near high 25%)<br />
Condition 8 = <strong className="text-text">Relative Strength</strong> (market leader, RS {`>`} 70)
</Note>
</div>
),
},
{
id: "fundamentals",
num: "4",
title: "Fundamental Requirements",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
75% of superperformer stocks had quarterly EPS growth {`>`}20% before their biggest move. Fundamentals separate real leaders from hype.
</p>
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-2">EPS Growth Tiers</p>
<div className="grid grid-cols-3 gap-3">
<StatBox label="Minimum" value="≥ 20%" sub="Below = disqualify" valueColor="green" />
<StatBox label="Preferred" value="2550%" sub="Most winners here" valueColor="yellow" />
<StatBox label="Super Stock" value="50%+" sub="Biggest movers" />
</div>
<Note>
<strong className="text-text">Acceleration matters most.</strong> Growth must be speeding up: this quarter&apos;s EPS growth rate {`>`} last quarter&apos;s. If growth decelerates (30% 22%), that&apos;s a warning even though 22% still looks decent.
</Note>
<div className="overflow-x-auto">
<table className="w-full text-xs border border-border rounded-lg overflow-hidden">
<thead>
<tr>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Check</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Threshold</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Why</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Quarterly EPS</td><td className="px-3 py-2 text-text-secondary"> 20%, accelerating</td><td className="px-3 py-2 text-text-muted">Core growth driver</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Annual EPS</td><td className="px-3 py-2 text-text-secondary"> 25% each of past 3 years</td><td className="px-3 py-2 text-text-muted">Sustained competitive advantage</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Revenue</td><td className="px-3 py-2 text-text-secondary"> 15% annual, 20% quarterly</td><td className="px-3 py-2 text-text-muted">Real demand, not cost-cutting</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Margins</td><td className="px-3 py-2 text-text-secondary">Stable or expanding</td><td className="px-3 py-2 text-text-muted">Pricing power, moat</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Institutions</td><td className="px-3 py-2 text-text-secondary">Holdings increasing</td><td className="px-3 py-2 text-text-muted">Smart money fueling the move</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Catalyst</td><td className="px-3 py-2 text-text-secondary">New product, FDA, contract</td><td className="px-3 py-2 text-text-muted">With: 50-100%+; without: 15-25%</td></tr>
</tbody>
</table>
</div>
<Note>
<strong className="text-text">Red flag:</strong> EPS growing but revenue flat/declining = &quot;fake growth&quot; from cost-cutting, layoffs, or buybacks. Not sustainable.
</Note>
</div>
),
},
{
id: "vcp",
num: "5",
title: "VCP — Volatility Contraction Pattern",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
VCP is the signature SEPA pattern. Think of price as a <strong className="text-text">spring being compressed</strong>: each pullback is tighter than the last. When compressed to the limit (all sellers exhausted), the spring releases that&apos;s the breakout.
</p>
<div className="border border-border rounded-lg p-4 bg-bg-elevated">
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mb-3">Typical VCP Contraction Sequence</p>
<div className="flex items-end gap-2 justify-center py-2">
{[
{ depth: "20%", h: 56, opacity: "opacity-40" },
{ depth: "12%", h: 36, opacity: "opacity-30" },
{ depth: "6%", h: 20, opacity: "opacity-25" },
{ depth: "3%", h: 10, opacity: "opacity-20" },
].map((c, i) => (
<div key={i} className="flex flex-col items-center gap-1.5">
<div className={`w-14 bg-text-secondary ${c.opacity} rounded`} style={{ height: `${c.h}px` }} />
<span className="text-[10px] text-text-muted font-mono">{c.depth}</span>
</div>
))}
<div className="text-text-muted px-1 self-center text-xs"></div>
<div className="flex flex-col items-center gap-1.5">
<div className="w-14 bg-text-secondary opacity-50 rounded" style={{ height: "64px" }} />
<span className="text-[10px] text-text-secondary font-mono font-semibold">Breakout</span>
</div>
</div>
<p className="text-[10px] text-text-muted mt-2 text-center">Each contraction is smaller volume dries up breakout on heavy volume</p>
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mt-4 mb-2">7 Identification Rules</p>
<div className="space-y-2">
<RuleItem label="Must" labelColor="green" title="1. Stock in Stage 2 uptrend" desc="No VCP exists in a downtrend — that's just a normal bounce." />
<RuleItem label="Must" labelColor="green" title="2. Pullback depths decrease (e.g. 20% → 12% → 6% → 3%)" desc="Minimum 3 contractions, 4-5 ideal. If 2nd pullback is deeper than 1st, it's NOT a VCP." />
<RuleItem label="Must" labelColor="green" title="3. Volume shrinks → Volume Dry-Up (VDU)" desc="The final contraction shows multi-week low volume — sellers are nearly depleted." />
<RuleItem label="Must" labelColor="green" title="4. Higher lows on each pullback" desc="Buyers stepping in at progressively higher prices = institutional accumulation." />
<RuleItem label="Must" labelColor="green" title="5. Clear pivot point (consolidation high)" desc="The entry target. VCP breakout = crossing this resistance level." />
<RuleItem label="Key" labelColor="yellow" title="6. RS > 70 (prefer 85-90+)" desc="Leader VCPs succeed far more often than laggard VCPs." />
<RuleItem label="Key" labelColor="yellow" title="7. Bull or neutral market environment" desc="Bear market VCP breakouts fail at much higher rates." />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-4">
<CompareColumn title="Quality VCP" type="positive" items={[
"Pullback depths strictly decreasing",
"Each low higher than the last",
"Volume decreasing → VDU at the end",
"Clear Stage 2 uptrend context",
"Breakout with volume ≥ 1.5x average",
]} />
<CompareColumn title="Fake VCP (Traps)" type="negative" items={[
"Irregular pullback depths",
"Lows not progressively higher",
"Volume not shrinking on pullbacks",
"Stock in a downtrend overall",
"Breakout with weak volume → falls back",
]} />
</div>
</div>
),
},
{
id: "other-patterns",
num: "6",
title: "Other Consolidation Patterns",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
Besides VCP, SEPA uses four more classic patterns. All share the same entry rule: <strong className="text-text">breakout above pivot + volume 1.5x average</strong>.
</p>
<div className="overflow-x-auto">
<table className="w-full text-xs border border-border rounded-lg overflow-hidden">
<thead>
<tr>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Pattern</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Depth</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Duration</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Key Feature</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Strength</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">VCP</td><td className="px-3 py-2">Decreasing &lt;3%</td><td className="px-3 py-2">320 wk</td><td className="px-3 py-2 text-text-muted">Multi-contraction + higher lows</td><td className="px-3 py-2 font-mono text-text-secondary">5/5</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Cup &amp; Handle</td><td className="px-3 py-2">Cup 12-35%, handle 12%</td><td className="px-3 py-2">765 wk</td><td className="px-3 py-2 text-text-muted">U-shaped base + small handle</td><td className="px-3 py-2 font-mono text-text-secondary">4/5</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Flat Base</td><td className="px-3 py-2"> 15%</td><td className="px-3 py-2">510 wk</td><td className="px-3 py-2 text-text-muted">Tight range near prior high</td><td className="px-3 py-2 font-mono text-text-secondary">3/5</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">Bull Flag</td><td className="px-3 py-2"> 50% of pole</td><td className="px-3 py-2">15 wk</td><td className="px-3 py-2 text-text-muted">Sharp advance + tight pullback</td><td className="px-3 py-2 font-mono text-text-secondary">4/5</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">High Tight Flag</td><td className="px-3 py-2"> 25% after 100%+ run</td><td className="px-3 py-2">14 wk</td><td className="px-3 py-2 text-text-muted">Rarest most powerful</td><td className="px-3 py-2 font-mono text-text-secondary">5/5</td></tr>
</tbody>
</table>
</div>
</div>
),
},
{
id: "entry",
num: "7",
title: "Precise Entry Points",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
&quot;Specific Entry Point&quot; is what SEPA stands for. Not &quot;looks about right&quot; a very specific price level with defined risk.
</p>
<Note>
<strong className="text-text">Pivot point =</strong> the highest price in the consolidation range. Below it, supply demand. Above it, demand overwhelms remaining supply.
</Note>
<div className="grid grid-cols-3 gap-3">
<StatBox label="Buy Zone" value="0 5%" sub="above pivot" valueColor="green" />
<StatBox label="Chase Zone" value="> 5%" sub="Don't enter — wait" valueColor="red" />
<StatBox label="Breakout Vol." value="≥ 1.5x" sub="20-day average" valueColor="yellow" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-2">
<CompareColumn title="True Breakout" type="positive" items={[
"Volume ≥ 1.5x average — big spike",
"Closes near the day's high",
"Volume Dry-Up preceded it",
"Continues higher next day",
]} />
<CompareColumn title="False Breakout" type="negative" items={[
"Volume below average — weak",
"Falls back below pivot by close",
"No VDU before the attempt",
"Drops back into the range quickly",
]} />
</div>
<Note>
<strong className="text-text">Avoid entering within 2 weeks of earnings.</strong> Earnings are binary events even a perfect chart setup can gap down on a miss.
</Note>
</div>
),
},
{
id: "position",
num: "8",
title: "Stop Loss & Position Sizing",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
<strong className="text-text">The most critical part of SEPA.</strong> Minervini: &quot;Not losing big is the only prerequisite for winning big.&quot;
</p>
<FormulaBox>
Shares = (Account × Risk%) ÷ (Entry Price Stop Price)
</FormulaBox>
<div className="border border-border rounded-lg p-4 bg-bg-elevated">
<p className="text-xs font-semibold text-text-secondary mb-2">Example: $100,000 account, 1% risk per trade</p>
<div className="space-y-1 text-xs text-text-muted leading-relaxed">
<p>1. Max loss = $100,000 × 1% = <strong className="text-text">$1,000</strong></p>
<p>2. Buy at $50, stop at 7% = $46.50 distance = <strong className="text-text">$3.50/share</strong></p>
<p>3. Shares = $1,000 ÷ $3.50 = <strong className="text-text">285 shares</strong> ($14,250 = 14.25% of account)</p>
<p>4. Target 1: $54.00 (+8%) sell half, move stop to breakeven</p>
<p>5. Target 2: $57.50 (+15%) R:R = $7.50/$3.50 = <strong className="text-green">2.14 : 1</strong></p>
</div>
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mt-4 mb-2">Stop Loss Evolution</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{[
["Phase 1: Entry", "Hard stop at entry 7-8%. Non-negotiable. Set it the moment you buy."],
["Phase 2: At +8%", "Sell half. Move stop → entry price (breakeven). Trade can no longer lose."],
["Phase 3: At +15%", "Sell 25% more. Trail remaining stop along 20MA. Close below 20MA = exit all."],
].map(([title, desc]) => (
<div key={title} className="border border-border rounded-lg p-3 bg-bg">
<p className="font-semibold text-sm text-text">{title}</p>
<p className="text-xs text-text-muted mt-1 leading-relaxed">{desc}</p>
</div>
))}
</div>
<div className="space-y-2 mt-4">
<RuleItem label="Never" labelColor="red" title="Never move stop DOWN" desc="Stops only move up. Moving down turns small losses into catastrophic ones." />
<RuleItem label="Never" labelColor="red" title="Never average down" desc="Adding to a loser is the fastest path to account destruction." />
<RuleItem label="Rule" labelColor="yellow" title="After 3-4 consecutive losses, reduce risk" desc="Cut risk to 0.5%, fewer positions. Diagnose: is it your execution or the market?" />
</div>
</div>
),
},
{
id: "pyramiding",
num: "9",
title: "Pyramiding vs Averaging Down",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
Pyramiding = do more of what&apos;s working. Averaging down = do more of what&apos;s failing.
</p>
<div className="overflow-x-auto">
<table className="w-full text-xs border border-border rounded-lg overflow-hidden">
<thead>
<tr>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Tranche</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">When</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Size</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary">Example</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">1st (Main)</td><td className="px-3 py-2 text-text-muted">Pivot breakout</td><td className="px-3 py-2 text-text-secondary">50%</td><td className="px-3 py-2 text-text-muted font-mono">100 @ $50</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">2nd (Add)</td><td className="px-3 py-2 text-text-muted">+8%, pullback to 20MA</td><td className="px-3 py-2 text-text-secondary">30%</td><td className="px-3 py-2 text-text-muted font-mono">60 @ $54</td></tr>
<tr className="border-t border-border"><td className="px-3 py-2 font-medium text-text">3rd (Add)</td><td className="px-3 py-2 text-text-muted">Next base breakout</td><td className="px-3 py-2 text-text-secondary">20%</td><td className="px-3 py-2 text-text-muted font-mono">35 @ $58</td></tr>
</tbody>
</table>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<CompareColumn title="Pyramid (Correct)" type="positive" items={[
"Largest position at lowest cost — max cushion",
"Only add when market proves you right",
"Even if adds fail, main position covers losses",
"Each add has a new confirming signal",
]} />
<CompareColumn title="Average Down (Dangerous)" type="negative" items={[
"Adding money where your thesis is proven wrong",
"'Lower average cost' is an illusion — real loss grows",
"$60 → $40 can still go to $5",
"Fastest path to account destruction",
]} />
</div>
</div>
),
},
{
id: "losses",
num: "10",
title: "Handling Losing Trades",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
~50% of trades lose money. That&apos;s expected. What matters is <strong className="text-text">how</strong> you lose.
</p>
<Note>
<strong className="text-text">Mindset:</strong> Minervini treats each loss as a &quot;business expense&quot; like a restaurant buying ingredients. Not every dish sells, but the restaurant isn&apos;t failing. The key is keeping each expense small and controlled.
</Note>
<p className="text-xs font-semibold uppercase tracking-wider text-text-muted mt-4 mb-2">Post-Loss Review 3 Questions</p>
<div className="space-y-2">
<div className="border border-border rounded-lg p-3 bg-bg">
<p className="font-medium text-sm text-text">Q1: Execution or strategy problem?</p>
<p className="text-xs text-text-muted mt-1 leading-relaxed">Execution (chased, no stop, weak volume entry) fix the habit. Strategy (misread pattern) study more examples.</p>
</div>
<div className="border border-border rounded-lg p-3 bg-bg">
<p className="font-medium text-sm text-text">Q2: &quot;Good loss&quot; or &quot;bad loss&quot;?</p>
<p className="text-xs text-text-muted mt-1 leading-relaxed">Good: followed all rules, market didn&apos;t cooperate <strong className="text-text-secondary">change nothing</strong>. Bad: broke rules <strong className="text-text-secondary">must eliminate</strong>.</p>
</div>
<div className="border border-border rounded-lg p-3 bg-bg">
<p className="font-medium text-sm text-text">Q3: Individual stock or overall market?</p>
<p className="text-xs text-text-muted mt-1 leading-relaxed">If recent breakouts keep failing, check the market first. If environment deteriorated, pause don&apos;t force it.</p>
</div>
</div>
</div>
),
},
{
id: "market",
num: "11",
title: "Market Environment",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary leading-relaxed">
The market environment is the <strong className="text-text">master switch</strong>. Even the best setups fail in bear markets.
</p>
<div className="overflow-x-auto">
<table className="w-full text-xs border border-border rounded-lg overflow-hidden">
<thead>
<tr>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-text-secondary"></th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-green">Bull</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-yellow">Choppy</th>
<th className="bg-bg-elevated px-3 py-2 text-left font-semibold text-red-400">Bear</th>
</tr>
</thead>
<tbody>
<tr className="border-t border-border">
<td className="px-3 py-2 font-medium text-text">Signal</td>
<td className="px-3 py-2 text-text-muted">Indices above 200MA, breadth expanding</td>
<td className="px-3 py-2 text-text-muted">Sideways, failed breakouts</td>
<td className="px-3 py-2 text-text-muted">Below 200MA, &gt;50% stocks below 200MA</td>
</tr>
<tr className="border-t border-border">
<td className="px-3 py-2 font-medium text-text">Risk/Trade</td>
<td className="px-3 py-2 text-text-secondary">12%</td>
<td className="px-3 py-2 text-text-secondary">0.51%</td>
<td className="px-3 py-2 text-text-secondary">0%</td>
</tr>
<tr className="border-t border-border">
<td className="px-3 py-2 font-medium text-text">Max Positions</td>
<td className="px-3 py-2 text-text-secondary">68</td>
<td className="px-3 py-2 text-text-secondary">23</td>
<td className="px-3 py-2 text-text-secondary">0 (all cash)</td>
</tr>
<tr className="border-t border-border">
<td className="px-3 py-2 font-medium text-text">Strategy</td>
<td className="px-3 py-2 text-text-secondary">Aggressive offense</td>
<td className="px-3 py-2 text-text-secondary">Best setups only</td>
<td className="px-3 py-2 text-text-secondary">100% cash</td>
</tr>
</tbody>
</table>
</div>
<Note>
<strong className="text-text">Key insight:</strong> Holding cash in a bear market IS a winning strategy. While others lose 30-50%, you preserve full ammunition for the next bull run.
</Note>
</div>
),
},
{
id: "quick-ref",
num: "QR",
title: "Quick Reference Card",
content: (
<div className="space-y-4">
<p className="text-sm text-text-secondary">Run through this checklist before every trade.</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="border border-border rounded-lg p-4 bg-bg">
<p className="font-semibold text-sm text-text mb-3">Pre-Entry Checklist</p>
<ul className="space-y-1.5">
<CheckItem>Stock in Stage 2 (bullish MA alignment)</CheckItem>
<CheckItem>Passes all 8 trend template conditions</CheckItem>
<CheckItem>Quarterly EPS 20% and accelerating</CheckItem>
<CheckItem>Revenue growth 15%</CheckItem>
<CheckItem>RS &gt; 70th percentile (prefer 90+)</CheckItem>
<CheckItem>Valid pattern identified (VCP, cup, flag)</CheckItem>
<CheckItem>Market environment: bull or neutral</CheckItem>
<CheckItem>&gt; 2 weeks until earnings</CheckItem>
<CheckItem>Buying within 0-5% of pivot point</CheckItem>
<CheckItem>Breakout volume 1.5x average</CheckItem>
<CheckItem>Stop loss set (entry 7-8%)</CheckItem>
</ul>
</div>
<div className="space-y-4">
<div className="border border-border rounded-lg p-4 bg-bg">
<p className="font-semibold text-sm text-text mb-3">Iron Rules</p>
<ul className="space-y-1.5">
<CheckItem>Stop set at entry non-negotiable</CheckItem>
<CheckItem>Risk 1-2% per trade</CheckItem>
<CheckItem>Stops only move UP, never down</CheckItem>
<CheckItem>Never average down</CheckItem>
<CheckItem>3 consecutive losses reduce risk</CheckItem>
</ul>
</div>
<div className="border border-border rounded-lg p-4 bg-bg">
<p className="font-semibold text-sm text-text mb-3">Profit-Taking</p>
<ul className="space-y-1.5">
<CheckItem>+8%: sell half, stop breakeven</CheckItem>
<CheckItem>+15%: sell 25% more</CheckItem>
<CheckItem>Remainder: trail with 20MA</CheckItem>
<CheckItem>Below 20MA: exit all</CheckItem>
</ul>
</div>
</div>
</div>
<FormulaBox>
Shares = (Account × Risk%) ÷ (Buy Price Stop Price)
</FormulaBox>
<div className="border border-border rounded-lg p-4 bg-bg-elevated text-center">
<p className="text-sm font-medium text-text-secondary italic">&quot;Not losing big is the only prerequisite for winning big.&quot;</p>
<p className="text-xs text-text-muted mt-1"> Mark Minervini</p>
</div>
</div>
),
},
];
// ─── Main Component ─────────────────────────────────────────────
export function SepaStudyGuide() {
const [openChapters, setOpenChapters] = useState<Set<string>>(new Set(["overview"]));
function toggle(id: string) {
setOpenChapters((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}
function expandAll() {
setOpenChapters(new Set(chapters.map((c) => c.id)));
}
function collapseAll() {
setOpenChapters(new Set());
}
return (
<div className="mt-8">
<div className="border border-border rounded-lg overflow-hidden">
{/* Header */}
<div className="border-b border-border px-4 py-3 bg-bg-elevated flex items-center justify-between">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-text-muted" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z" />
</svg>
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">Study Guide</span>
</div>
<div className="flex gap-2">
<button onClick={expandAll} className="text-[10px] text-text-muted hover:text-text-secondary transition-colors px-2 py-1 rounded hover:bg-bg-hover cursor-pointer">
Expand all
</button>
<button onClick={collapseAll} className="text-[10px] text-text-muted hover:text-text-secondary transition-colors px-2 py-1 rounded hover:bg-bg-hover cursor-pointer">
Collapse all
</button>
</div>
</div>
{/* Chapters */}
<div className="divide-y divide-border">
{chapters.map((ch) => {
const isOpen = openChapters.has(ch.id);
return (
<div key={ch.id}>
<button
onClick={() => toggle(ch.id)}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-bg-hover transition-colors text-left cursor-pointer"
>
<ChevronIcon open={isOpen} />
<span className="text-[10px] font-mono text-text-muted w-5 text-right shrink-0">
{ch.num}
</span>
<span className="text-sm font-medium text-text-secondary">{ch.title}</span>
</button>
{isOpen && (
<div className="px-5 pb-5 pt-1 sm:ml-8">
{ch.content}
</div>
)}
</div>
);
})}
</div>
{/* Footer */}
<div className="border-t border-border px-4 py-3 bg-bg-elevated">
<p className="text-[10px] text-text-muted text-center">
Based on Mark Minervini&apos;s &quot;Trade Like a Stock Market Wizard&quot; and &quot;Think &amp; Trade Like a Champion.&quot; For educational purposes only not investment advice.
</p>
</div>
</div>
</div>
);
}
-395
View File
@@ -1,395 +0,0 @@
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
type ReactNode,
} from "react";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface TerminalLine {
text: string;
color?: string;
delay?: number;
}
export interface TabContent {
label: string;
command: string;
lines: TerminalLine[];
}
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
interface TerminalAnimationContextValue {
activeTab: number;
setActiveTab: (index: number) => void;
commandTyped: string;
isTypingCommand: boolean;
showCursor: boolean;
visibleLines: number;
currentTab: TabContent;
tabs: TabContent[];
}
const TerminalAnimationContext = createContext<
TerminalAnimationContextValue | undefined
>(undefined);
function useTerminalAnimation() {
const ctx = useContext(TerminalAnimationContext);
if (!ctx) {
throw new Error(
"TerminalAnimation components must be used within TerminalAnimationRoot"
);
}
return ctx;
}
// ---------------------------------------------------------------------------
// Tab data
// ---------------------------------------------------------------------------
const accent = "var(--color-accent)";
const muted = "var(--color-text-muted)";
const secondary = "var(--color-text-secondary)";
const green = "var(--color-green)";
const terminalTabs: TabContent[] = [
{
label: "plugin",
command: "npx plugins add himself65/finance-skills",
lines: [
{ text: "", delay: 200 },
{ text: "┌ plugins", color: secondary, delay: 100 },
{ text: "◇ Source: https://github.com/himself65/finance-skills", color: muted, delay: 300 },
{ text: "│", color: muted, delay: 60 },
{ text: "● Repository cloned", color: green, delay: 400 },
{ text: "│", color: muted, delay: 60 },
{ text: "◇ Found 5 plugin(s)", color: muted, delay: 300 },
{ text: "│", color: muted, delay: 60 },
{ text: "│ finance-market-analysis 10 skills Stock analysis, earnings, options via yfinance", color: secondary, delay: 100 },
{ text: "│ finance-social-readers 5 skills Twitter, Discord, LinkedIn, Telegram, YC", color: secondary, delay: 100 },
{ text: "│ finance-startup-tools 1 skill Startup analysis", color: secondary, delay: 100 },
{ text: "│ finance-ui-tools 1 skill Generative UI design system", color: secondary, delay: 100 },
{ text: "│", color: muted, delay: 60 },
{ text: "│ Targets: Claude Code", color: muted, delay: 80 },
{ text: "│ Scope: user", color: muted, delay: 80 },
{ text: "│", color: muted, delay: 60 },
{ text: "◆ Install? Y", color: secondary, delay: 500 },
{ text: "◇ Preparing plugins for Claude Code...", color: muted, delay: 400 },
{ text: "│", color: muted, delay: 60 },
{ text: "◇ Adding marketplace", color: muted, delay: 200 },
{ text: "● Marketplace added", color: green, delay: 300 },
{ text: "│", color: muted, delay: 60 },
{ text: "◇ Installing 5 plugins (20 skills)...", color: muted, delay: 400 },
{ text: "● Installed finance-market-analysis", color: green, delay: 200 },
{ text: "● Installed finance-social-readers", color: green, delay: 200 },
{ text: "● Installed finance-data-providers", color: green, delay: 200 },
{ text: "● Installed finance-startup-tools", color: green, delay: 200 },
{ text: "● Installed finance-ui-tools", color: green, delay: 200 },
{ text: "│", color: muted, delay: 60 },
{ text: "● Done. Restart your agent tools to load the plugins.", color: green, delay: 200 },
],
},
];
// ---------------------------------------------------------------------------
// Root
// ---------------------------------------------------------------------------
function TerminalAnimationRoot({
tabs,
children,
}: {
tabs: TabContent[];
children: ReactNode;
}) {
const [activeTab, setActiveTab] = useState(0);
const [visibleLines, setVisibleLines] = useState(0);
const [commandTyped, setCommandTyped] = useState("");
const [isTypingCommand, setIsTypingCommand] = useState(true);
const [showCursor, setShowCursor] = useState(true);
const timeoutRef = useRef<ReturnType<typeof setTimeout>[]>([]);
const clearTimeouts = useCallback(() => {
timeoutRef.current.forEach(clearTimeout);
timeoutRef.current = [];
}, []);
const animateTab = useCallback(
(tabIndex: number) => {
clearTimeouts();
setVisibleLines(0);
setCommandTyped("");
setIsTypingCommand(true);
setShowCursor(true);
const tab = tabs[tabIndex];
if (!tab) return;
const command = tab.command;
let charIndex = 0;
const typeCommand = () => {
if (charIndex <= command.length) {
setCommandTyped(command.slice(0, charIndex));
charIndex++;
const t = setTimeout(typeCommand, 25 + Math.random() * 35);
timeoutRef.current.push(t);
} else {
const t = setTimeout(() => {
setIsTypingCommand(false);
showLines(0);
}, 250);
timeoutRef.current.push(t);
}
};
const showLines = (lineIndex: number) => {
if (lineIndex <= tab.lines.length) {
setVisibleLines(lineIndex);
if (lineIndex < tab.lines.length) {
const delay = tab.lines[lineIndex].delay ?? 100;
const t = setTimeout(() => showLines(lineIndex + 1), delay);
timeoutRef.current.push(t);
} else {
const t = setTimeout(() => setShowCursor(false), 600);
timeoutRef.current.push(t);
}
}
};
const t = setTimeout(typeCommand, 300);
timeoutRef.current.push(t);
},
[clearTimeouts, tabs]
);
useEffect(() => {
animateTab(activeTab);
return clearTimeouts;
}, [activeTab, animateTab, clearTimeouts]);
const currentTab = tabs[activeTab] ?? tabs[0];
return (
<TerminalAnimationContext.Provider
value={{
activeTab,
setActiveTab,
commandTyped,
isTypingCommand,
showCursor,
visibleLines,
currentTab,
tabs,
}}
>
{children}
</TerminalAnimationContext.Provider>
);
}
// ---------------------------------------------------------------------------
// Subcomponents
// ---------------------------------------------------------------------------
function TerminalWindow({
children,
minHeight = "20rem",
variant = "standalone",
}: {
children: ReactNode;
minHeight?: string;
variant?: "standalone" | "card";
}) {
const windowRef = useRef<HTMLDivElement>(null);
const [hasAnimated, setHasAnimated] = useState(false);
useEffect(() => {
const el = windowRef.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) setHasAnimated(true);
},
{ threshold: 0.1 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
if (variant === "card") {
return (
<div
ref={windowRef}
className="relative flex flex-col overflow-hidden transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)]"
style={{
height: minHeight,
transform: hasAnimated ? "translateY(0)" : "translateY(2rem)",
}}
>
{children}
</div>
);
}
return (
<div
ref={windowRef}
className="relative flex flex-col overflow-hidden rounded-xl border border-border bg-bg-elevated transition-transform duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)]"
style={{
height: minHeight,
transform: hasAnimated ? "translateY(0)" : "translateY(2rem)",
}}
>
{/* Title bar */}
<div className="flex shrink-0 items-center gap-2 px-4 py-3 border-b border-border">
<span className="w-3 h-3 rounded-full bg-[#ff5f57]" />
<span className="w-3 h-3 rounded-full bg-[#febc2e]" />
<span className="w-3 h-3 rounded-full bg-[#28c840]" />
<span className="ml-2 text-xs text-text-muted font-mono">terminal</span>
</div>
{children}
</div>
);
}
function TerminalTabList() {
const { activeTab, setActiveTab, tabs } = useTerminalAnimation();
if (tabs.length <= 1) return null;
return (
<div className="flex gap-1 px-4 pt-3" role="tablist" aria-label="Terminal commands">
{tabs.map((tab, i) => (
<button
key={tab.label}
role="tab"
aria-selected={activeTab === i}
onClick={() => setActiveTab(i)}
className={`px-3 py-1.5 text-xs font-mono rounded-md transition-colors ${
activeTab === i
? "bg-accent text-white"
: "text-text-muted hover:text-text-secondary hover:bg-bg-hover"
}`}
>
{tab.label}
</button>
))}
</div>
);
}
function TerminalContent() {
const {
commandTyped,
isTypingCommand,
showCursor,
visibleLines,
currentTab,
activeTab,
} = useTerminalAnimation();
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, [visibleLines]);
return (
<div ref={scrollRef} className="flex-1 overflow-y-auto px-5 py-4 font-mono text-sm leading-6">
{/* Command line */}
<div className="text-text-secondary">
<span className="text-text-muted">$ </span>
{commandTyped}
{isTypingCommand && showCursor && (
<span className="ml-0.5 inline-block w-[7px] h-[18px] translate-y-[3px] bg-text-secondary animate-caret-blink" />
)}
</div>
{/* Output */}
{!isTypingCommand && (
<div aria-live="polite" role="log">
{currentTab.lines.map((line, i) => {
if (i >= visibleLines) return null;
return (
<div key={`${activeTab}-${i}`}>
<span style={line.color ? { color: line.color } : undefined}>
{line.text || "\u00A0"}
</span>
</div>
);
})}
</div>
)}
{/* Trailing cursor */}
{!isTypingCommand &&
showCursor &&
visibleLines >= currentTab.lines.length && (
<div className="text-text-secondary mt-1">
<span className="text-text-muted">$ </span>
<span className="ml-0.5 inline-block w-[7px] h-[18px] translate-y-[3px] bg-text-secondary animate-caret-blink" />
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Composed export
// ---------------------------------------------------------------------------
function computeHeight(
tabs: TabContent[],
variant: "standalone" | "card" | undefined
): string {
const maxLines = Math.max(...tabs.map((t) => t.lines.length));
// 1 command line + output lines + 1 trailing cursor line
const totalLines = maxLines + 2;
// leading-6 = 1.5rem per line, py-4 = 2rem padding, mt-1 = 0.25rem cursor
let height = totalLines * 1.5 + 2 + 0.25;
if (variant !== "card") {
// Title bar: py-3 (1.5rem) + dots/text line (~1rem) + border
height += 2.75;
}
if (tabs.length > 1) {
// Tab list: pt-3 + button height ≈ 2.5rem
height += 2.5;
}
return `${height}rem`;
}
export function TerminalAnimation({
tabs = terminalTabs,
minHeight,
variant,
}: {
tabs?: TabContent[];
minHeight?: string;
variant?: "standalone" | "card";
} = {}) {
const resolvedHeight =
minHeight && minHeight !== "auto" ? minHeight : computeHeight(tabs, variant);
return (
<TerminalAnimationRoot tabs={tabs}>
<TerminalWindow minHeight={resolvedHeight} variant={variant}>
<TerminalTabList />
<TerminalContent />
</TerminalWindow>
</TerminalAnimationRoot>
);
}
-279
View File
@@ -1,279 +0,0 @@
export type SkillCategory =
| "analysis"
| "data"
| "risk"
| "sentiment"
| "strategy"
| "visualization";
export type PluginGroup =
| "market-analysis"
| "social-readers"
| "data-providers"
| "startup-tools"
| "ui-tools"
| "skill-creator";
export type SkillBadge = "new" | "paid";
export interface Skill {
name: string;
title: string;
description: string;
category: SkillCategory;
plugin: PluginGroup;
tags: string[];
badge?: SkillBadge;
}
export const categoryLabels: Record<SkillCategory, string> = {
analysis: "Analysis",
data: "Market Data",
risk: "Risk",
sentiment: "Sentiment",
strategy: "Trading Strategy",
visualization: "Visualization",
};
export const pluginGroupLabels: Record<PluginGroup, string> = {
"market-analysis": "Market Analysis",
"social-readers": "Social Readers",
"data-providers": "Data Providers",
"startup-tools": "Startup Tools",
"ui-tools": "UI Tools",
"skill-creator": "Skill Creator",
};
export const skills: Skill[] = [
{
name: "earnings-preview",
title: "Earnings Preview",
description:
"Generate a pre-earnings briefing for any stock — consensus estimates, beat/miss track record, and analyst sentiment overview.",
category: "analysis",
plugin: "market-analysis",
tags: ["Predictive", "EPS"],
},
{
name: "earnings-recap",
title: "Earnings Recap",
description:
"Post-earnings analysis covering actual vs estimated numbers, surprise magnitude, stock price reaction, and financial context.",
category: "analysis",
plugin: "market-analysis",
tags: ["Post-Earnings", "Reaction"],
},
{
name: "estimate-analysis",
title: "Estimate Analysis",
description:
"Deep-dive into analyst estimate revisions, EPS/revenue distributions, and growth projections across periods.",
category: "analysis",
plugin: "market-analysis",
tags: ["Revisions", "Trends"],
},
{
name: "etf-premium",
title: "ETF Premium/Discount",
description:
"Calculate ETF premium or discount vs NAV — single ETF snapshots, peer comparisons, and cross-category screener for 90+ ETFs.",
category: "analysis",
plugin: "market-analysis",
tags: ["ETF", "NAV", "Screener"],
badge: "new",
},
{
name: "yfinance-data",
title: "yfinance Data",
description:
"Fetch stock prices, financials, options chains, dividends, earnings, and analyst data from Yahoo Finance via yfinance.",
category: "data",
plugin: "market-analysis",
tags: ["Market Data", "Fundamentals"],
},
{
description:
category: "data",
plugin: "data-providers",
tags: ["API", "Options Flow"],
badge: "paid",
},
{
name: "options-payoff",
title: "Options Payoff",
description:
"Interactive payoff curve charts with dynamic sliders for any options strategy — spreads, straddles, condors, butterflies.",
category: "visualization",
plugin: "market-analysis",
tags: ["Interactive", "Black-Scholes"],
},
{
name: "stock-correlation",
title: "Stock Correlation",
description:
"Find correlated stocks, sector peers, and trading pairs with rolling correlation and co-movement analysis.",
category: "analysis",
plugin: "market-analysis",
tags: ["Correlation", "Pairs"],
},
{
name: "stock-liquidity",
title: "Stock Liquidity",
description:
"Analyze stock liquidity — bid-ask spreads, volume profiles, order book depth, market impact estimates, Amihud illiquidity ratio, and turnover ratios.",
category: "analysis",
plugin: "market-analysis",
tags: ["Liquidity", "Spreads", "Volume"],
badge: "new",
},
{
name: "startup-analysis",
title: "Startup Analysis",
description:
"Multi-perspective startup analysis — evaluate any company from VC investor, job applicant, and CEO/founder viewpoints with detailed frameworks.",
category: "analysis",
plugin: "startup-tools",
tags: ["Startups", "Due Diligence"],
badge: "new",
},
{
name: "hormuz-strait",
title: "Hormuz Strait Monitor",
description:
"Real-time Strait of Hormuz status — shipping transits, oil prices, stranded vessels, insurance risk, and diplomatic developments.",
category: "risk",
plugin: "data-providers",
tags: ["Geopolitical", "Oil"],
},
{
name: "generative-ui",
title: "Generative UI",
description:
"Design system for Claude's show_widget tool — render interactive HTML/SVG widgets inline in claude.ai conversations.",
category: "visualization",
plugin: "ui-tools",
tags: ["Widgets", "Design System"],
},
{
name: "twitter-reader",
title: "Twitter Reader",
description:
"Read-only Twitter/X access for financial research — search tweets, view bookmarks, and gather market sentiment.",
category: "sentiment",
plugin: "social-readers",
tags: ["Social"],
},
{
name: "telegram-reader",
title: "Telegram Reader",
description:
"Read-only Telegram channel access for financial news — export messages, search channels, monitor market intelligence.",
category: "sentiment",
plugin: "social-readers",
tags: ["Social"],
},
{
name: "discord-reader",
title: "Discord Reader",
description:
"Read-only Discord access for financial research — trading servers, crypto channels, and community sentiment.",
category: "sentiment",
plugin: "social-readers",
tags: ["Social"],
},
{
name: "linkedin-reader",
title: "LinkedIn Reader",
description:
"Read-only LinkedIn access for financial research — professional market commentary, analyst posts, and finance job search.",
category: "sentiment",
plugin: "social-readers",
tags: ["Social", "Jobs"],
badge: "new",
},
{
name: "yc-reader",
title: "YC Reader",
description:
"Y Combinator company data — browse batches, filter by industry/tag, track hiring, and research the YC startup ecosystem.",
category: "data",
plugin: "social-readers",
tags: ["YC", "Startups"],
badge: "new",
},
{
name: "opencli-reader",
title: "opencli Reader",
description:
"Generic read-only fallback for 90+ opencli adapters — Yahoo Finance, Bloomberg, Reuters, Eastmoney, Xueqiu, Reddit, HackerNews, Substack, arXiv, and more.",
category: "sentiment",
plugin: "social-readers",
tags: ["Fallback", "Multi-Source"],
badge: "new",
},
{
name: "finance-sentiment",
title: "Finance Sentiment",
description:
"Structured stock sentiment research — compare Reddit, X.com, news, and Polymarket using buzz, bullish %, mentions, and trend via the Adanos Finance API.",
category: "data",
plugin: "data-providers",
tags: ["Cross-Source", "API"],
badge: "paid",
},
{
name: "sepa-strategy",
title: "SEPA Strategy",
description:
"Mark Minervini's SEPA strategy analysis — trend template, VCP patterns, precise entry points, position sizing, and risk management for growth stocks.",
category: "strategy",
plugin: "market-analysis",
tags: ["Minervini", "VCP", "Swing Trading"],
badge: "new",
},
{
name: "saas-valuation-compression",
title: "SaaS Valuation Compression",
description:
"Analyze round-to-round valuation compression for SaaS companies — ARR multiples, macro attribution, AI narrative premium, and peer comparisons with inline visualizations.",
category: "analysis",
plugin: "market-analysis",
tags: ["SaaS", "Valuation", "VC"],
badge: "new",
},
{
name: "company-valuation",
title: "Company Valuation",
description:
"Triangulate intrinsic value via DCF, relative peer multiples, and sum-of-parts — blended implied price, WACC × g sensitivity grid, and Bull/Base/Bear scenarios.",
category: "analysis",
plugin: "market-analysis",
tags: ["DCF", "Valuation", "Peers", "SOTP"],
badge: "new",
},
{
name: "tradingview-reader",
title: "TradingView Reader",
description:
"Read-only TradingView desktop reader — quotes, full options chains with greeks/IV, expiries, screener results, news, alerts, watchlists, chart state, and screenshots via opencli + CDP.",
category: "data",
plugin: "data-providers",
tags: ["Desktop", "Options", "Screener"],
badge: "new",
},
{
name: "skill-creator",
title: "Skill Creator",
description:
"Create new skills, evaluate existing ones against a 10-dimension rubric, and benchmark skill performance with variance analysis.",
category: "analysis",
plugin: "skill-creator",
tags: ["Authoring", "Evals", "Quality"],
badge: "new",
},
];
export function getSkill(name: string): Skill | undefined {
return skills.find((s) => s.name === name);
}
-34
View File
@@ -1,34 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -3,7 +3,7 @@
"scripts": {
"bump": "ccbump"
},
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@11.1.2",
"devDependencies": {
"ccbump": "^0.2.1"
}
+131 -4059
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
packages:
- "apps/*"
allowBuilds:
sharp: true
unrs-resolver: true