add track-record reset; drop dead distance_penalty_factor knob
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 36s
Deploy / deploy (push) Successful in 24s

Track Record: new "Reset" action (POST /admin/track-record/reset) deletes all
trade setups so stats start fresh after material scoring/setup changes — live
setups regenerate on the next scan. Guarded by a confirm dialog.

Recommendation config: remove distance_penalty_factor, which was exposed in the
admin UI but consumed nowhere (the touch-probability model superseded it). A
knob that silently does nothing is worse than no knob. Remaining defaults are
left as-is — they're reasonable, and the honest way to tune them is backtesting
against accumulated outcomes, not invented "researched" numbers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 14:44:02 +02:00
parent 6e06f51bb6
commit 90618d186f
9 changed files with 62 additions and 13 deletions
+10
View File
@@ -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
# ---------------------------------------------------------------------------
-1
View File
@@ -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)
+13 -2
View File
@@ -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()))
-1
View File
@@ -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,
}
+7
View File
@@ -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);
}
@@ -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() {
<NumberInput label="Signal Alignment Weight" value={form.signal_alignment_weight} min={0} max={1} step={0.01} onChange={(v) => setField('signal_alignment_weight', v)} />
<NumberInput label="S/R Strength Weight" value={form.sr_strength_weight} min={0} max={1} step={0.01} onChange={(v) => setField('sr_strength_weight', v)} />
<NumberInput label="Distance Penalty Factor" value={form.distance_penalty_factor} min={0} max={1} step={0.01} onChange={(v) => setField('distance_penalty_factor', v)} />
<NumberInput label="Momentum-Technical Divergence Threshold" value={form.momentum_technical_divergence_threshold} min={0} max={100} onChange={(v) => setField('momentum_technical_divergence_threshold', v)} />
<NumberInput label="Fundamental-Technical Divergence Threshold" value={form.fundamental_technical_divergence_threshold} min={0} max={100} onChange={(v) => setField('fundamental_technical_divergence_threshold', v)} />
@@ -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 (
<div className="space-y-6">
<div className="glass-sm flex flex-wrap items-center justify-between gap-3 px-4 py-3">
@@ -145,9 +168,14 @@ export function TrackRecordPanel() {
evaluator runs nightly after OHLCV collection.
</p>
</Disclosure>
<Button onClick={() => evaluateMutation.mutate()} loading={evaluateMutation.isPending} className="shrink-0">
{evaluateMutation.isPending ? 'Evaluating…' : 'Evaluate Now'}
</Button>
<div className="flex shrink-0 items-center gap-2">
<Button onClick={() => evaluateMutation.mutate()} loading={evaluateMutation.isPending}>
{evaluateMutation.isPending ? 'Evaluating…' : 'Evaluate Now'}
</Button>
<Button variant="danger" onClick={onReset} loading={resetMutation.isPending}>
{resetMutation.isPending ? 'Resetting…' : 'Reset'}
</Button>
</div>
</div>
{isLoading && (
-1
View File
@@ -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;
}
@@ -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}