#!/usr/bin/env python3 """Рыночная симуляция Simbiot: конкуренты + клиенты + сценарии входа (t-261009123019-a32bb3). Модель поверх расширенного шаблона гипотез (docs/product-hypothesis-template.md): - конкуренты: цены, качество, доли (logit-выбор клиента); - клиенты: сегменты с WTP, ценностью продукта и оттоком подписки; - Simbiot: вход по сценариям (тёплый контур / presale-воронка / микс + сессии $150); - метрика: E[ΔAUM] = revenue − CAC − burn за 12 мес, Монте-Карло + чувствительность. Калибровка (v0.2, после sanity-чека): - подписка $100/mo — РЕКУРРЕНТНО (биллинг на активных, не разово); - logit_scale=100 у.е.: холодная сквозная конверсия ≈0.5–1.2% — верх плана SALES-PLAN-1000-US; - тёплая конверсия ≈20–40% (буст доверия на value) — не 77%; - допущения помечены в PARAMS, чувствительность — отдельным прогоном (--sens). Запуск: ./venv/bin/python scripts/market_sim.py [--trials N] [--seed S] [--md] [--sens] """ from __future__ import annotations import argparse import copy import math import random import statistics as st from dataclasses import dataclass BASE = { # Экономика харнеса (замер cli.py aum 09.10: burn 7д=28.55 у.е.) "aum0": 26.7, # у.е., docs/aum/balances.json "burn_month_base": 28.55 / 7 * 30.4, # ≈124 у.е./мес — замер стора (BudgetSpent-оценка) "burn_per_session": 5.0, # допущение: токены 1 сессии агента (60 мин) "burn_per_sub_extra": 1.0, # допущение: доп. тики агента на активного подписчика/мес # Presale (директива m-516173408): $100/mo, без скидок, места total=100 "presale_price": 100.0, "seats_total": 100, "monthly_churn": 0.08, # допущение: отток AI-подписок 8%/мес (чувствительность: 5–12%) # Сессии (ранбук session_productize): $150 one_time "session_price": 150.0, # Конкуренты (блок 1 шаблона: cheapest/closest/premium) "competitors": [ # name, цена $/мес, value (полезность у.е./мес для среднего клиента), share0 — априорная доля {"name": "DIY (LLM-подписка + ручные промпты)", "price": 25.0, "value": 30.0, "share0": 0.45}, {"name": "Upwork/AI-фрилансер (часами)", "price": 100.0, "value": 110.0, "share0": 0.30}, {"name": "AI-консультант соло (сессии)", "price": 150.0, "value": 140.0, "share0": 0.15}, {"name": "AI-студия (проектно)", "price": 300.0, "value": 260.0, "share0": 0.10}, ], # Simbiot: базовая value для сегмента s2; сегменты масштабируют (value_mult) "simbiot_value_base": 130.0, "logit_scale": 100.0, # делитель (value−price): калибрует конверсию (см. шапку) # Сегменты (блок 2-3 шаблона): weight, WTP, множитель ценности Simbiot "segments": [ {"name": "s1 соло-основатель (рутина, платит $50–200)", "w": 0.35, "wtp": 130.0, "vm": 1.0}, {"name": "s2 индиз/автоматизатор (платит $30–80)", "w": 0.40, "wtp": 60.0, "vm": 0.75}, {"name": "s3 операционка малого бизнеса (платит $150–500)", "w": 0.25, "wtp": 300.0, "vm": 1.3}, ], # Сценарии входа: n_warm/мес; visitors/мес (presale-лендинг); paid_share и cac — платный трафик "scenarios": { "A_warm_only": {"n_warm": 15, "visitors": [0] * 12, "paid_share": 0.0, "cac": 0.0, "session_rate": 0.12, "warm_mult": 1.3}, "D_no_entry": {"n_warm": 0, "visitors": [0] * 12, "paid_share": 0.0, "cac": 0.0, "session_rate": 0.0, "warm_mult": 1.0}, "B_presale_funnel": {"n_warm": 12, "visitors": [5000, 8000, 12000, 18000, 25000, 35000, 45000, 55000, 65000, 75000, 85000, 95000], "paid_share": 0.6, "cac": 22.0, "session_rate": 0.05, "warm_mult": 1.2}, "C_mixed": {"n_warm": 15, "visitors": [3000, 6000, 10000, 15000, 22000, 30000, 40000, 50000, 60000, 70000, 80000, 90000], "paid_share": 0.5, "cac": 22.0, "session_rate": 0.12, "warm_mult": 1.3}, }, "reach_in_market": 0.04, # допущение: 4% посетителей реально «в рынке» (чувствительность ×0.5/×2) "horizon_months": 12, } MC_TRIALS = 400 @dataclass class TrialResult: revenue_sub: float revenue_sessions: float cac_total: float burn_total: float d_aum: float subs_final: int sessions_total: int payback_month: int # 0 = не окупился за горизонт def logit_share(options, scale): us = {n: (v - p) / scale for n, v, p in options} mx = max(us.values()) exps = {n: math.exp(u - mx) for n, u in us.items()} z = sum(exps.values()) return {n: e / z for n, e in exps.items()} def share_simbiot(seg, p, warm_mult): opts = [(c["name"], c["value"], c["price"]) for c in p["competitors"]] val = p["simbiot_value_base"] * seg["vm"] * warm_mult opts.append(("simbiot", val, p["presale_price"])) return logit_share(opts, p["logit_scale"])["simbiot"] def run_trial(rng, p, sc): H = p["horizon_months"] subs = 0 rev_sub = rev_sess = cac_t = burn_t = 0.0 sessions = 0 payback = 0 cum = 0.0 segs, ws = p["segments"], [s["w"] for s in p["segments"]] for m in range(H): # отток subs = int(round(subs * (1 - p["monthly_churn"]))) # рекуррентный биллинг активных rev_sub += subs * p["presale_price"] # тёплые лиды for _ in range(sc["n_warm"]): seg = rng.choices(segs, weights=ws)[0] if rng.random() < share_simbiot(seg, p, sc["warm_mult"]) and subs < p["seats_total"]: subs += 1 # сессии $150 (разовый вход, часть тёплых) exp_booked = int(round(sc["n_warm"] * sc["session_rate"])) booked = rng.randint(0, exp_booked) if exp_booked > 0 else 0 sessions += booked rev_sess += booked * p["session_price"] # presale-воронка vis = sc["visitors"][m] if vis: for _ in range(int(vis * p["reach_in_market"])): seg = rng.choices(segs, weights=ws)[0] if rng.random() < share_simbiot(seg, p, 1.0) and subs < p["seats_total"]: subs += 1 if rng.random() < sc["paid_share"]: cac_t += sc["cac"] burn_t += p["burn_month_base"] + subs * p["burn_per_sub_extra"] + booked * p["burn_per_session"] cum = rev_sub + rev_sess - cac_t - burn_t if payback == 0 and cum > 0: payback = m + 1 return TrialResult(rev_sub, rev_sess, cac_t, burn_t, cum, subs, sessions, payback) def run_scenario(name, sc, p, trials, seed): rng = random.Random(seed) rs = [run_trial(rng, p, sc) for _ in range(trials)] d = [r.d_aum for r in rs] aum0 = p["aum0"] base_burn = p["burn_month_base"] * p["horizon_months"] # «не входить»: burn всё равно идёт return { "name": name, "E[rev sub]": st.mean(r.revenue_sub for r in rs), "E[rev sess]": st.mean(r.revenue_sessions for r in rs), "E[CAC]": st.mean(r.cac_total for r in rs), "E[burn]": st.mean(r.burn_total for r in rs), "E[dAUM]": st.mean(d), "P(dAUM>0)": sum(1 for x in d if x > 0) / len(d), "P(инкремент>0 vs без входа)": sum(1 for x in d if x > -base_burn) / len(d), "P(фильтр +1% AUM)": sum(1 for x in d if x >= 0.01 * aum0) / len(d), "subs_final med": st.median(r.subs_final for r in rs), "sessions med": st.median(r.sessions_total for r in rs), "payback med": st.median([r.payback_month for r in rs if r.payback_month > 0] or [0]), "payback>0": sum(1 for r in rs if r.payback_month > 0) / len(rs), } def sens_table(trials, seed): """Чувствительность E[dAUM] сценария C_mixed к ключевым допущениям.""" rows = [] variants = [ ("база", {}), ("churn 5%", {"monthly_churn": 0.05}), ("churn 12%", {"monthly_churn": 0.12}), ("logit_scale 80 (быстрее верят)", {"logit_scale": 80.0}), ("logit_scale 130 (скептичнее)", {"logit_scale": 130.0}), ("value −15%", {"simbiot_value_base": 130.0 * 0.85}), ("value +15%", {"simbiot_value_base": 130.0 * 1.15}), ("reach ×0.5 (трафик холоднее)", {"reach_in_market": 0.02}), ("reach ×2 (трафик горячее)", {"reach_in_market": 0.08}), ] for label, patch in variants: p = copy.deepcopy(BASE) p.update(patch) r = run_scenario("C_mixed", p["scenarios"]["C_mixed"], p, trials, seed) rows.append((label, r["E[dAUM]"], r["P(dAUM>0)"], r["subs_final med"])) return rows def main(): ap = argparse.ArgumentParser() ap.add_argument("--trials", type=int, default=MC_TRIALS) ap.add_argument("--seed", type=int, default=42) ap.add_argument("--md", action="store_true") ap.add_argument("--sens", action="store_true") a = ap.parse_args() p = BASE print(f"# Рыночная симуляция Simbiot: {a.trials} trial'ов, seed={a.seed}, горизонт {p['horizon_months']} мес") print(f"# база: AUM={p['aum0']} у.е., burn={p['burn_month_base']:.1f}/мес (~{p['burn_month_base']*12:.0f}/год);") print(f"# presale $100/mo рекуррентно (мест {p['seats_total']}), сессия $150; отток {p['monthly_churn']:.0%}/мес") keys = ["E[rev sub]", "E[rev sess]", "E[CAC]", "E[burn]", "E[dAUM]", "P(dAUM>0)", "P(инкремент>0 vs без входа)", "P(фильтр +1% AUM)", "subs_final med", "sessions med", "payback med", "payback>0"] rows = [run_scenario(n, sc, p, a.trials, a.seed) for n, sc in p["scenarios"].items()] if a.md: print("| сценарий | " + " | ".join(keys) + " |") print("|---|" + "---|" * len(keys)) for r in rows: print(f"| {r['name']} | " + " | ".join( (f"{r[k]:.0f}" if abs(r[k]) >= 10 else f"{r[k]:.3f}") if isinstance(r[k], float) else str(r[k]) for k in keys) + " |") else: for r in rows: print("\n===", r["name"]) for k in keys: v = r[k] print(f" {k:30s} {v:,.1f}" if isinstance(v, float) else f" {k:30s} {v}") if a.sens: print("\n# Чувствительность (C_mixed): E[dAUM], P(>0), подписчики") print("| вариант | E[dAUM] у.е. | P(dAUM>0) | subs med |") print("|---|---|---|---|") for label, e, pp, subs in sens_table(a.trials, a.seed): print(f"| {label} | {e:,.0f} | {pp:.2f} | {subs:.0f} |") # снимок долей (сегмент s1, тёплый буст 1.3) print("\n# Доли выбора, сегмент s1, тёплый контур (буст 1.3):") s1 = p["segments"][0] opts = [(c["name"], c["value"], c["price"]) for c in p["competitors"]] opts.append(("Simbiot $100/mo", p["simbiot_value_base"] * s1["vm"] * 1.3, p["presale_price"])) for n, sh in sorted(logit_share(opts, p["logit_scale"]).items(), key=lambda x: -x[1]): print(f" {sh*100:5.1f}% {n}") print("# то же, холодный (буст 1.0):") opts[-1] = ("Simbiot $100/mo", p["simbiot_value_base"] * s1["vm"], p["presale_price"]) for n, sh in sorted(logit_share(opts, p["logit_scale"]).items(), key=lambda x: -x[1]): print(f" {sh*100:5.1f}% {n}") if __name__ == "__main__": main()