Перейти к основному содержимому

Промо-готовность — подготовка к распродажам WB

Как заранее подготовить контент клиента к крупным акциям Wildberries и получить в 2-5 раз больше продаж, пока конкуренты надеются только на скидки.

Проблема

Wildberries проводит крупные распродажи 4-6 раз в год: Чёрная пятница, 11.11, Новый год, 8 марта, День России, Школьный базар. В эти периоды трафик на площадке взлетает в 3-10 раз — миллионы покупателей одновременно ищут товары. Это золотая жила для тех, кто готов.

Проблема: подавляющее большинство селлеров не готовят контент к промо. Их стратегия на распродажу — одна кнопка «снизить цену»:

  • Фото те же — зимние варежки сфотографированы на белом фоне, без праздничного контекста, без подарочной упаковки. Покупатель ищет «подарок на Новый год», а видит безликую карточку
  • Инфографика не адаптирована — нет акцента на скидку, нет сравнения «было/стало», нет urgency-элементов («только 3 дня!»)
  • Заголовки без промо-ключевиков — селлер не добавил в title слова «подарок», «набор», «скидка», «акция», по которым покупатели активно ищут именно в дни распродаж
  • Видео отсутствует — в период пиковой конкуренции карточка без видео проигрывает тем, у кого оно есть (видео повышает время просмотра, а это сигнал алгоритму WB)
  • Запасы не рассчитаны — товар заканчивается на второй день промо, карточка вылетает из поиска, а восстановление позиций занимает 2-4 недели

Результат: селлер ставит скидку 30%, теряет маржу, но не получает пропорционального роста продаж. А соседняя карточка с подготовленным контентом и скидкой 15% продаёт в 3 раза больше — потому что покупатель выбирает не самую дешёвую, а самую убедительную карточку.

Почему контент важнее скидки во время промо

Во время распродажи все снижают цены. Скидка перестаёт быть конкурентным преимуществом — это входной билет. Выигрывает тот, кто совмещает разумную скидку с контентом, заточенным под промо: праздничные фото, акционная инфографика, промо-ключевики в заголовке. Алгоритм WB тоже реагирует на подготовку: карточки с обновлённым контентом получают буст в ранжировании, потому что свежий контент = сигнал актуальности.

Пайплайн данных

Таймлайн подготовки к промо

gantt
title Подготовка к распродаже — 6 недель
dateFormat YYYY-MM-DD
axisFormat %d.%m

section Анализ (T-6 недель)
Тренды прошлых промо :analysis1, 2026-10-12, 7d
Аудит контента конкурентов :analysis2, 2026-10-12, 7d

section Контент (T-4 недели)
Промо-фотосъёмка :content1, 2026-10-19, 10d
Промо-инфографика :content2, 2026-10-26, 7d
SEO под промо-запросы :content3, 2026-10-26, 5d
Видео для промо :content4, 2026-10-26, 10d

section Загрузка (T-2 недели)
Загрузка контента на WB :upload1, 2026-11-09, 3d
Переиндексация WB :upload2, 2026-11-12, 10d

section Промо
Чёрная пятница :crit, promo, 2026-11-22, 5d

section Замер
Сбор метрик после промо :measure, 2026-11-27, 7d

Источники данных MPStats

ЭндпоинтМетодКлючевые поляЗачем
/wb/get/category/trendsGET72 точки помесячной выручки за 6 летВыявить пики прошлых промо: ноябрь, декабрь, март — исторические данные показывают точный масштаб подъёма
/wb/get/categoryPOSTpicscount, hasvideo, revenue, sales, final_price, basic_sale, start_priceТекущее состояние контента конкурентов — кто уже готовится к промо?
Ценовые поля из categoryPOSTfinal_price, start_price, basic_saleДинамика цен: как конкуренты выстраивают ценовую стратегию перед распродажей
Календарь распродаж WB на 2026 год

Wildberries проводит крупные промо с предсказуемой регулярностью. Основные даты для планирования контента:

Промо-событиеДатыПиковый трафикПодготовка контента
День влюблённых10-14 февраляx2-3До 1 января
23 февраля18-23 февраляx2-3До 10 января
8 марта1-8 мартаx3-5До 20 января
День России10-12 июняx1.5-2До 1 мая
Школьный базар15 авг - 5 сенx2-4До 1 июля
11.119-13 ноябряx3-5До 1 октября
Чёрная пятница22-28 ноябряx5-10До 15 октября
Новый год1-25 декабряx5-10До 1 ноября

