From 54beef9dd759319b11c92a33501209d3d8ab0943 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 9 Oct 2026 16:11:51 +0300 Subject: [PATCH] =?UTF-8?q?market=20simulation:=20logit=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D1=83=D1=80=D0=B5=D0=BD=D1=86=D0=B8=D1=8F=20+=20?= =?UTF-8?q?=D1=81=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20+=20=D1=81?= =?UTF-8?q?=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D0=B8=20=D0=B2=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0,=20MC=20400=20trials,=20E[dAUM]=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D1=8F=D0=BC=20+=20?= =?UTF-8?q?=D1=87=D1=83=D0=B2=D1=81=D1=82=D0=B2=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20(t-261009123019-a32bb3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/market-simulation-2026-10-09.md | 106 ++++++++++++ scripts/market_sim.py | 235 +++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 docs/market-simulation-2026-10-09.md create mode 100644 scripts/market_sim.py diff --git a/docs/market-simulation-2026-10-09.md b/docs/market-simulation-2026-10-09.md new file mode 100644 index 0000000..b978f79 --- /dev/null +++ b/docs/market-simulation-2026-10-09.md @@ -0,0 +1,106 @@ +# Рыночная симуляция Simbiot: конкуренты + клиенты + сценарии входа + +> t-261009123019-a32bb3 [L], 2026-10-09. Модель и прогоны: `scripts/market_sim.py` +> (400 trial'ов Монте-Карло, seed 42, горизонт 12 мес). Поверх расширенного шаблона +> гипотез `docs/product-hypothesis-template.md` (t-261009122125): блоки 1 (конкуренты/цена), +> 2 (клиент), 3 (сегменты/каналы) — напрямую параметризуют модель. +> Аргументы запуска: `./venv/bin/python scripts/market_sim.py --trials 400 --seed 42 --sens` + +## 1. Что моделируем + +Рынок «автономный агент вместо ручной рутины» с четырьмя конкурентами, тремя сегментами +клиентов и тремя стратегиями входа Simbiot (+базлайн «не входить»). + +**Клиент выбирает по logit-полезности**: U = (value − price) / scale. Simbiot конкурирует +ценой $100/mo (директива m-516173408 — без скидок, рекуррентно) и value-бустом доверия +у тёплых лидов (видели живой прод харнеса). + +**Конкуренты** (блок 1 шаблона; value — калибровочное допущение, цены — реальные бенчмарки): + +| Конкурент | Цена | value | Приорная доля рынка | +|---|---|---|---| +| DIY (LLM-подписка + ручные промпты) | $25/mo | 30 | 45% | +| Upwork/AI-фрилансер (часами) | ~$100/mo | 110 | 30% | +| AI-консультант соло (сессии) | ~$150/mo | 140 | 15% | +| AI-студия (проектно) | ~$300/mo | 260 | 10% | + +**Сегменты** (блок 2 шаблона): s1 соло-основатель (35% потока, WTP $130, value-множитель 1.0); +s2 индиз/автоматизатор (40%, WTP $60, ×0.75 — цена $100 выше их WTP, они остаются в DIY-лагере); +s3 операционка МБ (25%, WTP $300, ×1.3). + +**Калибровка честная**: logit_scale=100 даёт сквозную холодную конверсию presale-лендинга +≈0.5–1.1% — верх диапазона плана SALES-PLAN-1000-US; тёплая ≈35% (буст ×1.3). Отток 8%/мес. + +## 2. Сценарии входа + +| Сценарий | Тёплые лиды/мес | Посетители presale/мес (мес1→12) | Платный трафик | Сессии $150 | +|---|---|---|---|---| +| D_no_entry | 0 | 0 | — | — | +| A_warm_only | 15 | 0 | нет | 12%/тёплых | +| B_presale_funnel | 12 | 5k → 95k | 60% лидов, CAC $22 | 5% | +| C_mixed | 15 | 3k → 90k | 50% лидов, CAC $22 | 12% | + +Экономика харнеса — из живого замера: burn 124 у.е./мес (28.55/7д, store-BudgetSpent), ++1 у.е./мес на активного подписчика, +5 у.е. на сессию (допущения). AUM₀ = 26.7 у.е. + +## 3. Результаты (E[] по 400 trial'ам, 12 мес, у.е.) + +| Сценарий | rev подписки | rev сессии | CAC | burn | **E[ΔAUM]** | P(ΔAUM>0) | подписчики (медиана) | окупаемость | +|---|---|---|---|---|---|---|---|---| +| D_no_entry | 0 | 0 | 0 | 1 488 | **−1 488** | 0% | 0 | не окупается | +| A_warm_only | 24 119 | 1 840 | 0 | 1 852 | **+24 107** | 100% | 40 | 1-й мес | +| B_presale_funnel | 97 215 | 906 | 1 835 | 2 675 | **+93 611** | 100% | 100 (капа мест) | 2-й мес | +| C_mixed | 95 146 | 1 805 | 1 340 | 2 682 | **+92 929** | 100% | 100 (капа мест) | 2-й мес | + +Доли выбора (сегмент s1): Simbiot $100/mo — 34.8% тёплый / 26.6% холодный контур; далее +фрилансер 19–22%, DIY 18–21%, консультант 16–18%, студия 12–13%. + +## 4. Чувствительность (C_mixed) + +| Вариант | E[ΔAUM] | P(>0) | Подписчики | +|---|---|---|---| +| база | 92 929 | 1.00 | 100 | +| churn 5% / 12% | 96 285 / 88 331 | 1.00 | 100 | +| logit_scale 80 / 130 (вера в продукт) | 93 333 / 92 280 | 1.00 | 100 | +| value −15% / +15% | 91 563 / 93 713 | 1.00 | 100 | +| reach ×0.5 / ×2 (качество трафика) | 87 658 / 96 019 | 1.00 | 100 | + +Вывод устойчивости: разброс E[ΔAUM] ±6% по всем допущениям — **потому что капа мест (100) +связывает спрос во всех вариантах**. Модель говорит: узкое место — не конверсия и не цена, +а (а) лимит мест и (б) способность залить трафик в воронку. + +## 5. Выводы для решений + +1. **Входить — да, при любой стратегии**: даже A_warm_only без рекламного бюджета даёт + +24.1k у.е. за год против −1.5k у «не входить», окупаемость с 1-го месяца. Фильтр AUM + (+1% = +0.27 у.е.) пробивают все сценарии входа с запасом ×1000. +2. **Капа мест — главный ограничение роста**: B/C упираются в 100 мест (медиана). Расширение + лимита (директива, решение Alex) стоит на порядки больше для AUM, чем любая оптимизация + конверсии. Порядок: сначала пролить тёплый контур (A), поднять трафик (B/C), потом + поднимать капу. +3. **Тёплого контура не хватает на полный пролив**: A_warm_only оставляет ~60 мест + незанятыми — воронка presale (трафик) нужна как второй шаг, не как альтернатива. +4. **Сессии $150 — маленький, но почти чистый маржинальный доход** (~1.8k/год в C, + burn 5 у.е./сессия) и верх воронки к подписке. Реально заблокировано багом портала + id 13 (ранбук session_productize) — фикс портала стоит в цепочке. +5. **Цену не трогать**: директива запрещает скидки; модель подтверждает — паритет цены с + фрилансером ($100) при более высокой value уже даёт топ-долю выбора. Скидка не нужна + для победы в logit-выборе. + +## 6. Ограничения модели (честно) + +- value конкурентов и reach_in_market=4% — калибровочные допущения, не замеры; доля рынка + конкурентов (share0) — приор для калибровки value, в динамике выбора не участвует. +- Трафик-рампы посетителей (5k→95k/мес) — цель из SALES-PLAN-1000-US, не факт; при нулевом + трафике B/C деградируют к A_warm_only (+24k), не к отрицательному. +- Не моделировалось: реакция конкурентов на вход (ценовые войны), саппорт-нагрузка на + подписчиков, возвраты/refund, сезонность; burn-per-sub — плоские 1 у.е./мес. +- Сессии предполагают фикс бага product_create (id 13); до фикса компонента сессий нет. +- Модель deterministic-demand per month, no network effects (кейсы/соцдоказательство + повышают value — квазично через warm_mult). + +## 7. Артефакты + +- Модель: `scripts/market_sim.py` (детерминированный seed, `--sens` — чувствительность). +- Шаблон гипотез, на котором стоит модель: `docs/product-hypothesis-template.md`. +- Входные якоря: docs/PRICE-DIRECTIVE-SIMBIOT.md ($100/mo, 100 мест), artifacts/session-productize-runbook.md (сессия $150, витрина), docs/SALES-PLAN-1000-US (воронка), docs/aum.md (метрика ΔAUM). diff --git a/scripts/market_sim.py b/scripts/market_sim.py new file mode 100644 index 0000000..1d36621 --- /dev/null +++ b/scripts/market_sim.py @@ -0,0 +1,235 @@ +#!/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()