Ошибка в алгоритме проверки по заданному порогу плюс ранний выход

Делаю алгоритм расчета цены по данным из апи. Проблема в том, что сейчас часть цикла проверки идет в холостую, пока не кончатся попытки. Вот текущая основная структура:

  1. Получили список задач (артикулов) для изменения.
  2. Запросили по первой задачи данные, рассчитали.
  3. Если значение из апи не соответствует требуемому, то пересчитываем и отправляем обратно в апи новое значение. 4.Через паузу делаем запрос-проверку на соответствие. Если значения соответствуют, то идем к следующей задаче и так пока задачи не кончатся.

Проблема (детальнее будет видно в логе). Если сразу не совпало и пришлось отправить коррекцию в апи, то нет сразу срабатывания при совпадении - код работает пока есть попытки для отправки из конфига. И на последней попытке видит совпадение и выходит, не переходя к следующей задаче.

вот так выглядит это в логе:

  1. 341492329: price=3318, product=1519.0, SPP=37%, strat=min_price_all, min_comp=1131.0, limit=2400, calc_price=2460 → final=2460 - ### поясню: тут поучили из апи price=3318, посчитали, что должно быть calc_price=2460
  2. Отправляем новый price=2460 для 341492329 в API...
  3. пауза 6.5 # первая и корректная пауза перед отправкой запроса на проверку
  4. Retest 341492329 : price: 2460, product: 1126.0, SPP: 37%, min_price_all, min_comp=1131.0, limit: 2400 # Тут получили новые значения, и видим совпадение (есть допуск +-10) product: 1126.0 и min_comp=1131.0 - должен быть переход к следующей задаче 5. пауза 6.5 # но получаем паузу, снова цикл запроса/проверки и так столько, сколько задали таких попыток в конфиге WB_VERIFY_ATTEMPTS = 5 - не могу найти ошибку, как переходить дальше
  5. ? Достигнут целевой product стратегии: наш product=1126.00 vs target=1131.00 (tol=±10.0 ₽) — останавливаемся. # на финале попыток все-таки обнаруживаем соответствие, но бросаем - некорректный brake и не переходим дальше - тут тоже ошибка, которую не могу исправить. Но если в цикл ретеста не попали, например сразу было соответствие product=1126.00 vs target=1131.00 или попали в ограничение, ниже которого нельзя опускаться, то нормально идем дальше по всем задачам. Т.е. проблема в цикле ретеста.

если нужно вот весь код модуля https://github.com/vvalentin-dot/bot/blob/main/wb_runner.py