Ключевое правило: контент должен быть загружен минимум за 2 недели до начала промо, чтобы WB успел переиндексировать карточку.

Общая схема пайплайна

flowchart LR
A["GET /wb/get/category/trends\n72 месяца данных"] --> B["Идентификация промо-пиков\nnoябрь, декабрь, март"]
B --> C["Расчёт promo_uplift\nпик / среднее"]
C --> D["POST /wb/get/category\nТОП-100 в нише"]
D --> E["Аудит: сколько конкурентов\nуже обновили контент?"]
E --> F["Promo Readiness Score\n0-100 баллов"]
F --> G["Персональный план\nподготовки клиента"]

Анализ

Шаг 1: Выявление промо-пиков из исторических данных

import httpx
import statistics

MPSTATS_TOKEN = "YOUR_TOKEN"
BASE_URL = "https://mpstats.io/api"
HEADERS = {
"X-Mpstats-TOKEN": MPSTATS_TOKEN,
"Content-Type": "application/json",
}

# Промо-месяцы WB (индексы 0-11, где 0 = январь)
WB_PROMO_MONTHS = {
1: "23 февраля", # февраль
2: "8 марта", # март
10: "11.11 / Чёрная пятница", # ноябрь
11: "Новый год", # декабрь
}


def fetch_category_trends(category_path: str) -> list[float]:
"""Загрузить 72-месячный тренд категории."""
resp = httpx.get(
f"{BASE_URL}/wb/get/category/trends",
headers=HEADERS,
params={"path": category_path},
timeout=30,
)
resp.raise_for_status()
return resp.json()


def identify_promo_peaks(trends_72: list[float]) -> list[dict]:
"""
Находит промо-пики в историческом тренде.

Алгоритм:
1. Разбить 72 точки на 6 лет по 12 месяцев
2. Для каждого месяца рассчитать медиану по годам
3. Рассчитать promo_uplift = median_month / avg_all_months
4. Отметить месяцы с uplift > 1.3 как промо-пики
"""
# Разбиваем на годы
years_data = []
for year_idx in range(6):
year = trends_72[year_idx * 12 : (year_idx + 1) * 12]
if sum(year) > 0:
years_data.append(year)

if not years_data:
return []

# Медиана для каждого месяца
monthly_medians = []
for month_idx in range(12):
values = [y[month_idx] for y in years_data if y[month_idx] > 0]
median_val = statistics.median(values) if values else 0
monthly_medians.append(median_val)

# Среднемесячная выручка
avg_monthly = statistics.mean(monthly_medians) if monthly_medians else 1

# Рассчитываем uplift
peaks = []
for month_idx, median_val in enumerate(monthly_medians):
uplift = round(median_val / avg_monthly, 2) if avg_monthly > 0 else 1.0

promo_name = WB_PROMO_MONTHS.get(month_idx)
is_promo_month = uplift > 1.3 or promo_name is not None

peaks.append({
"month_idx": month_idx,
"month_name": [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь",
][month_idx],
"median_revenue": round(median_val),
"uplift": uplift,
"is_promo": is_promo_month,
"promo_event": promo_name,
})

return peaks


# --- Пример ---
trends = fetch_category_trends("Одежда/Женская одежда/Платья")
peaks = identify_promo_peaks(trends)

print("=== ПРОМО-ПИКИ КАТЕГОРИИ ===")
for p in peaks:
marker = " ** ПРОМО **" if p["is_promo"] else ""
bar = "█" * int(p["uplift"] * 10)
event = f" ({p['promo_event']})" if p["promo_event"] else ""
print(f"{p['month_name']:>10} | x{p['uplift']:.2f} | {bar}{event}{marker}")

Шаг 2: Аудит промо-готовности конкурентов

from datetime import datetime, timedelta


def fetch_category_top(category_path: str, d1: str, d2: str) -> list[dict]:
"""Загрузить ТОП-100 товаров в категории."""
resp = httpx.post(
f"{BASE_URL}/wb/get/category",
headers=HEADERS,
json={
"path": category_path,
"d1": d1,
"d2": d2,
"startRow": 0,
"endRow": 100,
},
timeout=30,
)
resp.raise_for_status()
return resp.json().get("data", [])


