From 7179cd384252338638aa169947bdb0ac80def1ce Mon Sep 17 00:00:00 2001 From: caspar Date: Sat, 21 Mar 2026 09:26:43 -0400 Subject: [PATCH] Fixed date issues. --- README.md | 71 +++++++++++++++- app/complex-chart/page.tsx | 73 ++++------------ app/symbol-details/page.tsx | 6 +- .../candlestick-chart/CandlestickGraph.tsx | 27 +++--- .../candlestick-chart/useCandlestickData.ts | 85 ++++++++----------- components/stock-chart/types.ts | 18 ++-- tsconfig.json | 22 +---- 7 files changed, 157 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index c4cdd51..61edaa3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,72 @@ # SoftTraderFrontend -The frontend for a lite Bloomberg terminal. \ No newline at end of file +The frontend for a lite Bloomberg terminal. + +## Features + +- **Dashboard** — Live clock, NYSE market status indicator (open/premarket/after-hours/closed), rotating stock cards for watched symbols, and news headlines +- **Watchlist** — Manage a personal list of stock symbols and topics, with swipeable rows and quick navigation to details +- **Symbol Details** — Live quotes with price change, volume, open/close data, collapsible stock chart (1D–3Y), news headlines with sentiment analysis, and configurable price alerts +- **Advanced Candlestick Chart** — Full candlestick charting with configurable timeframes and bar sizes, custom date ranges, and interactive crosshair +- **News & Sentiment** — Per-symbol news feed with pull-to-refresh and an aggregated sentimentality bar +- **Weather Widget** — Current conditions with configurable location and temperature unit (F/C) +- **Settings** — Dark/light theme toggle, weather location, temperature unit, 12h/24h time format + +## Tech Stack + +- **Framework:** [Next.js 16](https://nextjs.org/) (App Router) +- **UI:** [React 19](https://react.dev/), [Tailwind CSS 4](https://tailwindcss.com/), [shadcn/ui](https://ui.shadcn.com/) (Radix primitives) +- **Charts:** [Recharts](https://recharts.org/) (line/area), custom D3-based candlestick chart +- **Backend API:** `api.dongfeng-systems.org` (quotes, news, user interests, weather) +- **Analytics:** Vercel Analytics +- **Language:** TypeScript + +## Project Structure + +``` +app/ # Next.js App Router pages + page.tsx # Home / dashboard + interests/ # Watchlist + symbol-details/ # Individual stock detail view + complex-chart/ # Advanced candlestick chart + interest-details/ # Topic/interest news feed + add-item/ # Add symbol or topic + settings/ # User settings +components/ + layout/ # StockClockLayout, Sidebar + pages/ # Page-level components (FrontPage, InterestsPage, SettingsPage) + widgets/ # StockCard, NewsHeadlines, WeatherWidget, SentimentalityBar, etc. + stock-chart/ # Line/area stock chart + candlestick-chart/ # D3-based candlestick chart + ui/ # shadcn/ui primitives + providers/ # ThemeProvider +lib/ # API clients and utilities + stockApi.ts # Live quotes, historical data + newsApi.ts # News feed + userInterestsApi.ts # Watchlist/interests CRUD + weatherApi.ts # Weather data and preferences +``` + +## Getting Started + +### Prerequisites + +- Node.js 18+ + +### Install and Run + +```bash +npm install +npm run dev +``` + +The app will be available at [http://localhost:3000](http://localhost:3000). + +### Scripts + +| Command | Description | +| --------------- | ------------------------ | +| `npm run dev` | Start development server | +| `npm run build` | Production build | +| `npm run start` | Start production server | +| `npm run lint` | Run ESLint | diff --git a/app/complex-chart/page.tsx b/app/complex-chart/page.tsx index d6f73fc..7ce2c2d 100644 --- a/app/complex-chart/page.tsx +++ b/app/complex-chart/page.tsx @@ -10,17 +10,15 @@ import { CandlestickGraph } from '@/components/candlestick-chart' const TIMEFRAMES = ['Custom', '1D', '5D', '10D', '1M', '3M', '6M', 'YTD', '1Y', '2Y', '3Y'] as const type Timeframe = typeof TIMEFRAMES[number] -// Bar size options per timeframe — each bar size must be able to cover the -// visible range within IB API max duration limits: -// 1m→1D, 2m→2D, 5m→1W, 15m→2W, 30m/1h/2h/4h→1M, 1D/1W/1M→1Y+ +// Bar size options per timeframe const BAR_SIZE_MAP: Record, string[]> = { '1D': ['1m', '2m', '5m', '15m'], '5D': ['5m', '15m', '30m', '1h'], '10D': ['15m', '30m', '1h', '2h'], '1M': ['30m', '1h', '4h', '1D'], - '3M': ['1D', '1W'], - '6M': ['1D', '1W'], - 'YTD': ['1D', '1W'], + '3M': ['1h', '4h', '1D', '1W'], + '6M': ['4h', '1D', '1W'], + 'YTD': ['4h', '1D', '1W'], '1Y': ['1D', '1W'], '2Y': ['1D', '1W', '1M'], '3Y': ['1W', '1M'], @@ -142,36 +140,33 @@ function countTradingDays(start: Date, end: Date): number { } /** - * Get bar size options for custom timeframe based on date range. - * Thresholds match IB API max duration limits per bar size: - * 5m→1W(5 trading), 15m→2W(10), 30m/1h/2h/4h→1M(~20), 1D+→1Y+ + * Get bar size options for custom timeframe based on date range */ function getCustomBarSizeOptions(startDate: Date, endDate: Date): { options: string[], default: string } { const tradingDays = countTradingDays(startDate, endDate) - + if (tradingDays <= 1) { return { options: ['1m', '2m', '5m', '15m'], default: '5m' } } else if (tradingDays <= 5) { - return { options: ['5m', '15m', '30m', '1h'], default: '15m' } - } else if (tradingDays <= 10) { - return { options: ['15m', '30m', '1h', '2h'], default: '30m' } + return { options: ['5m', '15m', '30m', '1h'], default: '30m' } } else if (tradingDays <= 20) { - return { options: ['30m', '1h', '4h', '1D'], default: '1D' } + return { options: ['15m', '30m', '1h', '4h'], default: '1h' } + } else if (tradingDays <= 90) { + return { options: ['1h', '4h', '1D'], default: '1D' } + } else if (tradingDays <= 365) { + return { options: ['4h', '1D', '1W'], default: '1D' } + } else if (tradingDays <= 365 * 3) { + return { options: ['1D', '1W', '1M'], default: '1W' } } else { - return { options: ['1D', '1W'], default: '1D' } + return { options: ['1W', '1M'], default: '1M' } } } /** - * Format date to YYYY-MM-DD for input. - * Uses local-time methods to match how parseInputDate creates dates, - * avoiding the day-shift that toISOString() causes in non-UTC timezones. + * Format date to YYYY-MM-DD for input */ function formatDateForInput(date: Date): string { - const y = date.getFullYear() - const m = String(date.getMonth() + 1).padStart(2, '0') - const d = String(date.getDate()).padStart(2, '0') - return `${y}-${m}-${d}` + return date.toISOString().split('T')[0] } /** @@ -196,7 +191,6 @@ function ComplexChartContent() { const start = calculateStartDate('5D', lastTradingDay) return formatDateForInput(start) }) - const [savedChart, setSavedChart] = useState('') // Get available bar sizes based on current timeframe/dates const barSizeOptions = useMemo(() => { @@ -255,14 +249,6 @@ function ComplexChartContent() { } } - const handleGo = () => { - // Placeholder for Go button action - } - - const handleSaveChart = () => { - // Placeholder for Save chart action - } - return (
{/* Header */} @@ -310,31 +296,6 @@ function ComplexChartContent() { onChange={(e) => handleEndDateChange(e.target.value)} className="border border-border bg-input text-foreground text-sm px-3 py-1.5 cursor-pointer" /> - - -
- - {/* Right side */} -
- - -
diff --git a/app/symbol-details/page.tsx b/app/symbol-details/page.tsx index f538a60..843e6ed 100644 --- a/app/symbol-details/page.tsx +++ b/app/symbol-details/page.tsx @@ -153,10 +153,10 @@ function SymbolDetailsContent() { ))} - {/* Chart */} -
+ {/* Chart will be embedded here */} +
-
+
- {/* Notes overlay */} - {notes.map((note) => { - // Find the candle index for this note's time - const candleIndex = candles.findIndex(c => c.time === note.time); - if (candleIndex === -1) return null; - - // Check if note is in visible range - if (candleIndex < viewModel.visibleStartIndex || candleIndex > viewModel.visibleEndIndex) { - return null; - } - - const x = getXForIndex(candleIndex); - const y = getYForPrice(note.price); + {/* Notes overlay */} + {notes.map((note) => { + // Check if note is within the visible time range + if (candles.length === 0) return null; + + const startCandle = candles[viewModel.visibleStartIndex]; + const endCandle = candles[viewModel.visibleEndIndex]; + + if (!startCandle || !endCandle) return null; + if (note.time < startCandle.time || note.time > endCandle.time) return null; + + // Use interpolated time positioning (works across all bar sizes) + const x = getXForTime(note.time); + const y = getYForPrice(note.price); return ( = { + '1m': 1, '2m': 2, '5m': 7, '15m': 14, '30m': 28, + }; + duration = `${Math.min(3, maxDays[intradayBarSize] ?? 3)}+D`; apiBarSize = BAR_SIZE_MAP[intradayBarSize] || 'FiveMins'; } else { const tradingDays = calculateTradingDays(start, end); @@ -351,4 +340,4 @@ export function mergeCandles(existing: Candle[], incoming: Candle[]): Candle[] { // Sort by time and return return Array.from(existingMap.values()).sort((a, b) => a.time - b.time); -} +} diff --git a/components/stock-chart/types.ts b/components/stock-chart/types.ts index 9d897eb..1c69eb6 100644 --- a/components/stock-chart/types.ts +++ b/components/stock-chart/types.ts @@ -32,10 +32,16 @@ function formatInLocalTime( return new Intl.DateTimeFormat(undefined, opts).format(date) } -function getUtcDateKey(timestamp: string) { +/** + * Get the trading day key for a timestamp based on US Eastern time + * (NYSE operates in Eastern time, so trading days should be determined by ET, not UTC) + */ +function getTradingDayKey(timestamp: string) { const d = new Date(timestamp) if (Number.isNaN(d.getTime())) return '' - return d.toISOString().slice(0, 10) + + // Format in US Eastern time to get the correct trading day + return d.toLocaleDateString('en-CA', { timeZone: 'America/New_York' }) // 'en-CA' gives YYYY-MM-DD format } function sortByTimestamp(rows: T[]) { @@ -46,18 +52,18 @@ function sortByTimestamp(rows: T[]) { function keepLatestTradingDay(rows: T[]) { const sorted = sortByTimestamp(rows) - const distinctDays = [...new Set(sorted.map((r) => getUtcDateKey(r.timestamp)).filter(Boolean))] + const distinctDays = [...new Set(sorted.map((r) => getTradingDayKey(r.timestamp)).filter(Boolean))] if (!distinctDays.length) return sorted const latest = distinctDays[distinctDays.length - 1] - return sorted.filter((r) => getUtcDateKey(r.timestamp) === latest) + return sorted.filter((r) => getTradingDayKey(r.timestamp) === latest) } function keepLatestNTradingDays(rows: T[], n: number) { const sorted = sortByTimestamp(rows) - const distinctDays = [...new Set(sorted.map((r) => getUtcDateKey(r.timestamp)).filter(Boolean))] + const distinctDays = [...new Set(sorted.map((r) => getTradingDayKey(r.timestamp)).filter(Boolean))] const keep = new Set(distinctDays.slice(-n)) - return sorted.filter((r) => keep.has(getUtcDateKey(r.timestamp))) + return sorted.filter((r) => keep.has(getTradingDayKey(r.timestamp))) } function normalizeRawRows(input: unknown[]): RawHistoricalLinePoint[] { diff --git a/tsconfig.json b/tsconfig.json index 48d6d82..2a062b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,6 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "target": "ES6", "skipLibCheck": true, @@ -23,19 +19,9 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] }