mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-19 15:59:37 +00:00
163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
"""
|
||
Revenue Forecast — best / likely / worst over 30/60/90 days.
|
||
|
||
Each open deal contributes a probability-weighted slice of revenue.
|
||
Probabilities come from stage-historical win rates × deal-specific risk.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any
|
||
|
||
|
||
# Stage → base probability of close (calibrated over Pulse cohort)
|
||
STAGE_BASE_PROBABILITY: dict[str, float] = {
|
||
"new": 0.05,
|
||
"qualified": 0.15,
|
||
"discovery": 0.25,
|
||
"demo": 0.40,
|
||
"proposal": 0.55,
|
||
"negotiation": 0.70,
|
||
"verbal_yes": 0.85,
|
||
"won": 1.0,
|
||
"lost": 0.0,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class ForecastBand:
|
||
"""One scenario (best/likely/worst)."""
|
||
|
||
label: str # "best" / "likely" / "worst"
|
||
revenue_sar: float
|
||
n_deals_closing: int
|
||
confidence: float
|
||
|
||
|
||
@dataclass
|
||
class Forecast:
|
||
"""Full forecast for a customer over a horizon."""
|
||
|
||
customer_id: str
|
||
horizon_days: int
|
||
period_label: str
|
||
best: ForecastBand
|
||
likely: ForecastBand
|
||
worst: ForecastBand
|
||
deals_breakdown: list[dict[str, Any]] = field(default_factory=list)
|
||
risks_ar: list[str] = field(default_factory=list)
|
||
decisions_required_ar: list[str] = field(default_factory=list)
|
||
|
||
|
||
def _deal_close_probability(
|
||
*, stage: str, days_in_stage: int, multi_threaded: bool, value_sar: float
|
||
) -> float:
|
||
"""Compute the probability this specific deal closes within horizon."""
|
||
base = STAGE_BASE_PROBABILITY.get(stage, 0.10)
|
||
# Stalled penalty
|
||
if days_in_stage > 21:
|
||
base *= max(0.3, 1.0 - (days_in_stage - 21) * 0.02)
|
||
# Multi-threaded bonus (multiple decision-makers)
|
||
if multi_threaded:
|
||
base *= 1.15
|
||
# Very large deals are harder
|
||
if value_sar > 500_000:
|
||
base *= 0.85
|
||
return min(0.99, max(0.0, base))
|
||
|
||
|
||
def compute_forecast(
|
||
*,
|
||
customer_id: str,
|
||
open_deals: list[dict[str, Any]],
|
||
horizon_days: int = 30,
|
||
now: datetime | None = None,
|
||
) -> Forecast:
|
||
"""
|
||
Compute the customer's forecast over the next N days.
|
||
|
||
Each deal is dict with: id, stage, value_sar, last_activity_at,
|
||
days_in_stage, multi_threaded, expected_close_at (optional).
|
||
"""
|
||
n = now or datetime.now(timezone.utc).replace(tzinfo=None)
|
||
horizon_end = n + timedelta(days=horizon_days)
|
||
|
||
breakdown: list[dict[str, Any]] = []
|
||
expected = 0.0
|
||
best_total = 0.0
|
||
likely_total = 0.0
|
||
worst_total = 0.0
|
||
risks: list[str] = []
|
||
|
||
for d in open_deals:
|
||
if d.get("stage") in ("won", "lost"):
|
||
continue
|
||
prob = _deal_close_probability(
|
||
stage=d.get("stage", "new"),
|
||
days_in_stage=d.get("days_in_stage", 0),
|
||
multi_threaded=d.get("multi_threaded", False),
|
||
value_sar=d.get("value_sar", 0),
|
||
)
|
||
value = float(d.get("value_sar", 0))
|
||
breakdown.append({
|
||
"deal_id": d.get("id"),
|
||
"company_name": d.get("company_name", ""),
|
||
"stage": d.get("stage"),
|
||
"value_sar": value,
|
||
"probability": round(prob, 3),
|
||
"expected_value_sar": round(value * prob, 2),
|
||
})
|
||
expected += value * prob
|
||
# Best: optimistic — assume P(close) = min(1, p+0.2)
|
||
best_total += value * min(1.0, prob + 0.2)
|
||
# Likely: expected
|
||
likely_total += value * prob
|
||
# Worst: only deals at p ≥ 0.7 contribute
|
||
if prob >= 0.7:
|
||
worst_total += value * (prob - 0.1)
|
||
if prob < 0.2 and value > 100_000:
|
||
risks.append(
|
||
f"صفقة {d.get('company_name','—')} ({value:,.0f} ريال) احتمالها {prob*100:.0f}% فقط — قد لا تغلق هذا الشهر."
|
||
)
|
||
if d.get("days_in_stage", 0) > 30 and value > 50_000:
|
||
risks.append(
|
||
f"صفقة {d.get('company_name','—')} في نفس المرحلة منذ {d['days_in_stage']} يوم."
|
||
)
|
||
|
||
decisions: list[str] = []
|
||
if best_total - likely_total > likely_total * 0.5:
|
||
decisions.append(
|
||
"الفجوة بين best و likely كبيرة — حدد الـ 2-3 صفقات الأهم وركّز عليها."
|
||
)
|
||
if not breakdown:
|
||
decisions.append("لا صفقات مفتوحة — ابدأ Daily Growth Run الآن لبناء pipeline.")
|
||
|
||
return Forecast(
|
||
customer_id=customer_id,
|
||
horizon_days=horizon_days,
|
||
period_label=f"{n.date()} → {horizon_end.date()}",
|
||
best=ForecastBand(
|
||
label="best",
|
||
revenue_sar=round(best_total, 2),
|
||
n_deals_closing=sum(1 for b in breakdown if b["probability"] >= 0.5),
|
||
confidence=0.5,
|
||
),
|
||
likely=ForecastBand(
|
||
label="likely",
|
||
revenue_sar=round(likely_total, 2),
|
||
n_deals_closing=sum(1 for b in breakdown if b["probability"] >= 0.4),
|
||
confidence=0.7,
|
||
),
|
||
worst=ForecastBand(
|
||
label="worst",
|
||
revenue_sar=round(worst_total, 2),
|
||
n_deals_closing=sum(1 for b in breakdown if b["probability"] >= 0.7),
|
||
confidence=0.85,
|
||
),
|
||
deals_breakdown=breakdown,
|
||
risks_ar=risks[:5],
|
||
decisions_required_ar=decisions,
|
||
)
|