def audit_competitor_readiness(products: list[dict]) -> dict:
"""
Анализирует, сколько конкурентов из ТОП-100 уже готовятся к промо.

Признаки подготовки:
- Обновлённые фото (picscount > среднего)
- Наличие видео (hasvideo = 1)
- Изменение цен (basic_sale > 0 = заготовлена скидка)
- Высокий рейтинг + свежие отзывы (готовность к трафику)
"""
total = len(products)
if total == 0:
return {"total": 0, "prepared": 0, "prepared_pct": 0}

avg_pics = sum(p.get("picscount", 0) for p in products) / total

prepared = 0
readiness_signals = []

for product in products:
signals = 0

# Много фото (выше среднего + 3) — обновлённый контент
if product.get("picscount", 0) > avg_pics + 3:
signals += 1

# Есть видео
if product.get("hasvideo", 0) == 1:
signals += 1

# Заготовлена скидка (basic_sale > 0)
if product.get("basic_sale", 0) > 0:
signals += 1

# Высокий рейтинг (>= 4.5)
if product.get("rating", 0) >= 4.5:
signals += 1

# 3+ сигналов = конкурент готовится
if signals >= 3:
prepared += 1
readiness_signals.append({
"sku": product.get("id"),
"name": product.get("name", "")[:60],
"revenue": product.get("revenue", 0),
"signals": signals,
})

return {
"total": total,
"prepared": prepared,
"prepared_pct": round(prepared / total * 100),
"avg_pics_in_top": round(avg_pics, 1),
"top_prepared": sorted(
readiness_signals,
key=lambda x: x["revenue"],
reverse=True,
)[:5],
}

Шаг 3: Promo Readiness Score клиента

def calculate_promo_readiness(
client_product: dict,
competitor_audit: dict,
promo_uplift: float,
weeks_before_promo: int,
) -> dict:
"""
Рассчитывает Promo Readiness Score (0-100) для карточки клиента.

Компоненты:
- Свежесть контента (30%) — когда последний раз обновлялись фото
- Сезонная релевантность (25%) — контент адаптирован под промо?
- SEO для промо-ключевиков (20%) — есть ли "подарок", "скидка", "набор"
- Готовность стока (15%) — хватит ли товара на пик
- Ценовая конкурентоспособность (10%) — адекватная ли цена/скидка
"""
scores = {}

# 1. Свежесть контента (30%)
pics_count = client_product.get("picscount", 0)
has_video = client_product.get("hasvideo", 0)
avg_pics = competitor_audit.get("avg_pics_in_top", 8)

content_score = min(100, (pics_count / max(avg_pics, 1)) * 60)
if has_video:
content_score += 40
content_score = min(100, content_score)
scores["content_freshness"] = round(content_score)

# 2. Сезонная релевантность (25%)
# Оценка: есть ли промо-элементы в контенте
# В реальности: анализ через CV или ручной чек-лист
# Здесь: упрощённая оценка по наличию видео + кол-ву фото
seasonal_score = 0
if pics_count >= 10:
seasonal_score += 40 # Много фото = вероятно, есть сезонные
if has_video:
seasonal_score += 30
if client_product.get("has3d", 0):
seasonal_score += 30
scores["seasonal_relevance"] = min(100, seasonal_score)

# 3. SEO для промо-ключевиков (20%)
# Проверяем наличие промо-слов в названии
name = (client_product.get("name", "") or "").lower()
promo_keywords = ["подарок", "скидка", "набор", "акция", "sale", "промо", "праздн"]
keyword_hits = sum(1 for kw in promo_keywords if kw in name)
seo_score = min(100, keyword_hits * 35)
scores["promo_seo"] = seo_score

# 4. Готовность стока (15%)
# Оценка по остаткам: хватит ли на 5-7 дней пикового спроса
daily_sales = client_product.get("sales", 0) / 30 # средние продажи/день
peak_daily = daily_sales * promo_uplift # ожидаемые продажи в промо
# Для упрощения: если прогноз > 0, ставим 60 (нужна проверка стоков)
stock_score = 60 if peak_daily > 0 else 20
scores["stock_readiness"] = stock_score

# 5. Ценовая конкурентоспособность (10%)
basic_sale = client_product.get("basic_sale", 0)
if basic_sale >= 30:
price_score = 100
elif basic_sale >= 20:
price_score = 75
elif basic_sale >= 10:
price_score = 50
else:
price_score = 25
scores["price_competitiveness"] = price_score

