diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 6455186..eeba523 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getSetting, setSetting } from '@/lib/db'; -const VALID_KEYS = ['max_concurrent_accounts', 'tick_interval_seconds', 'master_dashboard_url', 'instance_name'] as const; +const VALID_KEYS = ['max_concurrent_accounts', 'tick_interval_seconds', 'master_dashboard_url', 'instance_name', 'trading_hours'] as const; type SettingKey = typeof VALID_KEYS[number]; export async function GET() { diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 95fb242..78a36fb 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -19,12 +19,14 @@ interface AppSettings { max_concurrent_accounts: string | null; master_dashboard_url: string | null; instance_name: string | null; + trading_hours: string | null; } export default function SettingsPage() { const [instruments, setInstruments] = useState([]); const [contracts, setContracts] = useState>({}); const [maxConcurrent, setMaxConcurrent] = useState('5'); + const [tradingHours, setTradingHours] = useState('full_cme'); const [masterUrl, setMasterUrl] = useState(''); const [instanceName, setInstanceName] = useState(''); const [saving, setSaving] = useState(false); @@ -41,6 +43,9 @@ export default function SettingsPage() { if (s.max_concurrent_accounts != null) { setMaxConcurrent(s.max_concurrent_accounts); } + if (s.trading_hours != null) { + setTradingHours(s.trading_hours); + } if (s.master_dashboard_url != null) { setMasterUrl(s.master_dashboard_url); } @@ -101,7 +106,7 @@ export default function SettingsPage() { await fetch('/api/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ max_concurrent_accounts: maxConcurrent, master_dashboard_url: masterUrl, instance_name: instanceName }), + body: JSON.stringify({ max_concurrent_accounts: maxConcurrent, trading_hours: tradingHours, master_dashboard_url: masterUrl, instance_name: instanceName }), }); setSaving(false); setSaved(true); @@ -127,7 +132,7 @@ export default function SettingsPage() { Trading
-
+

Max concurrent accounts

@@ -156,6 +161,22 @@ export default function SettingsPage() {

+
+
+

Allowed trading hours

+

+ 5 min buffer +

+
+ +
{/* ── Master Dashboard ── */} diff --git a/lib/auto-trade.ts b/lib/auto-trade.ts index afe8f8d..0952a27 100644 --- a/lib/auto-trade.ts +++ b/lib/auto-trade.ts @@ -51,39 +51,54 @@ function mapFirmConfig(firm: FirmWithAccounts): FirmConfig { // ── helpers ─────────────────────────────────────────────────────────────────── /** - * Returns true when CME futures markets are closed. - * CME hours: Sunday 5:00 PM – Friday 4:00 PM Central, with a daily halt 4:00–5:00 PM. - * So we block trading when: - * - It's 3:00 PM – 4:59 PM Central (stop early + daily halt) - * - It's Saturday (all day) - * - It's Sunday before 5:00 PM Central - * - It's Friday after 3:00 PM Central + * Returns true when trading is not allowed based on the trading_hours setting. + * + * "full_cme" — Sun 5:05 PM – Fri 2:55 PM Central (5 min buffer on each side) + * "equity_hours" — 8:35 AM – 2:55 PM Central, Mon–Fri (5 min buffer on each side) */ function isInNoTradeWindow(): boolean { const now = new Date(); - const centralTime = new Intl.DateTimeFormat('en-US', { + const centralParts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', + minute: 'numeric', hour12: false, timeZone: 'America/Chicago', - }); - const centralDay = new Intl.DateTimeFormat('en-US', { + }).formatToParts(now); + const hour = parseInt(centralParts.find((p) => p.type === 'hour')!.value, 10); + const minute = parseInt(centralParts.find((p) => p.type === 'minute')!.value, 10); + const day = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'America/Chicago', - }); - const hour = parseInt(centralTime.format(now), 10); - const day = centralDay.format(now); // "Sun", "Mon", ... "Sat" + }).format(now); // "Sun", "Mon", ... "Sat" + const timeMinutes = hour * 60 + minute; // minutes since midnight + + const mode = getSetting('trading_hours') ?? 'full_cme'; + + if (mode === 'equity_hours') { + // Equity hours: 8:30 AM – 3:00 PM Central with 5 min buffer = 8:35 AM – 2:55 PM + // Weekdays only + if (day === 'Sat' || day === 'Sun') return true; + const open = 8 * 60 + 35; // 8:35 AM + const close = 14 * 60 + 55; // 2:55 PM + return timeMinutes < open || timeMinutes >= close; + } + + // Full CME: Sun 5:00 PM – Fri 4:00 PM Central with 5 min buffer + // Open: 5:05 PM, Close: 2:55 PM (stop early), Daily halt: 2:55 PM – 5:05 PM // Saturday — market closed all day if (day === 'Sat') return true; - // Sunday — market opens at 5 PM Central - if (day === 'Sun' && hour < 17) return true; + // Sunday — market opens at 5:05 PM Central + const cmeOpen = 17 * 60 + 5; // 5:05 PM + if (day === 'Sun') return timeMinutes < cmeOpen; - // Friday — stop trading after 3 PM Central - if (day === 'Fri' && hour >= 15) return true; + // Friday — stop trading at 2:55 PM Central + const cmeClose = 14 * 60 + 55; // 2:55 PM + if (day === 'Fri' && timeMinutes >= cmeClose) return true; - // Mon–Thu: stop trading after 3 PM Central (market halts at 4 PM, reopens 5 PM) - if (hour >= 15 && hour < 17) return true; + // Mon–Thu: block 2:55 PM – 5:05 PM Central (early stop + daily halt + buffer) + if (timeMinutes >= cmeClose && timeMinutes < cmeOpen) return true; return false; } diff --git a/lib/db.ts b/lib/db.ts index ef7448e..d6f16ec 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -164,6 +164,7 @@ seedSetting.run('max_concurrent_accounts', '5'); seedSetting.run('tick_interval_seconds', '60'); seedSetting.run('master_dashboard_url', ''); seedSetting.run('instance_name', ''); +seedSetting.run('trading_hours', 'full_cme'); export function getSetting(key: string): string | null { const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;