diff --git a/app/routers/admin.py b/app/routers/admin.py index 870a8a5..0080793 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -274,6 +274,16 @@ async def cleanup_data( return APIEnvelope(status="success", data=counts) +@router.post("/admin/track-record/reset", response_model=APIEnvelope) +async def reset_track_record( + _admin: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + """Wipe the track record: delete all trade setups so stats start fresh.""" + counts = await admin_service.reset_trade_setups(db) + return APIEnvelope(status="success", data=counts) + + # --------------------------------------------------------------------------- # Job control # --------------------------------------------------------------------------- diff --git a/app/schemas/admin.py b/app/schemas/admin.py index 6a5e65b..6809e77 100644 --- a/app/schemas/admin.py +++ b/app/schemas/admin.py @@ -49,7 +49,6 @@ class RecommendationConfigUpdate(BaseModel): confidence_diff_threshold: float | None = Field(default=None, ge=0, le=100) signal_alignment_weight: float | None = Field(default=None, ge=0, le=1) sr_strength_weight: float | None = Field(default=None, ge=0, le=1) - distance_penalty_factor: float | None = Field(default=None, ge=0, le=1) momentum_technical_divergence_threshold: float | None = Field(default=None, ge=0, le=100) fundamental_technical_divergence_threshold: float | None = Field(default=None, ge=0, le=100) diff --git a/app/services/admin_service.py b/app/services/admin_service.py index fee453d..b49cad4 100644 --- a/app/services/admin_service.py +++ b/app/services/admin_service.py @@ -23,7 +23,6 @@ RECOMMENDATION_CONFIG_DEFAULTS: dict[str, float] = { "recommendation_confidence_diff_threshold": 20.0, "recommendation_signal_alignment_weight": 0.15, "recommendation_sr_strength_weight": 0.20, - "recommendation_distance_penalty_factor": 0.10, "recommendation_momentum_technical_divergence_threshold": 30.0, "recommendation_fundamental_technical_divergence_threshold": 40.0, } @@ -236,7 +235,6 @@ async def get_recommendation_config(db: AsyncSession) -> dict[str, float]: "confidence_diff_threshold": config["recommendation_confidence_diff_threshold"], "signal_alignment_weight": config["recommendation_signal_alignment_weight"], "sr_strength_weight": config["recommendation_sr_strength_weight"], - "distance_penalty_factor": config["recommendation_distance_penalty_factor"], "momentum_technical_divergence_threshold": config["recommendation_momentum_technical_divergence_threshold"], "fundamental_technical_divergence_threshold": config["recommendation_fundamental_technical_divergence_threshold"], } @@ -309,6 +307,19 @@ async def cleanup_data(db: AsyncSession, older_than_days: int) -> dict[str, int] return counts +async def reset_trade_setups(db: AsyncSession) -> dict[str, int]: + """Delete all trade setups, wiping the track record for a fresh start. + + Stats are derived from evaluated trade setups, so this resets the Track + Record to zero. Live setups regenerate on the next R:R scan. Used after + material changes to scoring / setup generation, when historical outcomes no + longer reflect current logic. + """ + result = await db.execute(delete(TradeSetup)) + await db.commit() + return {"trade_setups": result.rowcount} # type: ignore[attr-defined] + + async def get_pipeline_readiness(db: AsyncSession) -> list[dict]: """Return per-ticker readiness snapshot for ingestion/scoring/scanner pipeline.""" tickers_result = await db.execute(select(Ticker).order_by(Ticker.symbol.asc())) diff --git a/app/services/recommendation_service.py b/app/services/recommendation_service.py index c7e70f5..0c8ff2b 100644 --- a/app/services/recommendation_service.py +++ b/app/services/recommendation_service.py @@ -22,7 +22,6 @@ DEFAULT_RECOMMENDATION_CONFIG: dict[str, float] = { "recommendation_confidence_diff_threshold": 20.0, "recommendation_signal_alignment_weight": 0.15, "recommendation_sr_strength_weight": 0.20, - "recommendation_distance_penalty_factor": 0.10, "recommendation_momentum_technical_divergence_threshold": 30.0, "recommendation_fundamental_technical_divergence_threshold": 40.0, } diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 6aab610..692e04b 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -180,3 +180,10 @@ export function cleanupData(olderThanDays: number) { }) .then((r) => r.data); } + +// Track record +export function resetTrackRecord() { + return apiClient + .post<{ trade_setups: number }>('admin/track-record/reset') + .then((r) => r.data); +} diff --git a/frontend/src/components/admin/RecommendationSettings.tsx b/frontend/src/components/admin/RecommendationSettings.tsx index 65ebce3..6d32e40 100644 --- a/frontend/src/components/admin/RecommendationSettings.tsx +++ b/frontend/src/components/admin/RecommendationSettings.tsx @@ -9,7 +9,6 @@ const DEFAULTS: RecommendationConfig = { confidence_diff_threshold: 20, signal_alignment_weight: 0.15, sr_strength_weight: 0.2, - distance_penalty_factor: 0.1, momentum_technical_divergence_threshold: 30, fundamental_technical_divergence_threshold: 40, }; @@ -82,7 +81,6 @@ export function RecommendationSettings() { setField('signal_alignment_weight', v)} /> setField('sr_strength_weight', v)} /> - setField('distance_penalty_factor', v)} /> setField('momentum_technical_divergence_threshold', v)} /> setField('fundamental_technical_divergence_threshold', v)} /> diff --git a/frontend/src/components/signals/TrackRecordPanel.tsx b/frontend/src/components/signals/TrackRecordPanel.tsx index 4b0149e..cff4ec5 100644 --- a/frontend/src/components/signals/TrackRecordPanel.tsx +++ b/frontend/src/components/signals/TrackRecordPanel.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useActivation } from '../../hooks/useActivation'; import { activationSummary } from '../../lib/qualification'; import { usePerformance } from '../../hooks/usePerformance'; -import { triggerJob } from '../../api/admin'; +import { triggerJob, resetTrackRecord } from '../../api/admin'; import { Button } from '../ui/Button'; import { Callout } from '../ui/Callout'; import { Disclosure } from '../ui/Disclosure'; @@ -112,6 +112,29 @@ export function TrackRecordPanel() { }, }); + const resetMutation = useMutation({ + mutationFn: () => resetTrackRecord(), + onSuccess: (data) => { + toast.addToast('success', `Track record reset — ${data.trade_setups} setups cleared. Run the scanner to rebuild.`); + queryClient.invalidateQueries({ queryKey: ['performance'] }); + queryClient.invalidateQueries({ queryKey: ['trades'] }); + }, + onError: () => { + toast.addToast('error', 'Failed to reset track record'); + }, + }); + + const onReset = () => { + if ( + window.confirm( + 'Reset the track record? This permanently deletes ALL trade setups and their outcomes. ' + + 'Live setups will regenerate on the next R:R scan. This cannot be undone.', + ) + ) { + resetMutation.mutate(); + } + }; + return (
@@ -145,9 +168,14 @@ export function TrackRecordPanel() { evaluator runs nightly after OHLCV collection.

- +
+ + +
{isLoading && ( diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 57606cd..a4d0804 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -307,7 +307,6 @@ export interface RecommendationConfig { confidence_diff_threshold: number; signal_alignment_weight: number; sr_strength_weight: number; - distance_penalty_factor: number; momentum_technical_divergence_threshold: number; fundamental_technical_divergence_threshold: number; } diff --git a/tests/property/test_recommendation_properties.py b/tests/property/test_recommendation_properties.py index 52ba1f6..1d4f90b 100644 --- a/tests/property/test_recommendation_properties.py +++ b/tests/property/test_recommendation_properties.py @@ -37,7 +37,6 @@ def test_property_strength_monotonic_probability(strength_low, strength_high): config = { "recommendation_signal_alignment_weight": 0.15, "recommendation_sr_strength_weight": 0.20, - "recommendation_distance_penalty_factor": 0.10, } scores = {"technical": 65.0, "momentum": 65.0} @@ -74,7 +73,6 @@ def test_property_distance_probability_relationship(near_distance, far_distance) config = { "recommendation_signal_alignment_weight": 0.15, "recommendation_sr_strength_weight": 0.20, - "recommendation_distance_penalty_factor": 0.10, } scores = {"technical": 65.0, "momentum": 65.0}