add track-record reset; drop dead distance_penalty_factor knob
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:
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user