Skip to content

Harvester Yield — формула EV (SSOT) ​

Эта страница — единственный источник истины формулы EV Harvester. Правила проекта (CLAUDE.md §«EV validation») требуют прочитать эту страницу перед любым утверждением о доходности стратегии или решением о тюнинге параметра.

Реализация — crates/pm-analytics/src/harvester_yield.rs. Исторический источник вывода формулы — docs/Harvester Yield Calculator v2.pdf; PDF остаётся в репозитории как архивный файл, но SSOT-статус несёт эта страница: при расхождении страницы и кода верна реализация (правило проекта «Проверяемость: никакой интуиции» — код первичен), а страница правится вслед за ним.

Модель ​

Позиция Harvester входит на стороне фаворита по средней цене p_e, продаёт выигравшие по take-profit exit_price (боевой Harvester держит GTC ≈ 0.99 — НЕ досиживает до резолюции $1.00) и режет проигравшие либо стопом (реализуя s_eff), либо досиживая до $0. r — эффективность стопа: доля проигрышей, которую стоп реально успевает срезать, а не отдаёт резолюции.

E[PnL/share] = w·(exit_price − p_e) − (1 − w)·(p_e − r·s_eff)
w*           = (p_e − r·s_eff) / (exit_price − r·s_eff)          (E = 0)

Стратегия +EV ⟺ w > w* (edge_margin = w − w* > 0).

При r = 0 знаменатель убытка — полная цена входа, и break-even схлопывается в стену эффективности w* = p_e / exit_price: покупка фаворита по его же цене структурно не несёт края, если не считать реальный перекос (w > p_e/exit_price) или реально режущий левый хвост стоп (r·s_eff, снижающий w*).

GARBAGE-IN. Вердикт — точная арифметика; его ИСТИННОСТЬ целиком зависит от того, что входы измерены на ФАКТИЧЕСКИ реализованных исходах, а не на контрфактуале. w/p_e берутся из realised-закрытий бота (exit_reason, realized_pnl_usd), открытые позиции резолвятся до терминального исхода перед подсчётом (без survivorship — открытые проигравшие обязаны быть учтены), r/s_eff — из реальных стоп-филлов, а не из настроенного порога. Подробности и прецедент фантомных −EV от ride-to-resolution counterfactual — в doc-комментарии модуля harvester_yield.rs:52-70.

Публичный контракт harvester_yield.rs ​

YieldInputs — структура (:113-124) ​

Входная структура, пять полей:

ПолеТипСмысл
wDecimalРеализованный winrate (доля выигравших позиций), ∈ [0, 1]
p_eDecimalСредняя цена входа
rDecimalЭффективность стопа ∈ [0, 1] (0 — досиживать проигравших до $0)
s_effDecimalРеальная цена стоп-филла (не порог)
exit_priceDecimalЦена take-profit выхода (боевой Harvester ≈ 0.99; PDF использует 1.0)

YieldVerdict — структура-результат evaluate (:128-137) ​

Четыре поля результата (не функции — значения):

ПолеТипСмысл
w_starDecimalBreak-even winrate w*
edge_marginDecimalw − w*; положительное значение — +EV
e_pnl_per_shareDecimalОжидаемый PnL на долю при заданном w
positive_evbooledge_margin > 0

YieldInputs::break_even_winrate — метод (:145-152) ​

w* = (p_e − r·s_eff) / (exit_price − r·s_eff). НЕ использует self.w. Возвращает None, если знаменатель exit_price − r·s_eff неположителен (вырожденные входы).

YieldInputs::expected_pnl_per_share — метод (:155-159) ​

E[PnL/share] = w·(exit_price − p_e) − (1 − w)·(p_e − r·s_eff).

YieldInputs::evaluate — метод (:165-174) ​

Строит полный YieldVerdict из двух методов выше: вызывает break_even_winrate (и возвращает None, если он вернул None), считает edge_margin = w − w_star, e_pnl_per_share и positive_ev = edge_margin > 0. Это точка входа формы (а) примеров ниже.

YieldInputs::roi_rate — метод (:188-193) ​

roi_rate = E[PnL/share] / (p_e · t_hold) — доходность капитала на единицу времени удержания. Возвращает None, если p_e ≤ 0 либо t_hold ≤ 0. Не проверяется примерами ниже (форма блока не несёт t_hold); описан здесь как часть контракта.

take_profit_symmetric — свободная функция (:208-222) ​