и код блока ретеста:

   # РЕТЕСТ: ждём реальный product
            # Трекинг «последней цены для прошлого SPP» (нужен для точного лога good_price)
            if not hasattr(state, "last_price_for_spp"):
                state.last_price_for_spp = int(price_now)  # цена, при которой считался прошлый spp
                state.last_spp = int(spp)

            changed_attempt = 0 
            while changed_attempt < changed_attempts_max: #
                # 1) сначала ждём изменения product, не сжигая основную попытку
                unchanged_try = 0 
                product2 = None 
                comp_prices2 = None 

                while unchanged_try < unchanged_max:
                    _LG_MODULE.info(f"пауза {sleep_s}")
                    await asyncio.sleep(sleep_s)

                    # заново читаем карточку и конкурентов
                    p2, name2, supplier2, c_prices2, c_names2, c_supp2 = await fetch_sale_product(
                        session,
                        nmID,
                        [
                            item.competitor_nmID_1,
                            item.competitor_nmID_2,
                            item.competitor_nmID_3,
                            item.competitor_nmID_4,
                            item.competitor_nmID_5,
                        ],
                    )
                    product2 = p2
                    comp_prices2 = c_prices2

                    if float(product2 or 0) == float(prev_product or 0):
                        unchanged_try += 1
                        if unchanged_try >= unchanged_max:
                            _LG_MODULE.info(
                                "⛔ Ретест: product не изменился в течение %s повторов ожидания — считаем основную попытку израсходованной.",
                                unchanged_max
                            )
                            break
                        else:
                            _LG_MODULE.info(
                                "⏳ product не изменился после POST — повтор запроса без пересчёта (%s/%s).",
                                unchanged_try, unchanged_max
                            )
                            continue
                    else:
                        # есть новое значение product — переходим к перерасчёту
                        break

                changed_attempt += 1
                is_last_changed_attempt = (changed_attempt >= changed_attempts_max)

                # 2) пересчитали SPP и целевую цену на основе НОВОГО product
                spp2 = calc_spp(product2, final_price, sv.discount)

                # рассчитываем актуальный минимум конкурента по свежему списку comp_prices2
                comp_min_now = None
                if comp_prices2:
                    comp_min_now = min([c for c in comp_prices2 if c is not None], default=None)

                _LG_MODULE.info(
                    f"Retest {nmID} : price: {final_price}, РРЦ: {sv.rrc}, product: {product2}, SPP: {spp2}%, "
                    f"{strategy}, минимальная цена конкурента: {comp_min_now if comp_min_now is not None else '—'}, "
                    f"limit: {sv.limit_rub}, wb_buyer_product: {sv.wb_buyer_product}"
                )



            # --- захват «цены-срыва» (edge_bad_price): переход с нормального SPP к очень малому ---
            if not hasattr(state, "edge_bad_price"):
                setattr(state, "edge_bad_price", None)

            try:
                drop_pp = int(spp) - int(spp2)  # насколько упал SPP на этом шаге

                if state.edge_bad_price is None and drop_pp >= int(EDGE_DROP_PP):
                    # цена-срыв = текущая final_price, «последняя хорошая» = цена и SPP прошлого шага
                    state.edge_bad_price = int(final_price)
                    state.last_good_price = int(getattr(state, "last_price_for_spp", final_price))
                    state.last_good_spp = int(getattr(state, "last_spp", spp))

                    _LG_MODULE.info(
                        "? Срыв SPP: good_price=%s (SPP≈%s%%) → bad_price=%s (SPP≈%s%%), падение на %s п.п.",
                        state.last_good_price, state.last_good_spp, state.edge_bad_price, int(spp2), drop_pp
                    )
            except Exception as _e:

                _LG_MODULE.debug(f"edge_bad_price capture skipped: {_e}")

            # >>> PATCH: use _hit_target() in universal early-exit (BEGIN)
            # --- Универсальный ранний выход по целевому product стратегии ---
            try:
                pv2 = _pack_prices(p)
                pv2.product = float(product2)
                pv2.comp_min = min([c for c in (comp_prices2 or []) if c is not None], default=None)

                product_target = get_target_product(strategy, sv, pv2)
                if product_target is not None and product2 is not None:
                    if _hit_target(strategy, float(product2), float(product_target), float(WB_TOLERANCE_RUB)):
                        _LG_MODULE.info(
                            "? Достигнут целевой product стратегии: наш product=%.2f vs target=%.2f (tol=±%.1f ₽) — останавливаемся.",
                            float(product2), float(product_target), float(WB_TOLERANCE_RUB)
                        )
                        ok = True
                        break
            except Exception as _e:
                _LG_MODULE.debug(f"target-product early-exit check skipped: {_e}")
            # <<< PATCH: use _hit_target() in universal early-exit (END)

            # >>> PATCH: correct min-competitor exit condition (BEGIN)
            # --- ранний выход: наш product уже не выше минимального конкурента ---
            try:
                comp_min_now = None
                if comp_prices2:
                    comp_min_now = min([c for c in comp_prices2 if c is not None], default=None)
                if comp_min_now is not None and product2 is not None:
                    # для min_price-стратегий достаточно быть НЕ ВЫШЕ конкурента с гистерезисом tol
                    if float(product2) <= float(comp_min_now) + float(WB_TOLERANCE_RUB):
                        _LG_MODULE.info(
                            "? Достигнут минимум конкурента: наш product=%.2f ≤ comp_min=%.2f (+tol %.1f) — останавливаемся.",
                            float(product2), float(comp_min_now), float(WB_TOLERANCE_RUB)
                        )
                        ok = True
                        break
            except Exception as _e:
                _LG_MODULE.debug(f"comp_min early-exit check skipped: {_e}")
            # <<< PATCH: correct min-competitor exit condition (END)



            # обновляем «последнюю цену для текущего SPP» — теперь базой станет этот шаг
            try:
                state.last_price_for_spp = int(final_price)
                state.last_spp = int(spp2)
            except Exception:
                pass

            pv2 = _pack_prices(p)
            pv2.product = float(product2)
            pv2.comp_min = min([c for c in (comp_prices2 or []) if c is not None], default=None)

            calc2, final2_ideal, meta2 = compute_price_details(
                strategy, sv, pv2, int(spp2), category_discounts=cat_discounts
            )
            target2 = int(max(int(calc2), int(sv.limit_rub or 0)))

            # 3) лимит-пол + SPP не изменился → ранний выход
            if int(sv.limit_rub or 0) > 0 and int(final_price) == int(sv.limit_rub) and int(spp2) == int(spp):
                _LG_MODULE.info("? limit_rub активен и SPP не изменился — переходим к следующему артикулу.")
                ok = True
                break

            # 4) обновляем историю для детекции «пилы»
            state.note(price=int(final_price), spp=int(spp), product=float(prev_product or 0.0))

            # 5) адаптивное решение следующей цены
            next_price, reason = next_price_adaptive(
                price_now=int(final_price),
                target_price=int(target2),
                spp_prev=int(spp),
                spp_now=int(spp2),
                discount_pct=float(sv.discount or 0),
                comp_min=float(pv2.comp_min or 0),
                limit_floor=int(sv.limit_rub or 0),
                state=state,
            )
            _LG_MODULE.info(
                "? Новая рассчитанная цена (адаптивно): %s [%s]; edge_bad_price=%s; good_price=%s (SPP=%s%%)",
                next_price, reason,
                (getattr(state, 'edge_bad_price', None) if hasattr(state, 'edge_bad_price') else '—'),
                (getattr(state, 'good_price', None) if hasattr(state, 'good_price') else '—'),
                (getattr(state, 'good_spp', None) if hasattr(state, 'good_spp') else '—'),
            )

            # === Ранний выход: не даём прыгать обратно на цену-срыва
            try:
                if getattr(state, "edge_bad_price", None) is not None and int(next_price) == int(
                        state.edge_bad_price):
                    _LG_MODULE.info(
                        "? Кандидат равен захваченному price срыва %s — выходим. "
                        "Минимальная стабильная цена найдена на предыдущем шаге.",
                        state.edge_bad_price
                    )
                    ok = True
            except Exception as _e:
                _LG_MODULE.debug(f"edge_bad_price guard skipped: {_e}")

            if int(next_price) != int(final_price):
                if int(next_price) <= 0:
                    _LG_MODULE.error(f"? Отказ от отправки некорректного price={next_price} (≤ 0)")
                    break
                _LG_MODULE.info(f"? Отправляем новый price={next_price} для товара {nmID} в WB API...")
                await limiter.wait()
                await update_wb_price(item.nmID, int(next_price))
                final_price = int(next_price)
                prev_product = float(product2)
                # остаёмся в ретесте — ждём следующий реальный product
                continue

            # 6) next_price == final_price → проверяем, выполнена ли стратегия («минимальная, но держит»)
            if is_strategy_done(
                    price_now=int(final_price),
                    spp_now=int(spp2),
                    discount_pct=float(sv.discount or 0),
                    comp_min=float(pv2.comp_min or 0),
                    limit_floor=int(sv.limit_rub or 0),
                    state=state,
            ):
                _LG_MODULE.info(
                    "? Стратегия выполнена: price=%s удерживает target=%s; следующий шаг вниз ломает цель.",
                    final_price, (pv2.comp_min if pv2.comp_min is not None else "—"),
                )
                ok = True
                break

            # 7) микродельты — выходим (или продолжаем до исчерпания «основных» попыток)
            spp_delta = abs(float(spp2) - float(spp))
            prod_delta = abs(float(product2 or 0) - float(prev_product or 0))
            if spp_delta < 1.0 and prod_delta < 1.0:
                if is_last_changed_attempt:
                    _LG_MODULE.info("⛔ Ретест: попытки исчерпаны. Изменений нет (ΔSPP<1 п.п., Δproduct<1 ₽).")
                else:
                    _LG_MODULE.info(
                        "? Изменений нет (ΔSPП<1 п.п., Δproduct<1 ₽) — продолжаем до исчерпания попыток.")
                ok = True
                break

            # 8) не достигли цели — идём на следующую «основную» попытку
            prev_product = float(product2)
            spp = int(spp2)
            #continue  # ← это последний оператор ВНУТРИ while

            # ←← ВЫХОД ИЗ while changed_attempt < changed_attempts_max

            # финальные логи ретеста (один раз после выхода из while)
            if ok:
                _LG_MODULE.info(f"✅ Цена {final_price} для товара {item.nmID} успешно обновлена в Wildberries")
            else:
                _LG_MODULE.info(
                    f"⚠️ Не удалось добиться стабильного соответствия за {changed_attempts_max} попыток для {item.nmID}"
                )

            # переходим к следующему артикулу
            continue



    else:
        # ⬇️ Равенство final vs price_now: корректно логируем «совпало из-за лимита»
        if (meta.get("limit_hit") and int(final_price) == int(sv.limit_rub)) or (
                int(final_price) == int(sv.limit_rub) and int(calc_price) != int(final_price)
        ):
            _LG_MODULE.info(
                "⚠️ Цена %s достигла лимита: calc=%s → final=%s (limit=%s). "
                "Отправка не требуется, стратегия ограничена нижней границей.",
                item.nmID, calc_price, final_price, sv.limit_rub
            )
        else:
            _LG_MODULE.info(f"✅ Цена {item.nmID} уже соответствует стратегии — отправка не требуется.")

Помимо основного блока который описал через логи, есть в ретесте дополнительные условия, но они сейчас не актуальны, поэтому не описываю детально.

Если нужно, то в кратце - значение цены может резко улететь вверх и тогда нужно снижать его к неопределенному порогу небольшими шагами, пока вновь не сорвется. Определить место срыва и остановится на нем.


Ответы (0 шт):