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:
co-authored by
Claude Sonnet 4.6
parent
b2a1bdd1c3
commit
49580c6bb4
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export interface AccountConfig {
|
|||||||
minDayPnL: number;
|
minDayPnL: number;
|
||||||
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user