config.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """Configuration for daily ROI computation and approved execution."""
  2. from __future__ import annotations
  3. import os
  4. from dataclasses import asdict, dataclass
  5. from decimal import Decimal
  6. def env_flag(name: str, default: bool = False) -> bool:
  7. raw = os.getenv(name)
  8. if raw is None:
  9. return default
  10. return raw.strip().lower() in {"1", "true", "yes", "on"}
  11. @dataclass(frozen=True)
  12. class RoiConfig:
  13. daily_enabled: bool = False
  14. apply_enabled: bool = False
  15. sheet_approval_enabled: bool = False
  16. sheet_approval_poll_seconds: int = 60
  17. report_hour: int = 11
  18. report_minute: int = 0
  19. approval_ttl_minutes: int = 120
  20. scale_ratio: Decimal = Decimal("1.10")
  21. scale_cooldown_days: int = 3
  22. max_base_ratio: Decimal = Decimal("2.00")
  23. self_stop_min_age: int = 5
  24. self_up_min_age: int = 3
  25. self_min_avg_uv: float = 200
  26. partner_min_avg_uv: float = 200
  27. min_daily_cost: float = 100
  28. stop_quantile: float = 0.20
  29. up_quantile: float = 0.80
  30. gzh_adjust_rank_min: float = 0.10
  31. gzh_adjust_rank_max: float = 0.30
  32. @classmethod
  33. def from_env(cls) -> "RoiConfig":
  34. config = cls(
  35. daily_enabled=env_flag("DAILY_ROI_ENABLED"),
  36. apply_enabled=env_flag("ROI_APPLY_ENABLED"),
  37. sheet_approval_enabled=env_flag("ROI_SHEET_APPROVAL_ENABLED"),
  38. sheet_approval_poll_seconds=int(
  39. os.getenv("ROI_SHEET_APPROVAL_POLL_SECONDS", "60")
  40. ),
  41. report_hour=int(os.getenv("DAILY_ROI_HOUR", "11")),
  42. report_minute=int(os.getenv("DAILY_ROI_MINUTE", "0")),
  43. approval_ttl_minutes=int(
  44. os.getenv("ROI_APPROVAL_TTL_MINUTES", "120")
  45. ),
  46. scale_ratio=Decimal(os.getenv("ROI_SCALE_RATIO", "1.10")),
  47. scale_cooldown_days=int(os.getenv("ROI_SCALE_COOLDOWN_DAYS", "3")),
  48. max_base_ratio=Decimal(os.getenv("ROI_MAX_BASE_RATIO", "2.00")),
  49. self_stop_min_age=int(os.getenv("ROI_SELF_STOP_MIN_AGE", "5")),
  50. self_up_min_age=int(os.getenv("ROI_SELF_UP_MIN_AGE", "3")),
  51. self_min_avg_uv=float(os.getenv("ROI_SELF_MIN_AVG_UV", "200")),
  52. partner_min_avg_uv=float(
  53. os.getenv("ROI_PARTNER_MIN_AVG_UV", "200")
  54. ),
  55. min_daily_cost=float(os.getenv("ROI_MIN_DAILY_COST", "100")),
  56. stop_quantile=float(os.getenv("ROI_STOP_QUANTILE", "0.20")),
  57. up_quantile=float(os.getenv("ROI_UP_QUANTILE", "0.80")),
  58. gzh_adjust_rank_min=float(
  59. os.getenv("ROI_GZH_ADJUST_RANK_MIN", "0.10")
  60. ),
  61. gzh_adjust_rank_max=float(
  62. os.getenv("ROI_GZH_ADJUST_RANK_MAX", "0.30")
  63. ),
  64. )
  65. config.validate()
  66. return config
  67. def validate(self) -> None:
  68. if self.apply_enabled and not self.daily_enabled:
  69. raise ValueError(
  70. "ROI_APPLY_ENABLED=1 requires DAILY_ROI_ENABLED=1"
  71. )
  72. if self.sheet_approval_enabled and not self.apply_enabled:
  73. raise ValueError(
  74. "ROI_SHEET_APPROVAL_ENABLED=1 requires ROI_APPLY_ENABLED=1"
  75. )
  76. if self.sheet_approval_poll_seconds < 30:
  77. raise ValueError("ROI_SHEET_APPROVAL_POLL_SECONDS must be at least 30")
  78. if not 0 <= self.report_hour <= 23 or not 0 <= self.report_minute <= 59:
  79. raise ValueError("DAILY_ROI_HOUR/MINUTE is invalid")
  80. if self.approval_ttl_minutes < 1:
  81. raise ValueError("ROI_APPROVAL_TTL_MINUTES must be positive")
  82. if self.scale_ratio <= 1:
  83. raise ValueError("ROI_SCALE_RATIO must be greater than 1")
  84. if self.max_base_ratio < self.scale_ratio:
  85. raise ValueError("ROI_MAX_BASE_RATIO must cover ROI_SCALE_RATIO")
  86. if self.scale_cooldown_days < 1:
  87. raise ValueError("ROI_SCALE_COOLDOWN_DAYS must be positive")
  88. if not 0 < self.stop_quantile < self.up_quantile < 1:
  89. raise ValueError("ROI quantiles must satisfy 0 < stop < up < 1")
  90. def snapshot(self) -> dict[str, object]:
  91. values = asdict(self)
  92. values["scale_ratio"] = str(self.scale_ratio)
  93. values["max_base_ratio"] = str(self.max_base_ratio)
  94. return values