Add volume-based contract auto-resolver and CLAUDE.md

- New lib/contract-resolver.ts: picks the best contract month for each
  symbol by comparing Yahoo Finance volume between the front month
  (Tradovate suggest API) and the roll target (rollcontract API)
- lib/clients.ts: auto-resolves all enabled instruments 15s after startup
  and again daily at midnight via a setInterval check
- lib/tradovate-class.ts: findFrontMonthContract checks resolver cache
  first before falling back to the suggest API
- app/api/instruments/contracts/route.ts: GET returns cached contracts,
  POST triggers a fresh resolve
- app/settings/page.tsx: shows active contract + rolled badge per symbol;
  auto-resolves on load if cache is empty; removed manual Resolve button
- app/api/debug/route.ts: include entity data in recentEntityEvents
- CLAUDE.md: instructs Claude to always work on main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Senofy
2026-03-12 03:17:09 -05:00
co-authored by Claude Sonnet 4.6
parent 881c210366
commit d945e0038f
7 changed files with 400 additions and 20 deletions
+72 -18
View File
@@ -8,12 +8,20 @@ interface Instrument {
enabled: boolean;
}
interface ResolvedContract {
name: string;
alternative?: string;
frontVolume?: number;
rolledVolume?: number;
}
interface AppSettings {
max_concurrent_accounts: string | null;
}
export default function SettingsPage() {
const [instruments, setInstruments] = useState<Instrument[]>([]);
const [contracts, setContracts] = useState<Record<string, ResolvedContract | null>>({});
const [maxConcurrent, setMaxConcurrent] = useState<string>('5');
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
@@ -30,6 +38,24 @@ export default function SettingsPage() {
setMaxConcurrent(s.max_concurrent_accounts);
}
});
// Load cached contracts; if cache is empty, auto-resolve
fetch('/api/instruments/contracts')
.then((r) => r.json())
.then((data: Record<string, ResolvedContract | null>) => {
if (data.error) return;
const hasData = Object.values(data).some((v) => v !== null);
if (hasData) {
setContracts(data);
} else {
// Cache empty — trigger a fresh resolve automatically
fetch('/api/instruments/contracts', { method: 'POST' })
.then((r) => r.json())
.then((fresh) => { if (!fresh.error) setContracts(fresh); })
.catch(() => {});
}
})
.catch(() => {});
}, []);
async function toggle(symbol: string, enabled: boolean) {
@@ -121,29 +147,57 @@ export default function SettingsPage() {
<thead>
<tr className="border-b border-slate-200 bg-slate-50">
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Symbol</th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Active Contract</th>
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-slate-400">Enabled</th>
</tr>
</thead>
<tbody>
{instruments.map((instr) => (
<tr key={instr.symbol} className="border-b border-slate-100 last:border-0">
<td className="px-4 py-2.5 font-mono font-semibold text-slate-800">{instr.symbol}</td>
<td className="px-4 py-2.5 text-right">
<button
onClick={() => toggle(instr.symbol, !instr.enabled)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${
instr.enabled ? 'bg-blue-500' : 'bg-slate-200'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
instr.enabled ? 'translate-x-4' : 'translate-x-1'
{instruments.map((instr) => {
const c = contracts[instr.symbol];
const wasRolled = c?.alternative && c.rolledVolume != null && c.frontVolume != null && c.rolledVolume > c.frontVolume;
return (
<tr key={instr.symbol} className="border-b border-slate-100 last:border-0">
<td className="px-4 py-2.5 font-mono font-semibold text-slate-800">
{instr.symbol}
</td>
<td className="px-4 py-2.5">
{c ? (
<div className="flex items-center gap-2">
<span className={`font-mono text-sm ${wasRolled ? 'text-amber-600 font-semibold' : 'text-slate-600'}`}>
{c.name}
</span>
{c.frontVolume != null && c.rolledVolume != null && (
<span className="text-xs text-slate-400">
vol {Math.max(c.frontVolume, c.rolledVolume).toLocaleString()}
</span>
)}
{wasRolled && (
<span className="text-xs bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded font-medium">
rolled
</span>
)}
</div>
) : (
<span className="text-xs text-slate-300 italic"></span>
)}
</td>
<td className="px-4 py-2.5 text-right">
<button
onClick={() => toggle(instr.symbol, !instr.enabled)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${
instr.enabled ? 'bg-blue-500' : 'bg-slate-200'
}`}
/>
</button>
</td>
</tr>
))}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
instr.enabled ? 'translate-x-4' : 'translate-x-1'
}`}
/>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
)}