take_profit_symmetric(w, p_e, g, x, r) -> YieldInputs — строит YieldInputs для режима относительного take-profit: exit_price = p_e·(1 + g), s_eff = p_e·(1 − x). Используется вариантом относительного тейк-профита (подробный расчёт и сравнение с Harvester — docs/strategies/take-profit-yield.md). Не проверяется примерами ниже.

EfficiencyComparison — структура-результат compare_efficiency (:226-233) ​

ПолеТипСмысл
roi_rate_aDecimalroi_rate режима A на его времени удержания
roi_rate_bDecimalroi_rate режима B на его времени удержания
a_more_efficientboolroi_rate_a > roi_rate_b

compare_efficiency — свободная функция (:240-253) ​

compare_efficiency(a: &YieldInputs, t_a, b: &YieldInputs, t_b) -> Option<EfficiencyComparison> — сравнивает капитальную эффективность двух режимов выхода. None, если roi_rate любой из сторон вырожден. Не проверяется примерами ниже.

evaluate_resolved_positions — свободная функция (:268-289) ​

evaluate_resolved_positions(positions: &[(Decimal, bool)], r, s_eff, exit_price) -> Option<(YieldInputs, YieldVerdict)> — агрегирует w и p_e ИЗ positions САМА (доля выигравших и средняя цена входа; не принимает их параметрами), затем вызывает .evaluate() на получившихся YieldInputs. Возвращает None, если positions пуст либо знаменатель вырожден. Это точка входа формы (б) примеров ниже.

Проверяемые примеры (harvester-yield-examples) ​

Единственный fenced-блок ниже с info-string yaml harvester-yield-examples проверяется тестом crates/pm-analytics/tests/harvester_yield_doc_examples.rs. Блок — валидный JSON-массив (метка yaml — формат подсветки для читателя, не требование к парсеру: JSON — валидное подмножество YAML 1.2 для flow-style записи без якорей и тегов). Все поля Decimal — JSON-строки ("0.93", не 0.93): числовой литерал JSON прошёл бы через f64 и терял бы значащие цифры, строка этого избегает.

Записи двух форм:

  • (а) одиночный вход — {name, inputs: {w, p_e, r, s_eff, exit_price}, expect}, прогоняется через YieldInputs::evaluate().
  • (б) агрегат — {name, positions: [[price, won], …], r, s_eff, exit_price, expect}, прогоняется через evaluate_resolved_positions(...).

expect — либо объект {w_star, edge_margin, e_pnl_per_share, positive_ev}, либо null для вырожденного случая (знаменатель exit_price − r·s_eff ≤ 0).

Правило сравнения. Тест вызывает ТУ ЖЕ функцию (evaluate / evaluate_resolved_positions) на тех же входах и сравнивает результат со значением expect точным равенством Decimal (==). Допуск не нужен и не применяется: это не сверка с независимо вычисленным оракулом, а проверка того, что страница описывает РЕАЛЬНОЕ поведение реализации на конкретных входах. Расхождение хотя бы одной записи проваливает тест с указанием имени записи (name) и разошедшегося поля.

Правки формулы вносятся в harvester_yield.rs, ЗАТЕМ страница и блок ниже приводятся в соответствие — иначе тест немедленно падает.

yaml
[
  {
    "name": "efficiency_wall_positive_edge",
    "inputs": {
      "w": "0.93",
      "p_e": "0.891",
      "r": "0",
      "s_eff": "0",
      "exit_price": "0.99"
    },
    "expect": {
      "w_star": "0.9",
      "edge_margin": "0.03",
      "e_pnl_per_share": "0.0297",
      "positive_ev": true
    }
  },
  {
    "name": "high_entry_sub_breakeven_negative_edge",
    "inputs": {
      "w": "0.90",
      "p_e": "0.9405",
      "r": "0",
      "s_eff": "0",
      "exit_price": "0.99"
    },
    "expect": {
      "w_star": "0.95",
      "edge_margin": "-0.05",
      "e_pnl_per_share": "-0.0495",
      "positive_ev": false
    }
  },
  {
    "name": "degenerate_denominator_stop_saturates_exit",
    "inputs": {
      "w": "0.5",
      "p_e": "0.5",
      "r": "1.00",
      "s_eff": "1.00",
      "exit_price": "0.99"
    },
    "expect": null
  },
  {
    "name": "aggregate_two_entry_prices_negative_edge",
    "positions": [
      ["0.90", true],
      ["0.90", true],
      ["0.981", false],
      ["0.981", false]
    ],
    "r": "0",
    "s_eff": "0",
    "exit_price": "0.99",
    "expect": {
      "w_star": "0.95",
      "edge_margin": "-0.45",
      "e_pnl_per_share": "-0.4455",
      "positive_ev": false
    }
  }
]