Add daily target logic and consistency reference line to equity curve
- lib/trading-logic.ts: computeDailyTarget() computes the next trading
day's profit target via two paths:
• No positive days yet → profitTarget × consistency (first day)
• Positive days exist → maxDay / consistency gives the total profit
needed to satisfy the consistency rule; target maxDay when far away,
or the exact remaining amount when close
- Account detail page: display "Next Trading Day Amount" in Objectives card
- Equity curve: add indigo dashed reference line for the consistency target
(maxDay / consistency), labelled top-left to avoid overlapping the amber
profit-target line (top-right)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
645041c600
commit
532c2e2279
@@ -15,6 +15,7 @@ import {
|
|||||||
Dot,
|
Dot,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types';
|
import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types';
|
||||||
|
import { computeDailyTarget } from '@/lib/trading-logic';
|
||||||
|
|
||||||
interface DailyPnL {
|
interface DailyPnL {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -236,8 +237,18 @@ export default function AccountPage() {
|
|||||||
: Math.round(fifoTotal * 100) / 100;
|
: Math.round(fifoTotal * 100) / 100;
|
||||||
const profitPct = cfg?.accountSize ? (totalProfit / cfg.accountSize) * 100 : null;
|
const profitPct = cfg?.accountSize ? (totalProfit / cfg.accountSize) * 100 : null;
|
||||||
const profitPassed = cfg != null && totalProfit >= cfg.profitTarget;
|
const profitPassed = cfg != null && totalProfit >= cfg.profitTarget;
|
||||||
|
const dailyTarget = cfg && !isDead
|
||||||
|
? computeDailyTarget(cfg.profitTarget, cfg.consistency, totalProfit, dailyPnL)
|
||||||
|
: null;
|
||||||
const lossPassed = !hasLossLimit || (cfg != null && totalProfit >= cfg.minDayPnL);
|
const lossPassed = !hasLossLimit || (cfg != null && totalProfit >= cfg.minDayPnL);
|
||||||
|
|
||||||
|
// Consistency target: the total profit level at which the best day no longer
|
||||||
|
// violates the consistency ratio. Only meaningful once a positive day exists.
|
||||||
|
const maxDayPnL = dailyPnL.length > 0 ? Math.max(...dailyPnL.filter(d => d.pnl > 0).map(d => d.pnl)) : 0;
|
||||||
|
const consistencyTarget = cfg && maxDayPnL > 0
|
||||||
|
? Math.round(maxDayPnL / cfg.consistency * 100) / 100
|
||||||
|
: null;
|
||||||
|
|
||||||
// Build equity curve: FIFO daily increments, origin at $0
|
// Build equity curve: FIFO daily increments, origin at $0
|
||||||
let running = 0;
|
let running = 0;
|
||||||
const equityData = [
|
const equityData = [
|
||||||
@@ -260,7 +271,7 @@ export default function AccountPage() {
|
|||||||
const equityValues = equityData.map((d) => d.equity);
|
const equityValues = equityData.map((d) => d.equity);
|
||||||
// Y-axis domain: include profit target so its reference line stays visible
|
// Y-axis domain: include profit target so its reference line stays visible
|
||||||
const rawMin = Math.min(0, ...equityValues);
|
const rawMin = Math.min(0, ...equityValues);
|
||||||
const rawMax = Math.max(0, ...equityValues, cfg?.profitTarget ?? 0);
|
const rawMax = Math.max(0, ...equityValues, cfg?.profitTarget ?? 0, consistencyTarget ?? 0);
|
||||||
|
|
||||||
// Stroke gradient split: use only the actual equity range, NOT the profit target.
|
// Stroke gradient split: use only the actual equity range, NOT the profit target.
|
||||||
// The SVG gradient bounding box is the line's bbox, so inflating by profitTarget
|
// The SVG gradient bounding box is the line's bbox, so inflating by profitTarget
|
||||||
@@ -342,6 +353,14 @@ export default function AccountPage() {
|
|||||||
value={`$${fmt(cfg!.minDayPnL)}`}
|
value={`$${fmt(cfg!.minDayPnL)}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{dailyTarget != null && (
|
||||||
|
<div className="flex items-center justify-between py-3 border-b border-slate-100 last:border-0">
|
||||||
|
<span className="text-slate-500 text-sm">Next Trading Day Amount</span>
|
||||||
|
<span className="text-slate-800 font-bold tabular-nums text-sm">
|
||||||
|
${fmt(dailyTarget.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -421,6 +440,21 @@ export default function AccountPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{consistencyTarget != null && (
|
||||||
|
<ReferenceLine
|
||||||
|
y={consistencyTarget}
|
||||||
|
stroke="#6366f1"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeDasharray="4 3"
|
||||||
|
label={{
|
||||||
|
value: `$${fmt(consistencyTarget)} consistency`,
|
||||||
|
position: 'insideTopLeft',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
fill: '#4f46e5',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Green fill: positive equity only, fills down to y=0 */}
|
{/* Green fill: positive equity only, fills down to y=0 */}
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Compute the next trading day's profit target for an account.
|
||||||
|
*
|
||||||
|
* Path 1 – No positive trading days yet:
|
||||||
|
* target = profitTarget × consistency
|
||||||
|
*
|
||||||
|
* Path 2 – At least one positive day exists:
|
||||||
|
* maxDay = highest single-day P&L so far
|
||||||
|
* realTarget = maxDay / consistency (the total profit at which maxDay ≤ consistency% of total)
|
||||||
|
* needed = realTarget − totalProfit
|
||||||
|
*
|
||||||
|
* if needed > maxDay → target maxDay (still a long way from the real target; trade a normal day)
|
||||||
|
* else → target needed (close to the real target; aim for exactly what's left)
|
||||||
|
*/
|
||||||
|
export function computeDailyTarget(
|
||||||
|
profitTarget: number,
|
||||||
|
consistency: number,
|
||||||
|
totalProfit: number,
|
||||||
|
dailyPnL: { date: string; pnl: number }[]
|
||||||
|
): { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } {
|
||||||
|
const positiveDays = dailyPnL.filter((d) => d.pnl > 0);
|
||||||
|
|
||||||
|
if (positiveDays.length === 0) {
|
||||||
|
return {
|
||||||
|
amount: Math.round(profitTarget * consistency * 100) / 100,
|
||||||
|
path: 'first_day',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxDay = Math.max(...positiveDays.map((d) => d.pnl));
|
||||||
|
const realTarget = maxDay / consistency;
|
||||||
|
const needed = realTarget - totalProfit;
|
||||||
|
|
||||||
|
if (needed > maxDay) {
|
||||||
|
return { amount: Math.round(maxDay * 100) / 100, path: 'normal_day' };
|
||||||
|
}
|
||||||
|
return { amount: Math.round(Math.max(0, needed) * 100) / 100, path: 'reduced_day' };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user