docs(sec): SEC-9 закрыт — возврат залога при ничьей

Сводка: открыто 8 (было 9), закрыто 8. SEC-9 перенесён в закрытые с полной
историей: причина (close_bet.rs:66), замер доли ничьих (33.6%), решение
владельца (правка контракта), состав правок, собственная мутационная приёмка,
деплой и живой эффект (залипшие #0/#1 закрылись сами, ваулт −1000).
Хвост SEC-8 о «вечном залипании» помечен как устранённый.

Инструменты приёмки:
- tie-refund-accept.cjs — ставки циклом, проверка статуса на цепи
  (живой прогон: 6 возвратов из 25 ставок)
- tie-distribution.cjs, policy-economics.cjs — выбор политики ничьей
- vault-check.cjs — разбор Bet-аккаунтов сырыми байтами
This commit is contained in:
2026-09-14 02:19:28 +03:00
parent 6ee30c39fb
commit b8a1f8dc39
5 changed files with 241 additions and 5 deletions
+48
View File
@@ -0,0 +1,48 @@
// Экономика: что делает каждая политика ничьей с балансом дома.
// Считаем по реальному распределению |Δ| из живой истории.
// Ставка 1 фантик, выигрыш 1.3× (multiplier_bps=13000). Ничья: возврат 1.0 (залог целиком).
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(process.env.DB || '/app/data/price-history.db');
const rows = db.prepare('SELECT ts, price FROM price_history ORDER BY ts ASC').all();
const vals = rows.map((r) => ({ ts: Number(r.ts), p: Number(r.price) }));
const SCALE = 1e8, W = 15000, STEP = 1000;
const at = (ts) => { let lo = 0, hi = vals.length - 1, res = null;
while (lo <= hi) { const m = (lo + hi) >> 1;
if (vals[m].ts <= ts) { res = vals[m].p; lo = m + 1; } else hi = m - 1; } return res; };
const hist = {};
let n = 0;
for (let t = vals[0].ts; t + W <= vals[vals.length - 1].ts; t += STEP) {
const a = at(t), b = at(t + W);
if (a === null || b === null) continue;
const kop = Math.round(Math.abs(b - a) / SCALE * 100);
hist[kop] = (hist[kop] || 0) + 1; n++;
}
// Считаем деньги. Игрок ставит вслепую (случайный UP/DOWN) => половина окон выигрышная,
// половина проигрышная. На ничьих политика решает.
const MULT = 1.3;
function simulate(policy) {
let vaultDelta = 0, refunded = 0, decided = 0;
for (const [k, cnt] of Object.entries(hist)) {
const kop = Number(k);
if (kop === 0 && policy === 'refund') { vaultDelta += 0; refunded += cnt; continue; }
if (kop === 0 && policy === 'neighbour') { /* трактуем как решение: половина выиграла */
vaultDelta += cnt * (0.5 * (1 - MULT) + 0.5 * 1); decided += cnt; continue; }
if (kop === 0 && policy === 'house') { vaultDelta += cnt * 1; continue; } // дом забирает
decided += cnt;
vaultDelta += cnt * (0.5 * (1 - MULT) + 0.5 * 1);
}
return { vaultDelta, refunded, decided };
}
console.log('окон:', n);
for (const p of ['refund', 'neighbour', 'house']) {
const r = simulate(p);
const per = r.vaultDelta / n;
console.log(`политика=${p.padEnd(10)} дом за окно=${per.toFixed(4)} фантика | возвращено окон=${r.refunded} (${((100*r.refunded)/n).toFixed(1)}%)`);
}
console.log('--- при 1000 ставках ---');
for (const p of ['refund', 'neighbour', 'house']) {
const r = simulate(p);
console.log(` ${p.padEnd(10)} дом: ${(r.vaultDelta / n * 1000).toFixed(1)} фантиков на 1000 ставок`);
}