feat: perfiles de jitter intercambiables desde Telegram
Cinco perfiles con distinto punto en el compromiso entre variedad visible y
minutos de presencia de mas. Todos dan saldo 0. Se cambian en caliente con
/perfil, con botones, y al cambiar se vuelven a tirar los dados de todos los
dias del horizonte que no se hayan fichado ya.
min/dia madrugon entrada 09:00
discreto 2,8 1,3 2 minutos, moda 75%
natural 4,1 1,8 5 minutos, moda 51% <- por defecto
camuflado 5,5 2,1 4 minutos, moda 43%
disperso 7,9 3,0 6 minutos, moda 29%
sin_madrugar 5,7 1,3 2 minutos, moda 77%
(historico real del usuario: 4 minutos, moda 38%)
La distincion util no es solo el coste total sino DONDE cae. Adelantar la
entrada de la mañana alarga el dia; adelantar la vuelta de comer solo acorta la
comida y se sale a la misma hora. De ahi "sin_madrugar": entrada de la mañana lo
mas pegada posible a las 9 (madrugon 1,26 min/dia, el mas bajo de todos, por
debajo incluso de "discreto") y toda la variedad en la pausa y las salidas.
Para eso los perfiles admiten afinar por tramo con sufijo _w<n>:
entry_median_w0 gana a entry_median para el tramo de la mañana.
tools/perfiles.py mide cada perfil sobre los 226 dias laborables del calendario
real; las cifras de arriba salen de ahi y van guardadas como metadatos para que
/perfil las muestre sin recalcular.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from app import jitter
|
||||
from app import jitter, profiles
|
||||
from app.schedule import Calendar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,7 +46,19 @@ class Planner:
|
||||
self.cfg = cfg
|
||||
self.store = store
|
||||
self.tz = ZoneInfo(cfg.get("timezone", "Europe/Madrid"))
|
||||
self.j = cfg["jitter"]
|
||||
|
||||
@property
|
||||
def perfil(self) -> str:
|
||||
"""Perfil activo. Se lee del store en cada plan, para poder cambiarlo en caliente."""
|
||||
if self.store:
|
||||
nombre = self.store.get("perfil", self.cfg.get("perfil", profiles.DEFECTO))
|
||||
if profiles.existe(nombre):
|
||||
return nombre
|
||||
return self.cfg.get("perfil", profiles.DEFECTO)
|
||||
|
||||
@property
|
||||
def j(self) -> dict:
|
||||
return profiles.params(self.perfil)
|
||||
|
||||
# ------------------------------------------------------------------ drift
|
||||
def _drift_key(self, window_idx: int, typ: int) -> str:
|
||||
@@ -55,7 +67,8 @@ class Planner:
|
||||
def _drift(self, window_idx: int, typ: int, rng: random.Random) -> float:
|
||||
key = self._drift_key(window_idx, typ)
|
||||
prev = float(self.store.get(key, 0.0)) if self.store else 0.0
|
||||
nxt = jitter.next_drift(prev, self.j["drift_sigma"], self.j["drift_clamp"], rng)
|
||||
j = self.j
|
||||
nxt = jitter.next_drift(prev, j["drift_sigma"], j["drift_clamp"], rng)
|
||||
if self.store:
|
||||
self.store.put(key, nxt)
|
||||
return nxt
|
||||
@@ -69,6 +82,7 @@ class Planner:
|
||||
return []
|
||||
|
||||
rng = rng or random.Random()
|
||||
j = self.j
|
||||
out: list[PlannedCheck] = []
|
||||
idx = 0
|
||||
|
||||
@@ -78,19 +92,24 @@ class Planner:
|
||||
|
||||
# ENTRADA: siempre antes de la hora teorica. El tope se acota ademas
|
||||
# con el margen de entrada que declara el propio tipo de jornada.
|
||||
tope = min(self.j["entry_cap"], max(w.entry_margin * 60 // 2, 60)) if w.entry_margin \
|
||||
else self.j["entry_cap"]
|
||||
cap = profiles.valor(j, "entry", "cap", wi)
|
||||
tope = min(cap, max(w.entry_margin * 60 // 2, 60)) if w.entry_margin else cap
|
||||
off = jitter.offset_seconds(
|
||||
self.j["entry_floor"], self.j["entry_median"], self.j["entry_sigma"],
|
||||
profiles.valor(j, "entry", "floor", wi),
|
||||
profiles.valor(j, "entry", "median", wi),
|
||||
profiles.valor(j, "entry", "sigma", wi),
|
||||
tope, self._drift(wi, ENTRADA, rng), rng)
|
||||
off = max(off, self.j["entry_guard"])
|
||||
off = max(off, j["entry_guard"])
|
||||
out.append(PlannedCheck(idx, ENTRADA, wi, start, start - timedelta(seconds=off)))
|
||||
idx += 1
|
||||
|
||||
# SALIDA: siempre despues
|
||||
off = jitter.offset_seconds(
|
||||
self.j["exit_floor"], self.j["exit_median"], self.j["exit_sigma"],
|
||||
self.j["exit_cap"], self._drift(wi, SALIDA, rng), rng)
|
||||
profiles.valor(j, "exit", "floor", wi),
|
||||
profiles.valor(j, "exit", "median", wi),
|
||||
profiles.valor(j, "exit", "sigma", wi),
|
||||
profiles.valor(j, "exit", "cap", wi),
|
||||
self._drift(wi, SALIDA, rng), rng)
|
||||
out.append(PlannedCheck(idx, SALIDA, wi, end, end + timedelta(seconds=off)))
|
||||
idx += 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user