Add configurable trading hours setting
Dropdown on settings page with two options: - Full CME (5:00 PM – 3:00 PM CT) with 5 min buffer - Equity Hours (8:30 AM – 3:00 PM CT) with 5 min buffer Setting is read live each tick, no restart needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
11144612c7
commit
f498594b16
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getSetting, setSetting } from '@/lib/db';
|
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];
|
type SettingKey = typeof VALID_KEYS[number];
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
|
|||||||
+23
-2
@@ -19,12 +19,14 @@ interface AppSettings {
|
|||||||
max_concurrent_accounts: string | null;
|
max_concurrent_accounts: string | null;
|
||||||
master_dashboard_url: string | null;
|
master_dashboard_url: string | null;
|
||||||
instance_name: string | null;
|
instance_name: string | null;
|
||||||
|
trading_hours: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [instruments, setInstruments] = useState<Instrument[]>([]);
|
const [instruments, setInstruments] = useState<Instrument[]>([]);
|
||||||
const [contracts, setContracts] = useState<Record<string, ResolvedContract | null>>({});
|
const [contracts, setContracts] = useState<Record<string, ResolvedContract | null>>({});
|
||||||
const [maxConcurrent, setMaxConcurrent] = useState<string>('5');
|
const [maxConcurrent, setMaxConcurrent] = useState<string>('5');
|
||||||
|
const [tradingHours, setTradingHours] = useState<string>('full_cme');
|
||||||
const [masterUrl, setMasterUrl] = useState<string>('');
|
const [masterUrl, setMasterUrl] = useState<string>('');
|
||||||
const [instanceName, setInstanceName] = useState<string>('');
|
const [instanceName, setInstanceName] = useState<string>('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -41,6 +43,9 @@ export default function SettingsPage() {
|
|||||||
if (s.max_concurrent_accounts != null) {
|
if (s.max_concurrent_accounts != null) {
|
||||||
setMaxConcurrent(s.max_concurrent_accounts);
|
setMaxConcurrent(s.max_concurrent_accounts);
|
||||||
}
|
}
|
||||||
|
if (s.trading_hours != null) {
|
||||||
|
setTradingHours(s.trading_hours);
|
||||||
|
}
|
||||||
if (s.master_dashboard_url != null) {
|
if (s.master_dashboard_url != null) {
|
||||||
setMasterUrl(s.master_dashboard_url);
|
setMasterUrl(s.master_dashboard_url);
|
||||||
}
|
}
|
||||||
@@ -101,7 +106,7 @@ export default function SettingsPage() {
|
|||||||
await fetch('/api/settings', {
|
await fetch('/api/settings', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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);
|
setSaving(false);
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
@@ -127,7 +132,7 @@ export default function SettingsPage() {
|
|||||||
Trading
|
Trading
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm mb-8">
|
<div className="bg-white border border-slate-200 rounded-xl shadow-sm mb-8">
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-800">Max concurrent accounts</p>
|
<p className="text-sm font-medium text-slate-800">Max concurrent accounts</p>
|
||||||
<p className="text-xs text-slate-400 mt-0.5">
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
@@ -156,6 +161,22 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-800">Allowed trading hours</p>
|
||||||
|
<p className="text-xs text-slate-400 mt-0.5">
|
||||||
|
5 min buffer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={tradingHours}
|
||||||
|
onChange={(e) => setTradingHours(e.target.value)}
|
||||||
|
className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm font-mono text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="full_cme">Full CME (5:00 PM – 3:00 PM CT)</option>
|
||||||
|
<option value="equity_hours">Equity Hours (8:30 AM – 3:00 PM CT)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Master Dashboard ── */}
|
{/* ── Master Dashboard ── */}
|
||||||
|
|||||||
+34
-19
@@ -51,39 +51,54 @@ function mapFirmConfig(firm: FirmWithAccounts): FirmConfig {
|
|||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when CME futures markets are closed.
|
* Returns true when trading is not allowed based on the trading_hours setting.
|
||||||
* 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:
|
* "full_cme" — Sun 5:05 PM – Fri 2:55 PM Central (5 min buffer on each side)
|
||||||
* - It's 3:00 PM – 4:59 PM Central (stop early + daily halt)
|
* "equity_hours" — 8:35 AM – 2:55 PM Central, Mon–Fri (5 min buffer on each side)
|
||||||
* - It's Saturday (all day)
|
|
||||||
* - It's Sunday before 5:00 PM Central
|
|
||||||
* - It's Friday after 3:00 PM Central
|
|
||||||
*/
|
*/
|
||||||
function isInNoTradeWindow(): boolean {
|
function isInNoTradeWindow(): boolean {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const centralTime = new Intl.DateTimeFormat('en-US', {
|
const centralParts = new Intl.DateTimeFormat('en-US', {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
hour12: false,
|
hour12: false,
|
||||||
timeZone: 'America/Chicago',
|
timeZone: 'America/Chicago',
|
||||||
});
|
}).formatToParts(now);
|
||||||
const centralDay = new Intl.DateTimeFormat('en-US', {
|
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',
|
weekday: 'short',
|
||||||
timeZone: 'America/Chicago',
|
timeZone: 'America/Chicago',
|
||||||
});
|
}).format(now); // "Sun", "Mon", ... "Sat"
|
||||||
const hour = parseInt(centralTime.format(now), 10);
|
const timeMinutes = hour * 60 + minute; // minutes since midnight
|
||||||
const day = centralDay.format(now); // "Sun", "Mon", ... "Sat"
|
|
||||||
|
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
|
// Saturday — market closed all day
|
||||||
if (day === 'Sat') return true;
|
if (day === 'Sat') return true;
|
||||||
|
|
||||||
// Sunday — market opens at 5 PM Central
|
// Sunday — market opens at 5:05 PM Central
|
||||||
if (day === 'Sun' && hour < 17) return true;
|
const cmeOpen = 17 * 60 + 5; // 5:05 PM
|
||||||
|
if (day === 'Sun') return timeMinutes < cmeOpen;
|
||||||
|
|
||||||
// Friday — stop trading after 3 PM Central
|
// Friday — stop trading at 2:55 PM Central
|
||||||
if (day === 'Fri' && hour >= 15) return true;
|
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)
|
// Mon–Thu: block 2:55 PM – 5:05 PM Central (early stop + daily halt + buffer)
|
||||||
if (hour >= 15 && hour < 17) return true;
|
if (timeMinutes >= cmeClose && timeMinutes < cmeOpen) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ seedSetting.run('max_concurrent_accounts', '5');
|
|||||||
seedSetting.run('tick_interval_seconds', '60');
|
seedSetting.run('tick_interval_seconds', '60');
|
||||||
seedSetting.run('master_dashboard_url', '');
|
seedSetting.run('master_dashboard_url', '');
|
||||||
seedSetting.run('instance_name', '');
|
seedSetting.run('instance_name', '');
|
||||||
|
seedSetting.run('trading_hours', 'full_cme');
|
||||||
|
|
||||||
export function getSetting(key: string): string | null {
|
export function getSetting(key: string): string | null {
|
||||||
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
|
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user