# Итоговый Promo Readiness Score
weights = {
"content_freshness": 0.30,
"seasonal_relevance": 0.25,
"promo_seo": 0.20,
"stock_readiness": 0.15,
"price_competitiveness": 0.10,
}
total_score = sum(scores[k] * weights[k] for k in weights)

# Оценка
if total_score >= 80:
verdict = "READY"
emoji = "🟢"
action = "Карточка готова к промо. Мониторьте позиции."
elif total_score >= 60:
verdict = "PARTIALLY"
emoji = "🟡"
action = "Нужна доработка контента. Есть время — действуйте."
elif total_score >= 40:
verdict = "WEAK"
emoji = "🟠"
action = "Серьёзные пробелы. Срочно обновлять контент."
else:
verdict = "NOT_READY"
emoji = "🔴"
action = "Карточка не готова к промо. Нужна полная переработка."

return {
"total_score": round(total_score),
"verdict": verdict,
"emoji": emoji,
"action": action,
"breakdown": scores,
"weeks_until_promo": weeks_before_promo,
}

Пример промо-аудита

Категория: Аксессуары / Сумки / Женские сумки, подготовка к Чёрной пятнице.

КомпонентВесКлиентБаллКомментарий
Свежесть контента30%5 фото, нет видео35/100Конкуренты: в среднем 10 фото + видео
Сезонная релевантность25%Нет промо-элементов0/100Нет праздничных фото, нет бейджей
SEO для промо20%Нет промо-слов в title0/100Нужно добавить «подарок», «набор»
Готовность стока15%150 шт на складе60/100При x5 трафике хватит на 3 дня
Ценовая конкурентоспособность10%Скидка 15%50/100Конкуренты ставят 20-30%
ИТОГО100%25/100🔴 НЕ ГОТОВ
Дедлайн: контент должен быть загружен за 2 недели до промо

WB требуется 10-14 дней на полную переиндексацию карточки после обновления контента. Если загрузить новые фото за 3 дня до Чёрной пятницы — алгоритм не успеет их учесть, и карточка войдёт в промо с прежними позициями.

Жёсткий дедлайн для Чёрной пятницы 2026 (22 ноября):

  • Фотосъёмка: до 25 октября
  • Загрузка контента: до 8 ноября
  • Последний день изменений: 10 ноября

Каждый пропущенный день после дедлайна = потерянные продажи в самый прибыльный период года.

Дерево решений

graph TD
A["Промо через 6 недель\nАудит Promo Readiness"] --> B{"Promo Readiness\nScore?"}
B -->|"80-100 🟢 READY"| C["Мониторинг позиций\nТонкая настройка цен"]
B -->|"60-79 🟡 ЧАСТИЧНО"| D["Обновить SEO\nДобавить промо-бейджи\n2-3 недели"]
B -->|"40-59 🟠 СЛАБО"| E["Промо-пакет:\nФото + инфографика\n+ SEO + видео\n4-5 недель"]
B -->|"0-39 🔴 НЕ ГОТОВ"| F["Полная пересъёмка\n+ промо-стратегия\n6 недель"]
C --> G["Промо: максимум\nпродаж"]
D --> G
E --> G
F --> G

Действие Fotofactor

Услуга «Промо-пакет»

На основе Promo Readiness Score Fotofactor предлагает клиенту пакет экстренной подготовки к распродаже:

Таймлайн: 4-6 недель до события.

Состав промо-пакета:

  1. Сезонные фото-оверлеи — накладки на существующие фото с промо-элементами:

    • Подарочные коробки и банты (Новый год, 8 марта)
    • Бейдж «SALE» / «-30%» / «Хит продаж» (Чёрная пятница)
    • Сезонный фон: снежинки, весенние цветы, осенние листья
    • Стоимость: 15 000-25 000 руб (быстро и дёшево, если основные фото хорошие)
  2. Промо-инфографика — слайды, заточенные под распродажу:

    • Сравнение «Обычная цена / Цена на Чёрную пятницу»
    • «В подарочном наборе дешевле на 20%»
    • Таймер urgency: «Только 5 дней по этой цене»
    • Стоимость: 20 000-35 000 руб за комплект
  3. SEO-обновление под промо-запросы — добавление ключевых слов:

    • «подарок на [праздник]», «набор со скидкой», «акция [категория]»
    • Обновление title, description, характеристик
    • Стоимость: 5 000-10 000 руб за карточку
  4. Промо-видео — короткий ролик (15-30 сек) для карточки:

    • Распаковка в праздничном контексте
    • Демонстрация подарочной упаковки
    • Before/after или lifestyle-использование
    • Стоимость: 15 000-30 000 руб
