(cd "$(git rev-parse --show-toplevel)" && git apply --3way <<'EOF' diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..142bd580abbb48db484bfd861ba81eb3c1008b3f --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/marketbrain" +MARKET_DATA_PROVIDER="zerodha" + +ANGEL_ONE_BASE_URL="https://apiconnect.angelone.in" +ANGEL_ONE_API_KEY="" +ANGEL_ONE_CLIENT_CODE="" +ANGEL_ONE_ACCESS_TOKEN="" +ANGEL_ONE_FEED_TOKEN="" + +ZERODHA_BASE_URL="https://api.kite.trade" +ZERODHA_API_KEY="" +ZERODHA_ACCESS_TOKEN="" + +FYERS_BASE_URL="https://api-t1.fyers.in" +FYERS_CLIENT_ID="" +FYERS_ACCESS_TOKEN="" + +GEMINI_API_KEY="" +NEWS_FEED_URL="" +FII_DII_SOURCE_URL="https://www.nseindia.com/api/fiidiiTradeReact" +CRON_SECRET="" +WS_PORT="8081" +MARKET_REFRESH_INTERVAL_MS="3000" diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..957cd1545e307e26099a37413ec28183204daf88 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1507749e2be68869664aafbd7308edef0e525d11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +.next +.env +.env.local +.DS_Store +coverage +prisma/dev.db + +tsconfig.tsbuildinfo diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8affb9773fcba1f18c56611822bd30b4cfc67ae9 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# MARKET BRAIN V2 + +Production-ready Next.js dark institutional dashboard for Indian stock market traders and investors. + +V2 introduces a real service architecture: + +- Angel One SmartAPI provider +- Zerodha Kite Connect provider +- Fyers provider +- Live NIFTY, BANKNIFTY, FINNIFTY, and India VIX services +- FII/DII ingestion service +- OI, PCR, IV, and Max Pain calculation engine +- Gemini-powered news sentiment service +- Composite AI signal engine +- PostgreSQL storage layer +- Cron refresh endpoints/jobs +- WebSocket server for live updates + +## Setup + +```bash +npm install +cp .env.example .env +npm run db:generate +npm run dev +``` + +Set provider credentials in `.env`; the app displays configuration states until real provider connections return live market values. + +## Runtime commands + +```bash +npm run dev +npm run ws +npm run typecheck +npm run build +``` + +## API routes + +- `GET /api/health` +- `GET /api/market` +- `GET /api/stocks?q=RELIANCE` +- `GET /api/fii-dii` +- `GET /api/options?symbol=NSE:NIFTY50-INDEX` +- `GET /api/news` +- `GET /api/signals?symbol=RELIANCE` +- `POST /api/cron/refresh?job=indices` diff --git a/app/api/broker/oauth/route.ts b/app/api/broker/oauth/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..b865ab1487120016912c75797559b3baaa5e395c --- /dev/null +++ b/app/api/broker/oauth/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { providerConfig } from "@/lib/config"; + +const schema = z.object({ broker: z.enum(["angel-one", "zerodha", "fyers"]), redirectUri: z.string().url() }); + +export async function POST(request: NextRequest) { + const body = schema.parse(await request.json()); + if (body.broker === "zerodha") { + return NextResponse.json({ broker: body.broker, authUrl: `https://kite.zerodha.com/connect/login?v=3&api_key=${providerConfig.zerodha.apiKey ?? ""}`, status: "oauth-ready" }); + } + if (body.broker === "fyers") { + const url = new URL("https://api-t1.fyers.in/api/v3/generate-authcode"); + url.searchParams.set("client_id", providerConfig.fyers.clientId ?? ""); + url.searchParams.set("redirect_uri", body.redirectUri); + url.searchParams.set("response_type", "code"); + url.searchParams.set("state", "marketbrain"); + return NextResponse.json({ broker: body.broker, authUrl: url.toString(), status: "oauth-ready" }); + } + return NextResponse.json({ broker: body.broker, status: "token-login-required", message: "Angel One SmartAPI uses JWT/feed-token generation after client-code authentication; configure server-side SmartAPI credentials for live access." }); +} diff --git a/app/api/cron/refresh/route.ts b/app/api/cron/refresh/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7575a6b34c5a0327376ad4036d0976c7ef083b8 --- /dev/null +++ b/app/api/cron/refresh/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; +import { appConfig } from "@/lib/config"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { runCronJob, type CronJobName } from "@/lib/cron/jobs"; + +export async function POST(request: NextRequest) { + try { + if (appConfig.cronSecret && request.headers.get("x-cron-secret") !== appConfig.cronSecret) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const job = (request.nextUrl.searchParams.get("job") ?? "indices") as CronJobName; + return NextResponse.json(await runCronJob(job, Object.fromEntries(request.nextUrl.searchParams.entries()))); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/api/fii-dii/route.ts b/app/api/fii-dii/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..6830770aae763d78c5931a879581931ebbc9755d --- /dev/null +++ b/app/api/fii-dii/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { fiiDiiService } from "@/lib/services/fii-dii-service"; + +export async function GET() { + try { + return NextResponse.json(await fiiDiiService.ingestLatest()); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..96c494d2ebe3de89f6993f1dd9a8f1caf1da6e3e --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,6 @@ +import { NextResponse } from "next/server"; +import { marketService } from "@/lib/services/market-service"; + +export async function GET() { + return NextResponse.json({ providers: marketService.getProviderHealth(), at: new Date().toISOString() }); +} diff --git a/app/api/market/route.ts b/app/api/market/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d18029e6b126816303aef702df938a574de571d --- /dev/null +++ b/app/api/market/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { appConfig } from "@/lib/config"; +import { marketService } from "@/lib/services/market-service"; +import type { BrokerProviderName } from "@/lib/types"; + +export async function GET(request: NextRequest) { + try { + const provider = request.nextUrl.searchParams.get("provider") as BrokerProviderName | null; + const indices = await marketService.getLiveIndices(provider ?? undefined); + return NextResponse.json({ indices, providerHealth: marketService.getProviderHealth(), websocket: `ws://localhost:${appConfig.websocketPort}` }); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/api/news/route.ts b/app/api/news/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e50aebc4a44c1ccdb951041d6673fe803c5a650 --- /dev/null +++ b/app/api/news/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { newsSentimentService } from "@/lib/services/news-sentiment-service"; + +export async function GET() { + try { + return NextResponse.json({ sentiment: await newsSentimentService.fetchAndAnalyze(), scoreRange: [-100, 100] }); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/api/options/route.ts b/app/api/options/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae4c4ccb098416beffd72dfff51073945a8cc48c --- /dev/null +++ b/app/api/options/route.ts @@ -0,0 +1,14 @@ +import { NextRequest, NextResponse } from "next/server"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { oiPcrService } from "@/lib/services/oi-pcr-engine"; + +export async function GET(request: NextRequest) { + try { + const symbol = request.nextUrl.searchParams.get("symbol") ?? "NSE:NIFTY50-INDEX"; + const expiry = request.nextUrl.searchParams.get("expiry") ?? ""; + return NextResponse.json(await oiPcrService.getOptionIntelligence(symbol, expiry)); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/api/signals/route.ts b/app/api/signals/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf6ca5da37a48502c45f690d1af219a203d75822 --- /dev/null +++ b/app/api/signals/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { aiSignalEngine } from "@/lib/services/ai-signal-engine"; +import { fiiDiiService } from "@/lib/services/fii-dii-service"; +import { marketService } from "@/lib/services/market-service"; +import { newsSentimentService } from "@/lib/services/news-sentiment-service"; +import { oiPcrService } from "@/lib/services/oi-pcr-engine"; +import { technicalService } from "@/lib/services/technical-service"; + +export async function GET(request: NextRequest) { + try { + const symbol = request.nextUrl.searchParams.get("symbol") ?? "RELIANCE"; + const quote = await marketService.getStockQuote(symbol); + const [institutional, options, sentiment, technical] = await Promise.allSettled([ + fiiDiiService.ingestLatest(), + oiPcrService.getOptionIntelligence(`NSE:${symbol}-EQ`), + newsSentimentService.fetchAndAnalyze(), + technicalService.calculateFromStoredCandles(symbol) + ]); + const signal = aiSignalEngine.generate({ + quote, + institutional: institutional.status === "fulfilled" ? institutional.value : undefined, + options: options.status === "fulfilled" ? options.value : undefined, + sentiment: sentiment.status === "fulfilled" ? sentiment.value : undefined, + technical: technical.status === "fulfilled" ? technical.value : undefined + }); + return NextResponse.json({ signal }); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/api/stocks/route.ts b/app/api/stocks/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..472ec003ff9d2911f15e05bc19a3dcd4128cbce7 --- /dev/null +++ b/app/api/stocks/route.ts @@ -0,0 +1,15 @@ +import { NextRequest, NextResponse } from "next/server"; +import { toErrorResponse } from "@/lib/errors/service-error"; +import { marketService } from "@/lib/services/market-service"; +import type { BrokerProviderName } from "@/lib/types"; + +export async function GET(request: NextRequest) { + try { + const q = request.nextUrl.searchParams.get("q") ?? ""; + const provider = request.nextUrl.searchParams.get("provider") as BrokerProviderName | null; + return NextResponse.json(await marketService.searchStocks(q, provider ?? undefined)); + } catch (error) { + const response = toErrorResponse(error); + return NextResponse.json(response, { status: response.status }); + } +} diff --git a/app/broker-connect/page.tsx b/app/broker-connect/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..685565f1f5a1720588a9b3c390d4ccccb7a5903f --- /dev/null +++ b/app/broker-connect/page.tsx @@ -0,0 +1,7 @@ +import { PlugZap } from "lucide-react"; +import { Card, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { marketService } from "@/lib/services/market-service"; +export const metadata = { title: "Broker Connect" }; +const brokers = [{ id: "angel-one", label: "Angel One SmartAPI" }, { id: "zerodha", label: "Zerodha Kite Connect" }, { id: "fyers", label: "Fyers API" }]; +export default function BrokerPage() { const health = marketService.getProviderHealth(); return

Broker Connect

OAuth/token-ready broker connection UI for live market data, holdings, positions, margins, and order bridge integrations.

{brokers.map(b => { const provider = health.find((item)=>item.provider===b.id); return {b.label}

Secure credential flow, token refresh, profile scopes, and read-only portfolio sync.

Base URL: {provider?.baseUrl}

; })}
; } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..acbbdfcc9628a5e4f273c23ebcb255571b3bf9f9 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,12 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { MetricCard } from "@/components/market/metric-card"; +import { SignalTable } from "@/components/market/signal-table"; +import { BreadthHeatmap, PriceChart } from "@/components/market/charts"; +import { marketService } from "@/lib/services/market-service"; + +export const metadata = { title: "Dashboard" }; +export default async function DashboardPage() { + const result = await marketService.getLiveIndices().then((indices) => ({ indices, error: null })).catch((error) => ({ indices: [], error: error instanceof Error ? error.message : String(error) })); + return

Live Command Dashboard

Real index cards, provider health, service-backed search, heatmaps, and AI signal tape.

{result.error && Live market feed unavailable

{result.error}

}
{result.indices.map((i) => )}
Stock Search

Search is served by /api/stocks?q= and requires Angel One/Zerodha/Fyers credentials.

Intraday Price + VolumeSector Breadth Heatmap
; +} diff --git a/app/fii-dii/page.tsx b/app/fii-dii/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..45701757cfb81c0f3c38f034725697ddcbac5f80 --- /dev/null +++ b/app/fii-dii/page.tsx @@ -0,0 +1,6 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { MetricCard } from "@/components/market/metric-card"; +import { fiiDiiService } from "@/lib/services/fii-dii-service"; +import { formatCr } from "@/lib/utils"; +export const metadata = { title: "FII DII Dashboard" }; +export default async function FiiDiiPage() { const result = await fiiDiiService.ingestLatest().then((flow)=>({flow,error:null})).catch((error)=>({flow:null,error:error instanceof Error ? error.message : String(error)})); return

FII / DII Flow Dashboard

{result.error && FII/DII ingestion unavailable

{result.error}

}{result.flow && <>
Net Institutional Read

FII Net

{formatCr(result.flow.fiiNet)}

DII Net

{formatCr(result.flow.diiNet)}

}
; } diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000000000000000000000000000000000000..7334e2fd3cb93bf235e62d15b407a5e9e0996ab5 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,42 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 222 47% 5%; + --foreground: 210 40% 98%; + --card: 222 44% 8%; + --card-foreground: 210 40% 98%; + --popover: 222 44% 8%; + --popover-foreground: 210 40% 98%; + --primary: 160 84% 39%; + --primary-foreground: 166 100% 8%; + --secondary: 217 33% 17%; + --secondary-foreground: 210 40% 98%; + --muted: 217 33% 14%; + --muted-foreground: 215 20% 65%; + --accent: 217 91% 60%; + --accent-foreground: 210 40% 98%; + --destructive: 0 84% 60%; + --destructive-foreground: 210 40% 98%; + --border: 217 33% 18%; + --input: 217 33% 18%; + --ring: 160 84% 39%; + --positive: 150 72% 45%; + --negative: 0 72% 56%; + --warning: 42 92% 55%; + --radius: 0.75rem; + } +} + +@layer base { + * { @apply border-border; } + body { @apply bg-background text-foreground antialiased; } +} + +.terminal-glow { box-shadow: 0 0 32px rgba(16,185,129,.12), inset 0 1px 0 rgba(255,255,255,.04); } +.data-grid { background-size: 32px 32px; } +.number-ticker { font-variant-numeric: tabular-nums; } +::-webkit-scrollbar { height: 8px; width: 8px; } +::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 999px; } diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..837fed6a5a8cfa69229f61e63a6cfde5c99ca269 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import { AppShell } from "@/components/market/app-shell"; + +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); + +export const metadata: Metadata = { + metadataBase: new URL("https://marketbrain.co.in"), + title: { + default: "MARKET BRAIN — AI Indian Stock Market Intelligence", + template: "%s | MARKET BRAIN" + }, + description: "AI-powered institutional intelligence for Indian traders and investors with FII/DII, OI, PCR, technicals, sentiment, and signals.", + openGraph: { + title: "MARKET BRAIN", + description: "Institutional AI stock market intelligence for India.", + url: "https://marketbrain.co.in", + siteName: "MARKET BRAIN", + type: "website" + } +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/app/news-sentiment/page.tsx b/app/news-sentiment/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..898d511a9eed20056945aec5dd56c1ee8b25e23b --- /dev/null +++ b/app/news-sentiment/page.tsx @@ -0,0 +1,5 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { newsSentimentService } from "@/lib/services/news-sentiment-service"; +export const metadata = { title: "News Sentiment" }; +export default async function NewsPage() { const result = await newsSentimentService.fetchAndAnalyze().then((items)=>({items,error:null})).catch((error)=>({items:[],error:error instanceof Error ? error.message : String(error)})); return

Gemini News Sentiment Engine

Analyzes earnings, corporate, economic, RBI, and SEBI news with -100 to +100 scoring.

{result.error && News service configuration required

{result.error}

}
{result.items.map((n) => {n.category}

{n.title}

{n.rationale}

= 30 ? "border-positive/30 bg-positive/10 text-positive" : n.score <= -30 ? "border-negative/30 bg-negative/10 text-negative" : "border-warning/30 bg-warning/10 text-warning"}>{n.sentiment}{n.score}
)}
; } diff --git a/app/options-intelligence/page.tsx b/app/options-intelligence/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e9ae4604b75fae4b9d95182996a456ed27e006f6 --- /dev/null +++ b/app/options-intelligence/page.tsx @@ -0,0 +1,6 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { OptionsOiChart } from "@/components/market/charts"; +import { oiPcrService } from "@/lib/services/oi-pcr-engine"; +export const metadata = { title: "Options Intelligence" }; +export default async function OptionsPage({ searchParams }: { searchParams?: { symbol?: string; expiry?: string } }) { const symbol = searchParams?.symbol ?? "NSE:NIFTY50-INDEX"; const result = await oiPcrService.getOptionIntelligence(symbol, searchParams?.expiry ?? "").then((options)=>({options,error:null})).catch((error)=>({options:null,error:error instanceof Error ? error.message : String(error)})); return

Options Intelligence

{result.error && Option-chain provider required

{result.error}

}{result.options &&
}Option Chain OIGreeks

Greeks are populated from the configured option-chain provider when delta, gamma, theta, and vega fields are available or calculated from stored contracts.

; } +function Info({ k, v }: { k: string; v: React.ReactNode }) { return

{k}

{v}

; } diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1aaee42640ec7872fbecd8a01102c0fa3536fb06 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,15 @@ +import Link from "next/link"; +import { BrainCircuit, DatabaseZap, RadioTower, ShieldCheck, Zap } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardTitle } from "@/components/ui/card"; +import { MetricCard } from "@/components/market/metric-card"; +import { SignalTable } from "@/components/market/signal-table"; +import { BreadthHeatmap, PriceChart } from "@/components/market/charts"; +import { marketService } from "@/lib/services/market-service"; + +export default async function HomePage() { + const indicesResult = await marketService.getLiveIndices().then((indices) => ({ indices, error: null })).catch((error) => ({ indices: [], error: error instanceof Error ? error.message : String(error) })); + const health = marketService.getProviderHealth(); + return
V2 Real Service Architecture

MARKET BRAIN

AI-powered institutional Indian market intelligence wired for Angel One SmartAPI, Zerodha Kite Connect, Fyers, NSE FII/DII ingestion, Gemini sentiment, PostgreSQL, cron refresh, and WebSocket live updates.

{indicesResult.error && Provider configuration required

{indicesResult.error}

}
{indicesResult.indices.map((i) => )}
Live NIFTY FeedProduction Services
{health.map((provider) =>
{provider.provider}{provider.configured ? "Configured" : "Needs env"}
)}
WebSocket live updatesPostgreSQL storage layerCron refresh jobsGemini AI signal engine
News Sentiment Summary

Live sentiment uses NEWS_FEED_URL + GEMINI_API_KEY. Only configured live/news-feed sentiment is rendered in V2.

FII/DII Summary

FII/DII values are ingested from the configured NSE source through /api/fii-dii and persisted by cron.

Market Breadth
; +} diff --git a/app/portfolio/page.tsx b/app/portfolio/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d80dd8be472bc9b9622f15b4b2e1778a4b2a4b54 --- /dev/null +++ b/app/portfolio/page.tsx @@ -0,0 +1,3 @@ +import { Card, CardTitle } from "@/components/ui/card"; +export const metadata = { title: "Portfolio" }; +export default function PortfolioPage() { return

Portfolio

Holdings Value

Loaded from broker OAuth + PostgreSQL.

Open PnL

Calculated from live quotes after broker sync.

Allocation

Rendered from real holdings only.

Holdings & Positions

Connect Angel One, Zerodha, or Fyers to sync holdings and positions.

; } diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0288c1adc46d98859f6fd927c32ac58a268813f9 --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,5 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +export const metadata = { title: "Settings" }; +export default function SettingsPage() { return

Settings

AI ProviderMarket Data Providers
PostgreSQLCron & WebSocket
; } diff --git a/app/signals/page.tsx b/app/signals/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a4ae230fb80b0137e335719d6bf724e5528536d0 --- /dev/null +++ b/app/signals/page.tsx @@ -0,0 +1,9 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { SignalTable } from "@/components/market/signal-table"; +import { aiSignalEngine, intelligenceWeights } from "@/lib/services/ai-signal-engine"; +import { marketService } from "@/lib/services/market-service"; +import { signalTone } from "@/lib/utils"; +export const metadata = { title: "Signals" }; +export default async function SignalsPage({ searchParams }: { searchParams?: { symbol?: string } }) { const symbol = searchParams?.symbol ?? "RELIANCE"; const result = await marketService.getStockQuote(symbol).then((quote)=>({signal: aiSignalEngine.generate({ quote }), error:null})).catch((error)=>({signal:null,error:error instanceof Error ? error.message : String(error)})); return

Real AI Signal Engine

{result.error && Signal inputs unavailable

{result.error}

}{result.signal &&
Composite Intelligence Model
{result.signal.signal}

{result.signal.confidence}%

Confidence Score · Composite {result.signal.compositeScore}/100

    {result.signal.reasons.map((r) =>
  • • {r}
  • )}
Risk Engine
}Weights
{Object.entries(intelligenceWeights).map(([k,v]) => )}
; } +function Info({ k, v }: { k: string; v: React.ReactNode }) { return
{k}{v}
; } diff --git a/app/stock-analysis/page.tsx b/app/stock-analysis/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..02a365b14777b6e0096992f24b2b2b4bddf6d1e8 --- /dev/null +++ b/app/stock-analysis/page.tsx @@ -0,0 +1,16 @@ +import { Card, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { MetricCard } from "@/components/market/metric-card"; +import { OptionsOiChart, PriceChart } from "@/components/market/charts"; +import { marketService } from "@/lib/services/market-service"; +import { oiPcrService } from "@/lib/services/oi-pcr-engine"; + +export const metadata = { title: "Stock Analysis" }; +export default async function StockAnalysisPage({ searchParams }: { searchParams?: { symbol?: string } }) { + const symbol = searchParams?.symbol ?? "RELIANCE"; + const quote = await marketService.getStockQuote(symbol).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: error instanceof Error ? error.message : String(error) })); + const options = await oiPcrService.getOptionIntelligence(`NSE:${symbol}-EQ`).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: error instanceof Error ? error.message : String(error) })); + return

