Тема
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)
Входная структура, пять полей:
| Поле | Тип | Смысл |
|---|---|---|
w | Decimal | Реализованный winrate (доля выигравших позиций), ∈ [0, 1] |
p_e | Decimal | Средняя цена входа |
r | Decimal | Эффективность стопа ∈ [0, 1] (0 — досиживать проигравших до $0) |
s_eff | Decimal | Реальная цена стоп-филла (не порог) |
exit_price | Decimal | Цена take-profit выхода (боевой Harvester ≈ 0.99; PDF использует 1.0) |
YieldVerdict — структура-результат evaluate (:128-137)
Четыре поля результата (не функции — значения):
| Поле | Тип | Смысл |
|---|---|---|
w_star | Decimal | Break-even winrate w* |
edge_margin | Decimal | w − w*; положительное значение — +EV |
e_pnl_per_share | Decimal | Ожидаемый PnL на долю при заданном w |
positive_ev | bool | edge_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_a | Decimal | roi_rate режима A на его времени удержания |
roi_rate_b | Decimal | roi_rate режима B на его времени удержания |
a_more_efficient | bool | roi_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
}
}
]