Идеи промо-контента для разных событий

Чёрная пятница / 11.11:

  • Инфографика с перечёркнутой старой ценой и крупной новой
  • Бейдж «BLACK FRIDAY» на главном фото
  • Таблица сравнения: «Этот товар vs аналоги — почему мы дешевле»

Новый год / Рождество:

  • Товар в подарочной упаковке с бантом
  • Lifestyle-фото: товар под ёлкой, в руках улыбающегося человека
  • Инфографика «Идеальный подарок для [мамы/папы/подруги]»

8 марта:

  • Цветочные элементы на фото, мягкие пастельные оттенки
  • Бейдж «Подарок для неё»
  • Набор: основной товар + комплементарный аксессуар

Школьный базар (сентябрь):

  • Контекст «обратно в школу/офис»
  • Инфографика с чек-листом: «Собери набор к школе»
  • Фото в интерьере детской или рабочего стола

Тарификация промо-пакетов

ПакетСоставСтоимостьСрокДля кого
ЭкспрессОверлеи + SEO25 000-35 000 руб1 неделяPromo Score 60-79 (нужна доработка)
СтандартФото + инфографика + SEO50 000-70 000 руб2-3 неделиPromo Score 40-59 (серьёзные пробелы)
ПолныйПересъёмка + инфографика + SEO + видео80 000-120 000 руб4-6 недельPromo Score < 40 (не готов)
VIPПолный + стратегия ценообразования + мониторинг150 000-200 000 руб4-6 недельТоп-клиенты с 50+ SKU

Годовой промо-календарь для клиента

Fotofactor составляет клиенту персональный промо-план на год — когда и что обновлять:

gantt
title Промо-календарь клиента на 2026 год
dateFormat YYYY-MM-DD
axisFormat %b

section Подготовка контента
К 23 февраля :prep1, 2026-01-10, 2026-02-01
К 8 марта :prep2, 2026-01-20, 2026-02-15
К летнему сезону :prep3, 2026-04-15, 2026-05-15
К школьному базару :prep4, 2026-07-01, 2026-08-01
К 11.11 :prep5, 2026-09-20, 2026-10-25
К Чёрной пятнице :prep6, 2026-10-01, 2026-11-08
К Новому году :prep7, 2026-10-15, 2026-11-15

section Промо-события
23 февраля :milestone, m1, 2026-02-23, 0d
8 марта :milestone, m2, 2026-03-08, 0d
Летний сезон :peak1, 2026-06-01, 2026-07-31
Школьный базар :peak2, 2026-08-15, 2026-09-05
11.11 :milestone, m3, 2026-11-11, 0d
Чёрная пятница :crit, bf, 2026-11-22, 5d
Новогодний пик :crit, ny, 2026-12-01, 2026-12-31

section Замеры результатов
Результат 8 марта :mon1, 2026-03-15, 7d
Результат лета :mon2, 2026-08-01, 7d
Результат 11.11 :mon3, 2026-11-18, 7d
Результат ЧП + НГ :mon4, 2027-01-05, 10d

Предпродажная коммуникация

Шаблон outreach за 6 недель до промо:

Добрый день! Через 6 недель — Чёрная пятница, главная распродажа года на WB.

📊 Мы проанализировали вашу категорию:
- В прошлом ноябре трафик вырос в 5.2 раза
- Из ТОП-100 конкурентов 34% уже обновили контент
- Ваш Promo Readiness Score: 25/100 (не готов к промо)

⚠️ Проблемы карточки:
- 5 фото (у конкурентов в среднем 10)
- Нет видео (63% ТОП-10 уже с видео)
- Нет промо-ключевиков в заголовке
- Инфографика без акционных элементов

💡 Промо-пакет «Стандарт» за 4 недели:
— Промо-фотосъёмка (10 фото с праздничным контекстом)
— Акционная инфографика (5 слайдов: скидка, сравнение, набор)
— SEO-обновление под промо-запросы
— Стоимость: 60 000 руб

