add earnings-date guard — warn when a report falls in the target horizon
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 36s
Deploy / deploy (push) Successful in 25s

Finnhub's earnings calendar now supplies next_earnings_date through the
fundamentals chain; persisted on fundamental_data (migration 006) and exposed in
the fundamentals API. The recommendation panel warns when earnings fall within
the ~30-day target horizon (a report can gap price through stop/target) and
otherwise shows the next date. Informational only.

Deploy: run alembic upgrade (new fundamental_data.next_earnings_date column).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:44:08 +02:00
parent c4f2673799
commit f0b92a9718
13 changed files with 136 additions and 6 deletions
@@ -12,8 +12,19 @@ interface RecommendationPanelProps {
longSetup?: TradeSetup;
shortSetup?: TradeSetup;
currentPrice?: number;
nextEarningsDate?: string | null;
}
/** Whole days from today until an ISO date (negative if past). */
function daysUntil(iso: string): number | null {
const t = new Date(iso).getTime();
if (Number.isNaN(t)) return null;
return Math.ceil((t - Date.now()) / 86_400_000);
}
/** Earnings within the ~30-day target horizon can gap price through stop/target. */
const EARNINGS_HORIZON_DAYS = 30;
/**
* How far current price has drifted from the setup's entry. A setup whose
* entry is far from the live price (price already ran toward target, or fell
@@ -215,10 +226,11 @@ function RiskSettingsBar({ risk, update }: { risk: RiskSettings; update: (p: Par
);
}
export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPrice }: RecommendationPanelProps) {
export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPrice, nextEarningsDate }: RecommendationPanelProps) {
const { settings: risk, update: updateRisk } = useRiskSettings();
const regime = useMarketRegime().data;
const summary = longSetup?.recommendation_summary ?? shortSetup?.recommendation_summary;
const earningsDays = nextEarningsDate ? daysUntil(nextEarningsDate) : null;
const action = (summary?.action ?? 'NEUTRAL') as TradeSetup['recommended_action'];
const preferredDirection = recommendationActionDirection(action);
@@ -261,6 +273,17 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPric
<RiskSettingsBar risk={risk} update={updateRisk} />
{earningsDays != null && earningsDays >= 0 && (
earningsDays <= EARNINGS_HORIZON_DAYS ? (
<p className="rounded border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] text-amber-300">
Earnings in {earningsDays} day{earningsDays === 1 ? '' : 's'} ({nextEarningsDate}) inside the ~30-day
target horizon. A report can gap price through your stop or target; consider waiting or sizing down.
</p>
) : (
<p className="text-[11px] text-gray-500">Next earnings: {nextEarningsDate} ({earningsDays} days).</p>
)
)}
{preferredDirection !== 'neutral' && preferredSetup ? (
<div className="space-y-3">
<SetupCard setup={preferredSetup} action={action} currentPrice={currentPrice} risk={risk} regime={regime} />
+1
View File
@@ -285,6 +285,7 @@ export interface FundamentalResponse {
revenue_growth: number | null;
earnings_surprise: number | null;
market_cap: number | null;
next_earnings_date: string | null;
fetched_at: string | null;
unavailable_fields: Record<string, string>;
}
+1
View File
@@ -262,6 +262,7 @@ export default function TickerDetailPage() {
longSetup={longSetup}
shortSetup={shortSetup}
currentPrice={priceInfo?.price}
nextEarningsDate={fundamentals.data?.next_earnings_date}
/>
{/* Chart — always visible */}