← Back

Counter-trading (Поликопи) — Chunk B: инверсия стороны в демоне (money-path)

Статус: СПЕКА НА РЕВЬЮ. Код в money-path (copy-loop-mu.mjs, реальные деньги) НЕ написан/НЕ задеплоен. Деплой только после ОК Rick'а по процедуре §6. Предшествует: Chunk A 8b47b17GET /api/copy/market-tokens (резолв комплементарного токена). Уже в проде на Malaysia, поведение не меняет.

1. Механика (locked-дизайн 23 Jun)

Лидер покупает YES @ p → мы (fade) покупаем NO @ (1-p). Шортов на Polymarket нет → fade = покупка комплементарного outcome-токена того же бинарного рынка.

2. Конфиг (новые поля, pass-through в config JSON — Rust-валидация не нужна)

copy_subscriptions.config уже хранится как свободный JSON и проходит насквозь в демон. Добавляем в deriveCfg(c):

direction = (c.direction === 'fade') ? 'fade' : 'copy'   // дефолт copy → нулевая регрессия
fadeMin   = c.fadeMin != null ? Number(c.fadeMin) : 0.20
fadeMax   = c.fadeMax != null ? Number(c.fadeMax) : 0.80

Возврат deriveCfg дополняется { direction, fadeMin, fadeMax }. ⚠️ Существующее cfg.mode = режим АЛЛОКАЦИИ (fixed/percent/proportional), НЕ путать — поэтому новое поле зовётся direction, не mode.

3. Четыре точки правки демона (copy-loop-mu.mjs)

3.1 Хелпер резолва комплемента (новый, ~12 строк)

MARKET_TOKENS_URL = env, дефолт SUBS_URL.replace('/api/copy/active','/api/copy/market-tokens').

resolveComplement(conditionId, leaderAsset):
  GET {MARKET_TOKENS_URL}?cond=<conditionId>  (x-service-token)
  tokens = data.tokens.map(String)
  return tokens.find(id => id !== String(leaderAsset))  // комплемент = id != токена лидера

ERC1155-id комплемента не выводится арифметически → только lookup (chunk A эндпоинт).

3.2 BUY-ветка handleTradeForUser (fade-инверсия перед decideCopy)

Если cfg.direction === 'fade':

  1. Risk-cap по цене лидера: if (px < cfg.fadeMin || px > cfg.fadeMax) → skip (лог).
  2. Бинарность: const oc = String(t.outcome||'').toLowerCase(); if (oc!=='yes' && oc!=='no') → skip (non-binary).
  3. Комплемент: comp = await resolveComplement(t.conditionId, t.asset); if (!comp) → skip (лог fade: complement unresolved).
  4. Синтез fade-трейда для общего пайплайна:
    tf = { ...t, asset: comp, price: +(1 - px).toFixed(4),
           outcome: (oc === 'yes' ? 'No' : 'Yes') }
    
    Далее decideCopy/sizing/caps/exec.buy идут по tf БЕЗ изменений (size считается от 1-px).
  5. Запись в ledger (rec) добавляет:
    • direction: 'fade'
    • leaderAsset: t.asset ← оригинальный токен лидера (нужен для выхода, см. 3.3/3.4)
    • tokenId: comp, outcome: tf.outcome, leaderPrice: tf.price, marketId: t.conditionId copy-позиции: direction:'copy', leaderAsset = тот же tokenId (инвариант).

3.3 SELL-ветка (зеркальный выход) — РЕАЛ-ТАЙМ событие лидера

Сейчас: held = u.ledger.get(t.asset) — ключ по токену лидера. Для fade наш ключ = комплемент → не найдётся. Правка: если прямого held нет, искать fade-позицию по оригиналу лидера:

held = u.ledger.get(t.asset)
     || [...u.ledger.values()].find(p => p.direction==='fade'
            && String(p.leaderAsset)===String(t.asset)
            && String(p.leader).toLowerCase()===leader
            && p.status==='open')

Закрываем exec.sellAll({ tokenID: held.tokenId, price: 1 - Number(t.price) }) (наш комплемент по инвертированной цене). Остальной close-флоу без изменений.

3.4 ⚠️ reconcile-петля (reconcileUser, money-критично) — ФИКС ЛОЖНОГО ВЫХОДА

Строка ~251-254 сейчас: if (!lh.has(tid)) leaderExit. Для fade tid=комплемент, лидер его НЕ держит → ложный выход каждый цикл → мгновенное закрытие fade-позиции. Фикс:

const leaderTok = p.leaderAsset ? String(p.leaderAsset) : tid;  // fade сверяет ОРИГИНАЛ лидера
if (!lh.has(leaderTok)) { ch.leaderExit = true; ch.exitPrice = curPrice.get(tid) || 0; }
else ch.leaderStillIn = true;

tid (наш токен) по-прежнему используется для mark-PnL/баланса — меняется ТОЛЬКО ключ сверки наличия у лидера.

4. БД: 2 новые колонки в copy_positions (Rust + миграция)

Персист нужен, чтобы fade-позиции пережили рестарт демона (иначе leaderAsset/direction теряются → reconcile снова ложно закроет).

5. Edge-cases / риски

6. Процедура деплоя (money-path, ТОЛЬКО после ОК)

  1. Бэкап: scp malaysia:/root/poly-exec/copy-loop-mu.mjs ~/openclaw-backups/copy-loop-mu-<дата>.mjs.
  2. Правки локально в _impl/daemon-ref/node --check copy-loop-mu.mjs.
  3. БД-миграция на Malaysia (ALTER + бэкфилл) ДО рестарта демона.
  4. Rust API: market-tokens уже в проде; пересборка нужна только под §4 колонки → cargo build --release на Malaysia → pm2 restart polymarket-screener (проверить restarts=0).
  5. Демон: DRY-RUN сначала — выкатить с LIVE off / тестовый FOLLOWER allow-list, прогнать 🟢 WOULD COPY (fade) логи на реальных трейдах лидера, проверить что комплемент-токен и 1-px верные.
  6. Только потом LIVE: тихий рестарт по правилу (inline-env, pm2 save), мониторить банк-логи (✅ COPIED/closed … pnl≈$X).

7. Критерий успеха

📜 Git History

3fcc806docs(copy): chunk B spec — daemon fade-inversion (money-path, review-first)11 days ago
Show last diff
Loading...