📅 Дедлайн загрузки контента: 8 ноября (за 2 недели до промо).
Начать нужно сейчас.

Отправить персональный промо-аудит с разбивкой по каждой карточке?

Результат для клиента

Эффект подготовленного контента в промо

МетрикаБез подготовки (только скидка)С промо-контентомРазница
Рост продаж в промоx1.5-2x3-5В 2-3 раза больше
Конверсия карточки3-5% (обычная)8-15% (промо-контент)+100-200%
Позиция в промоПадает (конкуренция)Растёт (свежий контент = буст)+20-50 позиций
Средний чекНиже (глубокая скидка)Выше (меньшая скидка + ценность контента)+15-25% маржи
Остаточный эффектВозврат к базе через 3 дняРост сохраняется 2-4 недели после промоДолгосрочный эффект

Экономика для клиента

Пример: селлер сумок, 10 SKU, средняя цена 5 000 руб, обычные продажи 50 шт/день.

СценарийПродажи в промо (7 дней)ВыручкаЗатраты на контентЧистый доп. доход
Без подготовки (скидка -25%)100 шт/день × 7 = 700 шт2 625 000 руб0 руб
С промо-пакетом (скидка -15%)200 шт/день × 7 = 1400 шт5 950 000 руб60 000 руб+3 265 000 руб

ROI промо-пакета: 5 342% — каждый вложенный рубль в промо-контент приносит 54 рубля дополнительной выручки.

Экономика для Fotofactor

ПоказательЗначение
Средний чек промо-пакета50 000-100 000 руб
Промо-событий в году4-6
Выручка с одного клиента200 000-600 000 руб/год (только промо)
Маржинальность промо-пакета60-70% (шаблоны переиспользуются)
Конверсия в годовую подписку40%+ (клиент видит результат → подписывается на сезонный план)
Urgency как двигатель продаж

Промо-пакеты — это продукт с встроенным дедлайном. Клиенту не нужно объяснять «зачем» — промо через 4 недели, контент нужен сейчас. Это снимает главное возражение: «подумаю и вернусь». Думать некогда — Чёрная пятница не ждёт.

Шаблон urgency-сообщения:

«До Чёрной пятницы 28 дней. Последний день приёма заказов на промо-контент — через 7 дней. После этого мы физически не успеем сделать и загрузить контент до дедлайна WB. Места ограничены — студия может взять 8 клиентов в этот период.»

Этот формат даёт Fotofactor 4-6 волн продаж в год, привязанных к конкретным датам. Не нужно убеждать — нужно напомнить о дедлайне.

Полный скрипт

Минимальный рабочий пайплайн: от анализа трендов до расчёта Promo Readiness Score.

import httpx
import json
import statistics
from datetime import datetime, timedelta

TOKEN = "YOUR_MPSTATS_TOKEN"
BASE_URL = "https://mpstats.io/api"
HEADERS = {
"X-Mpstats-TOKEN": TOKEN,
"Content-Type": "application/json",
}


def fetch_trends(category_path: str) -> list[float]:
"""72-месячные тренды категории."""
resp = httpx.get(
f"{BASE_URL}/wb/get/category/trends",
headers=HEADERS,
params={"path": category_path},
timeout=30,
)
resp.raise_for_status()
return resp.json()


def fetch_top_products(category_path: str, d1: str, d2: str) -> list[dict]:
"""ТОП-100 товаров в категории."""
resp = httpx.post(
f"{BASE_URL}/wb/get/category",
headers=HEADERS,
json={
"path": category_path,
"d1": d1,
"d2": d2,
"startRow": 0,
"endRow": 100,
},
timeout=30,
)
resp.raise_for_status()
return resp.json().get("data", [])


def analyze_promo_peaks(trends_72: list[float]) -> list[dict]:
"""Находит промо-пики в историческом тренде."""
months = [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь",
]
years = [
trends_72[y * 12 : (y + 1) * 12]
for y in range(6)
if sum(trends_72[y * 12 : (y + 1) * 12]) > 0
]

if not years:
return []

medians = []
for m in range(12):
vals = [y[m] for y in years if y[m] > 0]
medians.append(statistics.median(vals) if vals else 0)

avg = statistics.mean(medians) or 1

return [
{
"month": months[i],
"uplift": round(medians[i] / avg, 2),
"is_peak": medians[i] / avg > 1.3,
}
for i in range(12)
]


