Add full Next.js autotrader app with SQLite persistence and live Tradovate data
- SQLite DB (better-sqlite3) with firms, account_configs, firm_fees, instruments tables - REST API routes: firms CRUD, account configs CRUD, state, accounts, instruments - Live Tradovate WebSocket client: login, sync, positions, auto-liq thresholds - Dashboard (app/page.tsx): per-firm account list with balance, day P&L, days traded, target progress, and Dead/Inactive/Flat status based on Tradovate auto-liq floors - Account detail page: objectives progress, daily P&L chart, consistency tracking - Per-firm settings page: account configs and instrument fee management - Dead detection uses trailingMaxDrawdownLimit - trailingMaxDrawdown from userAccountAutoLiqs; filters Tradovate sentinel value (999999999 = no limit) - FIFO P&L engine with commission accounting for daily P&L history - Removed manual maxLoss fallback in favour of live Tradovate auto-liq data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a9b6acd479
commit
dd18f91584
+73
-11
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types';
|
||||
|
||||
function getAccountConfig(name: string, firm: FirmConfig): AccountConfig | undefined {
|
||||
@@ -14,6 +15,11 @@ function fmt(value: number) {
|
||||
return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
/** Returns true when an account's balance has breached Tradovate's auto-liquidation floor. */
|
||||
function isAccountDead(account: AccountState): boolean {
|
||||
return account.autoLiqThreshold > 0 && account.amount <= account.autoLiqThreshold;
|
||||
}
|
||||
|
||||
function SettingsIcon() {
|
||||
return (
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -31,12 +37,26 @@ function ChevronIcon({ open }: { open: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AccountRow({ account, firm }: { account: AccountState; firm: FirmConfig }) {
|
||||
function DetailsIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountRow({ account, firm, hideDead }: { account: AccountState; firm: FirmConfig; hideDead: boolean }) {
|
||||
const cfg = getAccountConfig(account.name, firm);
|
||||
const dead = isAccountDead(account);
|
||||
|
||||
if (hideDead && dead) return null;
|
||||
|
||||
const pnlColor = account.realizedPnL > 0 ? 'text-green-600 font-medium' : account.realizedPnL < 0 ? 'text-red-600 font-medium' : 'text-slate-400';
|
||||
|
||||
return (
|
||||
<tr className={`border-b border-slate-100 hover:bg-slate-50 transition-colors${!account.active ? ' opacity-40' : ''}`}>
|
||||
<tr className={`border-b border-slate-100 hover:bg-slate-50 transition-colors${!account.active || dead ? ' opacity-50' : ''}`}>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-medium text-slate-800 pl-4">
|
||||
{account.name}
|
||||
@@ -53,22 +73,35 @@ function AccountRow({ account, firm }: { account: AccountState; firm: FirmConfig
|
||||
${cfg?.profitTarget?.toLocaleString() ?? '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{!account.active
|
||||
? <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">Inactive</span>
|
||||
: account.hasPosition
|
||||
? <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">In Trade</span>
|
||||
: <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-500">Flat</span>}
|
||||
<div className="flex items-center justify-between">
|
||||
{dead
|
||||
? <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-800 text-white">Dead</span>
|
||||
: !account.active
|
||||
? <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">Inactive</span>
|
||||
: account.hasPosition
|
||||
? <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">In Trade</span>
|
||||
: <span className="inline-block px-2.5 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-500">Flat</span>}
|
||||
<Link
|
||||
href={`/accounts/${account.id}`}
|
||||
className="text-slate-400 hover:text-slate-600 hover:bg-slate-200 p-1 rounded-md transition-colors inline-flex"
|
||||
title="View details"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DetailsIcon />
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function FirmRows({ state, firm, deleteMode, selected, onToggle }: {
|
||||
function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead }: {
|
||||
state: FirmState;
|
||||
firm: FirmConfig;
|
||||
deleteMode: boolean;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
hideDead: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
@@ -95,13 +128,14 @@ function FirmRows({ state, firm, deleteMode, selected, onToggle }: {
|
||||
<td /><td /><td /><td />
|
||||
<td className="px-4 py-3.5">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
<Link
|
||||
href={`/firms/${firm.id}/settings`}
|
||||
className="text-slate-400 hover:text-slate-600 hover:bg-slate-200 p-1 rounded-md transition-colors inline-flex"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Settings"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</Link>
|
||||
<ChevronIcon open={open} />
|
||||
</div>
|
||||
</td>
|
||||
@@ -116,7 +150,7 @@ function FirmRows({ state, firm, deleteMode, selected, onToggle }: {
|
||||
</tr>
|
||||
) : (
|
||||
state.accounts.map((acc) => (
|
||||
<AccountRow key={acc.id} account={acc} firm={firm} />
|
||||
<AccountRow key={acc.id} account={acc} firm={firm} hideDead={hideDead} />
|
||||
))
|
||||
)
|
||||
)}
|
||||
@@ -130,6 +164,7 @@ export default function Home() {
|
||||
const [firms, setFirms] = useState<FirmState[]>([]);
|
||||
const [deleteMode, setDeleteMode] = useState(false);
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const [hideDead, setHideDead] = useState(false);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
@@ -163,6 +198,13 @@ export default function Home() {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Count dead accounts across all firms for the toggle button label
|
||||
const deadCount = firms.reduce((total, firmState) => {
|
||||
const firmCfg = config.find((c) => c.firm === firmState.firm);
|
||||
if (!firmCfg) return total;
|
||||
return total + firmState.accounts.filter((acc) => isAccountDead(acc)).length;
|
||||
}, 0);
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
await Promise.all(
|
||||
[...selected].map((id) =>
|
||||
@@ -208,6 +250,18 @@ export default function Home() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{deadCount > 0 && (
|
||||
<button
|
||||
onClick={() => setHideDead((h) => !h)}
|
||||
className={`px-4 py-2 text-sm font-semibold rounded-lg transition-colors border ${
|
||||
hideDead
|
||||
? 'bg-slate-800 text-white border-slate-800 hover:bg-slate-700'
|
||||
: 'bg-white text-slate-600 border-slate-200 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
{hideDead ? `Show Dead (${deadCount})` : `Hide Dead (${deadCount})`}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => router.push('/add')}
|
||||
className="px-4 py-2 bg-green-100 hover:bg-green-200 text-green-700 text-sm font-semibold rounded-lg transition-colors"
|
||||
@@ -220,6 +274,13 @@ export default function Home() {
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="px-3 py-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors inline-flex items-center"
|
||||
title="Global Settings"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -254,6 +315,7 @@ export default function Home() {
|
||||
deleteMode={deleteMode}
|
||||
selected={selected.has(cfg.id)}
|
||||
onToggle={() => toggleSelected(cfg.id)}
|
||||
hideDead={hideDead}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user