mirror of
https://github.com/himself65/finance-skills.git
synced 2026-09-14 14:40:40 +08:00
chore: remove apps/web
Remove the unused Next.js web app along with its workspace declaration and lockfile entries.
This commit is contained in:
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
@AGENTS.md
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
@@ -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">☐</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'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. "Down 60%" 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">1–2</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">3–4</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">5–6</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 1→2 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="25–50%" 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's EPS growth rate {`>`} last quarter's. If growth decelerates (30% → 22%), that'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 = "fake growth" 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'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 → <3%</td><td className="px-3 py-2">3–20 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 & Handle</td><td className="px-3 py-2">Cup 12-35%, handle ≤12%</td><td className="px-3 py-2">7–65 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">5–10 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">1–5 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">1–4 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">
|
||||
"Specific Entry Point" is what SEPA stands for. Not "looks about right" — 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: "Not losing big is the only prerequisite for winning big."
|
||||
</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's working. Averaging down = do more of what'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'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 "business expense" — like a restaurant buying ingredients. Not every dish sells, but the restaurant isn'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: "Good loss" or "bad loss"?</p>
|
||||
<p className="text-xs text-text-muted mt-1 leading-relaxed">Good: followed all rules, market didn'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'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, >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">1–2%</td>
|
||||
<td className="px-3 py-2 text-text-secondary">0.5–1%</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">6–8</td>
|
||||
<td className="px-3 py-2 text-text-secondary">2–3</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 > 70th percentile (prefer 90+)</CheckItem>
|
||||
<CheckItem>Valid pattern identified (VCP, cup, flag…)</CheckItem>
|
||||
<CheckItem>Market environment: bull or neutral</CheckItem>
|
||||
<CheckItem>> 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">"Not losing big is the only prerequisite for winning big."</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's "Trade Like a Stock Market Wizard" and "Think & Trade Like a Champion." For educational purposes only — not investment advice.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -3,7 +3,7 @@
|
||||
"scripts": {
|
||||
"bump": "ccbump"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"packageManager": "pnpm@11.1.2",
|
||||
"devDependencies": {
|
||||
"ccbump": "^0.2.1"
|
||||
}
|
||||
|
||||
Generated
+131
-4059
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
allowBuilds:
|
||||
sharp: true
|
||||
unrs-resolver: true
|
||||
Reference in New Issue
Block a user