def calculate_readiness(product: dict, avg_pics: float) -> dict:
"""Рассчитывает Promo Readiness Score для одной карточки."""
# Свежесть контента (30%)
pics = product.get("picscount", 0)
video = product.get("hasvideo", 0)
content = min(100, (pics / max(avg_pics, 1)) * 60 + (40 if video else 0))

# Сезонная релевантность (25%)
seasonal = min(100, (40 if pics >= 10 else 0) + (30 if video else 0)
+ (30 if product.get("has3d", 0) else 0))

# SEO (20%)
name = (product.get("name", "") or "").lower()
promo_kw = ["подарок", "скидка", "набор", "акция", "sale", "промо"]
seo = min(100, sum(1 for kw in promo_kw if kw in name) * 35)

# Сток (15%)
stock = 60 if product.get("sales", 0) > 0 else 20

# Цена (10%)
sale = product.get("basic_sale", 0)
price = 100 if sale >= 30 else 75 if sale >= 20 else 50 if sale >= 10 else 25

total = (content * 0.30 + seasonal * 0.25 + seo * 0.20
+ stock * 0.15 + price * 0.10)

if total >= 80:
verdict, emoji = "READY", "🟢"
elif total >= 60:
verdict, emoji = "PARTIALLY", "🟡"
elif total >= 40:
verdict, emoji = "WEAK", "🟠"
else:
verdict, emoji = "NOT_READY", "🔴"

return {
"sku": product.get("id"),
"name": (product.get("name", "") or "")[:50],
"score": round(total),
"verdict": verdict,
"emoji": emoji,
"breakdown": {
"content_freshness": round(content),
"seasonal_relevance": round(seasonal),
"promo_seo": round(seo),
"stock_readiness": stock,
"price_competitiveness": price,
},
}


# --- Основной пайплайн ---
if __name__ == "__main__":
CATEGORY = "Аксессуары/Сумки/Женские сумки"
CLIENT_SKU = 12345678

today = datetime.now()
d1 = (today - timedelta(days=30)).strftime("%Y-%m-%d")
d2 = today.strftime("%Y-%m-%d")

# 1. Анализ промо-пиков
print("=== ПРОМО-ПИКИ КАТЕГОРИИ ===")
trends = fetch_trends(CATEGORY)
peaks = analyze_promo_peaks(trends)
for p in peaks:
bar = "█" * int(p["uplift"] * 10)
marker = " ** ПРОМО **" if p["is_peak"] else ""
print(f"{p['month']:>10} | x{p['uplift']:.2f} | {bar}{marker}")

# 2. Аудит конкурентов
print("\n=== АУДИТ КОНКУРЕНТОВ ===")
products = fetch_top_products(CATEGORY, d1, d2)
avg_pics = sum(p.get("picscount", 0) for p in products) / len(products) if products else 8
prepared = sum(
1 for p in products
if (p.get("picscount", 0) > avg_pics + 3)
and p.get("hasvideo", 0) == 1
)
print(f"ТОП-100: {prepared}% уже готовятся к промо")
print(f"Среднее кол-во фото в ТОПе: {avg_pics:.1f}")

# 3. Promo Readiness Score клиента
print("\n=== PROMO READINESS SCORE ===")
client = next((p for p in products if p.get("id") == CLIENT_SKU), None)

if client:
readiness = calculate_readiness(client, avg_pics)
print(f"SKU: {readiness['sku']}")
print(f"Score: {readiness['emoji']} {readiness['score']}/100 ({readiness['verdict']})")
print(f"Breakdown:")
for k, v in readiness["breakdown"].items():
print(f" {k}: {v}/100")
else:
print(f"SKU {CLIENT_SKU} не найден в ТОП-100. Используйте GET /wb/get/item для прямого запроса.")

# 4. Сохранение
output = {
"category": CATEGORY,
"client_sku": CLIENT_SKU,
"promo_peaks": peaks,
"competitor_audit": {
"total": len(products),
"prepared_pct": prepared,
"avg_pics": round(avg_pics, 1),
},
"client_readiness": readiness if client else None,
"generated_at": datetime.now().isoformat(),
}

filename = f"promo_readiness_{CLIENT_SKU}_{today:%Y%m%d}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(output, f, ensure_ascii=False, indent=2)
print(f"\nСохранено: {filename}")

Что дальше