ef523474ad
The 5-year backtest confirmed the EV gate adds negative value (high threshold = worst expectancy) and that 12-1 month momentum is the one price signal with a plausible, right-signed cross-sectional IC (~0.05). So "qualified" now means: clears the R:R + confidence floors AND the ticker ranks in the top `min_momentum_percentile` of the universe by 12-1 momentum that week. - qualification.py: drop expected_value_r / the EV gate; add a momentum-percentile gate (duck-typed `momentum_percentile`, only enforced when attached + threshold set, else defers to floors). Mirrored in frontend qualification.ts. - activation config/schema: min_expected_value -> min_momentum_percentile (default 80 = top quintile). ActivationSettings, DashboardPage (ranks/【shows】 momentum instead of EV), and the BacktestPanel sweep follow. - backtest: rank each ISO week's universe by 12-1 momentum, assign a percentile, and qualify the top slice; the sweep now sweeps the percentile cutoff. Also offload the backtest's per-ticker compute to a worker thread so the heavy ~5y run no longer blocks the API event loop (the "backend offline" flicker). Production setups don't carry momentum_percentile yet — wiring the scanner to attach it (a universe momentum-rank step) is the next step; until then the live gate defers to floors while the backtest measures the momentum selection. 330 backend tests pass; frontend build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.7 KiB
Python
102 lines
3.7 KiB
Python
"""Admin request/response schemas."""
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class UserManagement(BaseModel):
|
|
"""Schema for user access management."""
|
|
has_access: bool
|
|
|
|
|
|
class PasswordReset(BaseModel):
|
|
"""Schema for resetting a user's password."""
|
|
new_password: str = Field(..., min_length=6)
|
|
|
|
|
|
class CreateUserRequest(BaseModel):
|
|
"""Schema for admin-created user accounts."""
|
|
username: str = Field(..., min_length=1)
|
|
password: str = Field(..., min_length=6)
|
|
role: str = Field(default="user", pattern=r"^(user|admin)$")
|
|
has_access: bool = False
|
|
|
|
|
|
class RegistrationToggle(BaseModel):
|
|
"""Schema for toggling registration on/off."""
|
|
enabled: bool
|
|
|
|
|
|
class SystemSettingUpdate(BaseModel):
|
|
"""Schema for updating a system setting."""
|
|
value: str = Field(..., min_length=1)
|
|
|
|
|
|
class DataCleanupRequest(BaseModel):
|
|
"""Schema for data cleanup — delete records older than N days."""
|
|
older_than_days: int = Field(..., gt=0)
|
|
|
|
|
|
class JobToggle(BaseModel):
|
|
"""Schema for enabling/disabling a scheduled job."""
|
|
enabled: bool
|
|
|
|
|
|
class RecommendationConfigUpdate(BaseModel):
|
|
high_confidence_threshold: float | None = Field(default=None, ge=0, le=100)
|
|
moderate_confidence_threshold: float | None = Field(default=None, ge=0, le=100)
|
|
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)
|
|
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)
|
|
|
|
|
|
class TickerUniverseUpdate(BaseModel):
|
|
universe: Literal["sp500", "nasdaq100", "nasdaq_all"]
|
|
|
|
|
|
class ActivationConfigUpdate(BaseModel):
|
|
"""Activation gate: what counts as an actionable signal."""
|
|
min_momentum_percentile: float | None = Field(default=None, ge=0, le=100)
|
|
min_rr: float | None = Field(default=None, ge=0)
|
|
min_confidence: float | None = Field(default=None, ge=0, le=100)
|
|
min_target_probability: float | None = Field(default=None, ge=0, le=100)
|
|
require_high_conviction: bool | None = None
|
|
exclude_conflicts: bool | None = None
|
|
|
|
|
|
class ScheduleConfigUpdate(BaseModel):
|
|
"""Cron schedule for the pipelines + fundamentals. Crons are 5-field
|
|
(min hour dom month dow); timezone is an IANA name (e.g. Europe/Berlin)."""
|
|
schedule_timezone: str | None = Field(default=None, max_length=64)
|
|
schedule_daily_pipeline_cron: str | None = Field(default=None, max_length=120)
|
|
schedule_intraday_pipeline_cron: str | None = Field(default=None, max_length=120)
|
|
schedule_fundamentals_cron: str | None = Field(default=None, max_length=120)
|
|
|
|
|
|
class SentimentConfigUpdate(BaseModel):
|
|
"""Runtime sentiment LLM config. api_key is write-only; omit/empty to keep
|
|
the stored key."""
|
|
provider: Literal["openai", "gemini", "deepseek", "xai", "openai_compatible"] | None = None
|
|
model: str | None = Field(default=None, max_length=100)
|
|
api_key: str | None = Field(default=None, max_length=400)
|
|
base_url: str | None = Field(default=None, max_length=300)
|
|
|
|
|
|
class SentimentTestRequest(BaseModel):
|
|
ticker: str = Field(default="AAPL", max_length=10)
|
|
|
|
|
|
class AlertConfigUpdate(BaseModel):
|
|
"""Telegram alert config. bot_token is write-only; omit/empty to keep the
|
|
stored token."""
|
|
enabled: bool | None = None
|
|
bot_token: str | None = Field(default=None, max_length=200)
|
|
telegram_chat_id: str | None = Field(default=None, max_length=64)
|
|
qualified_enabled: bool | None = None
|
|
sr_proximity_enabled: bool | None = None
|
|
score_drop_enabled: bool | None = None
|
|
digest_enabled: bool | None = None
|