Add max_position_size to account configs

- DB migration: ALTER TABLE account_configs ADD COLUMN max_position_size INTEGER NOT NULL DEFAULT 0
- Added max_position_size to AccountConfigRow, createAccountConfig, updateAccountConfig in lib/db.ts
- Added maxPositionSize to AccountConfig type in types.ts (0 = no limit)
- GET /api/firms/[id] now returns maxPositionSize per account
- POST /api/firms/[id]/accounts and PUT /api/account-configs/[id] accept maxPositionSize
- Firm settings page: new Max Contracts column (blank = no limit)
- auto-trade: contracts capped at maxPositionSize when > 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Senofy
2026-03-09 03:51:09 -05:00
co-authored by Claude Sonnet 4.6
parent b2a1bdd1c3
commit 49580c6bb4
7 changed files with 51 additions and 10 deletions
+3 -1
View File
@@ -20,9 +20,10 @@ export async function PUT(
minTradingDays?: number; minTradingDays?: number;
accountSize?: number; accountSize?: number;
maxLoss?: number; maxLoss?: number;
maxPositionSize?: number;
}; };
const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss } = body; const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize } = body;
if ( if (
typeof prefix !== 'string' || !prefix.trim() || typeof prefix !== 'string' || !prefix.trim() ||
@@ -44,6 +45,7 @@ export async function PUT(
minTradingDays, minTradingDays,
accountSize, accountSize,
maxLoss: maxLoss ?? 0, maxLoss: maxLoss ?? 0,
maxPositionSize: maxPositionSize ?? 0,
}); });
if (!updated) { if (!updated) {
+4 -1
View File
@@ -24,9 +24,10 @@ export async function POST(
minTradingDays?: number; minTradingDays?: number;
accountSize?: number; accountSize?: number;
maxLoss?: number; maxLoss?: number;
maxPositionSize?: number;
}; };
const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss } = body; const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize } = body;
if ( if (
typeof prefix !== 'string' || !prefix.trim() || typeof prefix !== 'string' || !prefix.trim() ||
@@ -48,6 +49,7 @@ export async function POST(
minTradingDays, minTradingDays,
accountSize, accountSize,
maxLoss: maxLoss ?? 0, maxLoss: maxLoss ?? 0,
maxPositionSize: maxPositionSize ?? 0,
}); });
return NextResponse.json({ return NextResponse.json({
@@ -59,6 +61,7 @@ export async function POST(
minTradingDays: row.min_trading_days, minTradingDays: row.min_trading_days,
accountSize: row.account_size, accountSize: row.account_size,
maxLoss: row.max_loss, maxLoss: row.max_loss,
maxPositionSize: row.max_position_size,
}, { status: 201 }); }, { status: 201 });
} catch (err) { } catch (err) {
console.error('[POST /api/firms/:id/accounts]', err); console.error('[POST /api/firms/:id/accounts]', err);
+2
View File
@@ -31,6 +31,8 @@ export async function GET(
minDayPnL: a.min_day_pnl, minDayPnL: a.min_day_pnl,
minTradingDays: a.min_trading_days, minTradingDays: a.min_trading_days,
accountSize: a.account_size, accountSize: a.account_size,
maxLoss: a.max_loss,
maxPositionSize: a.max_position_size,
})), })),
}); });
} }
+23 -2
View File
@@ -13,6 +13,7 @@ interface AccountConfig {
minDayPnL: number; minDayPnL: number;
minTradingDays: number; minTradingDays: number;
accountSize: number; accountSize: number;
maxPositionSize: number;
} }
interface FirmConfig { interface FirmConfig {
@@ -49,6 +50,7 @@ function newRow(): RowState {
minDayPnL: -999, minDayPnL: -999,
minTradingDays: 5, minTradingDays: 5,
accountSize: 50_000, accountSize: 50_000,
maxPositionSize: 0,
dirty: true, dirty: true,
saving: false, saving: false,
error: '', error: '',
@@ -115,6 +117,7 @@ export default function FirmSettingsPage() {
minDayPnL: row.minDayPnL, minDayPnL: row.minDayPnL,
minTradingDays: row.minTradingDays, minTradingDays: row.minTradingDays,
accountSize: row.accountSize, accountSize: row.accountSize,
maxPositionSize: row.maxPositionSize,
}; };
if (row.id < 0) { if (row.id < 0) {
@@ -223,13 +226,14 @@ export default function FirmSettingsPage() {
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Consistency</th> <th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Consistency</th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Min Day P&L</th> <th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Min Day P&L</th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Min Trading Days</th> <th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Min Trading Days</th>
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-slate-400">Max Contracts</th>
<th className="px-3 py-3 w-32" /> <th className="px-3 py-3 w-32" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rows.length === 0 && firmName && ( {rows.length === 0 && firmName && (
<tr> <tr>
<td colSpan={7} className="px-4 py-6 text-center text-sm text-slate-400 italic"> <td colSpan={8} className="px-4 py-6 text-center text-sm text-slate-400 italic">
No account types yet No account types yet
</td> </td>
</tr> </tr>
@@ -354,6 +358,23 @@ export default function FirmSettingsPage() {
/> />
</td> </td>
{/* Max Contracts */}
<td className="px-4 py-2.5">
<input
type="number"
className={`${inputCls} w-20`}
value={row.maxPositionSize === 0 ? '' : row.maxPositionSize}
placeholder="No limit"
min={0}
step={1}
onChange={(e) =>
updateRow(row.id, {
maxPositionSize: e.target.value === '' ? 0 : Number(e.target.value),
})
}
/>
</td>
{/* Actions */} {/* Actions */}
<td className="px-3 py-2.5"> <td className="px-3 py-2.5">
<div className="flex items-center justify-end gap-1.5"> <div className="flex items-center justify-end gap-1.5">
@@ -402,7 +423,7 @@ export default function FirmSettingsPage() {
{/* Add row */} {/* Add row */}
<tr className="border-t border-dashed border-slate-200"> <tr className="border-t border-dashed border-slate-200">
<td colSpan={7} className="px-2 py-2"> <td colSpan={8} className="px-2 py-2">
<button <button
onClick={() => setRows((prev) => [...prev, newRow()])} onClick={() => setRows((prev) => [...prev, newRow()])}
className="w-full py-1.5 text-sm text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-lg transition-colors" className="w-full py-1.5 text-sm text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-lg transition-colors"
+3 -1
View File
@@ -41,6 +41,7 @@ function mapFirmConfig(firm: FirmWithAccounts): FirmConfig {
minTradingDays: a.min_trading_days, minTradingDays: a.min_trading_days,
accountSize: a.account_size, accountSize: a.account_size,
maxLoss: a.max_loss, maxLoss: a.max_loss,
maxPositionSize: a.max_position_size,
})), })),
}; };
} }
@@ -124,7 +125,8 @@ export async function runTrade(action: 'Buy' | 'Sell', symbol: string) {
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0); const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL); const target = computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL);
const contracts = Math.max(1, Math.ceil(target.amount / 1000)); const rawContracts = Math.max(1, Math.ceil(target.amount / 1000));
const contracts = cfg.maxPositionSize > 0 ? Math.min(rawContracts, cfg.maxPositionSize) : rawContracts;
const fill = await client.sendOrder(acc.id, contract.name, contracts, action, 'Market'); const fill = await client.sendOrder(acc.id, contract.name, contracts, action, 'Market');
// Wait briefly for the cash balance WebSocket update to reflect entry commission // Wait briefly for the cash balance WebSocket update to reflect entry commission
+14 -4
View File
@@ -43,6 +43,13 @@ try {
// Column already exists // Column already exists
} }
// Migration: add max_position_size column (0 = no limit)
try {
db.exec('ALTER TABLE account_configs ADD COLUMN max_position_size INTEGER NOT NULL DEFAULT 0');
} catch {
// Column already exists
}
// Seed default firms if empty // Seed default firms if empty
const firmCount = (db.prepare('SELECT COUNT(*) as count FROM firms').get() as { count: number }).count; const firmCount = (db.prepare('SELECT COUNT(*) as count FROM firms').get() as { count: number }).count;
if (firmCount === 0) { if (firmCount === 0) {
@@ -77,6 +84,7 @@ export interface AccountConfigRow {
min_trading_days: number; min_trading_days: number;
account_size: number; account_size: number;
max_loss: number; max_loss: number;
max_position_size: number;
} }
export interface FirmRow { export interface FirmRow {
@@ -129,11 +137,12 @@ export function createAccountConfig(firmId: number, data: {
minTradingDays: number; minTradingDays: number;
accountSize: number; accountSize: number;
maxLoss: number; maxLoss: number;
maxPositionSize: number;
}): AccountConfigRow { }): AccountConfigRow {
const stmt = db.prepare( const stmt = db.prepare(
'INSERT INTO account_configs (firm_id, prefix, profit_target, consistency, min_day_pnl, min_trading_days, account_size, max_loss) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' 'INSERT INTO account_configs (firm_id, prefix, profit_target, consistency, min_day_pnl, min_trading_days, account_size, max_loss, max_position_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
); );
const result = stmt.run(firmId, data.prefix, data.profitTarget, data.consistency, data.minDayPnL, data.minTradingDays, data.accountSize, data.maxLoss); const result = stmt.run(firmId, data.prefix, data.profitTarget, data.consistency, data.minDayPnL, data.minTradingDays, data.accountSize, data.maxLoss, data.maxPositionSize);
return db.prepare('SELECT * FROM account_configs WHERE id = ?').get(result.lastInsertRowid) as AccountConfigRow; return db.prepare('SELECT * FROM account_configs WHERE id = ?').get(result.lastInsertRowid) as AccountConfigRow;
} }
@@ -150,12 +159,13 @@ export function updateAccountConfig(id: number, data: {
minTradingDays: number; minTradingDays: number;
accountSize: number; accountSize: number;
maxLoss: number; maxLoss: number;
maxPositionSize: number;
}): boolean { }): boolean {
const result = db.prepare(` const result = db.prepare(`
UPDATE account_configs UPDATE account_configs
SET prefix = ?, profit_target = ?, consistency = ?, min_day_pnl = ?, min_trading_days = ?, account_size = ?, max_loss = ? SET prefix = ?, profit_target = ?, consistency = ?, min_day_pnl = ?, min_trading_days = ?, account_size = ?, max_loss = ?, max_position_size = ?
WHERE id = ? WHERE id = ?
`).run(data.prefix, data.profitTarget, data.consistency, data.minDayPnL, data.minTradingDays, data.accountSize, data.maxLoss, id); `).run(data.prefix, data.profitTarget, data.consistency, data.minDayPnL, data.minTradingDays, data.accountSize, data.maxLoss, data.maxPositionSize, id);
return result.changes > 0; return result.changes > 0;
} }
+1
View File
@@ -6,6 +6,7 @@ export interface AccountConfig {
minTradingDays: number; minTradingDays: number;
accountSize: number; accountSize: number;
maxLoss: number; // 0 = no limit; positive = account is "Dead" when loss exceeds this maxLoss: number; // 0 = no limit; positive = account is "Dead" when loss exceeds this
maxPositionSize: number; // 0 = no limit; positive = max contracts per trade
} }
export interface FirmConfig { export interface FirmConfig {