{symbol} Real-Time Analysis

Provider-backed LTP, delivery, VWAP, technicals, FII/DII flow, and option-chain intelligence.

{quote.error && Quote provider required

{quote.error}

}{quote.value &&
}
TradingView-style Price ActionMarket Data{quote.value ?
{quote.value.provider}} />
: }
Technical IndicatorsInstitutional AnalysisOptions Analysis{options.value ?
: }
Open Interest Profile
; +} +function Info({ k, v }: { k: string; v: React.ReactNode }) { return
{k}{v}
; } +function Empty({ text }: { text: string }) { return

{text}

; } diff --git a/app/watchlist/page.tsx b/app/watchlist/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..06e81a0362ea2b417a50151e982cf93df0da5224 --- /dev/null +++ b/app/watchlist/page.tsx @@ -0,0 +1,4 @@ +import { SignalTable } from "@/components/market/signal-table"; +import { Card, CardTitle } from "@/components/ui/card"; +export const metadata = { title: "Watchlist" }; +export default function WatchlistPage() { return

Watchlist

Watchlist rows are loaded from PostgreSQL and enriched by live services.

Storage-backed watchlist

Connect PostgreSQL and authenticated users to persist watchlist symbols. V2 renders only persisted watchlist data.

; } diff --git a/components/market/app-shell.tsx b/components/market/app-shell.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2f53f9e6517b784b6933641d3f11070eb660a82f --- /dev/null +++ b/components/market/app-shell.tsx @@ -0,0 +1,34 @@ +import Link from "next/link"; +import { Activity, BarChart3, Bell, BrainCircuit, Briefcase, Building2, CandlestickChart, Gauge, Home, Newspaper, PlugZap, Search, Settings, Star } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const nav = [ + ["Home", "/", Home], ["Dashboard", "/dashboard", Gauge], ["Stock Analysis", "/stock-analysis", CandlestickChart], ["FII DII", "/fii-dii", Building2], ["News", "/news-sentiment", Newspaper], ["Signals", "/signals", Activity], ["Watchlist", "/watchlist", Star], ["Portfolio", "/portfolio", Briefcase], ["Options", "/options-intelligence", BarChart3], ["Broker", "/broker-connect", PlugZap], ["Settings", "/settings", Settings] +] as const; + +export function AppShell({ children }: { children: React.ReactNode }) { + return ( +
+ +
+
+
+ MARKET BRAIN +
+
+
+
+
{children}
+
+
+ ); +} diff --git a/components/market/charts.tsx b/components/market/charts.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f9a82385a08bceddfc5b6520d3b1956214be0cd1 --- /dev/null +++ b/components/market/charts.tsx @@ -0,0 +1,18 @@ +"use client"; +import { Area, AreaChart, Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +import type { OptionChainRow } from "@/lib/types"; + +export function PriceChart({ data }: { data: Array<{ time: string; price: number }> }) { + if (!data.length) return
Live candle history will render after PostgreSQL candle ingestion is configured.
; + return
; +} + +export function BreadthHeatmap({ sectors }: { sectors: Array<{ sector: string; adv: number; dec: number; score: number }> }) { + if (!sectors.length) return
Sector breadth requires a configured market breadth ingestion job.
; + return
{sectors.map((item) =>
55 ? "16,185,129" : item.score < 50 ? "239,68,68" : "245,158,11"}, ${0.12 + item.score / 500})` }}>

{item.sector}

Adv {item.adv} / Dec {item.dec}

{item.score}

)}
; +} + +export function OptionsOiChart({ rows }: { rows: OptionChainRow[] }) { + if (!rows.length) return
Option chain OI will render after a provider with option-chain access is configured.
; + return
; +} diff --git a/components/market/metric-card.tsx b/components/market/metric-card.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0e2534a65e54bf62d6a94920409f20001d504cfa --- /dev/null +++ b/components/market/metric-card.tsx @@ -0,0 +1,8 @@ +import { ArrowDownRight, ArrowUpRight } from "lucide-react"; +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +export function MetricCard({ label, value, change, pct, sub }: { label: string; value: string | number; change: number; pct?: number; sub?: string }) { + const up = change >= 0; + return

{label}

{up ? : }

{typeof value === "number" ? value.toLocaleString("en-IN") : value}

{up ? "+" : ""}{change.toLocaleString("en-IN")} {pct !== undefined && `(${pct}%)`}

{sub &&

{sub}

}
; +} diff --git a/components/market/signal-table.tsx b/components/market/signal-table.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d22a3a07051216249c3bc88d1928c3d8512c5fe6 --- /dev/null +++ b/components/market/signal-table.tsx @@ -0,0 +1,8 @@ +import { Badge } from "@/components/ui/badge"; +import { Card, CardTitle } from "@/components/ui/card"; +import { signalTone } from "@/lib/utils"; +import type { AISignal } from "@/lib/types"; + +export function SignalTable({ signals }: { signals: AISignal[] }) { + return AI Signals{signals.length ?
{signals.map((row) => )}
StockSignalConfidenceCompositeTechnicalNewsInstitutional
{row.symbol}{row.signal}{row.confidence}%{row.compositeScore}{row.componentScores.technical}{row.componentScores.news}{row.componentScores.fiiDii}
:

No AI signals are shown until live quote, FII/DII, options, news, and technical services are configured.

}
; +} diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8b43aecee67f0e9d8c384d3f9cc74d0be2cbf723 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export function Badge({ className, ...props }: React.HTMLAttributes) { + return ; +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000000000000000000000000000000000000..daffde07e442eeabc01f2e1bf30803f117e1d2a8 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export function Button({ className, variant = "default", ...props }: React.ButtonHTMLAttributes & { variant?: "default" | "outline" | "ghost" }) { + return